codegraphcontext 0.3.3__tar.gz → 0.3.5__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 (87) hide show
  1. {codegraphcontext-0.3.3/src/codegraphcontext.egg-info → codegraphcontext-0.3.5}/PKG-INFO +6 -4
  2. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/README.md +2 -2
  3. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/pyproject.toml +4 -2
  4. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/cli_helpers.py +28 -4
  5. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/graph_builder.py +79 -47
  6. codegraphcontext-0.3.5/src/codegraphcontext/viz/server.py +272 -0
  7. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5/src/codegraphcontext.egg-info}/PKG-INFO +6 -4
  8. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext.egg-info/requires.txt +3 -1
  9. codegraphcontext-0.3.3/src/codegraphcontext/viz/server.py +0 -179
  10. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/LICENSE +0 -0
  11. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/MANIFEST.in +0 -0
  12. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/setup.cfg +0 -0
  13. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/__init__.py +0 -0
  14. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/__main__.py +0 -0
  15. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/__init__.py +0 -0
  16. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/config_manager.py +0 -0
  17. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/main.py +0 -0
  18. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/registry_commands.py +0 -0
  19. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/setup_macos.py +0 -0
  20. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/setup_wizard.py +0 -0
  21. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/cli/visualizer.py +0 -0
  22. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/__init__.py +0 -0
  23. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/bundle_registry.py +0 -0
  24. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/cgc_bundle.py +0 -0
  25. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/database.py +0 -0
  26. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/database_falkordb.py +0 -0
  27. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/database_falkordb_remote.py +0 -0
  28. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/database_kuzu.py +0 -0
  29. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/falkor_worker.py +0 -0
  30. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/jobs.py +0 -0
  31. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/core/watcher.py +0 -0
  32. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/prompts.py +0 -0
  33. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/server.py +0 -0
  34. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tool_definitions.py +0 -0
  35. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/__init__.py +0 -0
  36. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/advanced_language_query_tool.py +0 -0
  37. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/code_finder.py +0 -0
  38. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/handlers/analysis_handlers.py +0 -0
  39. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/handlers/indexing_handlers.py +0 -0
  40. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/handlers/management_handlers.py +0 -0
  41. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/handlers/query_handlers.py +0 -0
  42. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/handlers/watcher_handlers.py +0 -0
  43. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/c.py +0 -0
  44. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/cpp.py +0 -0
  45. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/csharp.py +0 -0
  46. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/dart.py +0 -0
  47. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/elixir.py +0 -0
  48. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/go.py +0 -0
  49. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/haskell.py +0 -0
  50. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/java.py +0 -0
  51. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/javascript.py +0 -0
  52. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/kotlin.py +0 -0
  53. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/perl.py +0 -0
  54. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/php.py +0 -0
  55. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/python.py +0 -0
  56. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/ruby.py +0 -0
  57. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/rust.py +0 -0
  58. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/scala.py +0 -0
  59. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/swift.py +0 -0
  60. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/typescript.py +0 -0
  61. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/languages/typescriptjsx.py +0 -0
  62. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/package_resolver.py +0 -0
  63. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/c_toolkit.py +0 -0
  64. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +0 -0
  65. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +0 -0
  66. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/dart_toolkit.py +0 -0
  67. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/go_toolkit.py +0 -0
  68. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +0 -0
  69. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/java_toolkit.py +0 -0
  70. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +0 -0
  71. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/perl_toolkit.py +0 -0
  72. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/python_toolkit.py +0 -0
  73. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +0 -0
  74. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/rust_toolkit.py +0 -0
  75. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/scala_toolkit.py +0 -0
  76. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/swift_toolkit.py +0 -0
  77. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +0 -0
  78. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/scip_indexer.py +0 -0
  79. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/scip_pb2.py +0 -0
  80. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/tools/system.py +0 -0
  81. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/utils/debug_log.py +0 -0
  82. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/utils/tree_sitter_manager.py +0 -0
  83. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext/utils/visualize_graph.py +0 -0
  84. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
  85. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
  86. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
  87. {codegraphcontext-0.3.3 → codegraphcontext-0.3.5}/src/codegraphcontext.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -41,10 +41,11 @@ Requires-Dist: watchdog>=3.0.0
41
41
  Requires-Dist: stdlibs>=2023.11.18
42
42
  Requires-Dist: typer[all]>=0.9.0
43
43
  Requires-Dist: rich>=13.7.0
44
- Requires-Dist: inquirerpy>=0.3.4
44
+ Requires-Dist: inquirerpy>=0.3.5
45
45
  Requires-Dist: python-dotenv>=1.0.0
46
46
  Requires-Dist: tree-sitter>=0.21.0
47
47
  Requires-Dist: tree-sitter-language-pack>=0.6.0
48
+ Requires-Dist: tree-sitter-c-sharp>=0.21.0
48
49
  Requires-Dist: pyyaml
49
50
  Requires-Dist: nbformat
50
51
  Requires-Dist: nbconvert>=7.16.6
@@ -56,6 +57,7 @@ Requires-Dist: uvicorn>=0.22.0
56
57
  Provides-Extra: parsing
57
58
  Requires-Dist: tree-sitter>=0.21.0; extra == "parsing"
58
59
  Requires-Dist: tree-sitter-language-pack>=0.6.0; extra == "parsing"
60
+ Requires-Dist: tree-sitter-c-sharp>=0.21.0; extra == "parsing"
59
61
  Provides-Extra: dev
60
62
  Requires-Dist: pytest>=7.4.0; extra == "dev"
61
63
  Requires-Dist: black>=23.11.0; extra == "dev"
@@ -160,7 +162,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
160
162
  ---
161
163
 
162
164
  ## Project Details
163
- - **Version:** 0.3.3
165
+ - **Version:** 0.3.5
164
166
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
165
167
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
166
168
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -247,7 +249,7 @@ _If you’re using CodeGraphContext in your project, feel free to open a PR and
247
249
  - `stdlibs>=2023.11.18`
248
250
  - `typer[all]>=0.9.0`
249
251
  - `rich>=13.7.0`
250
- - `inquirerpy>=0.3.4`
252
+ - `inquirerpy>=0.3.5`
251
253
  - `python-dotenv>=1.0.0`
252
254
  - `tree-sitter>=0.21.0`
253
255
  - `tree-sitter-language-pack>=0.6.0`
@@ -96,7 +96,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
96
96
  ---
97
97
 
98
98
  ## Project Details
99
- - **Version:** 0.3.3
99
+ - **Version:** 0.3.5
100
100
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
101
101
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
102
102
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -183,7 +183,7 @@ _If you’re using CodeGraphContext in your project, feel free to open a PR and
183
183
  - `stdlibs>=2023.11.18`
184
184
  - `typer[all]>=0.9.0`
185
185
  - `rich>=13.7.0`
186
- - `inquirerpy>=0.3.4`
186
+ - `inquirerpy>=0.3.5`
187
187
  - `python-dotenv>=1.0.0`
188
188
  - `tree-sitter>=0.21.0`
189
189
  - `tree-sitter-language-pack>=0.6.0`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codegraphcontext"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  description = "An MCP server that indexes local code into a graph database to provide context to AI assistants."
5
5
  authors = [{ name = "Shashank Shekhar Singh", email = "shashankshekharsingh1205@gmail.com" }]
6
6
  readme = "README.md"
@@ -20,10 +20,11 @@ dependencies = [
20
20
  "stdlibs>=2023.11.18",
21
21
  "typer[all]>=0.9.0",
22
22
  "rich>=13.7.0",
23
- "inquirerpy>=0.3.4",
23
+ "inquirerpy>=0.3.5",
24
24
  "python-dotenv>=1.0.0",
25
25
  "tree-sitter>=0.21.0",
26
26
  "tree-sitter-language-pack>=0.6.0",
27
+ "tree-sitter-c-sharp>=0.21.0",
27
28
  "pyyaml",
28
29
  "nbformat",
29
30
  "nbconvert>=7.16.6",
@@ -38,6 +39,7 @@ dependencies = [
38
39
  parsing = [
39
40
  "tree-sitter>=0.21.0",
40
41
  "tree-sitter-language-pack>=0.6.0",
42
+ "tree-sitter-c-sharp>=0.21.0",
41
43
  ]
42
44
  dev = [
43
45
  "pytest>=7.4.0",
@@ -359,16 +359,40 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000):
359
359
 
360
360
  # Determine the static directory (built React app)
361
361
  # This points to src/codegraphcontext/viz/dist where we build the website
362
- static_dir = Path(__file__).parent.parent / "viz" / "dist"
362
+ # (relative to src/codegraphcontext/cli/cli_helpers.py)
363
+ # Using .resolve() is more robust for path comparison and existence checks
364
+ this_file = Path(__file__).resolve()
365
+ package_root = this_file.parent.parent
366
+ static_dir = package_root / "viz" / "dist"
363
367
 
364
368
  # Fallback for development if not yet built in viz/dist
365
369
  if not static_dir.exists():
366
- dev_static_dir = Path.cwd() / "website" / "dist"
370
+ # Look for website/dist in the project root (3 levels up from cli/cli_helpers.py, 4 parents)
371
+ # 1: cli/, 2: codegraphcontext/, 3: src/, 4: project_root/
372
+ project_root = this_file.parent.parent.parent.parent
373
+ dev_static_dir = project_root / "website" / "dist"
374
+
375
+ # Also try one level up from package_root just in case of different layouts
376
+ alt_dev_dir = package_root.parent.parent / "website" / "dist"
377
+
367
378
  if dev_static_dir.exists():
368
379
  static_dir = dev_static_dir
380
+ elif alt_dev_dir.exists():
381
+ static_dir = alt_dev_dir
369
382
  else:
370
- console.print("[yellow]Warning: Visualization assets not found. Please run 'cd website && npm run build' first.[/yellow]")
371
- # We continue anyway to let the server start (helpful for dev)
383
+ # Last resort: try current working directory
384
+ cwd_static_dir = Path.cwd() / "website" / "dist"
385
+ if cwd_static_dir.exists():
386
+ static_dir = cwd_static_dir
387
+ else:
388
+ console.print(f"[yellow]Warning: Visualization assets not found.[/yellow]")
389
+ console.print(f"[dim]Checked paths:[/dim]")
390
+ console.print(f" [dim]- {static_dir}[/dim]")
391
+ console.print(f" [dim]- {dev_static_dir}[/dim]")
392
+ console.print(f" [dim]- {alt_dev_dir}[/dim]")
393
+ console.print(f" [dim]- {cwd_static_dir}[/dim]")
394
+ console.print("[dim]Please run 'cd website && npm run build' first.[/dim]")
395
+ # We continue anyway to let the server start (helpful for dev)
372
396
 
373
397
  # Construct the URL
374
398
  backend_url = f"http://localhost:{port}"
@@ -703,8 +703,7 @@ class GraphBuilder:
703
703
  'args': call.get('args', []),
704
704
  'full_call_name': call.get('full_name', called_name)
705
705
  }
706
-
707
- # Try Function caller -> Function callee
706
+ # Try Function caller -> Function callee
708
707
  if not self._safe_run_create(session, """
709
708
  OPTIONAL MATCH (caller:Function {name: $caller_name, path: $caller_file_path})
710
709
  OPTIONAL MATCH (called:Function {name: $called_name, path: $called_file_path})
@@ -713,51 +712,73 @@ class GraphBuilder:
713
712
  MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
714
713
  RETURN count(*) as created
715
714
  """, call_params):
716
-
717
- # Try Function caller -> Class callee (with __init__ resolution)
715
+
716
+ # Try Function caller -> Class.__init__ / constructor
718
717
  if not self._safe_run_create(session, """
719
718
  OPTIONAL MATCH (caller:Function {name: $caller_name, path: $caller_file_path})
720
719
  OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
721
720
  OPTIONAL MATCH (called)-[:CONTAINS]->(init:Function)
722
721
  WHERE init.name IN ["__init__", "constructor"]
723
- WITH caller, COALESCE(init, called) as final_target
724
- WHERE caller IS NOT NULL AND final_target IS NOT NULL
725
- MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(final_target)
722
+ WITH caller, init
723
+ WHERE caller IS NOT NULL AND init IS NOT NULL
724
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(init)
726
725
  RETURN count(*) as created
727
726
  """, call_params):
728
-
729
- # Try Class caller -> Function callee
727
+ # No __init__ found - link directly to the Class node
728
+ self._safe_run_create(session, """
729
+ OPTIONAL MATCH (caller:Function {name: $caller_name, path: $caller_file_path})
730
+ OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
731
+ WITH caller, called
732
+ WHERE caller IS NOT NULL AND called IS NOT NULL
733
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
734
+ RETURN count(*) as created
735
+ """, call_params)
736
+
737
+ # Try Class caller -> Function callee
738
+ if not self._safe_run_create(session, """
739
+ OPTIONAL MATCH (caller:Class {name: $caller_name, path: $caller_file_path})
740
+ OPTIONAL MATCH (called:Function {name: $called_name, path: $called_file_path})
741
+ WITH caller, called
742
+ WHERE caller IS NOT NULL AND called IS NOT NULL
743
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
744
+ RETURN count(*) as created
745
+ """, call_params):
746
+
747
+ # Try Class caller -> Class.__init__ / constructor
748
+ if not self._safe_run_create(session, """
749
+ OPTIONAL MATCH (caller:Class {name: $caller_name, path: $caller_file_path})
750
+ OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
751
+ OPTIONAL MATCH (called)-[:CONTAINS]->(init:Function)
752
+ WHERE init.name IN ["__init__", "constructor"]
753
+ WITH caller, init
754
+ WHERE caller IS NOT NULL AND init IS NOT NULL
755
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(init)
756
+ RETURN count(*) as created
757
+ """, call_params):
758
+ # No __init__ - link directly to the Class node
730
759
  if not self._safe_run_create(session, """
731
760
  OPTIONAL MATCH (caller:Class {name: $caller_name, path: $caller_file_path})
732
- OPTIONAL MATCH (called:Function {name: $called_name, path: $called_file_path})
761
+ OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
733
762
  WITH caller, called
734
763
  WHERE caller IS NOT NULL AND called IS NOT NULL
735
764
  MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
736
765
  RETURN count(*) as created
737
766
  """, call_params):
738
-
739
- # Try Class caller -> Class callee
767
+ # Fallback: Relaxed Global Search (Function caller -> any Function callee)
740
768
  if not self._safe_run_create(session, """
741
- OPTIONAL MATCH (caller:Class {name: $caller_name, path: $caller_file_path})
742
- OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
743
- OPTIONAL MATCH (called)-[:CONTAINS]->(init:Function)
744
- WHERE init.name IN ["__init__", "constructor"]
745
- WITH caller, COALESCE(init, called) as final_target
746
- WHERE caller IS NOT NULL AND final_target IS NOT NULL
747
- MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(final_target)
748
- RETURN count(*) as created
769
+ OPTIONAL MATCH (caller:Function {name: $caller_name, path: $caller_file_path})
770
+ OPTIONAL MATCH (called:Function {name: $called_name})
771
+ WITH caller, called
772
+ WHERE caller IS NOT NULL AND called IS NOT NULL
773
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
749
774
  """, call_params):
750
-
751
- # Fallback: Relaxed Global Search (Caller: Function/Class -> Callee: Function)
752
- # Used when path resolution failed or was ambiguous
753
- self._safe_run_create(session, """
754
- OPTIONAL MATCH (caller:Function {name: $caller_name, path: $caller_file_path})
755
- OPTIONAL MATCH (callerClass:Class {name: $caller_name, path: $caller_file_path})
756
- WITH COALESCE(caller, callerClass) as final_caller
775
+ # Fallback: Class caller -> any Function callee
776
+ self._safe_run_create(session, """
777
+ OPTIONAL MATCH (caller:Class {name: $caller_name, path: $caller_file_path})
757
778
  OPTIONAL MATCH (called:Function {name: $called_name})
758
- WITH final_caller, called
759
- WHERE final_caller IS NOT NULL AND called IS NOT NULL
760
- MERGE (final_caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
779
+ WITH caller, called
780
+ WHERE caller IS NOT NULL AND called IS NOT NULL
781
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
761
782
  """, call_params)
762
783
  else:
763
784
  # File-level calls: Try Function first, then Class
@@ -769,7 +790,7 @@ class GraphBuilder:
769
790
  'args': call.get('args', []),
770
791
  'full_call_name': call.get('full_name', called_name)
771
792
  }
772
-
793
+
773
794
  if not self._safe_run_create(session, """
774
795
  OPTIONAL MATCH (caller:File {path: $caller_file_path})
775
796
  OPTIONAL MATCH (called:Function {name: $called_name, path: $called_file_path})
@@ -778,26 +799,35 @@ class GraphBuilder:
778
799
  MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
779
800
  RETURN count(*) as created
780
801
  """, call_params):
781
-
802
+
803
+ # Try File caller -> Class.__init__ / constructor
782
804
  if not self._safe_run_create(session, """
783
805
  OPTIONAL MATCH (caller:File {path: $caller_file_path})
784
806
  OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
785
807
  OPTIONAL MATCH (called)-[:CONTAINS]->(init:Function)
786
808
  WHERE init.name IN ["__init__", "constructor"]
787
- WITH caller, COALESCE(init, called) as final_target
788
- WHERE caller IS NOT NULL AND final_target IS NOT NULL
789
- MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(final_target)
809
+ WITH caller, init
810
+ WHERE caller IS NOT NULL AND init IS NOT NULL
811
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(init)
790
812
  RETURN count(*) as created
791
813
  """, call_params):
792
-
793
- # Fallback: Relaxed Global Search (Caller: File -> Callee: Function)
794
- self._safe_run_create(session, """
814
+ # No __init__ - link directly to the Class node
815
+ if not self._safe_run_create(session, """
795
816
  OPTIONAL MATCH (caller:File {path: $caller_file_path})
796
- OPTIONAL MATCH (called:Function {name: $called_name})
817
+ OPTIONAL MATCH (called:Class {name: $called_name, path: $called_file_path})
797
818
  WITH caller, called
798
819
  WHERE caller IS NOT NULL AND called IS NOT NULL
799
820
  MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
800
- """, call_params)
821
+ RETURN count(*) as created
822
+ """, call_params):
823
+ # Fallback: Relaxed Global Search (File -> any Function)
824
+ self._safe_run_create(session, """
825
+ OPTIONAL MATCH (caller:File {path: $caller_file_path})
826
+ OPTIONAL MATCH (called:Function {name: $called_name})
827
+ WITH caller, called
828
+ WHERE caller IS NOT NULL AND called IS NOT NULL
829
+ MERGE (caller)-[:CALLS {line_number: $line_number, args: $args, full_call_name: $full_call_name}]->(called)
830
+ """, call_params)
801
831
 
802
832
  def _create_all_function_calls(self, all_file_data: list[Dict], imports_map: dict):
803
833
  """Create CALLS relationships for all functions after all files have been processed."""
@@ -1278,20 +1308,22 @@ class GraphBuilder:
1278
1308
 
1279
1309
  # Search for .cgcignore upwards
1280
1310
  cgcignore_path = None
1281
- ignore_root = path.resolve()
1282
-
1311
+ # ignore_root is always the indexed path itself so that file paths
1312
+ # are matched relative to the project being indexed. A parent
1313
+ # .cgcignore is still loaded (for monorepo support), but anchoring
1314
+ # to its directory would make patterns like "website/" incorrectly
1315
+ # filter out every file when indexing the website sub-directory.
1316
+ ignore_root = path.resolve() if path.is_dir() else path.resolve().parent
1317
+
1283
1318
  # Start search from path (or parent if path is file)
1284
- curr = path.resolve()
1285
- if not curr.is_dir():
1286
- curr = curr.parent
1319
+ curr = ignore_root
1287
1320
 
1288
1321
  # Walk up looking for .cgcignore
1289
1322
  while True:
1290
1323
  candidate = curr / ".cgcignore"
1291
1324
  if candidate.exists():
1292
1325
  cgcignore_path = candidate
1293
- ignore_root = curr
1294
- debug_log(f"Found .cgcignore at {ignore_root}")
1326
+ debug_log(f"Found .cgcignore at {curr} (filtering relative to {ignore_root})")
1295
1327
  break
1296
1328
  if curr.parent == curr: # Root hit
1297
1329
  break
@@ -0,0 +1,272 @@
1
+ from fastapi import FastAPI, HTTPException, Query, Request
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.responses import HTMLResponse, FileResponse
5
+ from pathlib import Path
6
+ import uvicorn
7
+ import json
8
+ import os
9
+ import sys
10
+ from typing import Optional, List, Dict, Any
11
+
12
+ from ..core.database import DatabaseManager
13
+ from ..utils.debug_log import debug_log
14
+
15
+ app = FastAPI()
16
+
17
+ # Enable CORS for development
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ # Global database manager (will be initialized when server starts)
26
+ db_manager: Optional[DatabaseManager] = None
27
+ # Path to static directory
28
+ _static_dir: Optional[str] = None
29
+
30
+ def set_db_manager(manager: DatabaseManager):
31
+ global db_manager
32
+ db_manager = manager
33
+
34
+ @app.get("/api/graph")
35
+ async def get_graph(repo_path: Optional[str] = None, cypher_query: Optional[str] = None):
36
+ if not db_manager:
37
+ raise HTTPException(status_code=500, detail="Database not initialized")
38
+
39
+ def get_eid(element):
40
+ if element is None: return None
41
+ if isinstance(element, (int, str)):
42
+ return str(element)
43
+
44
+ # If element is a dict (like Neo4j returned items or KuzuDB node/rel dicts)
45
+ if isinstance(element, dict):
46
+ # KuzuDB _src / _dst are directly {'offset': X, 'table': Y}
47
+ if 'offset' in element and 'table' in element:
48
+ return f"{element.get('table')}_{element.get('offset')}"
49
+
50
+ for key in ['_id', 'id', 'element_id']:
51
+ if key in element:
52
+ val = element[key]
53
+ if val is not None:
54
+ # KuzuDB returns dict IDs like {'offset': 1, 'table': 0} inside nodes
55
+ if isinstance(val, dict):
56
+ return f"{val.get('table', 0)}_{val.get('offset', 0)}"
57
+ return str(val)
58
+ return str(id(element))
59
+
60
+ # Try various ways to get ID (Neo4j, FalkorDB, etc. objects)
61
+ for attr in ['element_id', 'id', '_id']:
62
+ if hasattr(element, attr):
63
+ val = getattr(element, attr)
64
+ if val is not None:
65
+ # KuzuDB objects if any
66
+ if isinstance(val, dict):
67
+ return f"{val.get('table', 0)}_{val.get('offset', 0)}"
68
+ return str(val)
69
+ return str(id(element))
70
+
71
+ try:
72
+ nodes_dict = {}
73
+ edges = []
74
+
75
+ print(f"DEBUG: Starting get_graph with repo_path={repo_path}", flush=True)
76
+
77
+ with db_manager.get_driver().session() as session:
78
+ if cypher_query:
79
+ print(f"DEBUG: Executing custom query: {cypher_query}", flush=True)
80
+ result = session.run(cypher_query)
81
+ elif repo_path:
82
+ repo_path = str(Path(repo_path).resolve())
83
+ print(f"DEBUG: Fetching subgraph for: {repo_path}", flush=True)
84
+ query = """
85
+ MATCH (r:Repository {path: $repo_path})
86
+ OPTIONAL MATCH (r)-[:CONTAINS*0..]->(n)
87
+ WITH DISTINCT n
88
+ WHERE n IS NOT NULL
89
+ OPTIONAL MATCH (n)-[rel]->(m)
90
+ RETURN n, rel, m
91
+ """
92
+ result = session.run(query, repo_path=repo_path)
93
+ else:
94
+ print("DEBUG: Fetching global graph", flush=True)
95
+ query = "MATCH (n) OPTIONAL MATCH (n)-[rel]->(m) RETURN n, rel, m LIMIT 50000"
96
+ result = session.run(query)
97
+
98
+ record_count = 0
99
+ for record in result:
100
+ record_count += 1
101
+ # Use .get() to avoid KeyError if the query doesn't return all fields (n, rel, m)
102
+ for key in ['n', 'm']:
103
+ try:
104
+ node = record.get(key)
105
+ if node:
106
+ eid = get_eid(node)
107
+ if eid and eid not in nodes_dict:
108
+ # Extract labels
109
+ labels = []
110
+ if isinstance(node, dict):
111
+ # KuzuDB node label is under '_label'
112
+ if '_label' in node:
113
+ labels = [node['_label']]
114
+ elif 'label' in node:
115
+ labels = [node['label']]
116
+ else:
117
+ for label_attr in ['_labels', 'labels']:
118
+ if hasattr(node, label_attr):
119
+ attr_val = getattr(node, label_attr)
120
+ if attr_val:
121
+ labels = list(attr_val)
122
+ break
123
+
124
+ # Extract properties
125
+ props = {}
126
+ if isinstance(node, dict):
127
+ props = {k: v for k, v in node.items() if not k.startswith('_')}
128
+ else:
129
+ for prop_attr in ['properties', '_properties']:
130
+ if hasattr(node, prop_attr):
131
+ attr_val = getattr(node, prop_attr)
132
+ if attr_val:
133
+ props = dict(attr_val)
134
+ break
135
+
136
+ # Fallback if props still empty but node acts like dict
137
+ if not props and hasattr(node, 'items'):
138
+ try:
139
+ props = dict(node.items())
140
+ except: pass
141
+
142
+ # Extract name/label for frontend
143
+ # Prefer 'name' property, fallback to 'label', then 'path' or 'Unknown'
144
+ display_name = str(props.get('name', props.get('label', props.get('path', 'Unknown'))))
145
+
146
+ nodes_dict[eid] = {
147
+ "id": eid,
148
+ "name": display_name,
149
+ "label": display_name,
150
+ "type": str(labels[0]).capitalize() if labels else "Other",
151
+ "file": str(props.get('path', props.get('file', ''))),
152
+ "val": 4 if (labels and labels[0] in ['Repository', 'Class', 'Interface', 'Trait']) else 2,
153
+ "properties": props
154
+ }
155
+ except Exception as e:
156
+ print(f"DEBUG: Error parsing node: {e}", file=sys.stderr, flush=True)
157
+ continue
158
+
159
+ try:
160
+ rel = record.get('rel')
161
+ if rel is not None:
162
+ # One-shot debug: print type and keys/attrs of first rel
163
+ if not edges:
164
+ if isinstance(rel, dict):
165
+ print(f"DEBUG rel (dict): keys={list(rel.keys())}", file=sys.stderr, flush=True)
166
+ else:
167
+ print(f"DEBUG rel (obj): type={type(rel).__name__}, attrs={[a for a in dir(rel) if not a.startswith('__')]}", file=sys.stderr, flush=True)
168
+
169
+ # FalkorDB / KuzuDB may return rels as dicts OR objects
170
+ if isinstance(rel, dict):
171
+ rid = get_eid(rel)
172
+ # KuzuDB uses _src and _dst, FalkorDB uses src_node/dest_node
173
+ src = rel.get('_src', rel.get('src_node'))
174
+ dst = rel.get('_dst', rel.get('dest_node'))
175
+
176
+ source = get_eid(src) if src is not None else None
177
+ target = get_eid(dst) if dst is not None else None
178
+ rel_type = str(rel.get('_label', rel.get('relation', rel.get('type', 'RELATED')))).upper()
179
+ else:
180
+ rid = get_eid(rel)
181
+ start_node = None
182
+ end_node = None
183
+ for src_attr in ['start_node', 'src_node', '_src_node']:
184
+ if hasattr(rel, src_attr):
185
+ start_node = getattr(rel, src_attr)
186
+ break
187
+ for dest_attr in ['end_node', 'dest_node', '_dest_node']:
188
+ if hasattr(rel, dest_attr):
189
+ end_node = getattr(rel, dest_attr)
190
+ break
191
+ source = get_eid(start_node) if start_node is not None else None
192
+ target = get_eid(end_node) if end_node is not None else None
193
+ rel_type = "RELATED"
194
+ for rel_attr in ['type', 'relation', '_relation']:
195
+ if hasattr(rel, rel_attr):
196
+ rel_type = str(getattr(rel, rel_attr)).upper()
197
+ break
198
+
199
+ if source and target:
200
+ edges.append({
201
+ "id": rid,
202
+ "source": source,
203
+ "target": target,
204
+ "type": rel_type
205
+ })
206
+ except Exception as e:
207
+ print(f"DEBUG: Error parsing relationship: {e}", file=sys.stderr, flush=True)
208
+ pass
209
+
210
+ print(f"DEBUG: Processed {record_count} records. extracted {len(nodes_dict)} nodes and {len(edges)} edges.", file=sys.stderr, flush=True)
211
+
212
+ # Build a list of unique file paths from File-type nodes for the tree
213
+ file_paths = []
214
+ for n in nodes_dict.values():
215
+ if n.get("file") and str(n.get("type", "")).lower() == "file":
216
+ file_paths.append(str(n["file"]))
217
+ file_paths = sorted(list(set(file_paths)))
218
+
219
+ response_data = {
220
+ "nodes": list(nodes_dict.values()),
221
+ "links": edges,
222
+ "files": file_paths,
223
+ }
224
+
225
+ print(f"API SUCCESS: Returning graph with {len(response_data['nodes'])} nodes and {len(response_data['links'])} links.", file=sys.stderr, flush=True)
226
+ return response_data
227
+
228
+ except Exception as e:
229
+ debug_log(f"Error fetching graph: {str(e)}")
230
+ import traceback
231
+ traceback.print_exc()
232
+ # Still return a valid structure so the frontend doesn't crash, but with 500 status if raised
233
+ # Actually, let's just return a 500 error but with JSON body if possible
234
+ raise HTTPException(status_code=500, detail=str(e))
235
+
236
+ @app.get("/api/file")
237
+ async def get_file(path: str):
238
+ file_path = Path(path)
239
+ if not file_path.exists():
240
+ raise HTTPException(status_code=404, detail="File not found")
241
+
242
+ try:
243
+ with open(file_path, "r", encoding="utf-8") as f:
244
+ return {"content": f.read()}
245
+ except Exception as e:
246
+ raise HTTPException(status_code=500, detail=str(e))
247
+
248
+ # SPA fallback handler
249
+ @app.get("/{full_path:path}")
250
+ async def spa_fallback(request: Request, full_path: str):
251
+ global _static_dir
252
+ if not _static_dir:
253
+ return HTMLResponse("Static directory not configured", status_code=500)
254
+
255
+ # Filesystem path
256
+ file_path = Path(_static_dir) / full_path
257
+
258
+ # If the file exists and is a file, serve it normally
259
+ if file_path.exists() and file_path.is_file():
260
+ return FileResponse(file_path)
261
+
262
+ # Otherwise serve index.html (Standard SPA routing)
263
+ index_path = Path(_static_dir) / "index.html"
264
+ if index_path.exists():
265
+ return FileResponse(index_path)
266
+
267
+ return HTMLResponse("Not Found (Built UI not found in viz/dist)", status_code=404)
268
+
269
+ def run_server(host: str = "127.0.0.1", port: int = 8000, static_dir: Optional[str] = None):
270
+ global _static_dir
271
+ _static_dir = static_dir
272
+ uvicorn.run(app, host=host, port=port)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -41,10 +41,11 @@ Requires-Dist: watchdog>=3.0.0
41
41
  Requires-Dist: stdlibs>=2023.11.18
42
42
  Requires-Dist: typer[all]>=0.9.0
43
43
  Requires-Dist: rich>=13.7.0
44
- Requires-Dist: inquirerpy>=0.3.4
44
+ Requires-Dist: inquirerpy>=0.3.5
45
45
  Requires-Dist: python-dotenv>=1.0.0
46
46
  Requires-Dist: tree-sitter>=0.21.0
47
47
  Requires-Dist: tree-sitter-language-pack>=0.6.0
48
+ Requires-Dist: tree-sitter-c-sharp>=0.21.0
48
49
  Requires-Dist: pyyaml
49
50
  Requires-Dist: nbformat
50
51
  Requires-Dist: nbconvert>=7.16.6
@@ -56,6 +57,7 @@ Requires-Dist: uvicorn>=0.22.0
56
57
  Provides-Extra: parsing
57
58
  Requires-Dist: tree-sitter>=0.21.0; extra == "parsing"
58
59
  Requires-Dist: tree-sitter-language-pack>=0.6.0; extra == "parsing"
60
+ Requires-Dist: tree-sitter-c-sharp>=0.21.0; extra == "parsing"
59
61
  Provides-Extra: dev
60
62
  Requires-Dist: pytest>=7.4.0; extra == "dev"
61
63
  Requires-Dist: black>=23.11.0; extra == "dev"
@@ -160,7 +162,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
160
162
  ---
161
163
 
162
164
  ## Project Details
163
- - **Version:** 0.3.3
165
+ - **Version:** 0.3.5
164
166
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
165
167
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
166
168
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -247,7 +249,7 @@ _If you’re using CodeGraphContext in your project, feel free to open a PR and
247
249
  - `stdlibs>=2023.11.18`
248
250
  - `typer[all]>=0.9.0`
249
251
  - `rich>=13.7.0`
250
- - `inquirerpy>=0.3.4`
252
+ - `inquirerpy>=0.3.5`
251
253
  - `python-dotenv>=1.0.0`
252
254
  - `tree-sitter>=0.21.0`
253
255
  - `tree-sitter-language-pack>=0.6.0`
@@ -3,10 +3,11 @@ watchdog>=3.0.0
3
3
  stdlibs>=2023.11.18
4
4
  typer[all]>=0.9.0
5
5
  rich>=13.7.0
6
- inquirerpy>=0.3.4
6
+ inquirerpy>=0.3.5
7
7
  python-dotenv>=1.0.0
8
8
  tree-sitter>=0.21.0
9
9
  tree-sitter-language-pack>=0.6.0
10
+ tree-sitter-c-sharp>=0.21.0
10
11
  pyyaml
11
12
  nbformat
12
13
  nbconvert>=7.16.6
@@ -26,3 +27,4 @@ pytest-asyncio>=0.21.0
26
27
  [parsing]
27
28
  tree-sitter>=0.21.0
28
29
  tree-sitter-language-pack>=0.6.0
30
+ tree-sitter-c-sharp>=0.21.0
@@ -1,179 +0,0 @@
1
- from fastapi import FastAPI, HTTPException, Query, Request
2
- from fastapi.staticfiles import StaticFiles
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from fastapi.responses import HTMLResponse, FileResponse
5
- from pathlib import Path
6
- import uvicorn
7
- import json
8
- import os
9
- from typing import Optional, List, Dict, Any
10
-
11
- from ..core.database import DatabaseManager
12
- from ..utils.debug_log import debug_log
13
-
14
- app = FastAPI()
15
-
16
- # Enable CORS for development
17
- app.add_middleware(
18
- CORSMiddleware,
19
- allow_origins=["*"],
20
- allow_methods=["*"],
21
- allow_headers=["*"],
22
- )
23
-
24
- # Global database manager (will be initialized when server starts)
25
- db_manager: Optional[DatabaseManager] = None
26
- # Path to static directory
27
- _static_dir: Optional[str] = None
28
-
29
- def set_db_manager(manager: DatabaseManager):
30
- global db_manager
31
- db_manager = manager
32
-
33
- @app.get("/api/graph")
34
- async def get_graph(repo_path: Optional[str] = None, cypher_query: Optional[str] = None):
35
- if not db_manager:
36
- raise HTTPException(status_code=500, detail="Database not initialized")
37
-
38
- def get_eid(element):
39
- if not element: return None
40
- if isinstance(element, (int, str)):
41
- return str(element)
42
- # Try various ways to get ID (Neo4j, FalkorDB, etc.)
43
- for attr in ['element_id', 'id', '_id']:
44
- if hasattr(element, attr):
45
- val = getattr(element, attr)
46
- if val is not None: return str(val)
47
- return str(id(element))
48
-
49
- try:
50
- nodes_dict = {}
51
- edges = []
52
-
53
- with db_manager.get_driver().session() as session:
54
- if cypher_query:
55
- # Direct user query (filtered view)
56
- result = session.run(cypher_query)
57
- elif repo_path:
58
- repo_path = str(Path(repo_path).resolve())
59
- # Optimized subgraph query
60
- query = """
61
- MATCH (r:Repository {path: $repo_path})
62
- OPTIONAL MATCH (r)-[:CONTAINS*0..]->(n)
63
- WITH DISTINCT n
64
- WHERE n IS NOT NULL
65
- OPTIONAL MATCH (n)-[rel]->(m)
66
- RETURN n, rel, m
67
- """
68
- result = session.run(query, repo_path=repo_path)
69
- else:
70
- query = "MATCH (n) OPTIONAL MATCH (n)-[rel]->(m) RETURN n, rel, m LIMIT 5000"
71
- result = session.run(query)
72
-
73
- for record in result:
74
- for key in ['n', 'm']:
75
- node = record[key]
76
- if node:
77
- eid = get_eid(node)
78
- if eid not in nodes_dict:
79
- # FalkorDB / Neo4j labels compatibility
80
- labels = []
81
- if hasattr(node, 'labels'):
82
- labels = list(node.labels)
83
-
84
- # FalkorDB / Neo4j properties compatibility
85
- props = {}
86
- if hasattr(node, 'properties'):
87
- props = node.properties
88
- elif hasattr(node, 'items'):
89
- props = dict(node.items())
90
-
91
- nodes_dict[eid] = {
92
- "id": eid,
93
- "label": props.get('name', props.get('label', 'Unknown')),
94
- "type": labels[0].capitalize() if labels else "Other",
95
- "file": props.get('path', props.get('file', '')),
96
- "properties": props
97
- }
98
-
99
- rel = record['rel']
100
- if rel:
101
- rid = get_eid(rel)
102
-
103
- # FalkorDB / Neo4j compatibility for source/target nodes
104
- start_node = getattr(rel, 'start_node', getattr(rel, 'src_node', None))
105
- end_node = getattr(rel, 'end_node', getattr(rel, 'dest_node', None))
106
-
107
- source = get_eid(start_node)
108
- target = get_eid(end_node)
109
-
110
- if source and target:
111
- # relationship type/relation
112
- rel_type = "related"
113
- if hasattr(rel, 'type'):
114
- rel_type = rel.type
115
- elif hasattr(rel, 'relation'):
116
- rel_type = rel.relation
117
-
118
- edges.append({
119
- "id": rid,
120
- "source": source,
121
- "target": target,
122
- "type": str(rel_type).upper()
123
- })
124
-
125
- # Build a list of unique file paths from File-type nodes for the tree
126
- file_paths = sorted(set(
127
- n["file"] for n in nodes_dict.values()
128
- if n.get("file") and n.get("type", "").lower() == "file"
129
- ))
130
-
131
- return {
132
- "nodes": list(nodes_dict.values()),
133
- "edges": edges,
134
- "files": file_paths,
135
- }
136
-
137
- except Exception as e:
138
- debug_log(f"Error fetching graph: {str(e)}")
139
- import traceback
140
- traceback.print_exc()
141
- raise HTTPException(status_code=500, detail=str(e))
142
-
143
- @app.get("/api/file")
144
- async def get_file(path: str):
145
- file_path = Path(path)
146
- if not file_path.exists():
147
- raise HTTPException(status_code=404, detail="File not found")
148
-
149
- try:
150
- with open(file_path, "r", encoding="utf-8") as f:
151
- return {"content": f.read()}
152
- except Exception as e:
153
- raise HTTPException(status_code=500, detail=str(e))
154
-
155
- # SPA fallback handler
156
- @app.get("/{full_path:path}")
157
- async def spa_fallback(request: Request, full_path: str):
158
- global _static_dir
159
- if not _static_dir:
160
- return HTMLResponse("Static directory not configured", status_code=500)
161
-
162
- # Filesystem path
163
- file_path = Path(_static_dir) / full_path
164
-
165
- # If the file exists and is a file, serve it normally
166
- if file_path.exists() and file_path.is_file():
167
- return FileResponse(file_path)
168
-
169
- # Otherwise serve index.html (Standard SPA routing)
170
- index_path = Path(_static_dir) / "index.html"
171
- if index_path.exists():
172
- return FileResponse(index_path)
173
-
174
- return HTMLResponse("Not Found (Built UI not found in viz/dist)", status_code=404)
175
-
176
- def run_server(host: str = "127.0.0.1", port: int = 8000, static_dir: Optional[str] = None):
177
- global _static_dir
178
- _static_dir = static_dir
179
- uvicorn.run(app, host=host, port=port)