codegraphcontext 0.4.18__py3-none-any.whl → 0.5.1__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.
Files changed (61) hide show
  1. codegraphcontext/api/app.py +4 -1
  2. codegraphcontext/api/mcp_sse.py +10 -5
  3. codegraphcontext/api/router.py +3 -6
  4. codegraphcontext/cli/cli_helpers.py +30 -8
  5. codegraphcontext/cli/config_manager.py +50 -31
  6. codegraphcontext/cli/hook_manager.py +1 -1
  7. codegraphcontext/cli/main.py +92 -31
  8. codegraphcontext/cli/registry_commands.py +11 -4
  9. codegraphcontext/cli/setup_macos.py +6 -1
  10. codegraphcontext/cli/visualizer.py +25 -1
  11. codegraphcontext/core/__init__.py +71 -10
  12. codegraphcontext/core/cgc_bundle.py +1 -1
  13. codegraphcontext/core/database_falkordb.py +41 -2
  14. codegraphcontext/core/database_kuzu.py +93 -53
  15. codegraphcontext/core/database_ladybug.py +55 -23
  16. codegraphcontext/core/jobs.py +7 -2
  17. codegraphcontext/core/watcher.py +132 -275
  18. codegraphcontext/prompts.py +1 -1
  19. codegraphcontext/server.py +54 -5
  20. codegraphcontext/stdlibs.py +18 -0
  21. codegraphcontext/tool_definitions.py +188 -79
  22. codegraphcontext/tools/code_finder.py +17 -0
  23. codegraphcontext/tools/graph_builder.py +260 -8
  24. codegraphcontext/tools/handlers/analysis_handlers.py +5 -0
  25. codegraphcontext/tools/handlers/indexing_handlers.py +4 -1
  26. codegraphcontext/tools/handlers/management_handlers.py +9 -10
  27. codegraphcontext/tools/handlers/query_handlers.py +21 -3
  28. codegraphcontext/tools/handlers/watcher_handlers.py +2 -2
  29. codegraphcontext/tools/indexing/persistence/writer.py +377 -47
  30. codegraphcontext/tools/indexing/pipeline.py +22 -1
  31. codegraphcontext/tools/indexing/resolution/__init__.py +11 -1
  32. codegraphcontext/tools/indexing/resolution/calls.py +485 -53
  33. codegraphcontext/tools/indexing/resolution/inheritance.py +439 -3
  34. codegraphcontext/tools/indexing/resolution/post_resolution.py +40 -15
  35. codegraphcontext/tools/indexing/schema.py +5 -0
  36. codegraphcontext/tools/indexing/schema_contract.py +7 -0
  37. codegraphcontext/tools/languages/c.py +154 -2
  38. codegraphcontext/tools/languages/cpp.py +13 -1
  39. codegraphcontext/tools/languages/csharp.py +1 -0
  40. codegraphcontext/tools/languages/css.py +13 -4
  41. codegraphcontext/tools/languages/dart.py +170 -41
  42. codegraphcontext/tools/languages/elixir.py +9 -6
  43. codegraphcontext/tools/languages/go.py +56 -16
  44. codegraphcontext/tools/languages/haskell.py +80 -8
  45. codegraphcontext/tools/languages/html.py +23 -8
  46. codegraphcontext/tools/languages/javascript.py +2 -2
  47. codegraphcontext/tools/languages/lua.py +61 -19
  48. codegraphcontext/tools/languages/perl.py +37 -4
  49. codegraphcontext/tools/languages/php.py +56 -4
  50. codegraphcontext/tools/languages/python.py +11 -3
  51. codegraphcontext/tools/languages/rust.py +70 -6
  52. codegraphcontext/tools/languages/swift.py +61 -11
  53. codegraphcontext/tools/languages/typescript.py +63 -6
  54. codegraphcontext/tools/package_resolver.py +159 -364
  55. codegraphcontext/tools/system.py +8 -1
  56. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/METADATA +12 -6
  57. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/RECORD +61 -60
  58. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/WHEEL +0 -0
  59. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/entry_points.txt +0 -0
  60. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/licenses/LICENSE +0 -0
  61. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.1.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,10 @@ def create_app() -> FastAPI:
17
17
  app.add_middleware(
18
18
  CORSMiddleware,
19
19
  allow_origins=["*"], # In production, restrict this
20
- allow_credentials=True,
20
+ # Credentials must stay disabled while origins is a wildcard; the
21
+ # combination is rejected by browsers and would leak cookie-authed
22
+ # responses to any site.
23
+ allow_credentials=False,
21
24
  allow_methods=["*"],
22
25
  allow_headers=["*"],
23
26
  )
@@ -8,16 +8,17 @@ from mcp.types import Tool, TextContent, ServerCapabilities, ToolsCapability
8
8
  from mcp.server.sse import SseServerTransport
9
9
 
10
10
  from codegraphcontext.api.router import get_server
11
- from codegraphcontext.tool_definitions import TOOLS
11
+ from codegraphcontext.server import _strip_workspace_prefix, _apply_response_token_limit
12
12
 
13
13
  # Create the MCP Server instance using the SDK
14
14
  mcp_server = Server("CodeGraphContext")
15
15
 
16
16
  @mcp_server.list_tools()
17
17
  async def handle_list_tools() -> list[Tool]:
18
- """List available tools."""
18
+ """List available tools (honors disabledTools from mcp.json)."""
19
+ server = get_server()
19
20
  tools = []
20
- for name, defn in TOOLS.items():
21
+ for name, defn in server.tools.items():
21
22
  tools.append(Tool(
22
23
  name=name,
23
24
  description=defn["description"],
@@ -33,12 +34,16 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten
33
34
 
34
35
  # Execute via the existing handler logic
35
36
  result = await server.handle_tool_call(name, args)
37
+ result = _strip_workspace_prefix(result)
36
38
 
37
39
  if "error" in result:
38
40
  return [TextContent(type="text", text=f"Error: {result['error']}")]
39
41
 
40
- # Format result as JSON string for the AI
41
- return [TextContent(type="text", text=json.dumps(result, indent=2))]
42
+ # Format result as JSON string for the AI, with the same token budget
43
+ # the stdio transport applies.
44
+ response_text = json.dumps(result, indent=2)
45
+ response_text = _apply_response_token_limit(name, response_text)
46
+ return [TextContent(type="text", text=response_text)]
42
47
 
43
48
  # Create the SSE transport.
44
49
  # The messages_url is where the client will POST JSON-RPC messages.
@@ -91,12 +91,9 @@ async def index_repository(
91
91
  background_tasks: BackgroundTasks,
92
92
  server: MCPServer = Depends(get_server)
93
93
  ):
94
- args = {
95
- "path": request.path,
96
- "repo_name": request.repo_name,
97
- "branch": request.branch,
98
- "force": request.force
99
- }
94
+ # The add_code_to_graph handler only understands "path" (and is_dependency);
95
+ # repo_name/branch/force from the request model are not supported by it.
96
+ args = {"path": request.path}
100
97
 
101
98
  try:
102
99
  result = await server.handle_tool_call(
@@ -35,6 +35,7 @@ from .config_manager import (
35
35
  register_repo_in_context,
36
36
  ensure_first_run_bootstrap,
37
37
  ContextNotFoundError,
38
+ is_db_deletion_allowed,
38
39
  )
39
40
 
40
41
  console = Console()
@@ -673,14 +674,32 @@ def reindex_helper(path: str, context: Optional[str] = None):
673
674
  db_manager.close_driver()
674
675
 
675
676
 
676
- def update_helper(path: str, context: Optional[str] = None):
677
- """Update/refresh index for a path (alias for reindex)."""
677
+ def update_helper(path: str, context: Optional[str] = None, quiet: bool = False):
678
+ """Update/refresh index for a path (alias for reindex).
679
+
680
+ When *quiet* is True (e.g. when invoked from Git hooks with --quiet),
681
+ Rich console output, including progress rendering, is suppressed.
682
+ """
683
+ if quiet:
684
+ console.quiet = True
685
+ try:
686
+ reindex_helper(path, context)
687
+ finally:
688
+ console.quiet = False
689
+ return
678
690
  console.print("[cyan]Updating repository index...[/cyan]")
679
691
  reindex_helper(path, context)
680
692
 
681
693
 
682
694
  def clean_helper(context: Optional[str] = None):
683
695
  """Remove orphaned nodes and relationships from the database."""
696
+ if not is_db_deletion_allowed():
697
+ console.print(
698
+ "[bold red]Error:[/bold red] Database cleanup is disabled. "
699
+ "Set ALLOW_DB_DELETION=true in config to enable."
700
+ )
701
+ raise typer.Exit(code=1)
702
+
684
703
  services = _initialize_services(context)
685
704
  if not all(services[:3]):
686
705
  _fail_services_init()
@@ -738,6 +757,9 @@ def stats_helper(path: str = None, context: Optional[str] = None):
738
757
  if path:
739
758
  # Stats for specific repository
740
759
  path_obj = Path(path).resolve()
760
+ # Paths are stored with forward slashes (as_posix) in the graph DB,
761
+ # so lookups must use the same normalization on Windows too.
762
+ repo_path_str = path_obj.as_posix()
741
763
  console.print(f"[cyan]📊 Statistics for: {path_obj}[/cyan]\n")
742
764
 
743
765
  with db_manager.get_driver().session() as session:
@@ -746,7 +768,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
746
768
  MATCH (r:Repository {path: $path})
747
769
  RETURN r
748
770
  """
749
- result = session.run(repo_query, path=str(path_obj))
771
+ result = session.run(repo_query, path=repo_path_str)
750
772
  if not result.single():
751
773
  console.print(f"[red]Repository not found: {path_obj}[/red]")
752
774
  return
@@ -755,20 +777,20 @@ def stats_helper(path: str = None, context: Optional[str] = None):
755
777
  # Get stats using separate queries to handle depth and avoid Cartesian products
756
778
  # 1. Files
757
779
  file_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as c"
758
- file_count = session.run(file_query, path=str(path_obj)).single()["c"]
780
+ file_count = session.run(file_query, path=repo_path_str).single()["c"]
759
781
 
760
782
  # 2. Functions (including methods in classes)
761
783
  func_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(func:Function) RETURN count(func) as c"
762
- func_count = session.run(func_query, path=str(path_obj)).single()["c"]
784
+ func_count = session.run(func_query, path=repo_path_str).single()["c"]
763
785
 
764
786
  # 3. Classes
765
787
  class_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(c:Class) RETURN count(c) as c"
766
- class_count = session.run(class_query, path=str(path_obj)).single()["c"]
788
+ class_count = session.run(class_query, path=repo_path_str).single()["c"]
767
789
 
768
790
  # 4. Modules (imported) - Note: Module nodes are outside the repo structure usually, connected via IMPORTS
769
791
  # We need to traverse from files to modules
770
792
  module_query = "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File)-[:IMPORTS]->(m:Module) RETURN count(DISTINCT m) as c"
771
- module_count = session.run(module_query, path=str(path_obj)).single()["c"]
793
+ module_count = session.run(module_query, path=repo_path_str).single()["c"]
772
794
 
773
795
  table = Table(show_header=True, header_style="bold magenta")
774
796
  table.add_column("Metric", style="cyan")
@@ -869,7 +891,7 @@ def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional
869
891
  with code_finder.driver.session() as _s:
870
892
  _r = _s.run(
871
893
  "MATCH (n:File) WHERE n.path STARTS WITH $p RETURN count(n) AS c",
872
- p=str(path_obj) + "/"
894
+ p=path_obj.as_posix() + "/"
873
895
  )
874
896
  _count = _r.single()["c"]
875
897
  if _count > 100:
@@ -15,6 +15,19 @@ import yaml
15
15
 
16
16
  console = Console()
17
17
 
18
+
19
+ def _atomic_write_text(path: Path, content: str, *, secure: bool = False) -> None:
20
+ """Write *content* to *path* atomically (temp file + replace)."""
21
+ path.parent.mkdir(parents=True, exist_ok=True)
22
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
23
+ with open(tmp_path, "w", encoding="utf-8") as f:
24
+ f.write(content)
25
+ f.flush()
26
+ os.fsync(f.fileno())
27
+ os.replace(tmp_path, path)
28
+ if secure:
29
+ os.chmod(path, 0o600)
30
+
18
31
  # Configuration file location
19
32
  CONFIG_DIR = Path.home() / ".codegraphcontext"
20
33
  CONFIG_FILE = CONFIG_DIR / ".env"
@@ -385,30 +398,27 @@ def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
385
398
  credentials_to_write[key] = config[key]
386
399
 
387
400
  try:
388
- with open(CONFIG_FILE, "w", encoding="utf-8") as f:
389
- f.write("# CodeGraphContext Configuration\n")
390
- f.write(f"# Location: {CONFIG_FILE}\n\n")
391
-
392
- # Write database credentials first if they exist
393
- if credentials_to_write:
394
- f.write("# ===== Database Credentials =====\n")
395
- for key in sorted(DATABASE_CREDENTIAL_KEYS):
396
- if key in credentials_to_write:
397
- f.write(f"{key}={credentials_to_write[key]}\n")
398
- f.write("\n")
399
-
400
- # Write configuration settings
401
- f.write("# ===== Configuration Settings =====\n")
402
- for key, value in sorted(config.items()):
403
- # Skip database credentials (already written above)
404
- if key in DATABASE_CREDENTIAL_KEYS:
405
- continue
406
-
407
- description = CONFIG_DESCRIPTIONS.get(key, "")
408
- if description:
409
- f.write(f"# {description}\n")
410
- f.write(f"{key}={value}\n\n")
411
-
401
+ lines = [
402
+ "# CodeGraphContext Configuration",
403
+ f"# Location: {CONFIG_FILE}",
404
+ "",
405
+ ]
406
+ if credentials_to_write:
407
+ lines.append("# ===== Database Credentials =====")
408
+ for key in sorted(DATABASE_CREDENTIAL_KEYS):
409
+ if key in credentials_to_write:
410
+ lines.append(f"{key}={credentials_to_write[key]}")
411
+ lines.append("")
412
+ lines.append("# ===== Configuration Settings =====")
413
+ for key, value in sorted(config.items()):
414
+ if key in DATABASE_CREDENTIAL_KEYS:
415
+ continue
416
+ description = CONFIG_DESCRIPTIONS.get(key, "")
417
+ if description:
418
+ lines.append(f"# {description}")
419
+ lines.append(f"{key}={value}")
420
+ lines.append("")
421
+ _atomic_write_text(CONFIG_FILE, "\n".join(lines), secure=True)
412
422
  console.print(f"[green]✅ Configuration saved to {CONFIG_FILE}[/green]")
413
423
  except Exception as e:
414
424
  console.print(f"[red]Error saving config: {e}[/red]")
@@ -804,8 +814,10 @@ def save_context_config(cfg: ContextConfig) -> None:
804
814
  }
805
815
 
806
816
  try:
807
- with open(CONTEXT_CONFIG_FILE, "w", encoding="utf-8") as f:
808
- yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
817
+ _atomic_write_text(
818
+ CONTEXT_CONFIG_FILE,
819
+ yaml.dump(raw, default_flow_style=False, sort_keys=False),
820
+ )
809
821
  except Exception as e:
810
822
  console.print(f"[red]Error saving config.yaml: {e}[/red]")
811
823
 
@@ -887,10 +899,15 @@ def resolve_context(
887
899
 
888
900
  inherited_db = load_config().get("DEFAULT_DATABASE", "falkordb")
889
901
 
890
- # Copy global .env into local context for easy per-repo tweaking
902
+ # Copy global .env into local context for easy per-repo tweaking.
903
+ # Guard against the self-copy case: when cwd is the home directory,
904
+ # local_cgc resolves to CONFIG_DIR itself, so `local_cgc / ".env"` is
905
+ # CONFIG_FILE. Copying a file onto itself raises shutil.SameFileError,
906
+ # which crashes resolve_context for any session started from home.
891
907
  import shutil
892
- if CONFIG_FILE.exists():
893
- shutil.copy2(CONFIG_FILE, local_cgc / ".env")
908
+ _target_env = local_cgc / ".env"
909
+ if CONFIG_FILE.exists() and _target_env.resolve() != CONFIG_FILE.resolve():
910
+ shutil.copy2(CONFIG_FILE, _target_env)
894
911
 
895
912
  local_yaml = local_cgc / "config.yaml"
896
913
  if not local_yaml.exists():
@@ -1172,8 +1189,10 @@ def _save_workspace_mappings(mappings: Dict[str, Dict[str, str]]) -> None:
1172
1189
  raw = {}
1173
1190
  raw["workspace_mappings"] = mappings
1174
1191
  try:
1175
- with open(CONTEXT_CONFIG_FILE, "w", encoding="utf-8") as f:
1176
- yaml.dump(raw, f, default_flow_style=False, sort_keys=False)
1192
+ _atomic_write_text(
1193
+ CONTEXT_CONFIG_FILE,
1194
+ yaml.dump(raw, default_flow_style=False, sort_keys=False),
1195
+ )
1177
1196
  except Exception as e:
1178
1197
  console.print(f"[red]Error saving workspace mappings: {e}[/red]")
1179
1198
 
@@ -73,7 +73,7 @@ def install_hooks(start: Path | str | None = None, *, force: bool = False) -> Ho
73
73
  )
74
74
  hook_path.write_text(script, encoding="utf-8")
75
75
  mode = hook_path.stat().st_mode
76
- hook_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
76
+ hook_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP)
77
77
 
78
78
  _ensure_gitattributes(repo.root)
79
79
  _configure_merge_driver(repo.root)
@@ -85,8 +85,9 @@ app = typer.Typer(
85
85
  )
86
86
  console = Console(stderr=True)
87
87
 
88
- # Configure basic logging for the application.
89
- logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
88
+ # Configure basic logging for the application. Default to WARNING so CLI
89
+ # output stays clean; the root --debug flag switches this to DEBUG.
90
+ logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
90
91
 
91
92
 
92
93
  def get_version() -> str:
@@ -142,6 +143,7 @@ def mcp_start():
142
143
  # This typically happens if credentials are still not found after all checks.
143
144
  console.print(f"[bold red]Configuration Error:[/bold red] {e}")
144
145
  console.print("Please run `cgc neo4j setup` or use FalkorDB (default).")
146
+ raise typer.Exit(code=1) from e
145
147
  except KeyboardInterrupt:
146
148
  # Handle graceful shutdown on Ctrl+C.
147
149
  console.print("\n[bold yellow]Server stopped by user.[/bold yellow]")
@@ -177,8 +179,10 @@ def mcp_tools():
177
179
  except ValueError as e:
178
180
  console.print(f"[bold red]Error loading tools:[/bold red] {e}")
179
181
  console.print("Please ensure your database is configured correctly.")
182
+ raise typer.Exit(code=1) from e
180
183
  except Exception as e:
181
184
  console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
185
+ raise typer.Exit(code=1) from e
182
186
 
183
187
  # Abbreviation for mcp setup
184
188
  @app.command("m", rich_help_panel="Shortcuts")
@@ -391,7 +395,6 @@ def _load_credentials(cli_context_flag: Optional[str] = None):
391
395
  # IMPORTANT: DB-selection keys set in the shell must win over .env defaults.
392
396
  # E.g. `DEFAULT_DATABASE=falkordb cgc index …` must not be overridden by
393
397
  # DEFAULT_DATABASE=neo4j sitting in ~/.codegraphcontext/.env
394
- DB_OVERRIDE_KEYS = {"CGC_RUNTIME_DB_TYPE", "DEFAULT_DATABASE"}
395
398
  for key, value in merged_config.items():
396
399
  if value is not None: # Only set non-None values
397
400
  if key in runtime_env:
@@ -627,7 +630,7 @@ def bundle_export(
627
630
 
628
631
  services = _initialize_services(context)
629
632
  if not all(services[:3]):
630
- return
633
+ raise typer.Exit(code=1)
631
634
  db_manager, _, code_finder = services[:3]
632
635
 
633
636
  try:
@@ -697,7 +700,7 @@ def bundle_import(
697
700
 
698
701
  services = _initialize_services(context)
699
702
  if not all(services[:3]):
700
- return
703
+ raise typer.Exit(code=1)
701
704
  db_manager, graph_builder, code_finder = services[:3]
702
705
 
703
706
  try:
@@ -790,6 +793,53 @@ def bundle_load(
790
793
  console.print("[dim]Use 'cgc registry list' to see available bundles[/dim]")
791
794
  raise typer.Exit(code=1)
792
795
 
796
+ @bundle_app.command("merge")
797
+ def bundle_merge(
798
+ ancestor: str = typer.Argument(..., help="Common ancestor version of the bundle (%O)"),
799
+ current: str = typer.Argument(..., help="Current branch version of the bundle (%A); also the merge result"),
800
+ other: str = typer.Argument(..., help="Other branch version of the bundle (%B)"),
801
+ ):
802
+ """
803
+ Git merge driver for .cgc bundle files.
804
+
805
+ Registered by `cgc hook install` as `merge.cgc-bundle.driver = cgc bundle
806
+ merge %O %A %B`. Git invokes it with temp files holding the ancestor (%O),
807
+ current (%A) and other (%B) versions; the merge result must be left in the
808
+ %A file and the driver must exit 0 for the merge to proceed.
809
+
810
+ Bundles are binary snapshots that cannot be merged line-by-line, so the
811
+ strategy is: if both sides are identical, accept either; otherwise keep
812
+ the current branch's version (already in the %A file) and warn that the
813
+ bundle should be regenerated with `cgc export` after the merge.
814
+ """
815
+ current_path = Path(current)
816
+ other_path = Path(other)
817
+
818
+ def _read_bytes(p: Path) -> bytes:
819
+ # A missing or empty temp file means the side has no content
820
+ # (e.g. the bundle is a new file on both branches → empty ancestor).
821
+ try:
822
+ return p.read_bytes() if p.exists() else b""
823
+ except OSError:
824
+ return b""
825
+
826
+ current_bytes = _read_bytes(current_path)
827
+ other_bytes = _read_bytes(other_path)
828
+
829
+ if current_bytes == other_bytes:
830
+ # Both branches have identical bundle contents; nothing to do.
831
+ raise typer.Exit(code=0)
832
+
833
+ # Keep the current branch's version (the %A file already contains it).
834
+ console.print(
835
+ "[yellow]⚠ Conflicting changes to a .cgc bundle were detected during merge. "
836
+ "Keeping the current branch's version.[/yellow]"
837
+ )
838
+ console.print(
839
+ "[yellow]The bundle may be stale — regenerate it with 'cgc export' after the merge.[/yellow]"
840
+ )
841
+ raise typer.Exit(code=0)
842
+
793
843
  # ============================================================================
794
844
  # HOOK COMMAND GROUP - Git integration
795
845
  # ============================================================================
@@ -1146,11 +1196,18 @@ def doctor():
1146
1196
  all_checks_passed = False
1147
1197
  elif default_db == "falkordb":
1148
1198
  try:
1149
- import falkordblite # noqa: F401
1150
- console.print(" [green]✓[/green] FalkorDB Lite is installed")
1199
+ from codegraphcontext.core import is_falkordb_usable
1200
+
1201
+ if is_falkordb_usable():
1202
+ console.print(" [green]✓[/green] FalkorDB Lite is installed")
1203
+ else:
1204
+ raise ImportError("FalkorDB Lite is not available on this platform")
1151
1205
  except ImportError:
1152
- console.print(" [yellow]⚠[/yellow] FalkorDB Lite not installed (Python 3.12+ only)")
1206
+ # falkordb is the configured/default backend, so a missing
1207
+ # FalkorDB Lite means the database cannot work — fail the check.
1208
+ console.print(" [red]✗[/red] FalkorDB Lite not installed (Python 3.12+ only)")
1153
1209
  console.print(" Run: pip install falkordblite")
1210
+ all_checks_passed = False
1154
1211
  else:
1155
1212
  console.print(f" [yellow]⚠[/yellow] No connectivity probe for backend '{default_db}'")
1156
1213
  except Exception as e:
@@ -1275,7 +1332,7 @@ def update(
1275
1332
  _load_credentials()
1276
1333
  if path is None:
1277
1334
  path = str(Path.cwd())
1278
- update_helper(path, context)
1335
+ update_helper(path, context, quiet=quiet)
1279
1336
 
1280
1337
  @app.command()
1281
1338
  def clean(
@@ -1344,7 +1401,7 @@ def delete(
1344
1401
  # Delete all repositories
1345
1402
  services = _initialize_services(context)
1346
1403
  if not all(services[:3]):
1347
- return
1404
+ raise typer.Exit(code=1)
1348
1405
  db_manager, graph_builder, code_finder = services[:3]
1349
1406
 
1350
1407
  try:
@@ -1409,7 +1466,14 @@ def delete(
1409
1466
  console.print("[red]Error: Please provide a path or use --all to delete all repositories[/red]")
1410
1467
  console.print("Usage: cgc delete <path> or cgc delete --all")
1411
1468
  raise typer.Exit(code=1)
1412
-
1469
+
1470
+ if not config_manager.is_db_deletion_allowed():
1471
+ console.print(
1472
+ "[bold red]Error:[/bold red] Repository deletion is disabled. "
1473
+ "Set ALLOW_DB_DELETION=true in config to enable."
1474
+ )
1475
+ raise typer.Exit(code=1)
1476
+
1413
1477
  delete_helper(path, context)
1414
1478
 
1415
1479
 
@@ -1593,7 +1657,7 @@ def find_by_name(
1593
1657
  _load_credentials()
1594
1658
  services = _initialize_services(context)
1595
1659
  if not all(services[:3]):
1596
- return
1660
+ raise typer.Exit(code=1)
1597
1661
  db_manager, graph_builder, code_finder = services[:3]
1598
1662
 
1599
1663
  # Resolve effective fuzzy setting: CLI flag wins, else config, else true.
@@ -1725,7 +1789,7 @@ def find_by_pattern(
1725
1789
  _load_credentials()
1726
1790
  services = _initialize_services(context)
1727
1791
  if not all(services[:3]):
1728
- return
1792
+ raise typer.Exit(code=1)
1729
1793
  db_manager, graph_builder, code_finder = services[:3]
1730
1794
 
1731
1795
  try:
@@ -1818,7 +1882,7 @@ def find_by_type(
1818
1882
  _load_credentials()
1819
1883
  services = _initialize_services(context)
1820
1884
  if not all(services[:3]):
1821
- return
1885
+ raise typer.Exit(code=1)
1822
1886
  db_manager, graph_builder, code_finder = services[:3]
1823
1887
 
1824
1888
  try:
@@ -1873,7 +1937,7 @@ def find_by_variable(
1873
1937
  _load_credentials()
1874
1938
  services = _initialize_services(context)
1875
1939
  if not all(services[:3]):
1876
- return
1940
+ raise typer.Exit(code=1)
1877
1941
  db_manager, graph_builder, code_finder = services[:3]
1878
1942
 
1879
1943
  try:
@@ -1919,7 +1983,7 @@ def find_by_content_search(
1919
1983
  _load_credentials()
1920
1984
  services = _initialize_services(context)
1921
1985
  if not all(services[:3]):
1922
- return
1986
+ raise typer.Exit(code=1)
1923
1987
  db_manager, graph_builder, code_finder = services[:3]
1924
1988
 
1925
1989
  try:
@@ -1978,7 +2042,7 @@ def find_by_decorator_search(
1978
2042
  _load_credentials()
1979
2043
  services = _initialize_services(context)
1980
2044
  if not all(services[:3]):
1981
- return
2045
+ raise typer.Exit(code=1)
1982
2046
  db_manager, graph_builder, code_finder = services[:3]
1983
2047
 
1984
2048
  try:
@@ -2026,7 +2090,7 @@ def find_by_argument_search(
2026
2090
  _load_credentials()
2027
2091
  services = _initialize_services(context)
2028
2092
  if not all(services[:3]):
2029
- return
2093
+ raise typer.Exit(code=1)
2030
2094
  db_manager, graph_builder, code_finder = services[:3]
2031
2095
 
2032
2096
  try:
@@ -2082,7 +2146,7 @@ def analyze_calls(
2082
2146
  _load_credentials()
2083
2147
  services = _initialize_services(context)
2084
2148
  if not all(services[:3]):
2085
- return
2149
+ raise typer.Exit(code=1)
2086
2150
  db_manager, graph_builder, code_finder = services[:3]
2087
2151
 
2088
2152
  try:
@@ -2138,7 +2202,7 @@ def analyze_callers(
2138
2202
  _load_credentials()
2139
2203
  services = _initialize_services(context)
2140
2204
  if not all(services[:3]):
2141
- return
2205
+ raise typer.Exit(code=1)
2142
2206
  db_manager, graph_builder, code_finder = services[:3]
2143
2207
 
2144
2208
  try:
@@ -2199,7 +2263,7 @@ def analyze_chain(
2199
2263
  _load_credentials()
2200
2264
  services = _initialize_services(context)
2201
2265
  if not all(services[:3]):
2202
- return
2266
+ raise typer.Exit(code=1)
2203
2267
  db_manager, graph_builder, code_finder = services[:3]
2204
2268
 
2205
2269
  try:
@@ -2269,7 +2333,7 @@ def analyze_kotlin_call_audit(
2269
2333
  _load_credentials()
2270
2334
  services = _initialize_services(context)
2271
2335
  if not all(services[:3]):
2272
- return
2336
+ raise typer.Exit(code=1)
2273
2337
  db_manager, _, code_finder = services[:3]
2274
2338
 
2275
2339
  try:
@@ -2329,7 +2393,7 @@ def analyze_dependencies(
2329
2393
  _load_credentials()
2330
2394
  services = _initialize_services(context)
2331
2395
  if not all(services[:3]):
2332
- return
2396
+ raise typer.Exit(code=1)
2333
2397
  db_manager, graph_builder, code_finder = services[:3]
2334
2398
 
2335
2399
  try:
@@ -2394,7 +2458,7 @@ def analyze_inheritance_tree(
2394
2458
  _load_credentials()
2395
2459
  services = _initialize_services(context)
2396
2460
  if not all(services[:3]):
2397
- return
2461
+ raise typer.Exit(code=1)
2398
2462
  db_manager, graph_builder, code_finder = services[:3]
2399
2463
 
2400
2464
  try:
@@ -2464,7 +2528,7 @@ def analyze_complexity(
2464
2528
  _load_credentials()
2465
2529
  services = _initialize_services(context)
2466
2530
  if not all(services[:3]):
2467
- return
2531
+ raise typer.Exit(code=1)
2468
2532
  db_manager, graph_builder, code_finder = services[:3]
2469
2533
 
2470
2534
  _FILE_EXTENSIONS = ('.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.rb',
@@ -2541,7 +2605,7 @@ def analyze_dead_code(
2541
2605
  _load_credentials()
2542
2606
  services = _initialize_services(context)
2543
2607
  if not all(services[:3]):
2544
- return
2608
+ raise typer.Exit(code=1)
2545
2609
  db_manager, graph_builder, code_finder = services[:3]
2546
2610
 
2547
2611
  try:
@@ -2595,7 +2659,7 @@ def analyze_overrides(
2595
2659
  _load_credentials()
2596
2660
  services = _initialize_services(context)
2597
2661
  if not all(services[:3]):
2598
- return
2662
+ raise typer.Exit(code=1)
2599
2663
  db_manager, graph_builder, code_finder = services[:3]
2600
2664
 
2601
2665
  try:
@@ -2650,7 +2714,7 @@ def analyze_variable_usage(
2650
2714
  _load_credentials()
2651
2715
  services = _initialize_services(context)
2652
2716
  if not all(services[:3]):
2653
- return
2717
+ raise typer.Exit(code=1)
2654
2718
  db_manager, graph_builder, code_finder = services[:3]
2655
2719
 
2656
2720
  try:
@@ -2854,9 +2918,6 @@ def main(
2854
2918
  os.environ["CGC_RUNTIME_DB_TYPE"] = database
2855
2919
  # Initialize context object for sharing state with subcommands
2856
2920
  ctx.ensure_object(dict)
2857
-
2858
- if database:
2859
- os.environ["CGC_RUNTIME_DB_TYPE"] = database
2860
2921
 
2861
2922
  # Store visual flag in context for subcommands to access
2862
2923
  if visual:
@@ -289,9 +289,16 @@ def download_bundle(name: str, output_dir: Optional[str] = None, auto_load: bool
289
289
  if auto_load:
290
290
  console.print("[cyan]Using existing bundle for loading...[/cyan]")
291
291
  return str(output_path)
292
- return
292
+ return False
293
293
  output_path.unlink()
294
-
294
+
295
+ from ..utils.path_sandbox import is_safe_download_url
296
+
297
+ if not is_safe_download_url(download_url):
298
+ console.print("[bold red]Refusing to download from untrusted URL.[/bold red]")
299
+ console.print("[dim]Only HTTPS downloads from approved hosts are allowed.[/dim]")
300
+ raise typer.Exit(code=1)
301
+
295
302
  # Download with progress bar
296
303
  try:
297
304
  console.print(f"[cyan]Downloading {clean_filename}...[/cyan]")
@@ -435,8 +442,8 @@ def load_bundle_command(bundle_name: str, clear_existing: bool = False):
435
442
  stats["nodes"] = int(part.split(":")[1].strip().replace(",", ""))
436
443
  elif "Edges:" in part:
437
444
  stats["edges"] = int(part.split(":")[1].strip().replace(",", ""))
438
- except:
439
- pass
445
+ except Exception as parse_exc:
446
+ console.print(f"[dim]Could not parse bundle stats from message: {parse_exc}[/dim]")
440
447
 
441
448
  return (True, message, stats)
442
449
  else:
@@ -16,12 +16,17 @@ def _brew_install_neo4j(run_command, console) -> str:
16
16
  def _brew_start(service: str, run_command, console) -> bool:
17
17
  return run_command(["brew", "services", "start", service], console, check=False) is not None
18
18
 
19
+ def _escape_cypher_password(password: str) -> str:
20
+ return password.replace("\\", "\\\\").replace("'", "\\'")
21
+
22
+
19
23
  def _set_initial_password(new_pw: str, run_command, console) -> bool:
24
+ escaped_pw = _escape_cypher_password(new_pw)
20
25
  cmd = [
21
26
  "cypher-shell",
22
27
  "-u", "neo4j",
23
28
  "-p", "neo4j",
24
- f"ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO '{new_pw}'",
29
+ f"ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO '{escaped_pw}'",
25
30
  ]
26
31
  return run_command(cmd, console, check=False) is not None
27
32