codegraphcontext 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

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.
@@ -4,6 +4,7 @@ import uuid
4
4
  import urllib.parse
5
5
  from pathlib import Path
6
6
  import time
7
+ from typing import Optional
7
8
  from rich.console import Console
8
9
  from rich.table import Table
9
10
  from rich.progress import (
@@ -339,44 +340,55 @@ def cypher_helper_visual(query: str):
339
340
  console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
340
341
  finally:
341
342
  db_manager.close_driver()
342
-
343
-
344
343
  import webbrowser
344
+ import urllib.parse
345
+ from ..viz.server import run_server, set_db_manager
345
346
 
346
- def visualize_helper(query: str):
347
- """"Generates a visualization."""
347
+ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000):
348
+ """"Generates an interactive visualization using the Playground UI."""
348
349
  services = _initialize_services()
349
350
  if not all(services):
350
351
  return
351
352
 
352
353
  db_manager, _, _ = services
353
354
 
354
- # Check Backend Type
355
- backend = getattr(db_manager, "name", "").lower()
356
- if not backend:
357
- # Fallback check
358
- if "FalkorDB" in db_manager.__class__.__name__:
359
- backend = "falkordb"
360
- elif "Kuzu" in db_manager.__class__.__name__:
361
- backend = "kuzudb"
362
- else:
363
- backend = "neo4j"
364
-
365
- if backend == "falkordb":
366
- _visualize_falkordb(db_manager)
367
- elif backend == "kuzudb":
368
- _visualize_kuzudb(db_manager)
369
- else:
370
- try:
371
- encoded_query = urllib.parse.quote(query)
372
- visualization_url = f"http://localhost:7474/browser/?cmd=edit&arg={encoded_query}"
373
- console.print("[green]Graph visualization URL:[/green]")
374
- console.print(visualization_url)
375
- console.print("Open the URL in your browser to see the graph.")
376
- except Exception as e:
377
- console.print(f"[bold red]An error occurred while generating URL:[/bold red] {e}")
378
- finally:
379
- db_manager.close_driver()
355
+ # Set the DB manager for the server
356
+ set_db_manager(db_manager)
357
+
358
+ # Determine the static directory (built React app)
359
+ static_dir = Path(__file__).parent.parent / "viz" / "dist"
360
+ if not static_dir.exists():
361
+ console.print("[yellow]Warning: Visualizer UI assets not found in package. Using fallback static dir.[/yellow]")
362
+ # Fallback for development
363
+ static_dir = Path.cwd() / "website" / "dist"
364
+
365
+ # Construct the URL
366
+ backend_url = f"http://localhost:{port}"
367
+ params = {"backend": backend_url}
368
+ if repo_path:
369
+ params["repo_path"] = str(Path(repo_path).resolve())
370
+
371
+ query_string = urllib.parse.urlencode(params)
372
+ visualization_url = f"{backend_url}/playground?{query_string}"
373
+
374
+ console.print(f"[green]Starting visualizer server on {backend_url}...[/green]")
375
+ console.print(f"[cyan]Opening Playground UI:[/cyan] {visualization_url}")
376
+
377
+ # Open browser in a separate thread/process if possible, or just before starting server
378
+ def open_browser():
379
+ import time
380
+ time.sleep(1.5) # Give the server a moment to start
381
+ webbrowser.open(visualization_url)
382
+
383
+ import threading
384
+ threading.Thread(target=open_browser, daemon=True).start()
385
+
386
+ try:
387
+ run_server(host="127.0.0.1", port=port, static_dir=str(static_dir))
388
+ except Exception as e:
389
+ console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
390
+ finally:
391
+ db_manager.close_driver()
380
392
 
381
393
  def _visualize_falkordb(db_manager):
382
394
  console.print("[dim]Generating FalkorDB visualization (showing up to 500 relationships)...[/dim]")
@@ -994,15 +994,15 @@ def delete(
994
994
  delete_helper(path)
995
995
 
996
996
  @app.command()
997
- def visualize(query: Optional[str] = typer.Argument(None, help="The Cypher query to visualize.")):
997
+ def visualize(
998
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Path to the repository to visualize."),
999
+ port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on.")
1000
+ ):
998
1001
  """
999
- Generates a URL to visualize a Cypher query in the Neo4j Browser.
1000
- If no query is provided, a default query will be used.
1002
+ Launches the interactive Playground UI to visualize the code graph.
1001
1003
  """
1002
- if query is None:
1003
- query = "MATCH p=()-->() RETURN p"
1004
1004
  _load_credentials()
1005
- visualize_helper(query)
1005
+ visualize_helper(repo, port)
1006
1006
 
1007
1007
  @app.command("list")
1008
1008
  def list_repositories():
@@ -2145,9 +2145,13 @@ def delete_abbrev(
2145
2145
  delete(path, all_repos)
2146
2146
 
2147
2147
  @app.command("v", rich_help_panel="Shortcuts")
2148
- def visualize_abbrev(query: Optional[str] = typer.Argument(None, help="Cypher query")):
2148
+ def visualize_abbrev(
2149
+ repo: Optional[str] = typer.Argument(None, help="Path to the repository to visualize."),
2150
+ port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on.")
2151
+ ):
2149
2152
  """Shortcut for 'cgc visualize'"""
2150
- visualize(query)
2153
+ _load_credentials()
2154
+ visualize_helper(repo, port)
2151
2155
 
2152
2156
  @app.command("w", rich_help_panel="Shortcuts")
2153
2157
  def watch_abbrev(path: str = typer.Argument(".", help="Path to watch")):
@@ -161,9 +161,13 @@ class CGCBundle:
161
161
  with tempfile.TemporaryDirectory() as temp_dir:
162
162
  temp_path = Path(temp_dir)
163
163
 
164
- # Step 1: Extract ZIP
164
+ # Step 1: Extract ZIP (with Zip Slip protection)
165
165
  info_logger("Extracting bundle...")
166
166
  with zipfile.ZipFile(bundle_path, 'r') as zip_ref:
167
+ for entry in zip_ref.namelist():
168
+ resolved = (temp_path / entry).resolve()
169
+ if not str(resolved).startswith(str(temp_path.resolve())):
170
+ return False, f"Zip Slip detected: entry '{entry}' escapes target directory"
167
171
  zip_ref.extractall(temp_path)
168
172
 
169
173
  # Step 2: Validate bundle
@@ -1,6 +1,6 @@
1
1
  # src/codegraphcontext/tools/code_finder.py
2
2
  import logging
3
- import re
3
+
4
4
  from typing import Any, Dict, List, Literal, Optional
5
5
  from pathlib import Path
6
6
 
@@ -348,7 +348,6 @@ class CodeFinder:
348
348
  def what_does_function_call(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
349
349
  """Find what functions a specific function calls using CALLS relationships"""
350
350
  with self.driver.session() as session:
351
- repo_filter = "AND called.path STARTS WITH $repo_path" if repo_path else ""
352
351
  if path:
353
352
  # Convert path to absolute path
354
353
  absolute_file_path = str(Path(path).resolve())
@@ -450,16 +449,16 @@ class CodeFinder:
450
449
  def find_class_hierarchy(self, class_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
451
450
  """Find class inheritance relationships using INHERITS relationships"""
452
451
  with self.driver.session() as session:
453
- repo_filter = "WHERE 1=1 AND parent.path STARTS WITH $repo_path" if repo_path else ""
452
+ repo_filter = "AND parent.path STARTS WITH $repo_path" if repo_path else ""
454
453
  if path:
455
454
  match_clause = "MATCH (child:Class {name: $class_name, path: $path})"
456
455
  else:
457
456
  match_clause = "MATCH (child:Class {name: $class_name})"
458
457
 
459
458
  parents_query = f"""
460
- {{match_clause}}
459
+ {match_clause}
461
460
  MATCH (child)-[:INHERITS]->(parent:Class)
462
- WHERE 1=1 {{repo_filter}}
461
+ WHERE 1=1 {repo_filter}
463
462
  OPTIONAL MATCH (parent_file:File)-[:CONTAINS]->(parent)
464
463
  RETURN DISTINCT
465
464
  parent.name as parent_class,
@@ -471,11 +470,11 @@ class CodeFinder:
471
470
  """
472
471
  parents_result = session.run(parents_query, class_name=class_name, path=path, repo_path=repo_path)
473
472
 
474
- repo_filter_child = "WHERE 1=1 AND grandchild.path STARTS WITH $repo_path" if repo_path else ""
473
+ repo_filter_child = "AND grandchild.path STARTS WITH $repo_path" if repo_path else ""
475
474
  children_query = f"""
476
- {{match_clause}}
475
+ {match_clause}
477
476
  MATCH (grandchild:Class)-[:INHERITS]->(child)
478
- WHERE 1=1 {{repo_filter_child}}
477
+ WHERE 1=1 {repo_filter_child}
479
478
  OPTIONAL MATCH (child_file:File)-[:CONTAINS]->(grandchild)
480
479
  RETURN DISTINCT
481
480
  grandchild.name as child_class,
@@ -513,10 +512,10 @@ class CodeFinder:
513
512
  def find_function_overrides(self, function_name: str, repo_path: Optional[str] = None) -> List[Dict]:
514
513
  """Find all implementations of a function across different classes"""
515
514
  with self.driver.session() as session:
516
- repo_filter = "WHERE 1=1 AND class.path STARTS WITH $repo_path" if repo_path else ""
515
+ repo_filter = "AND class.path STARTS WITH $repo_path" if repo_path else ""
517
516
  result = session.run(f"""
518
517
  MATCH (class:Class)-[:CONTAINS]->(func:Function {{name: $function_name}})
519
- WHERE 1=1 {{repo_filter}}
518
+ WHERE 1=1 {repo_filter}
520
519
  OPTIONAL MATCH (file:File)-[:CONTAINS]->(class)
521
520
  RETURN DISTINCT
522
521
  class.name as class_name,
@@ -821,69 +820,69 @@ class CodeFinder:
821
820
  "instances": variable_instances.data()
822
821
  }
823
822
 
824
- def analyze_code_relationships(self, query_type: str, target: str, context: Optional[str] = None) -> Dict[str, Any]:
823
+ def analyze_code_relationships(self, query_type: str, target: str, context: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
825
824
  """Main method to analyze different types of code relationships with fixed return types"""
826
825
  query_type = query_type.lower().strip()
827
826
 
828
827
  try:
829
828
  if query_type == "find_callers":
830
- results = self.who_calls_function(target, context)
829
+ results = self.who_calls_function(target, context, repo_path=repo_path)
831
830
  return {
832
831
  "query_type": "find_callers", "target": target, "context": context, "results": results,
833
832
  "summary": f"Found {len(results)} functions that call '{target}'"
834
833
  }
835
834
 
836
835
  elif query_type == "find_callees":
837
- results = self.what_does_function_call(target, context)
836
+ results = self.what_does_function_call(target, context, repo_path=repo_path)
838
837
  return {
839
838
  "query_type": "find_callees", "target": target, "context": context, "results": results,
840
839
  "summary": f"Function '{target}' calls {len(results)} other functions"
841
840
  }
842
841
 
843
842
  elif query_type == "find_importers":
844
- results = self.who_imports_module(target)
843
+ results = self.who_imports_module(target, repo_path=repo_path)
845
844
  return {
846
845
  "query_type": "find_importers", "target": target, "results": results,
847
846
  "summary": f"Found {len(results)} files that import '{target}'"
848
847
  }
849
848
 
850
849
  elif query_type == "find_functions_by_argument":
851
- results = self.find_functions_by_argument(target, context)
850
+ results = self.find_functions_by_argument(target, context, repo_path=repo_path)
852
851
  return {
853
852
  "query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
854
853
  "summary": f"Found {len(results)} functions that take '{target}' as an argument"
855
854
  }
856
855
 
857
856
  elif query_type == "find_functions_by_decorator":
858
- results = self.find_functions_by_decorator(target, context)
857
+ results = self.find_functions_by_decorator(target, context, repo_path=repo_path)
859
858
  return {
860
859
  "query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
861
860
  "summary": f"Found {len(results)} functions decorated with '{target}'"
862
861
  }
863
862
 
864
863
  elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
865
- results = self.who_modifies_variable(target)
864
+ results = self.who_modifies_variable(target, repo_path=repo_path)
866
865
  return {
867
866
  "query_type": "who_modifies", "target": target, "results": results,
868
867
  "summary": f"Found {len(results)} containers that hold variable '{target}'"
869
868
  }
870
869
 
871
870
  elif query_type in ["class_hierarchy", "inheritance", "extends"]:
872
- results = self.find_class_hierarchy(target, context)
871
+ results = self.find_class_hierarchy(target, context, repo_path=repo_path)
873
872
  return {
874
873
  "query_type": "class_hierarchy", "target": target, "results": results,
875
874
  "summary": f"Class '{target}' has {len(results['parent_classes'])} parents, {len(results['child_classes'])} children, and {len(results['methods'])} methods"
876
875
  }
877
876
 
878
877
  elif query_type in ["overrides", "implementations", "polymorphism"]:
879
- results = self.find_function_overrides(target)
878
+ results = self.find_function_overrides(target, repo_path=repo_path)
880
879
  return {
881
880
  "query_type": "overrides", "target": target, "results": results,
882
881
  "summary": f"Found {len(results)} implementations of function '{target}'"
883
882
  }
884
883
 
885
884
  elif query_type in ["dead_code", "unused", "unreachable"]:
886
- results = self.find_dead_code()
885
+ results = self.find_dead_code(repo_path=repo_path)
887
886
  return {
888
887
  "query_type": "dead_code", "results": results,
889
888
  "summary": f"Found {len(results['potentially_unused_functions'])} potentially unused functions"
@@ -891,21 +890,21 @@ class CodeFinder:
891
890
 
892
891
  elif query_type == "find_complexity":
893
892
  limit = int(context) if context and context.isdigit() else 10
894
- results = self.find_most_complex_functions(limit)
893
+ results = self.find_most_complex_functions(limit, repo_path=repo_path)
895
894
  return {
896
895
  "query_type": "find_complexity", "limit": limit, "results": results,
897
896
  "summary": f"Found the top {len(results)} most complex functions"
898
897
  }
899
898
 
900
899
  elif query_type == "find_all_callers":
901
- results = self.find_all_callers(target, context)
900
+ results = self.find_all_callers(target, context, repo_path=repo_path)
902
901
  return {
903
902
  "query_type": "find_all_callers", "target": target, "context": context, "results": results,
904
903
  "summary": f"Found {len(results)} direct and indirect callers of '{target}'"
905
904
  }
906
905
 
907
906
  elif query_type == "find_all_callees":
908
- results = self.find_all_callees(target, context)
907
+ results = self.find_all_callees(target, context, repo_path=repo_path)
909
908
  return {
910
909
  "query_type": "find_all_callees", "target": target, "context": context, "results": results,
911
910
  "summary": f"Found {len(results)} direct and indirect callees of '{target}'"
@@ -916,7 +915,7 @@ class CodeFinder:
916
915
  start_func, end_func = target.split('->', 1)
917
916
  # max_depth can be passed as context, default to 5 if not provided or invalid
918
917
  max_depth = int(context) if context and context.isdigit() else 5
919
- results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth)
918
+ results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth, repo_path=repo_path)
920
919
  return {
921
920
  "query_type": "call_chain", "target": target, "results": results,
922
921
  "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
@@ -928,14 +927,14 @@ class CodeFinder:
928
927
  }
929
928
 
930
929
  elif query_type in ["module_deps", "module_dependencies", "module_usage"]:
931
- results = self.find_module_dependencies(target)
930
+ results = self.find_module_dependencies(target, repo_path=repo_path)
932
931
  return {
933
932
  "query_type": "module_dependencies", "target": target, "results": results,
934
933
  "summary": f"Module '{target}' is imported by {len(results['imported_by_files'])} files"
935
934
  }
936
935
 
937
936
  elif query_type in ["variable_scope", "var_scope", "variable_usage_scope"]:
938
- results = self.find_variable_usage_scope(target)
937
+ results = self.find_variable_usage_scope(target, repo_path=repo_path)
939
938
  return {
940
939
  "query_type": "variable_scope", "target": target, "results": results,
941
940
  "summary": f"Variable '{target}' has {len(results['instances'])} instances across different scopes"
@@ -14,7 +14,20 @@ from ..utils.debug_log import debug_log, info_logger, error_logger, warning_logg
14
14
  from tree_sitter import Language, Parser
15
15
  from ..utils.tree_sitter_manager import get_tree_sitter_manager
16
16
  from ..cli.config_manager import get_config_value
17
-
17
+ import fnmatch
18
+
19
+ DEFAULT_IGNORE_PATTERNS = [
20
+ "*.png",
21
+ "*.jpg",
22
+ "*.jpeg",
23
+ "*.gif",
24
+ "*.svg",
25
+ "*.mp4",
26
+ "*.mp3",
27
+ "*.zip",
28
+ "*.tar",
29
+ "*.gz",
30
+ ]
18
31
 
19
32
  class TreeSitterParser:
20
33
  """A generic parser wrapper for a specific language using tree-sitter."""
@@ -181,6 +194,38 @@ class GraphBuilder:
181
194
  except Exception as e:
182
195
  warning_logger(f"Schema creation warning: {e}")
183
196
 
197
+ @staticmethod
198
+ def _sanitize_props(props: Dict) -> Dict:
199
+ """Return a copy of *props* with all values coerced to database-safe types.
200
+
201
+ FalkorDB and KùzuDB only accept node properties that are primitives
202
+ (str, int, float, bool, None) or flat lists of primitives. Complex
203
+ values such as tuples, dicts, or lists-of-dicts that come from language
204
+ parsers (e.g. C's ``detailed_args`` or Scala's tuple ``class_context``)
205
+ are serialized to a JSON string so the data is preserved rather than
206
+ being silently dropped.
207
+ """
208
+ import json
209
+
210
+ def _is_primitive(v):
211
+ return isinstance(v, (str, int, float, bool)) or v is None
212
+
213
+ def _is_flat_list(v):
214
+ return isinstance(v, list) and all(_is_primitive(item) for item in v)
215
+
216
+ def _coerce(v):
217
+ if _is_primitive(v):
218
+ return v
219
+ if _is_flat_list(v):
220
+ return v
221
+ # Tuples, dicts, lists-of-dicts, nested structures → JSON string
222
+ try:
223
+ return json.dumps(v, default=str)
224
+ except Exception:
225
+ return str(v)
226
+
227
+ return {k: _coerce(v) for k, v in props.items()}
228
+
184
229
 
185
230
  def _pre_scan_for_imports(self, files: list[Path]) -> dict:
186
231
  """Dispatches pre-scan to the correct language-specific implementation."""
@@ -381,7 +426,12 @@ class GraphBuilder:
381
426
  MERGE (f)-[:CONTAINS]->(n)
382
427
  """
383
428
 
384
- session.run(query, path=file_path_str, name=item['name'], line_number=item['line_number'], props=item)
429
+ # Strip non-primitive fields (dicts, tuples, lists-of-dicts)
430
+ # before writing to the database to avoid runtime errors such as
431
+ # "Property values can only be of primitive types or arrays of
432
+ # primitive types" raised by FalkorDB / KùzuDB.
433
+ safe_props = self._sanitize_props(item)
434
+ session.run(query, path=file_path_str, name=item['name'], line_number=item['line_number'], props=safe_props)
385
435
 
386
436
  if label == 'Function':
387
437
  for arg_name in item.get('args', []):
@@ -1231,16 +1281,35 @@ class GraphBuilder:
1231
1281
  break
1232
1282
  curr = curr.parent
1233
1283
 
1284
+ spec = None
1234
1285
  if cgcignore_path:
1235
1286
  with open(cgcignore_path) as f:
1236
- ignore_patterns = f.read().splitlines()
1287
+ user_patterns = [line.strip() for line in f.read().splitlines() if line.strip() and not line.strip().startswith('#')]
1288
+ ignore_patterns = DEFAULT_IGNORE_PATTERNS + user_patterns
1237
1289
  spec = pathspec.PathSpec.from_lines('gitwildmatch', ignore_patterns)
1238
1290
  else:
1239
- spec = None
1291
+ # No .cgcignore found — create one in the project root with default patterns
1292
+ # so the user can see and customize what's being ignored
1293
+ project_root = path.resolve() if path.is_dir() else path.resolve().parent
1294
+ new_cgcignore = project_root / ".cgcignore"
1295
+ try:
1296
+ cgcignore_content = "# Auto-generated by CodeGraphContext\n"
1297
+ cgcignore_content += "# Default ignore patterns for binary/media files\n"
1298
+ cgcignore_content += "# Add your own patterns below\n\n"
1299
+ cgcignore_content += "\n".join(DEFAULT_IGNORE_PATTERNS) + "\n"
1300
+ new_cgcignore.write_text(cgcignore_content)
1301
+ info_logger(f"Created default .cgcignore at {new_cgcignore}")
1302
+ except OSError as e:
1303
+ warning_logger(f"Could not create .cgcignore at {new_cgcignore}: {e}")
1304
+ spec = pathspec.PathSpec.from_lines('gitwildmatch', DEFAULT_IGNORE_PATTERNS)
1240
1305
 
1241
1306
  supported_extensions = self.parsers.keys()
1242
1307
  all_files = path.rglob("*") if path.is_dir() else [path]
1243
- files = [f for f in all_files if f.is_file() and f.suffix in supported_extensions]
1308
+
1309
+ # Previously only files with supported extensions were indexed.
1310
+ # Updated to include all files so that unsupported file types
1311
+ # can still be represented as minimal File nodes in the graph.
1312
+ files = [f for f in all_files if f.is_file()]
1244
1313
 
1245
1314
  # Filter default ignored directories
1246
1315
  ignore_dirs_str = get_config_value("IGNORE_DIRS") or ""
@@ -1291,10 +1360,28 @@ class GraphBuilder:
1291
1360
  self.job_manager.update_job(job_id, current_file=str(file))
1292
1361
  repo_path = path.resolve() if path.is_dir() else file.parent.resolve()
1293
1362
  file_data = self.parse_file(repo_path, file, is_dependency)
1363
+ # Previously only files with supported extensions were indexed.
1364
+ # Updated to include all files so that unsupported file types
1365
+ # can still be represented as minimal File nodes in the graph.
1294
1366
  if "error" not in file_data:
1295
- self.add_file_to_graph(file_data, repo_name, imports_map)
1367
+ try:
1368
+ self.add_file_to_graph(file_data, repo_name, imports_map)
1369
+ except Exception as file_err:
1370
+ # Re-raise with the offending file path so the user
1371
+ # can identify which source file triggered the error.
1372
+ raise RuntimeError(
1373
+ f"{file_err} (while indexing file: {file})"
1374
+ ) from file_err
1296
1375
  all_file_data.append(file_data)
1376
+
1377
+ # Previously only files with supported extensions were indexed.
1378
+ # Updated to include all files so that unsupported file types
1379
+ # can still be represented as minimal File nodes in the graph.
1380
+ else:
1381
+ # create minimal node if parser not available
1382
+ self.add_minimal_file_node(file, repo_path, is_dependency)
1297
1383
  processed_count += 1
1384
+
1298
1385
  if job_id:
1299
1386
  self.job_manager.update_job(job_id, processed_files=processed_count)
1300
1387
  await asyncio.sleep(0.01)
@@ -1318,3 +1405,67 @@ class GraphBuilder:
1318
1405
  self.job_manager.update_job(
1319
1406
  job_id, status=status, end_time=datetime.now(), errors=[str(e)]
1320
1407
  )
1408
+
1409
+ # Create a minimal File node for unsupported file types.
1410
+ # These files do not contain parsed entities but should still
1411
+ # appear in the repository graph as requested in issue #707.
1412
+ def add_minimal_file_node(self, file_path: Path, repo_path: Path, is_dependency: bool = False):
1413
+
1414
+ file_path_str = str(file_path.resolve())
1415
+ file_name = file_path.name
1416
+ repo_name = repo_path.name
1417
+ repo_path_str = str(repo_path.resolve())
1418
+
1419
+ with self.driver.session() as session:
1420
+
1421
+ session.run(
1422
+ """
1423
+ MERGE (r:Repository {path: $repo_path})
1424
+ SET r.name = $repo_name
1425
+ """,
1426
+ repo_path=repo_path_str,
1427
+ repo_name=repo_name
1428
+ )
1429
+
1430
+ session.run(
1431
+ """
1432
+ MERGE (f:File {path: $file_path})
1433
+ SET f.name = $file_name,
1434
+ f.is_dependency = $is_dependency
1435
+ """,
1436
+ file_path=file_path_str,
1437
+ file_name=file_name,
1438
+ is_dependency=is_dependency
1439
+ )
1440
+
1441
+ # Establish directory structure
1442
+ file_path_obj = Path(file_path_str)
1443
+ repo_path_obj = Path(repo_path_str)
1444
+ try:
1445
+ relative_path_to_file = file_path_obj.relative_to(repo_path_obj)
1446
+ except ValueError:
1447
+ # Fallback if not relative
1448
+ relative_path_to_file = Path(file_path_obj.name)
1449
+
1450
+ parent_path = repo_path_str
1451
+ parent_label = 'Repository'
1452
+
1453
+ for part in relative_path_to_file.parts[:-1]:
1454
+ current_path = Path(parent_path) / part
1455
+ current_path_str = str(current_path)
1456
+
1457
+ session.run(f"""
1458
+ MATCH (p:{parent_label} {{path: $parent_path}})
1459
+ MERGE (d:Directory {{path: $current_path}})
1460
+ SET d.name = $part
1461
+ MERGE (p)-[:CONTAINS]->(d)
1462
+ """, parent_path=parent_path, current_path=current_path_str, part=part)
1463
+
1464
+ parent_path = current_path_str
1465
+ parent_label = 'Directory'
1466
+
1467
+ session.run(f"""
1468
+ MATCH (p:{parent_label} {{path: $parent_path}})
1469
+ MATCH (f:File {{path: $file_path}})
1470
+ MERGE (p)-[:CONTAINS]->(f)
1471
+ """, parent_path=parent_path, file_path=file_path_str)
@@ -1,5 +1,5 @@
1
1
  # src/codegraphcontext/tools/package_resolver.py
2
- import importlib
2
+ import importlib.util
3
3
  import stdlibs
4
4
  from pathlib import Path
5
5
  import subprocess
@@ -10,25 +10,27 @@ from ..utils.debug_log import debug_log
10
10
  def _get_python_package_path(package_name: str) -> Optional[str]:
11
11
  """
12
12
  Finds the local installation path of a Python package.
13
+ Uses importlib.util.find_spec() to locate the module without executing its code.
13
14
  """
14
15
  try:
15
16
  debug_log(f"Getting local path for Python package: {package_name}")
16
- module = importlib.import_module(package_name)
17
- if hasattr(module, '__file__') and module.__file__:
18
- module_file = Path(module.__file__)
17
+ spec = importlib.util.find_spec(package_name)
18
+ if spec is None:
19
+ return None
20
+ if spec.origin and spec.origin != "frozen":
21
+ module_file = Path(spec.origin)
19
22
  if module_file.name == '__init__.py':
20
23
  return str(module_file.parent)
21
24
  elif package_name in stdlibs.module_names:
22
25
  return str(module_file)
23
26
  else:
24
27
  return str(module_file.parent)
25
- elif hasattr(module, '__path__'):
26
- if isinstance(module.__path__, list) and module.__path__:
27
- return str(Path(module.__path__[0]))
28
- else:
29
- return str(Path(str(module.__path__)))
28
+ elif spec.submodule_search_locations:
29
+ locations = list(spec.submodule_search_locations)
30
+ if locations:
31
+ return str(Path(locations[0]))
30
32
  return None
31
- except ImportError:
33
+ except (ModuleNotFoundError, ValueError):
32
34
  return None
33
35
  except Exception as e:
34
36
  debug_log(f"Error getting local path for {package_name}: {e}")
@@ -0,0 +1,178 @@
1
+
2
+ from fastapi import FastAPI, HTTPException, Query, Request
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import HTMLResponse, FileResponse
6
+ from pathlib import Path
7
+ import uvicorn
8
+ import json
9
+ import os
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):
36
+ if not db_manager:
37
+ raise HTTPException(status_code=500, detail="Database not initialized")
38
+
39
+ def get_eid(element):
40
+ if not element: return None
41
+ if isinstance(element, (int, str)):
42
+ return str(element)
43
+ # Try various ways to get ID (Neo4j, FalkorDB, etc.)
44
+ for attr in ['element_id', 'id', '_id']:
45
+ if hasattr(element, attr):
46
+ val = getattr(element, attr)
47
+ if val is not None: return str(val)
48
+ return str(id(element))
49
+
50
+ try:
51
+ nodes_dict = {}
52
+ edges = []
53
+
54
+ with db_manager.get_driver().session() as session:
55
+ if repo_path:
56
+ repo_path = str(Path(repo_path).resolve())
57
+ # Optimized subgraph query
58
+ query = """
59
+ MATCH (r:Repository {path: $repo_path})
60
+ OPTIONAL MATCH (r)-[:CONTAINS*0..]->(n)
61
+ WITH DISTINCT n
62
+ WHERE n IS NOT NULL
63
+ OPTIONAL MATCH (n)-[rel]->(m)
64
+ RETURN n, rel, m
65
+ """
66
+ result = session.run(query, repo_path=repo_path)
67
+ else:
68
+ query = "MATCH (n) OPTIONAL MATCH (n)-[rel]->(m) RETURN n, rel, m LIMIT 5000"
69
+ result = session.run(query)
70
+
71
+ for record in result:
72
+ for key in ['n', 'm']:
73
+ node = record[key]
74
+ if node:
75
+ eid = get_eid(node)
76
+ if eid not in nodes_dict:
77
+ # FalkorDB / Neo4j labels compatibility
78
+ labels = []
79
+ if hasattr(node, 'labels'):
80
+ labels = list(node.labels)
81
+
82
+ # FalkorDB / Neo4j properties compatibility
83
+ props = {}
84
+ if hasattr(node, 'properties'):
85
+ props = node.properties
86
+ elif hasattr(node, 'items'):
87
+ props = dict(node.items())
88
+
89
+ nodes_dict[eid] = {
90
+ "id": eid,
91
+ "label": props.get('name', props.get('label', 'Unknown')),
92
+ "type": labels[0].lower() if labels else "default",
93
+ "file": props.get('path', ''),
94
+ "properties": props
95
+ }
96
+
97
+ rel = record['rel']
98
+ if rel:
99
+ rid = get_eid(rel)
100
+
101
+ # FalkorDB / Neo4j compatibility for source/target nodes
102
+ start_node = getattr(rel, 'start_node', getattr(rel, 'src_node', None))
103
+ end_node = getattr(rel, 'end_node', getattr(rel, 'dest_node', None))
104
+
105
+ source = get_eid(start_node)
106
+ target = get_eid(end_node)
107
+
108
+ if source and target:
109
+ # relationship type/relation
110
+ rel_type = "related"
111
+ if hasattr(rel, 'type'):
112
+ rel_type = rel.type
113
+ elif hasattr(rel, 'relation'):
114
+ rel_type = rel.relation
115
+
116
+ edges.append({
117
+ "id": rid,
118
+ "source": source,
119
+ "target": target,
120
+ "type": str(rel_type).lower()
121
+ })
122
+
123
+ return {
124
+ "nodes": list(nodes_dict.values()),
125
+ "edges": edges,
126
+ "files": {}
127
+ }
128
+
129
+ except Exception as e:
130
+ debug_log(f"Error fetching graph: {str(e)}")
131
+ # Print stack trace for debugging if possible
132
+ import traceback
133
+ traceback.print_exc()
134
+ raise HTTPException(status_code=500, detail=str(e))
135
+
136
+ @app.get("/api/file")
137
+ async def get_file(path: str):
138
+ file_path = Path(path)
139
+ if not file_path.exists():
140
+ raise HTTPException(status_code=404, detail="File not found")
141
+
142
+ try:
143
+ with open(file_path, "r", encoding="utf-8") as f:
144
+ return {"content": f.read()}
145
+ except Exception as e:
146
+ raise HTTPException(status_code=500, detail=str(e))
147
+
148
+ # SPA fallback handler
149
+ @app.get("/{full_path:path}")
150
+ async def spa_fallback(request: Request, full_path: str):
151
+ global _static_dir
152
+ if not _static_dir:
153
+ return HTMLResponse("Static directory not configured", status_code=500)
154
+
155
+ # Filesystem path
156
+ file_path = Path(_static_dir) / full_path
157
+
158
+ # If the file exists and is a file, serve it normally (handled by StaticFiles usually,
159
+ # but we need this for routes that don't match StaticFiles mount)
160
+ if file_path.exists() and file_path.is_file():
161
+ return FileResponse(file_path)
162
+
163
+ # Otherwise serve index.html
164
+ index_path = Path(_static_dir) / "index.html"
165
+ if index_path.exists():
166
+ return FileResponse(index_path)
167
+
168
+ return HTMLResponse("Not Found", status_code=404)
169
+
170
+ def run_server(host: str = "127.0.0.1", port: int = 8000, static_dir: Optional[str] = None):
171
+ global _static_dir
172
+ _static_dir = static_dir
173
+ if static_dir:
174
+ # Mount API first
175
+ # We don't mount "/" with StaticFiles because we use spa_fallback for all routes
176
+ pass
177
+
178
+ 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.0
3
+ Version: 0.3.2
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
@@ -46,12 +46,13 @@ 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
48
  Requires-Dist: pyyaml
49
- Requires-Dist: pytest
50
49
  Requires-Dist: nbformat
51
50
  Requires-Dist: nbconvert>=7.16.6
52
51
  Requires-Dist: pathspec>=0.12.1
53
52
  Requires-Dist: falkordb>=0.1.0
54
53
  Requires-Dist: falkordblite>=0.1.0; sys_platform != "win32" and python_version >= "3.12"
54
+ Requires-Dist: fastapi>=0.100.0
55
+ Requires-Dist: uvicorn>=0.22.0
55
56
  Provides-Extra: parsing
56
57
  Requires-Dist: tree-sitter>=0.21.0; extra == "parsing"
57
58
  Requires-Dist: tree-sitter-language-pack>=0.6.0; extra == "parsing"
@@ -63,6 +64,18 @@ Dynamic: license-file
63
64
 
64
65
  # 🏗️ CodeGraphContext (CGC)
65
66
 
67
+ **Turn code repositories into a queryable graph for AI agents.**
68
+
69
+ 🌐 **Languages:**
70
+ - 🇬🇧 [English](README.md)
71
+ - 🇨🇳 [中文](README.zh-CN.md)
72
+ - 🇰🇷 [한국어](README.kor.md)
73
+ - 🇯🇵 日本語 (Soon)
74
+ - 🇷🇺 Русский (Soon)
75
+ - 🇪🇸 Español (Soon)
76
+
77
+ 🌍 **Help translate CodeGraphContext to your language by raising an issue & PR on https://github.com/Shashankss1205/CodeGraphContext/issues!**
78
+
66
79
  <p align="center">
67
80
  <br>
68
81
  <b>Bridge the gap between deep code graphs and AI context.</b>
@@ -147,7 +160,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
147
160
  ---
148
161
 
149
162
  ## Project Details
150
- - **Version:** 0.3.0
163
+ - **Version:** 0.3.2
151
164
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
152
165
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
153
166
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -352,7 +365,7 @@ cgc watch .
352
365
  cgc help
353
366
  ```
354
367
 
355
- **See the full [CLI Commands Guide](CLI_Commands.md) for all available commands and usage scenarios.**
368
+ **See the full [CLI Commands Guide](docs/CLI_COMPLETE_REFERENCE.md) for all available commands and usage scenarios.**
356
369
 
357
370
  ### 🎨 Premium Interactive Visualization
358
371
  CodeGraphContext can generate stunning, interactive knowledge graphs of your code. Unlike static diagrams, these are premium web-based explorers:
@@ -4,16 +4,16 @@ codegraphcontext/prompts.py,sha256=E5P55paM0oHfBcNVfxkxpXRGZnya1kr_mKDg9i49FwM,6
4
4
  codegraphcontext/server.py,sha256=gcb6V4x0Oh8haBOCm2WBjTCO9FDukrSalAMMApL-oc8,12806
5
5
  codegraphcontext/tool_definitions.py,sha256=_0ahezQSURX_ewWydT2sFcvC6OFGdMoPA7KwgWNVcks,11482
6
6
  codegraphcontext/cli/__init__.py,sha256=v6CMDVKM5d_sXn3S5nZNf0phXn0IdrnhLazUoen9k9w,38
7
- codegraphcontext/cli/cli_helpers.py,sha256=2T8eP1RmM_p2Z1XfZDv44wx3Hf01jF_b3KGoWVBG0lk,35945
7
+ codegraphcontext/cli/cli_helpers.py,sha256=zlcPv0cReflgTpTM2Nj0jCB3pHvVRCqfvyAM-hCEm6c,36598
8
8
  codegraphcontext/cli/config_manager.py,sha256=MK7GMGZ4hd8-ZOShXBd_KDtNqHQ3ngdbef5KM_naPrQ,15899
9
- codegraphcontext/cli/main.py,sha256=PSJ5BxQhbdNdCWWyYvW9-6COCiw852iVi-zhP43nYPA,86328
9
+ codegraphcontext/cli/main.py,sha256=D-nlcT90ah3rHQ_h7TIq451EA4WN_HxOmXKm9EY2Hnw,86497
10
10
  codegraphcontext/cli/registry_commands.py,sha256=30rJm4SeS0n1jax4JVuhuv4zYLzyMmlHcCSnsDDhgc8,19964
11
11
  codegraphcontext/cli/setup_macos.py,sha256=Xjlv_9jk9qv8Gh7stpH1pvlalzC0Fg176y7jc5G1zh0,3575
12
12
  codegraphcontext/cli/setup_wizard.py,sha256=yVXKqvAtUM0UC4tUreiS6C5utJbBO5Et2MiPUPWit9s,41393
13
13
  codegraphcontext/cli/visualizer.py,sha256=rZOLPx44wlaxVXGLR6uOaiL2GaQvxEnpyDQ4tjRDVC8,45750
14
14
  codegraphcontext/core/__init__.py,sha256=r-dC2b8hn5e4ZBVfWnCcBYMTQVcayxG5-eA5Do6uSM4,7278
15
15
  codegraphcontext/core/bundle_registry.py,sha256=kLvTLEl99QwfyAw8a7-4hqconLCtS64G9ECPVdfVBsM,7476
16
- codegraphcontext/core/cgc_bundle.py,sha256=YHL_uamATXYVweh5qbg9TQjT1lKlt69gXyF-xKKk67c,31353
16
+ codegraphcontext/core/cgc_bundle.py,sha256=lxaImBnEJMiscrGssLTgLaeEa7tY33xztjbxOhr6O5U,31686
17
17
  codegraphcontext/core/database.py,sha256=2UL8AyE6vX5mVHg1zcD74TYEREmddhKzN7Bjy2THQwM,11435
18
18
  codegraphcontext/core/database_falkordb.py,sha256=IKRcUwqdCR92ymX9rNAQSs49RWPxug_78utwePEcliQ,18842
19
19
  codegraphcontext/core/database_falkordb_remote.py,sha256=aMiuJTB_-2rBUpbAmX8ElZiZrYZbOXK-On3ncgMCyhY,7118
@@ -23,9 +23,9 @@ codegraphcontext/core/jobs.py,sha256=d6v_IERdEcDlIsz3CW3p0QMp3N-6dgQICs3FZRPgPgU
23
23
  codegraphcontext/core/watcher.py,sha256=42AWDv7LUE5v_HlY_Rvvlk8deDdlUBvM3hI-DMDWAOk,9813
24
24
  codegraphcontext/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  codegraphcontext/tools/advanced_language_query_tool.py,sha256=APtC-KSkXXSrVLwiw3kgi6Q-xaqXl6skLWHPyuNh358,3670
26
- codegraphcontext/tools/code_finder.py,sha256=KFBBRaWfviix2myDl9qJfNxfeBl5amG5QNsKYmqCjog,55343
27
- codegraphcontext/tools/graph_builder.py,sha256=7wFLzW6S0Ria7oSfpma9KKHA-uEGjuCMz3lnq7MCTR0,72017
28
- codegraphcontext/tools/package_resolver.py,sha256=b8ksgCxR-rHcVCPTkzmjOYNuJvZGGsCDQtUqMuVKhzo,18613
26
+ codegraphcontext/tools/code_finder.py,sha256=2wd05dMrPW8xZDBUixS4BaipNbrE4rttqSD6JYEeQM4,55552
27
+ codegraphcontext/tools/graph_builder.py,sha256=dUIJKRZRlC0WHINNOqHLmw20I28cf_ePti-NcGEEcnE,78728
28
+ codegraphcontext/tools/package_resolver.py,sha256=KtbdMReTezszjdsqYniL-Xb-QUsrAJWtf1NSiyIPkLI,18704
29
29
  codegraphcontext/tools/scip_indexer.py,sha256=gxe5-f090wonHEWYDT1CV6b3SpSTQtwng6A1SfPX_LA,19970
30
30
  codegraphcontext/tools/scip_pb2.py,sha256=dwOMNKlu6VyLq5h8kTPZRDqxrwfVL8yw7I7ziokFG48,98229
31
31
  codegraphcontext/tools/system.py,sha256=DGeavZoPxzV78wwApV4f7fdBFQRa8oeOSv5wprHSjRE,5733
@@ -71,9 +71,10 @@ codegraphcontext/tools/query_tool_languages/typescript_toolkit.py,sha256=3S4hpmO
71
71
  codegraphcontext/utils/debug_log.py,sha256=Qg7jwyeg7x2h3Ur_2S34bdMCkHdlk_ngHfPwa97A9vE,2836
72
72
  codegraphcontext/utils/tree_sitter_manager.py,sha256=bIuKYN1aj1Zi6BnksGjZSLZzgBwuFRselZzyUpbToAU,9180
73
73
  codegraphcontext/utils/visualize_graph.py,sha256=Ntq8l8SvAOvsOp19QKByjEwU7-5rPa_XfcGLOrUehq4,5003
74
- codegraphcontext-0.3.0.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
75
- codegraphcontext-0.3.0.dist-info/METADATA,sha256=FH-FTUU-gexxDfZePS6rqliF6540hUmv3ARwXJ_qO-I,21093
76
- codegraphcontext-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
77
- codegraphcontext-0.3.0.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
78
- codegraphcontext-0.3.0.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
79
- codegraphcontext-0.3.0.dist-info/RECORD,,
74
+ codegraphcontext/viz/server.py,sha256=KiB6UARhVHGJ3ANi1R8i5RKnDVwqOULS8kKng-L-6GI,6530
75
+ codegraphcontext-0.3.2.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
76
+ codegraphcontext-0.3.2.dist-info/METADATA,sha256=Bc0OVmQ-9RaxbL4YYE5bIGuqSDcRUbHMwtsrbxjjziY,21579
77
+ codegraphcontext-0.3.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
78
+ codegraphcontext-0.3.2.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
79
+ codegraphcontext-0.3.2.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
80
+ codegraphcontext-0.3.2.dist-info/RECORD,,