codegraphcontext 0.4.18__py3-none-any.whl → 0.5.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.
Files changed (126) hide show
  1. codegraphcontext/api/app.py +4 -1
  2. codegraphcontext/api/mcp_sse.py +63 -22
  3. codegraphcontext/api/router.py +3 -6
  4. codegraphcontext/cli/cli_helpers.py +87 -13
  5. codegraphcontext/cli/config_manager.py +152 -38
  6. codegraphcontext/cli/hook_manager.py +1 -1
  7. codegraphcontext/cli/main.py +172 -42
  8. codegraphcontext/cli/registry_commands.py +11 -4
  9. codegraphcontext/cli/setup_macos.py +6 -1
  10. codegraphcontext/cli/setup_wizard.py +61 -56
  11. codegraphcontext/cli/visualizer.py +25 -1
  12. codegraphcontext/core/__init__.py +71 -10
  13. codegraphcontext/core/cgc_bundle.py +1 -1
  14. codegraphcontext/core/cgcignore.py +4 -1
  15. codegraphcontext/core/database_embedded_kuzu.py +1265 -0
  16. codegraphcontext/core/database_falkordb.py +41 -2
  17. codegraphcontext/core/database_kuzu.py +37 -1163
  18. codegraphcontext/core/database_ladybug.py +37 -1145
  19. codegraphcontext/core/graph_query.py +45 -0
  20. codegraphcontext/core/jobs.py +7 -2
  21. codegraphcontext/core/watcher.py +114 -188
  22. codegraphcontext/prompts.py +1 -1
  23. codegraphcontext/server.py +111 -19
  24. codegraphcontext/stdlibs.py +18 -0
  25. codegraphcontext/tool_definitions.py +188 -79
  26. codegraphcontext/tools/code_finder.py +33 -19
  27. codegraphcontext/tools/graph_builder.py +266 -10
  28. codegraphcontext/tools/handlers/analysis_handlers.py +5 -0
  29. codegraphcontext/tools/handlers/indexing_handlers.py +4 -1
  30. codegraphcontext/tools/handlers/management_handlers.py +9 -10
  31. codegraphcontext/tools/handlers/query_handlers.py +21 -3
  32. codegraphcontext/tools/handlers/watcher_handlers.py +2 -2
  33. codegraphcontext/tools/indexing/embeddings.py +63 -49
  34. codegraphcontext/tools/indexing/persistence/writer.py +437 -49
  35. codegraphcontext/tools/indexing/pipeline.py +66 -1
  36. codegraphcontext/tools/indexing/resolution/__init__.py +11 -1
  37. codegraphcontext/tools/indexing/resolution/calls.py +527 -70
  38. codegraphcontext/tools/indexing/resolution/inheritance.py +448 -4
  39. codegraphcontext/tools/indexing/resolution/post_resolution.py +40 -15
  40. codegraphcontext/tools/indexing/schema.py +5 -0
  41. codegraphcontext/tools/indexing/schema_contract.py +7 -0
  42. codegraphcontext/tools/indexing/scip_pipeline.py +25 -10
  43. codegraphcontext/tools/indexing/vector_resolver.py +72 -57
  44. codegraphcontext/tools/languages/c.py +154 -2
  45. codegraphcontext/tools/languages/cpp.py +13 -1
  46. codegraphcontext/tools/languages/csharp.py +1 -0
  47. codegraphcontext/tools/languages/css.py +13 -4
  48. codegraphcontext/tools/languages/dart.py +170 -41
  49. codegraphcontext/tools/languages/elixir.py +9 -6
  50. codegraphcontext/tools/languages/go.py +56 -16
  51. codegraphcontext/tools/languages/haskell.py +80 -8
  52. codegraphcontext/tools/languages/html.py +23 -8
  53. codegraphcontext/tools/languages/javascript.py +2 -2
  54. codegraphcontext/tools/languages/lua.py +61 -19
  55. codegraphcontext/tools/languages/perl.py +37 -4
  56. codegraphcontext/tools/languages/php.py +56 -4
  57. codegraphcontext/tools/languages/python.py +13 -3
  58. codegraphcontext/tools/languages/rust.py +70 -6
  59. codegraphcontext/tools/languages/swift.py +61 -11
  60. codegraphcontext/tools/languages/typescript.py +63 -6
  61. codegraphcontext/tools/package_resolver.py +159 -364
  62. codegraphcontext/tools/query_tool_languages/c_toolkit.py +1 -1
  63. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +2 -2
  64. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +2 -2
  65. codegraphcontext/tools/query_tool_languages/go_toolkit.py +1 -1
  66. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +3 -3
  67. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +1 -1
  68. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +2 -2
  69. codegraphcontext/tools/query_tool_languages/python_toolkit.py +1 -1
  70. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +1 -1
  71. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +1 -1
  72. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +2 -2
  73. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +2 -2
  74. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +1 -1
  75. codegraphcontext/tools/scip_indexer.py +45 -3
  76. codegraphcontext/tools/system.py +8 -1
  77. codegraphcontext/tools/tree_sitter_parser.py +41 -96
  78. codegraphcontext/utils/debug_log.py +5 -0
  79. codegraphcontext/utils/gcf_encoder.py +58 -0
  80. codegraphcontext/utils/visualize_graph.py +461 -0
  81. codegraphcontext/viz/server.py +348 -32
  82. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/METADATA +201 -23
  83. codegraphcontext-0.5.2.dist-info/RECORD +132 -0
  84. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/WHEEL +1 -1
  85. codegraphcontext/viz/dist/assets/__vite-browser-external-9wXp6ZBx.js +0 -1
  86. codegraphcontext/viz/dist/assets/function-calls-BtRHrqa2.png +0 -0
  87. codegraphcontext/viz/dist/assets/graph-total-D1fBAugo.png +0 -0
  88. codegraphcontext/viz/dist/assets/hero-graph-2voMJp2a.jpg +0 -0
  89. codegraphcontext/viz/dist/assets/hierarchy-DGADo0YT.png +0 -0
  90. codegraphcontext/viz/dist/assets/index-C-187lf0.js +0 -5587
  91. codegraphcontext/viz/dist/assets/index-fNAa6jgv.css +0 -1
  92. codegraphcontext/viz/dist/assets/parser-pyodide.worker-BgsDfaad.js +0 -370
  93. codegraphcontext/viz/dist/assets/parser.worker-_nvrecvj.js +0 -233
  94. codegraphcontext/viz/dist/assets/tree-sitter-qKYAACSa.wasm +0 -0
  95. codegraphcontext/viz/dist/cgcIcon.png +0 -0
  96. codegraphcontext/viz/dist/favicon.ico +0 -0
  97. codegraphcontext/viz/dist/index.html +0 -32
  98. codegraphcontext/viz/dist/logo-icon.svg +0 -85
  99. codegraphcontext/viz/dist/logo.svg +0 -100
  100. codegraphcontext/viz/dist/placeholder.svg +0 -1
  101. codegraphcontext/viz/dist/preview-image.png +0 -0
  102. codegraphcontext/viz/dist/robots.txt +0 -14
  103. codegraphcontext/viz/dist/wasm/tree-sitter-c.wasm +0 -0
  104. codegraphcontext/viz/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  105. codegraphcontext/viz/dist/wasm/tree-sitter-core.js +0 -1
  106. codegraphcontext/viz/dist/wasm/tree-sitter-cpp.wasm +0 -0
  107. codegraphcontext/viz/dist/wasm/tree-sitter-dart.wasm +0 -0
  108. codegraphcontext/viz/dist/wasm/tree-sitter-go.wasm +0 -0
  109. codegraphcontext/viz/dist/wasm/tree-sitter-java.wasm +0 -0
  110. codegraphcontext/viz/dist/wasm/tree-sitter-javascript.wasm +0 -0
  111. codegraphcontext/viz/dist/wasm/tree-sitter-kotlin.wasm +0 -0
  112. codegraphcontext/viz/dist/wasm/tree-sitter-perl.wasm +0 -1
  113. codegraphcontext/viz/dist/wasm/tree-sitter-php.wasm +0 -0
  114. codegraphcontext/viz/dist/wasm/tree-sitter-python.wasm +0 -0
  115. codegraphcontext/viz/dist/wasm/tree-sitter-ruby.wasm +0 -0
  116. codegraphcontext/viz/dist/wasm/tree-sitter-rust.wasm +0 -0
  117. codegraphcontext/viz/dist/wasm/tree-sitter-swift.wasm +0 -0
  118. codegraphcontext/viz/dist/wasm/tree-sitter-tsx.wasm +0 -0
  119. codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm +0 -0
  120. codegraphcontext/viz/dist/wasm/tree-sitter.wasm +0 -0
  121. codegraphcontext/viz/dist/wasm/web-tree-sitter.js +0 -4007
  122. codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm +0 -0
  123. codegraphcontext-0.4.18.dist-info/RECORD +0 -166
  124. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/entry_points.txt +0 -0
  125. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/licenses/LICENSE +0 -0
  126. {codegraphcontext-0.4.18.dist-info → codegraphcontext-0.5.2.dist-info}/top_level.txt +0 -0
@@ -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:
@@ -1238,27 +1295,61 @@ def doctor():
1238
1295
 
1239
1296
 
1240
1297
 
1298
+ @app.command()
1241
1299
  @app.command()
1242
1300
  def index(
1243
1301
  path: Optional[str] = typer.Argument(None, help="Path to the directory or file to index. Defaults to the current directory."),
1244
1302
  force: bool = typer.Option(False, "--force", "-f", help="Force re-index (delete existing and rebuild)"),
1245
1303
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use (overrides mode/default)"),
1304
+ summarize: bool = typer.Option(False, "--summarize", "-s", help="Show a summary of the indexed codebase after indexing"),
1246
1305
  ):
1247
1306
  """
1248
1307
  Indexes a directory or file by adding it to the code graph.
1249
1308
  If no path is provided, it indexes the current directory.
1250
-
1309
+
1251
1310
  Use --force to delete the existing index and rebuild from scratch.
1311
+ Use --summarize to display a summary after indexing.
1252
1312
  """
1253
1313
  _load_credentials()
1254
1314
  if path is None:
1255
1315
  path = str(Path.cwd())
1256
-
1257
- if force:
1258
- console.print("[yellow]Force re-indexing (--force flag detected)[/yellow]")
1259
- reindex_helper(path, context)
1260
- else:
1261
- index_helper(path, context)
1316
+
1317
+ try:
1318
+ if force:
1319
+ console.print("[yellow]Force re-indexing (--force flag detected)[/yellow]")
1320
+ reindex_helper(path, context)
1321
+ else:
1322
+ index_helper(path, context)
1323
+ except Exception as e:
1324
+ if str(e):
1325
+ console.print(f"[red]An error occurred during indexing: {e}[/red]")
1326
+
1327
+ if summarize:
1328
+ import os
1329
+
1330
+ py_files = []
1331
+ for root, dirs, files in os.walk(path):
1332
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['venv', '__pycache__', 'node_modules']]
1333
+ for file in files:
1334
+ if file.endswith('.py'):
1335
+ py_files.append(os.path.join(root, file))
1336
+
1337
+ total_lines = 0
1338
+ for f in py_files:
1339
+ try:
1340
+ with open(f, 'r', encoding='utf-8', errors='ignore') as fp:
1341
+ total_lines += len(fp.readlines())
1342
+ except:
1343
+ pass
1344
+
1345
+ console.print("\n[bold cyan]📊 Codebase Summary:[/bold cyan]")
1346
+ console.print(f" • Path indexed : [green]{path}[/green]")
1347
+ console.print(f" • Python files : [yellow]{len(py_files)}[/yellow]")
1348
+ console.print(f" • Total lines : [yellow]{total_lines}[/yellow]")
1349
+ console.print(f"\n • Run [bold]cgc analyze complexity[/bold] to find complex functions")
1350
+ console.print(f" • Run [bold]cgc analyze dead-code[/bold] to find unused code")
1351
+ console.print(f" • Run [bold]cgc list[/bold] to see all indexed repositories")
1352
+ console.print("\n[dim]Tip: Use --summarize anytime after indexing to see this.[/dim]")
1262
1353
 
1263
1354
  @app.command()
1264
1355
  def update(
@@ -1275,7 +1366,7 @@ def update(
1275
1366
  _load_credentials()
1276
1367
  if path is None:
1277
1368
  path = str(Path.cwd())
1278
- update_helper(path, context)
1369
+ update_helper(path, context, quiet=quiet)
1279
1370
 
1280
1371
  @app.command()
1281
1372
  def clean(
@@ -1344,7 +1435,7 @@ def delete(
1344
1435
  # Delete all repositories
1345
1436
  services = _initialize_services(context)
1346
1437
  if not all(services[:3]):
1347
- return
1438
+ raise typer.Exit(code=1)
1348
1439
  db_manager, graph_builder, code_finder = services[:3]
1349
1440
 
1350
1441
  try:
@@ -1409,7 +1500,14 @@ def delete(
1409
1500
  console.print("[red]Error: Please provide a path or use --all to delete all repositories[/red]")
1410
1501
  console.print("Usage: cgc delete <path> or cgc delete --all")
1411
1502
  raise typer.Exit(code=1)
1412
-
1503
+
1504
+ if not config_manager.is_db_deletion_allowed():
1505
+ console.print(
1506
+ "[bold red]Error:[/bold red] Repository deletion is disabled. "
1507
+ "Set ALLOW_DB_DELETION=true in config to enable."
1508
+ )
1509
+ raise typer.Exit(code=1)
1510
+
1413
1511
  delete_helper(path, context)
1414
1512
 
1415
1513
 
@@ -1455,6 +1553,7 @@ def report(
1455
1553
  @app.command()
1456
1554
  def visualize(
1457
1555
  repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Path to the repository to visualize."),
1556
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host interface to bind to."),
1458
1557
  port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on."),
1459
1558
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
1460
1559
  ):
@@ -1462,7 +1561,7 @@ def visualize(
1462
1561
  Launches the interactive UI to visualize the code graph.
1463
1562
  """
1464
1563
  _load_credentials()
1465
- visualize_helper(repo, port, context)
1564
+ visualize_helper(repo, host, port, context)
1466
1565
 
1467
1566
  @app.command("list")
1468
1567
  def list_repositories(
@@ -1501,6 +1600,14 @@ def watch(
1501
1600
  "--poll",
1502
1601
  help="Use watchdog's polling observer for Docker bind mounts and network filesystems.",
1503
1602
  ),
1603
+ sync_on_start: bool = typer.Option(
1604
+ False,
1605
+ "--sync-on-start",
1606
+ help=(
1607
+ "Synchronize already-indexed files before watching. "
1608
+ "Defaults off; use 'cgc index --force' for a full re-index."
1609
+ ),
1610
+ ),
1504
1611
  ):
1505
1612
  """
1506
1613
  Watch a directory for file changes and automatically update the code graph.
@@ -1511,6 +1618,7 @@ def watch(
1511
1618
 
1512
1619
  The watcher will:
1513
1620
  - Perform an initial scan if the directory is not yet indexed
1621
+ - Attach immediately for already-indexed directories unless --sync-on-start is passed
1514
1622
  - Monitor for file creation, modification, deletion, and moves
1515
1623
  - Automatically re-index affected files and update relationships
1516
1624
 
@@ -1520,12 +1628,13 @@ def watch(
1520
1628
  cgc watch . # Watch current directory
1521
1629
  cgc watch /path/to/project # Watch specific directory
1522
1630
  cgc watch --poll . # Use polling for Docker/NFS/SMB mounts
1631
+ cgc watch --sync-on-start . # Reconcile current files before watching
1523
1632
  cgc w . # Using shortcut alias
1524
1633
 
1525
1634
  Set CGC_WATCH_POLLING=1 to use polling without passing --poll.
1526
1635
  """
1527
1636
  _load_credentials()
1528
- watch_helper(path, context, use_polling=poll or None)
1637
+ watch_helper(path, context, use_polling=poll or None, sync_on_start=sync_on_start)
1529
1638
 
1530
1639
  @app.command()
1531
1640
  def unwatch(
@@ -1593,7 +1702,7 @@ def find_by_name(
1593
1702
  _load_credentials()
1594
1703
  services = _initialize_services(context)
1595
1704
  if not all(services[:3]):
1596
- return
1705
+ raise typer.Exit(code=1)
1597
1706
  db_manager, graph_builder, code_finder = services[:3]
1598
1707
 
1599
1708
  # Resolve effective fuzzy setting: CLI flag wins, else config, else true.
@@ -1725,7 +1834,7 @@ def find_by_pattern(
1725
1834
  _load_credentials()
1726
1835
  services = _initialize_services(context)
1727
1836
  if not all(services[:3]):
1728
- return
1837
+ raise typer.Exit(code=1)
1729
1838
  db_manager, graph_builder, code_finder = services[:3]
1730
1839
 
1731
1840
  try:
@@ -1818,7 +1927,7 @@ def find_by_type(
1818
1927
  _load_credentials()
1819
1928
  services = _initialize_services(context)
1820
1929
  if not all(services[:3]):
1821
- return
1930
+ raise typer.Exit(code=1)
1822
1931
  db_manager, graph_builder, code_finder = services[:3]
1823
1932
 
1824
1933
  try:
@@ -1873,7 +1982,7 @@ def find_by_variable(
1873
1982
  _load_credentials()
1874
1983
  services = _initialize_services(context)
1875
1984
  if not all(services[:3]):
1876
- return
1985
+ raise typer.Exit(code=1)
1877
1986
  db_manager, graph_builder, code_finder = services[:3]
1878
1987
 
1879
1988
  try:
@@ -1919,7 +2028,7 @@ def find_by_content_search(
1919
2028
  _load_credentials()
1920
2029
  services = _initialize_services(context)
1921
2030
  if not all(services[:3]):
1922
- return
2031
+ raise typer.Exit(code=1)
1923
2032
  db_manager, graph_builder, code_finder = services[:3]
1924
2033
 
1925
2034
  try:
@@ -1978,7 +2087,7 @@ def find_by_decorator_search(
1978
2087
  _load_credentials()
1979
2088
  services = _initialize_services(context)
1980
2089
  if not all(services[:3]):
1981
- return
2090
+ raise typer.Exit(code=1)
1982
2091
  db_manager, graph_builder, code_finder = services[:3]
1983
2092
 
1984
2093
  try:
@@ -2026,7 +2135,7 @@ def find_by_argument_search(
2026
2135
  _load_credentials()
2027
2136
  services = _initialize_services(context)
2028
2137
  if not all(services[:3]):
2029
- return
2138
+ raise typer.Exit(code=1)
2030
2139
  db_manager, graph_builder, code_finder = services[:3]
2031
2140
 
2032
2141
  try:
@@ -2082,7 +2191,7 @@ def analyze_calls(
2082
2191
  _load_credentials()
2083
2192
  services = _initialize_services(context)
2084
2193
  if not all(services[:3]):
2085
- return
2194
+ raise typer.Exit(code=1)
2086
2195
  db_manager, graph_builder, code_finder = services[:3]
2087
2196
 
2088
2197
  try:
@@ -2138,7 +2247,7 @@ def analyze_callers(
2138
2247
  _load_credentials()
2139
2248
  services = _initialize_services(context)
2140
2249
  if not all(services[:3]):
2141
- return
2250
+ raise typer.Exit(code=1)
2142
2251
  db_manager, graph_builder, code_finder = services[:3]
2143
2252
 
2144
2253
  try:
@@ -2199,7 +2308,7 @@ def analyze_chain(
2199
2308
  _load_credentials()
2200
2309
  services = _initialize_services(context)
2201
2310
  if not all(services[:3]):
2202
- return
2311
+ raise typer.Exit(code=1)
2203
2312
  db_manager, graph_builder, code_finder = services[:3]
2204
2313
 
2205
2314
  try:
@@ -2269,7 +2378,7 @@ def analyze_kotlin_call_audit(
2269
2378
  _load_credentials()
2270
2379
  services = _initialize_services(context)
2271
2380
  if not all(services[:3]):
2272
- return
2381
+ raise typer.Exit(code=1)
2273
2382
  db_manager, _, code_finder = services[:3]
2274
2383
 
2275
2384
  try:
@@ -2329,7 +2438,7 @@ def analyze_dependencies(
2329
2438
  _load_credentials()
2330
2439
  services = _initialize_services(context)
2331
2440
  if not all(services[:3]):
2332
- return
2441
+ raise typer.Exit(code=1)
2333
2442
  db_manager, graph_builder, code_finder = services[:3]
2334
2443
 
2335
2444
  try:
@@ -2394,7 +2503,7 @@ def analyze_inheritance_tree(
2394
2503
  _load_credentials()
2395
2504
  services = _initialize_services(context)
2396
2505
  if not all(services[:3]):
2397
- return
2506
+ raise typer.Exit(code=1)
2398
2507
  db_manager, graph_builder, code_finder = services[:3]
2399
2508
 
2400
2509
  try:
@@ -2444,7 +2553,7 @@ def analyze_inheritance_tree(
2444
2553
  @analyze_app.command("complexity")
2445
2554
  def analyze_complexity(
2446
2555
  path: Optional[str] = typer.Argument(None, help="Function name or file path to analyze"),
2447
- threshold: int = typer.Option(10, "--threshold", "-t", help="Complexity threshold for warnings"),
2556
+ threshold: Optional[int] = typer.Option(None, "--threshold", "-t", help="Complexity threshold for warnings (default: from config or 10)"),
2448
2557
  limit: int = typer.Option(20, "--limit", "-l", help="Maximum results to show"),
2449
2558
  file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path to scope analysis"),
2450
2559
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
@@ -2464,9 +2573,20 @@ def analyze_complexity(
2464
2573
  _load_credentials()
2465
2574
  services = _initialize_services(context)
2466
2575
  if not all(services[:3]):
2467
- return
2576
+ raise typer.Exit(code=1)
2468
2577
  db_manager, graph_builder, code_finder = services[:3]
2469
2578
 
2579
+ # Read threshold from config if not explicitly provided via CLI
2580
+ if threshold is None:
2581
+ configured = config_manager.get_config_value("COMPLEXITY_THRESHOLD")
2582
+ if configured is not None:
2583
+ try:
2584
+ threshold = int(configured)
2585
+ except (ValueError, TypeError):
2586
+ threshold = 10
2587
+ else:
2588
+ threshold = 10
2589
+
2470
2590
  _FILE_EXTENSIONS = ('.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.rb',
2471
2591
  '.java', '.cpp', '.c', '.cs', '.swift', '.kt', '.scala',
2472
2592
  '.php', '.lua', '.zig', '.ex', '.exs', '.r', '.m', '.sh')
@@ -2541,7 +2661,7 @@ def analyze_dead_code(
2541
2661
  _load_credentials()
2542
2662
  services = _initialize_services(context)
2543
2663
  if not all(services[:3]):
2544
- return
2664
+ raise typer.Exit(code=1)
2545
2665
  db_manager, graph_builder, code_finder = services[:3]
2546
2666
 
2547
2667
  try:
@@ -2595,7 +2715,7 @@ def analyze_overrides(
2595
2715
  _load_credentials()
2596
2716
  services = _initialize_services(context)
2597
2717
  if not all(services[:3]):
2598
- return
2718
+ raise typer.Exit(code=1)
2599
2719
  db_manager, graph_builder, code_finder = services[:3]
2600
2720
 
2601
2721
  try:
@@ -2650,7 +2770,7 @@ def analyze_variable_usage(
2650
2770
  _load_credentials()
2651
2771
  services = _initialize_services(context)
2652
2772
  if not all(services[:3]):
2653
- return
2773
+ raise typer.Exit(code=1)
2654
2774
  db_manager, graph_builder, code_finder = services[:3]
2655
2775
 
2656
2776
  try:
@@ -2782,9 +2902,22 @@ def visualize_abbrev(
2782
2902
  def watch_abbrev(
2783
2903
  path: str = typer.Argument(".", help="Path to watch"),
2784
2904
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2905
+ poll: bool = typer.Option(
2906
+ False,
2907
+ "--poll",
2908
+ help="Use watchdog's polling observer for Docker bind mounts and network filesystems.",
2909
+ ),
2910
+ sync_on_start: bool = typer.Option(
2911
+ False,
2912
+ "--sync-on-start",
2913
+ help=(
2914
+ "Synchronize already-indexed files before watching. "
2915
+ "Defaults off; use 'cgc index --force' for a full re-index."
2916
+ ),
2917
+ ),
2785
2918
  ):
2786
2919
  """Shortcut for 'cgc watch'"""
2787
- watch(path, context=context)
2920
+ watch(path, context=context, poll=poll, sync_on_start=sync_on_start)
2788
2921
 
2789
2922
 
2790
2923
  # ============================================================================
@@ -2854,9 +2987,6 @@ def main(
2854
2987
  os.environ["CGC_RUNTIME_DB_TYPE"] = database
2855
2988
  # Initialize context object for sharing state with subcommands
2856
2989
  ctx.ensure_object(dict)
2857
-
2858
- if database:
2859
- os.environ["CGC_RUNTIME_DB_TYPE"] = database
2860
2990
 
2861
2991
  # Store visual flag in context for subcommands to access
2862
2992
  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