codegraphcontext 0.3.9__py3-none-any.whl → 0.4.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 (88) hide show
  1. codegraphcontext/cli/cli_helpers.py +33 -14
  2. codegraphcontext/cli/config_manager.py +13 -1
  3. codegraphcontext/cli/main.py +49 -40
  4. codegraphcontext/cli/registry_commands.py +4 -84
  5. codegraphcontext/cli/setup_wizard.py +23 -2
  6. codegraphcontext/core/__init__.py +19 -30
  7. codegraphcontext/core/bundle_registry.py +12 -2
  8. codegraphcontext/core/cgc_bundle.py +3 -3
  9. codegraphcontext/core/cgcignore.py +118 -0
  10. codegraphcontext/core/watcher.py +6 -6
  11. codegraphcontext/server.py +8 -1
  12. codegraphcontext/tools/code_finder.py +126 -38
  13. codegraphcontext/tools/graph_builder.py +174 -1583
  14. codegraphcontext/tools/handlers/analysis_handlers.py +3 -2
  15. codegraphcontext/tools/handlers/indexing_handlers.py +2 -1
  16. codegraphcontext/tools/handlers/management_handlers.py +8 -3
  17. codegraphcontext/tools/handlers/query_handlers.py +0 -4
  18. codegraphcontext/tools/handlers/watcher_handlers.py +4 -5
  19. codegraphcontext/tools/indexing/__init__.py +1 -0
  20. codegraphcontext/tools/indexing/constants.py +25 -0
  21. codegraphcontext/tools/indexing/discovery.py +64 -0
  22. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  23. codegraphcontext/tools/indexing/persistence/writer.py +758 -0
  24. codegraphcontext/tools/indexing/pipeline.py +129 -0
  25. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  26. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  27. codegraphcontext/tools/indexing/resolution/calls.py +204 -0
  28. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  29. codegraphcontext/tools/indexing/sanitize.py +41 -0
  30. codegraphcontext/tools/indexing/schema.py +79 -0
  31. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  32. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  33. codegraphcontext/tools/languages/dart.py +14 -4
  34. codegraphcontext/tools/languages/haskell.py +359 -475
  35. codegraphcontext/tools/languages/java.py +0 -24
  36. codegraphcontext/tools/languages/javascript.py +1 -1
  37. codegraphcontext/tools/languages/perl.py +7 -2
  38. codegraphcontext/tools/languages/php.py +27 -0
  39. codegraphcontext/tools/languages/rust.py +18 -21
  40. codegraphcontext/tools/languages/scala.py +1 -1
  41. codegraphcontext/tools/languages/typescript.py +0 -2
  42. codegraphcontext/tools/scip_indexer.py +2 -2
  43. codegraphcontext/tools/system.py +6 -2
  44. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  45. codegraphcontext/utils/repo_path.py +27 -0
  46. codegraphcontext/utils/tree_sitter_manager.py +1 -0
  47. codegraphcontext/viz/dist/assets/__vite-browser-external-9wXp6ZBx.js +1 -0
  48. codegraphcontext/viz/dist/assets/function-calls-BtRHrqa2.png +0 -0
  49. codegraphcontext/viz/dist/assets/graph-total-D1fBAugo.png +0 -0
  50. codegraphcontext/viz/dist/assets/hero-graph-2voMJp2a.jpg +0 -0
  51. codegraphcontext/viz/dist/assets/hierarchy-DGADo0YT.png +0 -0
  52. codegraphcontext/viz/dist/assets/index-BJT3EMmQ.js +5571 -0
  53. codegraphcontext/viz/dist/assets/index-DjDPHWki.css +1 -0
  54. codegraphcontext/viz/dist/assets/parser.worker-CZgm11E5.js +208 -0
  55. codegraphcontext/viz/dist/assets/tree-sitter-qKYAACSa.wasm +0 -0
  56. codegraphcontext/viz/dist/favicon.ico +0 -0
  57. codegraphcontext/viz/dist/index.html +31 -0
  58. codegraphcontext/viz/dist/placeholder.svg +1 -0
  59. codegraphcontext/viz/dist/preview-image.png +0 -0
  60. codegraphcontext/viz/dist/robots.txt +14 -0
  61. codegraphcontext/viz/dist/wasm/tree-sitter-c.wasm +0 -0
  62. codegraphcontext/viz/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  63. codegraphcontext/viz/dist/wasm/tree-sitter-core.js +1 -0
  64. codegraphcontext/viz/dist/wasm/tree-sitter-cpp.wasm +0 -0
  65. codegraphcontext/viz/dist/wasm/tree-sitter-dart.wasm +0 -0
  66. codegraphcontext/viz/dist/wasm/tree-sitter-go.wasm +0 -0
  67. codegraphcontext/viz/dist/wasm/tree-sitter-java.wasm +0 -0
  68. codegraphcontext/viz/dist/wasm/tree-sitter-javascript.wasm +0 -0
  69. codegraphcontext/viz/dist/wasm/tree-sitter-kotlin.wasm +0 -0
  70. codegraphcontext/viz/dist/wasm/tree-sitter-perl.wasm +1 -0
  71. codegraphcontext/viz/dist/wasm/tree-sitter-php.wasm +0 -0
  72. codegraphcontext/viz/dist/wasm/tree-sitter-python.wasm +0 -0
  73. codegraphcontext/viz/dist/wasm/tree-sitter-ruby.wasm +0 -0
  74. codegraphcontext/viz/dist/wasm/tree-sitter-rust.wasm +0 -0
  75. codegraphcontext/viz/dist/wasm/tree-sitter-swift.wasm +0 -0
  76. codegraphcontext/viz/dist/wasm/tree-sitter-tsx.wasm +0 -0
  77. codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm +0 -0
  78. codegraphcontext/viz/dist/wasm/tree-sitter.wasm +0 -0
  79. codegraphcontext/viz/dist/wasm/web-tree-sitter.js +4007 -0
  80. codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm +0 -0
  81. codegraphcontext/viz/server.py +10 -0
  82. {codegraphcontext-0.3.9.dist-info → codegraphcontext-0.4.1.dist-info}/METADATA +8 -7
  83. codegraphcontext-0.4.1.dist-info/RECORD +132 -0
  84. codegraphcontext-0.3.9.dist-info/RECORD +0 -81
  85. {codegraphcontext-0.3.9.dist-info → codegraphcontext-0.4.1.dist-info}/WHEEL +0 -0
  86. {codegraphcontext-0.3.9.dist-info → codegraphcontext-0.4.1.dist-info}/entry_points.txt +0 -0
  87. {codegraphcontext-0.3.9.dist-info → codegraphcontext-0.4.1.dist-info}/licenses/LICENSE +0 -0
  88. {codegraphcontext-0.3.9.dist-info → codegraphcontext-0.4.1.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,7 @@ from ..tools.code_finder import CodeFinder
24
24
  from ..tools.graph_builder import GraphBuilder
25
25
  from ..tools.package_resolver import get_local_package_path
26
26
  from ..utils.debug_log import info_logger, warning_logger
27
+ from ..utils.repo_path import any_repo_matches_path
27
28
  from .config_manager import resolve_context, ResolvedContext, register_repo_in_context, ensure_first_run_bootstrap
28
29
 
29
30
  console = Console()
@@ -175,8 +176,8 @@ def index_helper(path: str, context: Optional[str] = None):
175
176
  return
176
177
 
177
178
  indexed_repos = code_finder.list_indexed_repositories()
178
- repo_exists = any(Path(repo["path"]).resolve() == path_obj for repo in indexed_repos)
179
-
179
+ repo_exists = any_repo_matches_path(indexed_repos, path_obj)
180
+
180
181
  if repo_exists:
181
182
  # Check if the repository actually has files (not just an empty node from interrupted indexing)
182
183
  # Use variable-length path to handle both flat (Repository->File) and
@@ -184,7 +185,7 @@ def index_helper(path: str, context: Optional[str] = None):
184
185
  try:
185
186
  with db_manager.get_driver().session() as session:
186
187
  result = session.run(
187
- "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(f) as file_count",
188
+ "MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(DISTINCT f) as file_count",
188
189
  path=str(path_obj)
189
190
  )
190
191
  record = result.single()
@@ -284,7 +285,7 @@ def list_repos_helper(context: Optional[str] = None):
284
285
 
285
286
  for repo in repos:
286
287
  repo_type = "Dependency" if repo.get("is_dependency") else "Project"
287
- table.add_row(repo["name"], repo["path"], repo_type)
288
+ table.add_row(repo.get("name") or "", str(repo.get("path") or ""), repo_type)
288
289
 
289
290
  console.print(table)
290
291
  except Exception as e:
@@ -377,7 +378,7 @@ import urllib.parse
377
378
  from ..viz.server import run_server, set_db_manager
378
379
 
379
380
  def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context: Optional[str] = None):
380
- """"Generates an interactive visualization using the Playground UI."""
381
+ """Generates an interactive visualization using the Playground UI."""
381
382
  services = _initialize_services(context)
382
383
  if not all(services[:3]):
383
384
  return
@@ -415,15 +416,33 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
415
416
  if cwd_static_dir.exists():
416
417
  static_dir = cwd_static_dir
417
418
  else:
418
- console.print(f"[yellow]Warning: Visualization assets not found.[/yellow]")
419
- console.print(f"[dim]Checked paths:[/dim]")
420
- console.print(f" [dim]- {static_dir}[/dim]")
419
+ console.print("[bold red]Visualization assets not found.[/bold red]")
420
+ console.print("[dim]Checked paths:[/dim]")
421
+ console.print(f" [dim]- {package_root / 'viz' / 'dist'}[/dim]")
421
422
  console.print(f" [dim]- {dev_static_dir}[/dim]")
422
423
  console.print(f" [dim]- {alt_dev_dir}[/dim]")
423
424
  console.print(f" [dim]- {cwd_static_dir}[/dim]")
424
- console.print("[dim]Please run 'cd website && npm run build' first.[/dim]")
425
- # We continue anyway to let the server start (helpful for dev)
426
-
425
+ console.print(
426
+ "[dim]If you installed from PyPI, upgrade after the next release "
427
+ "(wheels must bundle viz/dist). If you are developing from source, run:[/dim]"
428
+ )
429
+ console.print(" [cyan]./scripts/sync_viz_dist.sh[/cyan]")
430
+ console.print(
431
+ "[dim]or[/dim] [cyan]cd website && npm ci && npm run build[/cyan] "
432
+ "[dim]then sync[/dim] [cyan]website/dist[/cyan] [dim]→[/dim] "
433
+ "[cyan]src/codegraphcontext/viz/dist[/cyan][dim].[/dim]"
434
+ )
435
+ db_manager.close_driver()
436
+ raise SystemExit(1)
437
+
438
+ index_html = static_dir / "index.html"
439
+ if not index_html.is_file():
440
+ console.print(
441
+ f"[bold red]Invalid visualization bundle:[/bold red] missing {index_html}"
442
+ )
443
+ db_manager.close_driver()
444
+ raise SystemExit(1)
445
+
427
446
  # Construct the URL
428
447
  backend_url = f"http://localhost:{port}"
429
448
  params = {"backend": backend_url}
@@ -471,8 +490,8 @@ def reindex_helper(path: str, context: Optional[str] = None):
471
490
 
472
491
  # Check if already indexed
473
492
  indexed_repos = code_finder.list_indexed_repositories()
474
- repo_exists = any(Path(repo["path"]).resolve() == path_obj for repo in indexed_repos)
475
-
493
+ repo_exists = any_repo_matches_path(indexed_repos, path_obj)
494
+
476
495
  if repo_exists:
477
496
  console.print(f"[yellow]Deleting existing index for: {path_obj}[/yellow]")
478
497
  try:
@@ -671,7 +690,7 @@ def watch_helper(path: str, context: Optional[str] = None):
671
690
  # transient empty result from list_indexed_repositories never triggers a
672
691
  # destructive full rescan of an already-populated graph.
673
692
  indexed_repos = code_finder.list_indexed_repositories()
674
- is_indexed = any(Path(repo["path"]).resolve() == path_obj for repo in indexed_repos)
693
+ is_indexed = any_repo_matches_path(indexed_repos, path_obj)
675
694
  if not is_indexed:
676
695
  # Fallback: count File nodes whose path starts with this repo's path.
677
696
  # If > 100 exist, the repo is clearly already indexed — skip the scan.
@@ -78,7 +78,7 @@ CONFIG_DESCRIPTIONS = {
78
78
 
79
79
  # Valid values for each config key
80
80
  CONFIG_VALIDATORS = {
81
- "DEFAULT_DATABASE": ["neo4j", "falkordb", "kuzudb"],
81
+ "DEFAULT_DATABASE": ["neo4j", "falkordb", "falkordb-remote", "kuzudb"],
82
82
  "INDEX_VARIABLES": ["true", "false"],
83
83
  "ALLOW_DB_DELETION": ["true", "false"],
84
84
  "DEBUG_LOGS": ["true", "false"],
@@ -219,6 +219,18 @@ def find_local_env() -> Optional[Path]:
219
219
  return None
220
220
 
221
221
 
222
+ def codegraphcontext_dotenv_at_cwd(cwd: Optional[Path] = None) -> Optional[Path]:
223
+ """
224
+ Return ``<cwd>/.codegraphcontext/.env`` if that file exists, else None.
225
+
226
+ *cwd* defaults to ``Path.cwd()``. Parent directories are **not** searched—same rule as
227
+ local context resolution (``find_local_cgc_dir``).
228
+ """
229
+ root = (cwd or Path.cwd()).resolve()
230
+ candidate = root / ".codegraphcontext" / ".env"
231
+ return candidate if candidate.exists() else None
232
+
233
+
222
234
  def save_config(config: Dict[str, str], preserve_db_credentials: bool = True):
223
235
  """
224
236
  Save configuration to file.
@@ -19,7 +19,6 @@ import logging
19
19
  import json
20
20
  import os
21
21
  from pathlib import Path
22
- from dotenv import load_dotenv, find_dotenv, set_key
23
22
  from importlib.metadata import version as pkg_version, PackageNotFoundError
24
23
 
25
24
  from codegraphcontext.server import MCPServer
@@ -111,6 +110,7 @@ def mcp_setup():
111
110
  - VS Code, Cursor, Windsurf
112
111
  - Claude Desktop, Gemini CLI
113
112
  - Cline, RooCode, Amazon Q Developer
113
+ - OpenCode (prints stdio config + link to vendor docs)
114
114
 
115
115
  Works with FalkorDB by default (no database setup needed).
116
116
  """
@@ -290,19 +290,18 @@ def _load_credentials():
290
290
  Uses per-variable precedence - each variable is loaded from the highest priority source.
291
291
  Priority order (highest to lowest):
292
292
  1. Local `mcp.json` env vars (highest - explicit MCP server config)
293
- 2. Local `.env` in project directory (high - project-specific overrides)
293
+ 2. ``<cwd>/.codegraphcontext/.env`` only (no parent-directory walk)
294
294
  3. Global `~/.codegraphcontext/.env` (lowest - user defaults)
295
+
296
+ Step 2 skips duplicate loading when that file is the same path as the global file.
297
+ Arbitrary repo-root `.env` files are not loaded—only CodeGraphContext config paths.
295
298
  """
296
299
  from dotenv import dotenv_values
297
- from codegraphcontext.cli.config_manager import ensure_config_dir
300
+ from codegraphcontext.cli.config_manager import (
301
+ ensure_config_dir,
302
+ codegraphcontext_dotenv_at_cwd,
303
+ )
298
304
 
299
- # Capture DATABASE_TYPE from actual shell env BEFORE we load .env files.
300
- # If the user ran `DATABASE_TYPE=falkordb cgc …` we must not let
301
- # DEFAULT_DATABASE=neo4j in .env steal priority later.
302
- shell_db_type = os.environ.get('DATABASE_TYPE')
303
- if shell_db_type and not os.environ.get('CGC_RUNTIME_DB_TYPE'):
304
- os.environ['CGC_RUNTIME_DB_TYPE'] = shell_db_type
305
-
306
305
  # Ensure config directory exists (lazy initialization)
307
306
  ensure_config_dir()
308
307
 
@@ -319,14 +318,16 @@ def _load_credentials():
319
318
  except Exception as e:
320
319
  console.print(f"[yellow]Warning: Could not load global .env: {e}[/yellow]")
321
320
 
322
- # 2. Local project .env (higher priority - project-specific overrides)
321
+ # 2. <cwd>/.codegraphcontext/.env only (overrides global when distinct)
323
322
  try:
324
- dotenv_path = find_dotenv(usecwd=True, raise_error_if_not_found=False)
325
- if dotenv_path:
326
- config_sources.append(dotenv_values(dotenv_path))
327
- config_source_names.append(str(dotenv_path))
323
+ local_cgc_env = codegraphcontext_dotenv_at_cwd(Path.cwd())
324
+ if local_cgc_env and local_cgc_env.resolve() != global_env_path.resolve():
325
+ config_sources.append(dotenv_values(str(local_cgc_env)))
326
+ config_source_names.append(str(local_cgc_env))
328
327
  except Exception as e:
329
- console.print(f"[yellow]Warning: Could not load .env from current directory: {e}[/yellow]")
328
+ console.print(
329
+ f"[yellow]Warning: Could not load .codegraphcontext/.env at cwd: {e}[/yellow]"
330
+ )
330
331
 
331
332
  # 1. Local mcp.json (highest priority - explicit MCP server config)
332
333
  mcp_file_path = Path.cwd() / "mcp.json"
@@ -348,9 +349,9 @@ def _load_credentials():
348
349
 
349
350
  # Apply merged config to environment.
350
351
  # IMPORTANT: DB-selection keys set in the shell must win over .env defaults.
351
- # E.g. `DATABASE_TYPE=falkordb cgc index …` must not be overridden by
352
+ # E.g. `DEFAULT_DATABASE=falkordb cgc index …` must not be overridden by
352
353
  # DEFAULT_DATABASE=neo4j sitting in ~/.codegraphcontext/.env
353
- DB_OVERRIDE_KEYS = {"DATABASE_TYPE", "CGC_RUNTIME_DB_TYPE", "DEFAULT_DATABASE"}
354
+ DB_OVERRIDE_KEYS = {"CGC_RUNTIME_DB_TYPE", "DEFAULT_DATABASE"}
354
355
  for key, value in merged_config.items():
355
356
  if value is not None: # Only set non-None values
356
357
  # Never let .env clobber a DB-type key that the user already set in the shell
@@ -369,16 +370,10 @@ def _load_credentials():
369
370
 
370
371
 
371
372
  # Show which database is actually being used.
372
- # When DATABASE_TYPE is explicitly set, trust it. When it's left to auto-
373
- # detect, call get_database_manager() so the banner can never lie: e.g. if
374
- # falkordblite is installed but its native .so is missing (frozen bundle),
375
- # the factory falls back to KùzuDB and we display that correctly.
373
+ # When CGC_RUNTIME_DB_TYPE or DEFAULT_DATABASE is set, trust it. Otherwise
374
+ # call get_database_manager() so the banner matches factory fallbacks.
376
375
  runtime_db = os.environ.get("CGC_RUNTIME_DB_TYPE")
377
- explicit_db = (
378
- runtime_db
379
- or os.environ.get("DEFAULT_DATABASE")
380
- or os.environ.get("DATABASE_TYPE")
381
- )
376
+ explicit_db = runtime_db or os.environ.get("DEFAULT_DATABASE")
382
377
 
383
378
  if explicit_db:
384
379
  default_db = explicit_db.lower()
@@ -416,12 +411,9 @@ def _load_credentials():
416
411
  if host:
417
412
  console.print(f"[cyan]Using database: FalkorDB Remote ({host})[/cyan]")
418
413
  else:
419
- console.print("[yellow]⚠ DATABASE_TYPE=falkordb-remote but FALKORDB_HOST not set.[/yellow]")
420
- elif default_db == "falkordb":
421
- if os.environ.get("FALKORDB_HOST"):
422
- console.print(f"[cyan]Using database: FalkorDB Remote ({os.environ.get('FALKORDB_HOST')})[/cyan]")
423
- else:
424
- console.print("[cyan]Using database: FalkorDB[/cyan]")
414
+ console.print(
415
+ "[yellow]⚠ DEFAULT_DATABASE=falkordb-remote but FALKORDB_HOST not set.[/yellow]"
416
+ )
425
417
  else:
426
418
  console.print(f"[cyan]Using database: {default_db}[/cyan]")
427
419
 
@@ -474,7 +466,7 @@ def config_reset():
474
466
  console.print("[yellow]Reset cancelled[/yellow]")
475
467
 
476
468
  @config_app.command("db")
477
- def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j' or 'falkordb'")):
469
+ def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j', 'falkordb', 'falkordb-remote', or 'kuzudb'")):
478
470
  """
479
471
  Quickly switch the default database backend.
480
472
 
@@ -483,14 +475,19 @@ def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j'
483
475
  Examples:
484
476
  cgc config db neo4j
485
477
  cgc config db falkordb
478
+ cgc config db kuzudb
486
479
  """
487
480
  backend = backend.lower()
488
- if backend not in ['falkordb', 'falkordb-remote', 'neo4j']:
481
+ if backend not in ['falkordb', 'falkordb-remote', 'neo4j', 'kuzudb']:
489
482
  console.print(f"[bold red]Invalid backend: {backend}[/bold red]")
490
- console.print("Must be 'falkordb', 'falkordb-remote', or 'neo4j'")
483
+ console.print("Must be 'falkordb', 'falkordb-remote', 'neo4j', or 'kuzudb'")
491
484
  raise typer.Exit(code=1)
492
485
 
493
- config_manager.set_config_value("DEFAULT_DATABASE", backend)
486
+ updated = config_manager.set_config_value("DEFAULT_DATABASE", backend)
487
+ if not updated:
488
+ console.print(f"[bold red]Failed to switch default database to {backend}[/bold red]")
489
+ raise typer.Exit(code=1)
490
+
494
491
  console.print(f"[green]✔ Default database switched to {backend}[/green]")
495
492
 
496
493
  # ============================================================================
@@ -672,10 +669,12 @@ def bundle_load(
672
669
  @app.command("export", rich_help_panel="Bundle Shortcuts")
673
670
  def export_shortcut(
674
671
  output: str = typer.Argument(..., help="Output path for the .cgc bundle file"),
675
- repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Specific repository path to export")
672
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Specific repository path to export"),
673
+ no_stats: bool = typer.Option(False, "--no-stats", help="Skip generating statistics in the bundle"),
674
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
676
675
  ):
677
676
  """Shortcut for 'cgc bundle export'"""
678
- bundle_export(output, repo, False)
677
+ bundle_export(output, repo, no_stats, context)
679
678
 
680
679
  @app.command("load", rich_help_panel="Bundle Shortcuts")
681
680
  def load_shortcut(
@@ -796,6 +795,7 @@ def doctor():
796
795
 
797
796
  # 1. Check configuration
798
797
  console.print("[bold]1. Checking Configuration...[/bold]")
798
+ config = {}
799
799
  try:
800
800
  config = config_manager.load_config()
801
801
 
@@ -851,6 +851,15 @@ def doctor():
851
851
  all_checks_passed = False
852
852
  else:
853
853
  console.print(f" [yellow]⚠[/yellow] Neo4j credentials not set. Run 'cgc neo4j setup'")
854
+ elif default_db == "kuzudb":
855
+ from importlib.util import find_spec
856
+
857
+ if find_spec("kuzu") is not None:
858
+ console.print(f" [green]✓[/green] KuzuDB is installed")
859
+ else:
860
+ console.print(f" [red]✗[/red] KuzuDB is not installed")
861
+ console.print(f" Run: pip install kuzu")
862
+ all_checks_passed = False
854
863
  else:
855
864
  # FalkorDB
856
865
  try:
@@ -2313,7 +2322,7 @@ def main(
2313
2322
  None,
2314
2323
  "--database",
2315
2324
  "-db",
2316
- help="[Global] Temporarily override database backend (falkordb or neo4j) for any command"
2325
+ help="[Global] Temporarily override database backend (falkordb, falkordb-remote, neo4j, or kuzudb) for any command"
2317
2326
  ),
2318
2327
  visual: bool = typer.Option(
2319
2328
  False,
@@ -17,93 +17,12 @@ console = Console()
17
17
 
18
18
  GITHUB_ORG = "CodeGraphContext"
19
19
  GITHUB_REPO = "CodeGraphContext"
20
- REGISTRY_API_URL = f"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/releases"
21
- MANIFEST_URL = f"https://github.com/{GITHUB_ORG}/{GITHUB_REPO}/releases/download/on-demand-bundles/manifest.json"
22
20
 
23
21
 
24
22
  def fetch_available_bundles() -> List[Dict[str, Any]]:
25
- """
26
- Fetch all available bundles from GitHub Releases.
27
- Returns a list of bundle dictionaries with metadata.
28
- Preserves all versions - no deduplication.
29
- """
30
- all_bundles = []
31
-
32
- try:
33
- # 1. Fetch on-demand bundles from manifest
34
- try:
35
- response = requests.get(MANIFEST_URL, timeout=10)
36
- if response.status_code == 200:
37
- manifest = response.json()
38
- if manifest.get('bundles'):
39
- for bundle in manifest['bundles']:
40
- bundle['source'] = 'on-demand'
41
- # Ensure bundle has a full_name field (with version info)
42
- if 'bundle_name' in bundle:
43
- # Extract full name without .cgc extension
44
- bundle['full_name'] = bundle['bundle_name'].replace('.cgc', '')
45
- all_bundles.append(bundle)
46
- except Exception as e:
47
- console.print(f"[dim]Note: Could not fetch on-demand bundles: {e}[/dim]")
48
-
49
- # 2. Fetch weekly pre-indexed bundles
50
- try:
51
- response = requests.get(REGISTRY_API_URL, timeout=10)
52
- if response.status_code == 200:
53
- releases = response.json()
54
-
55
- # Find weekly releases (bundles-YYYYMMDD pattern)
56
- weekly_releases = [r for r in releases if r['tag_name'].startswith('bundles-') and r['tag_name'] != 'bundles-latest']
57
-
58
- if weekly_releases:
59
- # Get the most recent weekly release
60
- latest_weekly = weekly_releases[0]
61
-
62
- for asset in latest_weekly.get('assets', []):
63
- if asset['name'].endswith('.cgc'):
64
- # Full bundle name without extension
65
- full_name = asset['name'].replace('.cgc', '')
66
-
67
- # Parse bundle name
68
- name_parts = full_name.split('-')
69
- bundle = {
70
- 'name': name_parts[0], # Base package name
71
- 'full_name': full_name, # Complete name with version
72
- 'repo': f"{name_parts[0]}/{name_parts[0]}", # Simplified
73
- 'bundle_name': asset['name'],
74
- 'version': name_parts[1] if len(name_parts) > 1 else 'latest',
75
- 'commit': name_parts[2] if len(name_parts) > 2 else 'unknown',
76
- 'size': f"{asset['size'] / 1024 / 1024:.1f}MB",
77
- 'download_url': asset['browser_download_url'],
78
- 'generated_at': asset['updated_at'],
79
- 'source': 'weekly'
80
- }
81
- all_bundles.append(bundle)
82
- except Exception as e:
83
- console.print(f"[dim]Note: Could not fetch weekly bundles: {e}[/dim]")
84
-
85
- # Normalize all bundles to have required fields
86
- for bundle in all_bundles:
87
- # Ensure 'name' field exists (base package name)
88
- if 'name' not in bundle:
89
- repo = bundle.get('repo', '')
90
- if '/' in repo:
91
- bundle['name'] = repo.split('/')[-1]
92
- else:
93
- # Extract from full_name or bundle_name
94
- full_name = bundle.get('full_name', bundle.get('bundle_name', 'unknown'))
95
- bundle['name'] = full_name.split('-')[0]
96
-
97
- # Ensure 'full_name' exists
98
- if 'full_name' not in bundle:
99
- bundle['full_name'] = bundle.get('bundle_name', bundle.get('name', 'unknown')).replace('.cgc', '')
100
-
101
- # NO DEDUPLICATION - Keep all versions
102
- return all_bundles
103
-
104
- except Exception as e:
105
- console.print(f"[bold red]Error fetching bundles: {e}[/bold red]")
106
- return []
23
+ """Fetch all available bundles from GitHub Releases (delegates to core BundleRegistry)."""
24
+ from ..core.bundle_registry import BundleRegistry
25
+ return BundleRegistry.fetch_available_bundles()
107
26
 
108
27
 
109
28
  def _get_base_package_name(bundle_name: str) -> str:
@@ -213,6 +132,7 @@ def search_bundles(query: str):
213
132
  matching_bundles = [
214
133
  b for b in bundles
215
134
  if query_lower in b.get('name', '').lower() or
135
+ query_lower in b.get('full_name', '').lower() or
216
136
  query_lower in b.get('repo', '').lower() or
217
137
  query_lower in b.get('description', '').lower()
218
138
  ]
@@ -135,12 +135,26 @@ def convert_mcp_json_to_yaml():
135
135
  yaml.dump(mcp_config, yaml_file, default_flow_style=False)
136
136
  console.print(f"[green]Generated devfile.yaml for Amazon Q Developer at {yaml_path}[/green]")
137
137
 
138
+ def _print_opencode_mcp_instructions(mcp_config: dict) -> None:
139
+ """OpenCode manages MCP in its own UI; we only print the stdio snippet + doc link."""
140
+ console.print("\n[bold cyan]OpenCode[/bold cyan]")
141
+ console.print(
142
+ "Register a stdio MCP server in OpenCode using the same command, args, and env as below "
143
+ "(mirror your generated mcp.json so OpenCode and the CLI share one database)."
144
+ )
145
+ console.print(
146
+ "\n[dim]Vendor guide:[/dim] https://opencode.ai/docs/ko/mcp-servers/#_top"
147
+ )
148
+ console.print("\n[bold]Suggested MCP server JSON:[/bold]")
149
+ console.print(json.dumps(mcp_config, indent=2))
150
+
151
+
138
152
  def _configure_ide(mcp_config):
139
153
  """Asks user for their IDE and configures it automatically."""
140
154
  questions = [
141
155
  {
142
156
  "type": "confirm",
143
- "message": "Automatically configure your IDE/CLI (VS Code, Cursor, Windsurf, Claude, Gemini, Cline, RooCode, ChatGPT Codex, Amazon Q Developer, Aider, Kiro, Antigravity)?",
157
+ "message": "Automatically configure your IDE/CLI (VS Code, Cursor, Windsurf, Claude, Gemini, Cline, RooCode, ChatGPT Codex, Amazon Q Developer, Aider, Kiro, Antigravity, OpenCode)?",
144
158
  "name": "configure_ide",
145
159
  "default": True,
146
160
  }
@@ -154,7 +168,7 @@ def _configure_ide(mcp_config):
154
168
  {
155
169
  "type": "list",
156
170
  "message": "Choose your IDE/CLI to configure:",
157
- "choices": ["VS Code", "Cursor", "Windsurf", "Claude code", "Gemini CLI", "ChatGPT Codex", "Cline", "RooCode", "Amazon Q Developer", "JetBrainsAI", "Aider", "Kiro", "Antigravity", "None of the above"],
171
+ "choices": ["VS Code", "Cursor", "Windsurf", "Claude code", "Gemini CLI", "ChatGPT Codex", "Cline", "RooCode", "Amazon Q Developer", "JetBrainsAI", "Aider", "Kiro", "Antigravity", "OpenCode", "None of the above"],
158
172
  "name": "ide_choice",
159
173
  }
160
174
  ]
@@ -165,6 +179,13 @@ def _configure_ide(mcp_config):
165
179
  console.print("\n[cyan]You can add the MCP server manually to your IDE/CLI.[/cyan]")
166
180
  return
167
181
 
182
+ if ide_choice == "OpenCode":
183
+ _print_opencode_mcp_instructions(mcp_config)
184
+ console.print(
185
+ "\n[green]When you have pasted this into OpenCode, reload MCP and run "
186
+ "`cgc mcp start` from a terminal to verify the server starts cleanly.[/green]"
187
+ )
188
+ return
168
189
 
169
190
  if ide_choice in ["VS Code", "Cursor", "Claude code", "Gemini CLI", "ChatGPT Codex", "Cline", "Windsurf", "RooCode", "Amazon Q Developer", "JetBrainsAI", "Aider", "Kiro", "Antigravity"]:
170
191
  console.print(f"\n[bold cyan]Configuring for {ide_choice}...[/bold cyan]")
@@ -3,18 +3,16 @@
3
3
  Core database management module.
4
4
 
5
5
  Supports Neo4j, FalkorDB Lite, remote FalkorDB, and KùzuDB backends.
6
- Use DATABASE_TYPE environment variable to switch:
7
- - DATABASE_TYPE=kuzudb - Uses embedded KùzuDB (Recommended for cross-platform zero-config)
8
- - DATABASE_TYPE=falkordb - Uses embedded FalkorDB Lite (Unix-only)
9
- - DATABASE_TYPE=falkordb-remote - Uses a remote/hosted FalkorDB server over TCP
10
- - DATABASE_TYPE=neo4j - Uses Neo4j server
11
- - If not set, auto-detects based on what's available
12
-
13
- Priority (no DATABASE_TYPE set):
14
- 1. FalkorDB Lite (Unix + Python 3.12+ + falkordblite installed)
15
- 2. KùzuDB (cross-platform fallback)
16
- 3. Remote FalkorDB (if FALKORDB_HOST is set)
17
- 4. Neo4j (if credentials are configured)
6
+
7
+ Explicit backend selection (see ``get_database_manager``):
8
+ - ``CGC_RUNTIME_DB_TYPE`` — per-invocation override (CLI ``--database`` / MCP resolved context).
9
+ - ``DEFAULT_DATABASE`` configured default from ``cgc config db …`` / CodeGraphContext ``.env``.
10
+
11
+ When neither is set, implicit selection:
12
+ - Remote FalkorDB if ``FALKORDB_HOST`` is set (explicit remote signal).
13
+ - Else **Unix**: FalkorDB Lite when Python 3.12+ and ``falkordblite`` work; else KùzuDB if
14
+ installed; else Neo4j if credentials exist.
15
+ - Else **Windows**: KùzuDB if installed; else Neo4j if credentials exist.
18
16
  """
19
17
  import os
20
18
  import platform
@@ -59,22 +57,14 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
59
57
  Factory function to get the appropriate database manager based on configuration.
60
58
 
61
59
  Selection logic:
62
- 1. Runtime Override: 'CGC_RUNTIME_DB_TYPE' (set via --database flag)
63
- 2. Configured Default: 'DEFAULT_DATABASE' (set via 'cgc default database')
64
- 3. Legacy Env Var: 'DATABASE_TYPE'
65
- 4. Implicit Default: KùzuDB (Best cross-platform zero-config)
66
- 5. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
67
- 6. Fallback Default: FalkorDB Lite (if Unix and available)
68
- 7. Fallback: Neo4j (if configured)
60
+ 1. Runtime override: ``CGC_RUNTIME_DB_TYPE`` (CLI ``--database``, MCP context).
61
+ 2. Configured default: ``DEFAULT_DATABASE`` (``cgc config db …``, CodeGraphContext ``.env``).
62
+ 3. Implicit: ``FALKORDB_HOST`` remote FalkorDB; else Unix → FalkorDB Lite when available,
63
+ then KùzuDB; Windows KùzuDB first; Neo4j if configured.
69
64
  """
70
65
  from codegraphcontext.utils.debug_log import info_logger
71
66
 
72
- # 1. Runtime Override (CLI flag) or Config/Env
73
- db_type = os.getenv('CGC_RUNTIME_DB_TYPE')
74
- if not db_type:
75
- db_type = os.getenv('DEFAULT_DATABASE')
76
- if not db_type:
77
- db_type = os.getenv('DATABASE_TYPE')
67
+ db_type = os.getenv("CGC_RUNTIME_DB_TYPE") or os.getenv("DEFAULT_DATABASE")
78
68
 
79
69
  if db_type:
80
70
  db_type = db_type.lower()
@@ -124,14 +114,13 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
124
114
  else:
125
115
  raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', or 'neo4j'.")
126
116
 
127
- # 4. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
128
- # This takes priority over zero-config local backends because it's an explicit signal
117
+ # Implicit: remote FalkorDB when FALKORDB_HOST is set (explicit infra signal)
129
118
  if _is_falkordb_remote_configured():
130
119
  from .database_falkordb_remote import FalkorDBRemoteManager
131
120
  info_logger("Using remote FalkorDB (auto-detected via FALKORDB_HOST)")
132
121
  return FalkorDBRemoteManager()
133
122
 
134
- # 5. Implicit Default -> FalkorDB Lite (Unix Zero Config)
123
+ # Implicit: FalkorDB Lite on Unix when available (typical embedded default there)
135
124
  if _is_falkordb_available():
136
125
  from .database_falkordb import FalkorDBManager, FalkorDBUnavailableError
137
126
  try:
@@ -145,13 +134,13 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
145
134
  )
146
135
  # fall through to KùzuDB below
147
136
 
148
- # 6. Implicit Default -> KùzuDB (Best Zero Config)
137
+ # Implicit: KùzuDB (typical on Windows; Unix fallback when Falkor Lite unavailable)
149
138
  if _is_kuzudb_available():
150
139
  from .database_kuzu import KuzuDBManager
151
140
  info_logger(f"Using KùzuDB (default) at {db_path or 'default path'}")
152
141
  return KuzuDBManager(db_path=db_path)
153
142
 
154
- # 7. Fallback if configured
143
+ # Implicit: Neo4j when configured
155
144
  if _is_neo4j_configured():
156
145
  from .database import DatabaseManager
157
146
  info_logger("Using Neo4j Server (auto-detected)")
@@ -5,6 +5,16 @@ import logging
5
5
 
6
6
  logger = logging.getLogger(__name__)
7
7
 
8
+
9
+ def _github_headers() -> dict:
10
+ """Return GitHub API headers, including auth token if available."""
11
+ import os
12
+ headers = {"Accept": "application/vnd.github.v3+json"}
13
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
14
+ if token:
15
+ headers["Authorization"] = f"token {token}"
16
+ return headers
17
+
8
18
  GITHUB_ORG = "CodeGraphContext"
9
19
  GITHUB_REPO = "CodeGraphContext"
10
20
  REGISTRY_API_URL = f"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_REPO}/releases"
@@ -27,7 +37,7 @@ class BundleRegistry:
27
37
 
28
38
  # 1. Fetch on-demand bundles from manifest
29
39
  try:
30
- response = requests.get(MANIFEST_URL, timeout=10)
40
+ response = requests.get(MANIFEST_URL, headers=_github_headers(), timeout=10)
31
41
  if response.status_code == 200:
32
42
  manifest = response.json()
33
43
  if manifest.get('bundles'):
@@ -43,7 +53,7 @@ class BundleRegistry:
43
53
 
44
54
  # 2. Fetch weekly pre-indexed bundles
45
55
  try:
46
- response = requests.get(REGISTRY_API_URL, timeout=10)
56
+ response = requests.get(REGISTRY_API_URL, headers=_github_headers(), timeout=10)
47
57
  if response.status_code == 200:
48
58
  releases = response.json()
49
59
 
@@ -384,7 +384,7 @@ class CGCBundle:
384
384
  if repo_path:
385
385
  query = """
386
386
  MATCH (n)
387
- WHERE n.path STARTS WITH $repo_path OR n.path STARTS WITH $repo_path
387
+ WHERE n.path STARTS WITH $repo_path
388
388
  RETURN n, labels(n) as labels
389
389
  """
390
390
  params = {"repo_path": str(repo_path.resolve())}
@@ -437,8 +437,8 @@ class CGCBundle:
437
437
  if repo_path:
438
438
  query = """
439
439
  MATCH (n)-[r]->(m)
440
- WHERE (n.path STARTS WITH $repo_path OR n.path STARTS WITH $repo_path)
441
- OR (m.path STARTS WITH $repo_path OR m.path STARTS WITH $repo_path)
440
+ WHERE (n.path STARTS WITH $repo_path)
441
+ OR (m.path STARTS WITH $repo_path)
442
442
  RETURN n, r, m, type(r) as rel_type
443
443
  """
444
444
  params = {"repo_path": str(repo_path.resolve())}