codegraphcontext 0.4.11__py3-none-any.whl → 0.4.13__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 (28) hide show
  1. codegraphcontext/api/router.py +1 -1
  2. codegraphcontext/cli/config_manager.py +10 -1
  3. codegraphcontext/cli/main.py +166 -109
  4. codegraphcontext/cli/registry_commands.py +45 -24
  5. codegraphcontext/cli/setup_wizard.py +2 -2
  6. codegraphcontext/core/__init__.py +16 -3
  7. codegraphcontext/core/bundle_registry.py +6 -44
  8. codegraphcontext/core/cgc_bundle.py +142 -46
  9. codegraphcontext/core/database_falkordb.py +13 -6
  10. codegraphcontext/core/database_nornic.py +11 -3
  11. codegraphcontext/tools/advanced_language_query_tool.py +11 -12
  12. codegraphcontext/tools/code_finder.py +17 -0
  13. codegraphcontext/tools/graph_builder.py +4 -0
  14. codegraphcontext/tools/indexing/persistence/writer.py +7 -7
  15. codegraphcontext/tools/indexing/pre_scan.py +2 -0
  16. codegraphcontext/tools/indexing/resolution/calls.py +2 -1
  17. codegraphcontext/tools/languages/elisp.py +682 -0
  18. codegraphcontext/tools/query_tool_languages/elisp_toolkit.py +73 -0
  19. codegraphcontext/tools/scip_indexer.py +1 -11
  20. codegraphcontext/tools/tree_sitter_parser.py +4 -0
  21. codegraphcontext/utils/git_utils.py +13 -0
  22. codegraphcontext/utils/tree_sitter_manager.py +5 -0
  23. {codegraphcontext-0.4.11.dist-info → codegraphcontext-0.4.13.dist-info}/METADATA +14 -13
  24. {codegraphcontext-0.4.11.dist-info → codegraphcontext-0.4.13.dist-info}/RECORD +28 -26
  25. {codegraphcontext-0.4.11.dist-info → codegraphcontext-0.4.13.dist-info}/WHEEL +0 -0
  26. {codegraphcontext-0.4.11.dist-info → codegraphcontext-0.4.13.dist-info}/entry_points.txt +0 -0
  27. {codegraphcontext-0.4.11.dist-info → codegraphcontext-0.4.13.dist-info}/licenses/LICENSE +0 -0
  28. {codegraphcontext-0.4.11.dist-info → codegraphcontext-0.4.13.dist-info}/top_level.txt +0 -0
@@ -87,7 +87,7 @@ async def execute_query(
87
87
  server: MCPServer = Depends(get_server)
88
88
  ):
89
89
  result = await server.handle_tool_call("execute_cypher_query", {
90
- "query": request.query,
90
+ "cypher_query": request.query,
91
91
  "params": request.params
92
92
  })
93
93
 
@@ -31,6 +31,7 @@ DEFAULT_CONFIG = {
31
31
  "FALKORDB_PATH": str(CONFIG_DIR / "global" / "db" / "falkordb"),
32
32
  "FALKORDB_SOCKET_PATH": str(CONFIG_DIR / "global" / "db" / "falkordb.sock"),
33
33
  "LADYBUGDB_PATH": str(CONFIG_DIR / "global" / "db" / "ladybugdb"),
34
+ "KUZUDB_PATH": str(CONFIG_DIR / "global" / "db" / "kuzudb"),
34
35
  "INDEX_VARIABLES": "true",
35
36
  "ALLOW_DB_DELETION": "false",
36
37
  "DEBUG_LOGS": "false",
@@ -62,6 +63,8 @@ DEFAULT_CONFIG = {
62
63
  "ENABLE_VECTOR_RESOLVE": "false",
63
64
  "CGC_EMBEDDING_MODEL": "local",
64
65
  "CGC_EMBEDDING_BATCH_SIZE": "256",
66
+ # Default fuzzy matching behavior for `cgc find name` (overridable per-command with --fuzzy/--no-fuzzy)
67
+ "FUZZY_SEARCH": "true",
65
68
  }
66
69
 
67
70
  # Configuration key descriptions
@@ -70,6 +73,7 @@ CONFIG_DESCRIPTIONS = {
70
73
  "FALKORDB_PATH": "Path to FalkorDB database file",
71
74
  "FALKORDB_SOCKET_PATH": "Path to FalkorDB Unix socket",
72
75
  "LADYBUGDB_PATH": "Path to LadybugDB database directory",
76
+ "KUZUDB_PATH": "Path to KuzuDB database directory",
73
77
  "INDEX_VARIABLES": "Index variable nodes in the graph (lighter graph if false)",
74
78
  "ALLOW_DB_DELETION": "Allow full database deletion commands",
75
79
  "DEBUG_LOGS": "Enable debug logging (for development/troubleshooting)",
@@ -123,6 +127,10 @@ CONFIG_DESCRIPTIONS = {
123
127
  "Number of function texts to embed per batch when ENABLE_VECTOR_RESOLVE=true. "
124
128
  "Larger values are faster but use more RAM. Default: 256. Reduce to 64 if you hit memory errors."
125
129
  ),
130
+ "FUZZY_SEARCH": (
131
+ "Enable fuzzy matching by default for `cgc find name` (true|false). "
132
+ "Per-invocation overrides are available via --fuzzy / --no-fuzzy."
133
+ ),
126
134
  }
127
135
 
128
136
  # Valid values for each config key
@@ -143,6 +151,7 @@ CONFIG_VALIDATORS = {
143
151
  "ENABLE_INHERIT_RESOLVE": ["true", "false"],
144
152
  "ENABLE_VECTOR_RESOLVE": ["true", "false"],
145
153
  "CGC_EMBEDDING_MODEL": ["local", "openai"],
154
+ "FUZZY_SEARCH": ["true", "false"],
146
155
  }
147
156
  DEFAULT_CGCIGNORE_PATTERNS = """\
148
157
  # Default .cgcignore patterns
@@ -447,7 +456,7 @@ def validate_config_value(key: str, value: str) -> tuple[bool, Optional[str]]:
447
456
  except Exception as e:
448
457
  return False, f"Cannot create log directory: {e}"
449
458
 
450
- if key in ("FALKORDB_PATH", "FALKORDB_SOCKET_PATH", "LADYBUGDB_PATH"):
459
+ if key in ("FALKORDB_PATH", "FALKORDB_SOCKET_PATH", "LADYBUGDB_PATH", "KUZUDB_PATH"):
451
460
  # Validate path is writable
452
461
  db_path = Path(normalize_config_path(value, absolute=True))
453
462
  try:
@@ -250,7 +250,7 @@ def context_list():
250
250
  @context_app.command("create")
251
251
  def context_create(
252
252
  name: str = typer.Argument(..., help="Name of the new context"),
253
- database: str = typer.Option(None, "--database", "-d", help="Database backend (falkordb, kuzudb, neo4j). Defaults to DEFAULT_DATABASE from config."),
253
+ database: str = typer.Option(None, "--database", "--db", "-db", "-d", help="Database backend (falkordb, kuzudb, neo4j). Defaults to DEFAULT_DATABASE from config."),
254
254
  db_path: str = typer.Option(None, "--db-path", help="Explicit path for the DB (defaults to ~/.codegraphcontext/contexts/<name>/db)"),
255
255
  ):
256
256
  """Create a new logical context."""
@@ -285,23 +285,20 @@ def context_default(
285
285
  # CREDENTIALS LOADING PRECEDENCE
286
286
  # ============================================================================
287
287
 
288
- def _load_credentials():
288
+ def _load_credentials(cli_context_flag: Optional[str] = None):
289
289
  """
290
290
  Loads configuration and credentials from various sources into environment variables.
291
291
  Uses per-variable precedence - each variable is loaded from the highest priority source.
292
292
  Priority order (highest to lowest):
293
293
  1. Runtime environment variables (shell/CI)
294
- 2. Local `.env` in project directory (project-specific overrides)
294
+ 2. Local `.codegraphcontext/.env` and `.env` in the current project directory (project-specific overrides)
295
295
  3. Global `~/.codegraphcontext/.env` (user defaults, including `cgc config set`)
296
296
  4. Local `mcp.json` env vars (project defaults)
297
- 1. Local `mcp.json` env vars (highest - explicit MCP server config)
298
- 2. ``<cwd>/.codegraphcontext/.env`` only (no parent-directory walk)
299
- 3. Global `~/.codegraphcontext/.env` (lowest - user defaults)
300
297
 
301
- Step 2 skips duplicate loading when that file is the same path as the global file.
302
- Arbitrary repo-root `.env` files are not loaded—only CodeGraphContext config paths.
298
+ Duplicate loading is skipped when the local file resolves to the same path as the global file.
299
+ Arbitrary parent directory `.env` files are not loaded—ensuring isolation.
303
300
  """
304
- from dotenv import dotenv_values, find_dotenv
301
+ from dotenv import dotenv_values
305
302
  from codegraphcontext.cli.config_manager import (
306
303
  ensure_config_dir,
307
304
  codegraphcontext_dotenv_at_cwd,
@@ -336,7 +333,7 @@ def _load_credentials():
336
333
  mcp_file_path = Path.cwd() / "mcp.json"
337
334
  if mcp_file_path.exists():
338
335
  try:
339
- with open(mcp_file_path, "r") as f:
336
+ with open(mcp_file_path, "r", encoding="utf-8", errors="replace") as f:
340
337
  mcp_config = json.load(f)
341
338
  server_env = mcp_config.get("mcpServers", {}).get("CodeGraphContext", {}).get("env", {})
342
339
  if isinstance(server_env, dict):
@@ -355,15 +352,17 @@ def _load_credentials():
355
352
  global_env_path = Path.home() / ".codegraphcontext" / ".env"
356
353
  if global_env_path.exists():
357
354
  try:
358
- _append_source(str(global_env_path), dotenv_values(str(global_env_path)))
355
+ with open(global_env_path, "r", encoding="utf-8", errors="replace") as f:
356
+ _append_source(str(global_env_path), dotenv_values(stream=f))
359
357
  except Exception as e:
360
358
  console.print(f"[yellow]Warning: Could not load global .env: {e}[/yellow]")
361
359
 
362
- # 2. Local project .env (project-specific overrides)
360
+ # 2. Local project .env (project-specific overrides - restricted to CWD only, no parent walk)
363
361
  try:
364
- dotenv_path = find_dotenv(usecwd=True, raise_error_if_not_found=False)
365
- if dotenv_path:
366
- _append_source(str(dotenv_path), dotenv_values(dotenv_path))
362
+ local_dot_env = Path.cwd() / ".env"
363
+ if local_dot_env.exists() and local_dot_env.resolve() != global_env_path.resolve():
364
+ with open(local_dot_env, "r", encoding="utf-8", errors="replace") as f:
365
+ _append_source(str(local_dot_env), dotenv_values(stream=f))
367
366
  except Exception as e:
368
367
  console.print(f"[yellow]Warning: Could not load .env from current directory: {e}[/yellow]")
369
368
 
@@ -371,8 +370,9 @@ def _load_credentials():
371
370
  try:
372
371
  local_cgc_env = codegraphcontext_dotenv_at_cwd(Path.cwd())
373
372
  if local_cgc_env and local_cgc_env.resolve() != global_env_path.resolve():
374
- config_sources.append(dotenv_values(str(local_cgc_env)))
375
- config_source_names.append(str(local_cgc_env))
373
+ with open(local_cgc_env, "r", encoding="utf-8", errors="replace") as f:
374
+ vals = dotenv_values(stream=f)
375
+ _append_source(str(local_cgc_env), vals)
376
376
  except Exception as e:
377
377
  console.print(
378
378
  f"[yellow]Warning: Could not load .codegraphcontext/.env at cwd: {e}[/yellow]"
@@ -418,31 +418,62 @@ def _load_credentials():
418
418
  )
419
419
 
420
420
 
421
- # Show which database is actually being used.
422
- # When CGC_RUNTIME_DB_TYPE or DEFAULT_DATABASE is set, trust it. Otherwise
423
- # call get_database_manager() so the banner matches factory fallbacks.
424
- runtime_db = os.environ.get("CGC_RUNTIME_DB_TYPE")
425
- database_type = os.environ.get("DATABASE_TYPE")
426
- default_database = os.environ.get("DEFAULT_DATABASE")
421
+ # Detect the context to see if it specifies a custom database
422
+ if cli_context_flag is None:
423
+ import sys
424
+ for i, arg in enumerate(sys.argv):
425
+ if arg in ("--context", "-c"):
426
+ if i + 1 < len(sys.argv):
427
+ cli_context_flag = sys.argv[i + 1]
428
+ break
429
+ elif arg.startswith("--context="):
430
+ cli_context_flag = arg.split("=", 1)[1]
431
+ break
432
+
433
+ from codegraphcontext.cli.config_manager import resolve_context
434
+ ctx = None
435
+ try:
436
+ ctx = resolve_context(cli_context_flag)
437
+ except Exception:
438
+ pass
427
439
 
428
- explicit_db = runtime_db or database_type or default_database
440
+ # Determine if there is a runtime database override.
441
+ runtime_db = os.environ.get("CGC_RUNTIME_DB_TYPE")
442
+ has_runtime_override = (
443
+ runtime_db is not None
444
+ or "DATABASE_TYPE" in runtime_env
445
+ or "DEFAULT_DATABASE" in runtime_env
446
+ )
429
447
 
448
+ # If there is no runtime override, but the context defines a database,
449
+ # set DEFAULT_DATABASE to the context database to ensure that's what gets initialized.
450
+ if not has_runtime_override and ctx and ctx.mode != "global" and ctx.database:
451
+ os.environ["DEFAULT_DATABASE"] = ctx.database
452
+
453
+ # Now select the database based on precedence:
454
+ # 1. CGC_RUNTIME_DB_TYPE
455
+ # 2. DATABASE_TYPE or DEFAULT_DATABASE from runtime environment (shell variables)
456
+ # 3. Context database
457
+ # 4. DATABASE_TYPE or DEFAULT_DATABASE from merged config files (.env files)
458
+ # 5. Auto-detect fallback
430
459
  if runtime_db:
460
+ default_db = runtime_db.lower()
431
461
  db_source = "runtime-env (CGC_RUNTIME_DB_TYPE)"
432
462
  elif "DATABASE_TYPE" in runtime_env:
463
+ default_db = runtime_env["DATABASE_TYPE"].lower()
433
464
  db_source = "environment (DATABASE_TYPE)"
434
465
  elif "DEFAULT_DATABASE" in runtime_env:
466
+ default_db = runtime_env["DEFAULT_DATABASE"].lower()
435
467
  db_source = "environment (DEFAULT_DATABASE)"
436
- elif database_type and "DATABASE_TYPE" in key_source_map:
468
+ elif not has_runtime_override and ctx and ctx.mode != "global" and ctx.database:
469
+ default_db = ctx.database.lower()
470
+ db_source = f"context ({ctx.context_name or 'resolved'})"
471
+ elif os.environ.get("DATABASE_TYPE") and "DATABASE_TYPE" in key_source_map:
472
+ default_db = os.environ["DATABASE_TYPE"].lower()
437
473
  db_source = key_source_map["DATABASE_TYPE"]
438
- elif default_database and "DEFAULT_DATABASE" in key_source_map:
474
+ elif os.environ.get("DEFAULT_DATABASE") and "DEFAULT_DATABASE" in key_source_map:
475
+ default_db = os.environ["DEFAULT_DATABASE"].lower()
439
476
  db_source = key_source_map["DEFAULT_DATABASE"]
440
- else:
441
- db_source = "auto-detect"
442
- explicit_db = runtime_db or os.environ.get("DEFAULT_DATABASE")
443
-
444
- if explicit_db:
445
- default_db = explicit_db.lower()
446
477
  else:
447
478
  # No explicit choice — ask the factory which backend it will use
448
479
  try:
@@ -455,6 +486,7 @@ def _load_credentials():
455
486
  default_db = "falkordb" if _is_falkordb_available() else "kuzudb"
456
487
  db_source = "auto-detect"
457
488
 
489
+ # Print selection banner
458
490
  if default_db == "neo4j":
459
491
  has_neo4j_creds = all([
460
492
  os.environ.get("NEO4J_URI"),
@@ -473,20 +505,14 @@ def _load_credentials():
473
505
  console.print(f"[cyan]Using database: falkordb (source: {db_source})[/cyan]")
474
506
  elif default_db == "kuzudb":
475
507
  console.print(f"[cyan]Using database: kuzudb (source: {db_source})[/cyan]")
508
+ elif default_db == "ladybugdb":
509
+ console.print(f"[cyan]Using database: ladybugdb (source: {db_source})[/cyan]")
476
510
  elif default_db == "falkordb-remote":
477
511
  host = os.environ.get("FALKORDB_HOST")
478
512
  if host:
479
513
  console.print(f"[cyan]Using database: falkordb-remote (source: {db_source}, host: {host})[/cyan]")
480
514
  else:
481
515
  console.print("[yellow]⚠ DATABASE_TYPE=falkordb-remote but FALKORDB_HOST not set.[/yellow]")
482
- elif default_db == "falkordb":
483
- if os.environ.get("FALKORDB_HOST"):
484
- console.print(f"[cyan]Using database: falkordb-remote (source: {db_source}, host: {os.environ.get('FALKORDB_HOST')})[/cyan]")
485
- else:
486
- console.print(f"[cyan]Using database: falkordb (source: {db_source})[/cyan]")
487
- console.print(
488
- "[yellow]⚠ DEFAULT_DATABASE=falkordb-remote but FALKORDB_HOST not set.[/yellow]"
489
- )
490
516
  else:
491
517
  console.print(f"[cyan]Using database: {default_db} (source: {db_source})[/cyan]")
492
518
 
@@ -543,7 +569,7 @@ def config_reset():
543
569
  console.print("[yellow]Reset cancelled[/yellow]")
544
570
 
545
571
  @config_app.command("db")
546
- def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j', 'falkordb', 'falkordb-remote', or 'kuzudb'")):
572
+ def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j', 'falkordb', 'falkordb-remote', 'kuzudb', or 'ladybugdb'")):
547
573
  """
548
574
  Quickly switch the default database backend.
549
575
 
@@ -608,7 +634,7 @@ def bundle_export(
608
634
  if repo_path:
609
635
  console.print(f"[dim]Repository: {repo_path}[/dim]")
610
636
  else:
611
- console.print(f"[dim]Exporting all repositories[/dim]")
637
+ console.print("[dim]Exporting all repositories[/dim]")
612
638
 
613
639
  bundle = CGCBundle(db_manager)
614
640
  success, message = bundle.export_to_bundle(
@@ -719,7 +745,7 @@ def bundle_load(
719
745
 
720
746
  # Try to download from registry
721
747
  console.print(f"[yellow]Bundle '{bundle_name}' not found locally.[/yellow]")
722
- console.print(f"[cyan]Attempting to download from registry...[/cyan]")
748
+ console.print("[cyan]Attempting to download from registry...[/cyan]")
723
749
 
724
750
  try:
725
751
  from .registry_commands import download_bundle
@@ -739,7 +765,7 @@ def bundle_load(
739
765
 
740
766
  except Exception as e:
741
767
  console.print(f"[bold red]Error: {e}[/bold red]")
742
- console.print(f"[dim]Use 'cgc registry list' to see available bundles[/dim]")
768
+ console.print("[dim]Use 'cgc registry list' to see available bundles[/dim]")
743
769
  raise typer.Exit(code=1)
744
770
 
745
771
  # Shortcut commands at root level
@@ -859,7 +885,7 @@ def registry_download(
859
885
  bundle_path = download_bundle(name, output_dir, auto_load=load)
860
886
 
861
887
  if load and bundle_path:
862
- console.print(f"\n[cyan]Loading bundle...[/cyan]")
888
+ console.print("\n[cyan]Loading bundle...[/cyan]")
863
889
  bundle_import(bundle_path, clear=False)
864
890
 
865
891
  @registry_app.command("request")
@@ -912,13 +938,13 @@ def doctor():
912
938
  if config_manager.CONFIG_FILE.exists():
913
939
  console.print(f" [green]✓[/green] Config loaded from {config_manager.CONFIG_FILE}")
914
940
  else:
915
- console.print(f" [yellow]ℹ[/yellow] No .env config found, using defaults")
941
+ console.print(" [yellow]ℹ[/yellow] No .env config found, using defaults")
916
942
  console.print(f" [dim]Config will be created at: {config_manager.CONFIG_FILE}[/dim]")
917
943
 
918
944
  if config_manager.CONTEXT_CONFIG_FILE.exists():
919
945
  console.print(f" [green]✓[/green] Context config loaded from {config_manager.CONTEXT_CONFIG_FILE}")
920
946
  else:
921
- console.print(f" [yellow]ℹ[/yellow] No Context config found")
947
+ console.print(" [yellow]ℹ[/yellow] No Context config found")
922
948
  console.print(f" [dim]Context config will be auto-generated at: {config_manager.CONTEXT_CONFIG_FILE}[/dim]")
923
949
 
924
950
  # Validate each config value
@@ -929,12 +955,12 @@ def doctor():
929
955
  invalid_configs.append(f"{key}: {error_msg}")
930
956
 
931
957
  if invalid_configs:
932
- console.print(f" [red]✗[/red] Invalid configuration values found:")
958
+ console.print(" [red]✗[/red] Invalid configuration values found:")
933
959
  for err in invalid_configs:
934
960
  console.print(f" - {err}")
935
961
  all_checks_passed = False
936
962
  else:
937
- console.print(f" [green]✓[/green] All configuration values are valid")
963
+ console.print(" [green]✓[/green] All configuration values are valid")
938
964
  except Exception as e:
939
965
  console.print(f" [red]✗[/red] Configuration error: {e}")
940
966
  all_checks_passed = False
@@ -974,10 +1000,10 @@ def doctor():
974
1000
  console.print(" Start Neo4j Desktop or run: docker run -d -p 7687:7687 -p 7474:7474 neo4j")
975
1001
  all_checks_passed = False
976
1002
 
977
- console.print(f" [cyan]Testing Neo4j authentication/query...[/cyan]")
1003
+ console.print(" [cyan]Testing Neo4j authentication/query...[/cyan]")
978
1004
  is_connected, error_msg = DatabaseManager.test_connection(uri, username, password, database=database_name)
979
1005
  if is_connected:
980
- console.print(f" [green]✓[/green] Neo4j connection successful")
1006
+ console.print(" [green]✓[/green] Neo4j connection successful")
981
1007
  else:
982
1008
  console.print(f" [red]✗[/red] Neo4j connection failed (source: {db_source})")
983
1009
  console.print(f" Reason: {error_msg}")
@@ -986,28 +1012,28 @@ def doctor():
986
1012
  from importlib.util import find_spec
987
1013
 
988
1014
  if find_spec("kuzu") is not None:
989
- console.print(f" [green]✓[/green] KuzuDB is installed")
1015
+ console.print(" [green]✓[/green] KuzuDB is installed")
990
1016
  else:
991
- console.print(f" [red]✗[/red] KuzuDB is not installed")
992
- console.print(f" Run: pip install kuzu")
1017
+ console.print(" [red]✗[/red] KuzuDB is not installed")
1018
+ console.print(" Run: pip install kuzu")
993
1019
  all_checks_passed = False
994
1020
  elif default_db == "ladybugdb":
995
1021
  from importlib.util import find_spec
996
1022
 
997
1023
  if find_spec("ladybug") is not None:
998
- console.print(f" [green]✓[/green] LadybugDB core (ladybug) is installed")
1024
+ console.print(" [green]✓[/green] LadybugDB core (ladybug) is installed")
999
1025
  else:
1000
- console.print(f" [red]✗[/red] LadybugDB core (ladybug) is not installed")
1001
- console.print(f" Run: pip install ladybug")
1026
+ console.print(" [red]✗[/red] LadybugDB core (ladybug) is not installed")
1027
+ console.print(" Run: pip install ladybug")
1002
1028
  all_checks_passed = False
1003
1029
  else:
1004
1030
  # FalkorDB
1005
1031
  try:
1006
1032
  import falkordb
1007
- console.print(f" [green]✓[/green] FalkorDB Lite is installed")
1033
+ console.print(" [green]✓[/green] FalkorDB Lite is installed")
1008
1034
  except ImportError:
1009
- console.print(f" [yellow]⚠[/yellow] FalkorDB Lite not installed (Python 3.12+ only)")
1010
- console.print(f" Run: pip install falkordblite")
1035
+ console.print(" [yellow]⚠[/yellow] FalkorDB Lite not installed (Python 3.12+ only)")
1036
+ console.print(" Run: pip install falkordblite")
1011
1037
  except Exception as e:
1012
1038
  console.print(f" [red]✗[/red] Database check error: {e}")
1013
1039
  all_checks_passed = False
@@ -1016,11 +1042,11 @@ def doctor():
1016
1042
  console.print("\n[bold]3. Checking Tree-Sitter Installation...[/bold]")
1017
1043
  try:
1018
1044
  from tree_sitter import Language, Parser
1019
- console.print(f" [green]✓[/green] tree-sitter is installed")
1045
+ console.print(" [green]✓[/green] tree-sitter is installed")
1020
1046
 
1021
1047
  try:
1022
1048
  from tree_sitter_language_pack import get_language
1023
- console.print(f" [green]✓[/green] tree-sitter-language-pack is installed")
1049
+ console.print(" [green]✓[/green] tree-sitter-language-pack is installed")
1024
1050
 
1025
1051
  from codegraphcontext.utils.tree_sitter_manager import LANGUAGE_ALIASES, LANGUAGE_PACK_NAMES
1026
1052
  all_langs = sorted(set(LANGUAGE_ALIASES.values()))
@@ -1038,7 +1064,7 @@ def doctor():
1038
1064
  if unavailable:
1039
1065
  console.print(f" [yellow]⚠[/yellow] Unavailable: {', '.join(unavailable)}")
1040
1066
  except ImportError:
1041
- console.print(f" [red]✗[/red] tree-sitter-language-pack not installed")
1067
+ console.print(" [red]✗[/red] tree-sitter-language-pack not installed")
1042
1068
  all_checks_passed = False
1043
1069
  except ImportError as e:
1044
1070
  console.print(f" [red]✗[/red] tree-sitter not installed: {e}")
@@ -1056,12 +1082,12 @@ def doctor():
1056
1082
  try:
1057
1083
  test_file.touch()
1058
1084
  test_file.unlink()
1059
- console.print(f" [green]✓[/green] Config directory is writable")
1085
+ console.print(" [green]✓[/green] Config directory is writable")
1060
1086
  except Exception as e:
1061
1087
  console.print(f" [red]✗[/red] Config directory not writable: {e}")
1062
1088
  all_checks_passed = False
1063
1089
  else:
1064
- console.print(f" [yellow]⚠[/yellow] Config directory doesn't exist, will be created on first use")
1090
+ console.print(" [yellow]⚠[/yellow] Config directory doesn't exist, will be created on first use")
1065
1091
  except Exception as e:
1066
1092
  console.print(f" [red]✗[/red] Permission check error: {e}")
1067
1093
  all_checks_passed = False
@@ -1073,7 +1099,7 @@ def doctor():
1073
1099
  if cgc_path:
1074
1100
  console.print(f" [green]✓[/green] cgc command found at: {cgc_path}")
1075
1101
  else:
1076
- console.print(f" [yellow]⚠[/yellow] cgc command not in PATH (using python -m cgc)")
1102
+ console.print(" [yellow]⚠[/yellow] cgc command not in PATH (using python -m cgc)")
1077
1103
 
1078
1104
  # Final summary
1079
1105
  console.print("\n" + "=" * 60)
@@ -1388,17 +1414,22 @@ app.add_typer(find_app, name="find")
1388
1414
  @find_app.command("name")
1389
1415
  def find_by_name(
1390
1416
  ctx: typer.Context,
1391
- name: str = typer.Argument(..., help="Exact name to search for"),
1417
+ name: str = typer.Argument(..., help="Name to search for"),
1392
1418
  type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type (function, class, file, module)"),
1419
+ fuzzy: Optional[bool] = typer.Option(None, "--fuzzy/--no-fuzzy", help="Enable/disable fuzzy matching for this command. Overrides the FUZZY_SEARCH config value (default: true)."),
1393
1420
  visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1394
1421
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1395
1422
  ):
1396
1423
  """
1397
- Find code elements by exact name.
1398
-
1424
+ Find code elements by name.
1425
+
1426
+ Fuzzy matching is enabled by default (configurable via the FUZZY_SEARCH
1427
+ config key, or per-invocation with --fuzzy / --no-fuzzy).
1428
+
1399
1429
  Examples:
1400
1430
  cgc find name MyClass
1401
1431
  cgc find name calculate --type function
1432
+ cgc find name MyClass --no-fuzzy
1402
1433
  cgc find name MyClass --visual
1403
1434
  """
1404
1435
  _load_credentials()
@@ -1406,14 +1437,22 @@ def find_by_name(
1406
1437
  if not all(services[:3]):
1407
1438
  return
1408
1439
  db_manager, graph_builder, code_finder = services[:3]
1409
-
1440
+
1441
+ # Resolve effective fuzzy setting: CLI flag wins, else config, else true.
1442
+ if fuzzy is None:
1443
+ from codegraphcontext.cli.config_manager import load_config
1444
+ cfg_value = load_config().get("FUZZY_SEARCH", "true")
1445
+ fuzzy_search = str(cfg_value).strip().lower() == "true"
1446
+ else:
1447
+ fuzzy_search = fuzzy
1448
+
1410
1449
  try:
1411
1450
  results = []
1412
-
1451
+
1413
1452
  # Search based on type filter
1414
1453
  if type is None or type.lower() == 'all':
1415
- funcs = code_finder.find_by_function_name(name, fuzzy_search=False)
1416
- classes = code_finder.find_by_class_name(name, fuzzy_search=False)
1454
+ funcs = code_finder.find_by_function_name(name, fuzzy_search=fuzzy_search)
1455
+ classes = code_finder.find_by_class_name(name, fuzzy_search=fuzzy_search)
1417
1456
  variables = code_finder.find_by_variable_name(name)
1418
1457
  modules = code_finder.find_by_module_name(name)
1419
1458
  imports = code_finder.find_imports(name)
@@ -1445,11 +1484,11 @@ def find_by_name(
1445
1484
  results.append(row)
1446
1485
 
1447
1486
  elif type.lower() == 'function':
1448
- results = code_finder.find_by_function_name(name, fuzzy_search=False)
1487
+ results = code_finder.find_by_function_name(name, fuzzy_search=fuzzy_search)
1449
1488
  for r in results: r['type'] = 'Function'
1450
-
1489
+
1451
1490
  elif type.lower() == 'class':
1452
- results = code_finder.find_by_class_name(name, fuzzy_search=False)
1491
+ results = code_finder.find_by_class_name(name, fuzzy_search=fuzzy_search)
1453
1492
  for r in results: r['type'] = 'Class'
1454
1493
 
1455
1494
  elif type.lower() == 'variable':
@@ -2226,10 +2265,10 @@ def analyze_inheritance_tree(
2226
2265
 
2227
2266
  @analyze_app.command("complexity")
2228
2267
  def analyze_complexity(
2229
- path: Optional[str] = typer.Argument(None, help="Specific function name to analyze"),
2268
+ path: Optional[str] = typer.Argument(None, help="Function name or file path to analyze"),
2230
2269
  threshold: int = typer.Option(10, "--threshold", "-t", help="Complexity threshold for warnings"),
2231
2270
  limit: int = typer.Option(20, "--limit", "-l", help="Maximum results to show"),
2232
- file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path (only used when function name is provided)"),
2271
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path to scope analysis"),
2233
2272
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2234
2273
  ):
2235
2274
  """
@@ -2240,16 +2279,55 @@ def analyze_complexity(
2240
2279
  cgc analyze complexity --threshold 15 # Functions over threshold
2241
2280
  cgc analyze complexity my_function # Specific function
2242
2281
  cgc analyze complexity my_function -f file.py # Specific function in file
2282
+ cgc analyze complexity src/main.py # Most complex functions in file
2283
+ cgc analyze complexity main.py # Most complex functions in file
2284
+ cgc analyze complexity --file src/main.py # Alternative file syntax
2243
2285
  """
2244
2286
  _load_credentials()
2245
2287
  services = _initialize_services(context)
2246
2288
  if not all(services[:3]):
2247
2289
  return
2248
2290
  db_manager, graph_builder, code_finder = services[:3]
2249
-
2291
+
2292
+ _FILE_EXTENSIONS = ('.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.rb',
2293
+ '.java', '.cpp', '.c', '.cs', '.swift', '.kt', '.scala',
2294
+ '.php', '.lua', '.zig', '.ex', '.exs', '.r', '.m', '.sh')
2295
+
2296
+ def _is_file_path(value: str) -> bool:
2297
+ if '/' in value or '\\' in value:
2298
+ return True
2299
+ return any(value.endswith(ext) for ext in _FILE_EXTENSIONS)
2300
+
2301
+ def _render_complexity_table(results, title):
2302
+ if not results:
2303
+ console.print("[yellow]No complexity data available for this file[/yellow]")
2304
+ return
2305
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2306
+ table.add_column("Function", style="cyan")
2307
+ table.add_column("Complexity", style="yellow", justify="right")
2308
+ table.add_column("Location", style="dim", overflow="fold")
2309
+ for func in results:
2310
+ complexity = func.get('complexity', 0)
2311
+ color = "red" if complexity > threshold else "yellow" if complexity > threshold/2 else "green"
2312
+ fpath = func.get('path', '')
2313
+ line_str = str(func.get('line_number', ''))
2314
+ location_str = f"{fpath}:{line_str}" if line_str else fpath
2315
+ table.add_row(
2316
+ func.get('function_name', ''),
2317
+ f"[{color}]{complexity}[/{color}]",
2318
+ location_str
2319
+ )
2320
+ console.print(f"\n[bold cyan]{title}[/bold cyan]")
2321
+ console.print(table)
2322
+ console.print(f"\n[dim]{len([f for f in results if f.get('complexity', 0) > threshold])} function(s) exceed threshold[/dim]")
2323
+
2250
2324
  try:
2251
- if path:
2252
- # Specific function
2325
+ if path and _is_file_path(path):
2326
+ # File path provided as positional argument
2327
+ results = code_finder.find_most_complex_functions_in_file(path, limit)
2328
+ _render_complexity_table(results, f"Most Complex Functions in '{path}' (threshold: {threshold}):")
2329
+ elif path:
2330
+ # Specific function name
2253
2331
  result = code_finder.get_cyclomatic_complexity(path, file)
2254
2332
  if result:
2255
2333
  console.print(f"\n[bold cyan]Complexity for '{path}':[/bold cyan]")
@@ -2258,35 +2336,14 @@ def analyze_complexity(
2258
2336
  console.print(f" Line: [dim]{result.get('line_number', '')}[/dim]")
2259
2337
  else:
2260
2338
  console.print(f"[yellow]Function '{path}' not found or has no complexity data[/yellow]")
2339
+ elif file:
2340
+ # --file option without positional arg
2341
+ results = code_finder.find_most_complex_functions_in_file(file, limit)
2342
+ _render_complexity_table(results, f"Most Complex Functions in '{file}' (threshold: {threshold}):")
2261
2343
  else:
2262
- # Most complex functions
2344
+ # Global - most complex functions
2263
2345
  results = code_finder.find_most_complex_functions(limit)
2264
-
2265
- if not results:
2266
- console.print("[yellow]No complexity data available[/yellow]")
2267
- return
2268
-
2269
- table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2270
- table.add_column("Function", style="cyan")
2271
- table.add_column("Complexity", style="yellow", justify="right")
2272
- table.add_column("Location", style="dim", overflow="fold")
2273
-
2274
- for func in results:
2275
- complexity = func.get('complexity', 0)
2276
- color = "red" if complexity > threshold else "yellow" if complexity > threshold/2 else "green"
2277
- path = func.get('path', '')
2278
- line_str = str(func.get('line_number', ''))
2279
- location_str = f"{path}:{line_str}" if line_str else path
2280
-
2281
- table.add_row(
2282
- func.get('function_name', ''),
2283
- f"[{color}]{complexity}[/{color}]",
2284
- location_str
2285
- )
2286
-
2287
- console.print(f"\n[bold cyan]Most Complex Functions (threshold: {threshold}):[/bold cyan]")
2288
- console.print(table)
2289
- console.print(f"\n[dim]{len([f for f in results if f.get('complexity', 0) > threshold])} function(s) exceed threshold[/dim]")
2346
+ _render_complexity_table(results, f"Most Complex Functions (threshold: {threshold}):")
2290
2347
  finally:
2291
2348
  db_manager.close_driver()
2292
2349
 
@@ -2333,7 +2390,7 @@ def analyze_dead_code(
2333
2390
  location_str
2334
2391
  )
2335
2392
 
2336
- console.print(f"\n[bold yellow]⚠️ Potentially Unused Functions:[/bold yellow]")
2393
+ console.print("\n[bold yellow]⚠️ Potentially Unused Functions:[/bold yellow]")
2337
2394
  console.print(table)
2338
2395
  console.print(f"\n[dim]Total: {len(unused_funcs)} function(s)[/dim]")
2339
2396
  console.print(f"[dim]Note: {results.get('note', '')}[/dim]")