codegraphcontext 0.5.2__py3-none-any.whl → 0.5.3__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.
- codegraphcontext/api/app.py +6 -1
- codegraphcontext/api/auth.py +123 -0
- codegraphcontext/api/router.py +9 -5
- codegraphcontext/cli/cli_helpers.py +179 -20
- codegraphcontext/cli/config_manager.py +56 -14
- codegraphcontext/cli/hook_manager.py +26 -4
- codegraphcontext/cli/main.py +208 -16
- codegraphcontext/cli/project_config.py +218 -0
- codegraphcontext/cli/registry_commands.py +6 -6
- codegraphcontext/cli/simulator.py +343 -0
- codegraphcontext/core/bundle_registry.py +45 -6
- codegraphcontext/core/cgc_bundle.py +102 -31
- codegraphcontext/core/cgcignore.py +29 -11
- codegraphcontext/core/database.py +5 -1
- codegraphcontext/core/database_embedded_kuzu.py +37 -2
- codegraphcontext/core/database_falkordb.py +141 -30
- codegraphcontext/core/database_falkordb_remote.py +31 -11
- codegraphcontext/core/database_nornic.py +5 -1
- codegraphcontext/core/simulator.py +589 -0
- codegraphcontext/core/watcher.py +67 -11
- codegraphcontext/prompts.py +52 -2
- codegraphcontext/server.py +77 -4
- codegraphcontext/tool_definitions.py +171 -20
- codegraphcontext/tools/code_finder.py +39 -26
- codegraphcontext/tools/graph_builder.py +39 -10
- codegraphcontext/tools/handlers/analysis_handlers.py +28 -9
- codegraphcontext/tools/handlers/management_handlers.py +33 -7
- codegraphcontext/tools/handlers/query_handlers.py +10 -4
- codegraphcontext/tools/indexing/discovery.py +6 -2
- codegraphcontext/tools/indexing/embeddings.py +8 -0
- codegraphcontext/tools/indexing/persistence/writer.py +104 -137
- codegraphcontext/tools/indexing/pipeline.py +65 -17
- codegraphcontext/tools/indexing/pre_scan.py +14 -2
- codegraphcontext/tools/indexing/resolution/post_resolution.py +11 -20
- codegraphcontext/tools/indexing/schema.py +89 -47
- codegraphcontext/tools/indexing/schema_contract.py +1 -0
- codegraphcontext/tools/languages/c.py +1 -1
- codegraphcontext/tools/languages/cpp.py +1 -1
- codegraphcontext/tools/languages/css.py +1 -1
- codegraphcontext/tools/languages/dart.py +1 -1
- codegraphcontext/tools/languages/elisp.py +1 -1
- codegraphcontext/tools/languages/elixir.py +3 -3
- codegraphcontext/tools/languages/go.py +3 -3
- codegraphcontext/tools/languages/html.py +1 -1
- codegraphcontext/tools/languages/javascript.py +32 -27
- codegraphcontext/tools/languages/lua.py +1 -1
- codegraphcontext/tools/languages/perl.py +1 -1
- codegraphcontext/tools/languages/python.py +78 -44
- codegraphcontext/tools/languages/ruby.py +2 -2
- codegraphcontext/tools/languages/rust.py +1 -1
- codegraphcontext/tools/languages/typescript.py +31 -27
- codegraphcontext/tools/languages/typescriptjsx.py +3 -3
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +22 -18
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +1 -1
- codegraphcontext/tools/report_generator.py +7 -7
- codegraphcontext/tools/system.py +13 -16
- codegraphcontext/utils/cypher_ddl.py +80 -0
- codegraphcontext/utils/cypher_readonly.py +5 -0
- codegraphcontext/viz/server.py +20 -3
- {codegraphcontext-0.5.2.dist-info → codegraphcontext-0.5.3.dist-info}/METADATA +25 -5
- {codegraphcontext-0.5.2.dist-info → codegraphcontext-0.5.3.dist-info}/RECORD +65 -60
- {codegraphcontext-0.5.2.dist-info → codegraphcontext-0.5.3.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.5.2.dist-info → codegraphcontext-0.5.3.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.5.2.dist-info → codegraphcontext-0.5.3.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.5.2.dist-info → codegraphcontext-0.5.3.dist-info}/top_level.txt +0 -0
codegraphcontext/cli/main.py
CHANGED
|
@@ -25,6 +25,7 @@ from importlib.metadata import version as pkg_version, PackageNotFoundError
|
|
|
25
25
|
from codegraphcontext.server import MCPServer
|
|
26
26
|
from .setup_wizard import run_neo4j_setup_wizard, configure_mcp_client
|
|
27
27
|
from . import config_manager
|
|
28
|
+
from . import project_config
|
|
28
29
|
# Import the new helper functions
|
|
29
30
|
from .cli_helpers import (
|
|
30
31
|
index_helper,
|
|
@@ -82,6 +83,8 @@ app = typer.Typer(
|
|
|
82
83
|
name="cgc",
|
|
83
84
|
help="CodeGraphContext: An MCP server for AI-powered code analysis.",
|
|
84
85
|
add_completion=True,
|
|
86
|
+
# `-h` is accepted as --help on every command, not just at the root.
|
|
87
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
85
88
|
)
|
|
86
89
|
console = Console(stderr=True)
|
|
87
90
|
|
|
@@ -576,7 +579,7 @@ def config_reset():
|
|
|
576
579
|
console.print("[yellow]Reset cancelled[/yellow]")
|
|
577
580
|
|
|
578
581
|
@config_app.command("db")
|
|
579
|
-
def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j', 'falkordb', 'falkordb-remote', 'kuzudb', or 'ladybugdb'")):
|
|
582
|
+
def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j', 'falkordb', 'falkordb-remote', 'kuzudb', 'nornic', or 'ladybugdb'")):
|
|
580
583
|
"""
|
|
581
584
|
Quickly switch the default database backend.
|
|
582
585
|
|
|
@@ -588,9 +591,9 @@ def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j'
|
|
|
588
591
|
cgc config db kuzudb
|
|
589
592
|
"""
|
|
590
593
|
backend = backend.lower()
|
|
591
|
-
if backend not in ['falkordb', 'falkordb-remote', 'neo4j', 'kuzudb', 'ladybugdb']:
|
|
594
|
+
if backend not in ['falkordb', 'falkordb-remote', 'neo4j', 'kuzudb', 'nornic', 'ladybugdb']:
|
|
592
595
|
console.print(f"[bold red]Invalid backend: {backend}[/bold red]")
|
|
593
|
-
console.print("Must be 'falkordb', 'falkordb-remote', 'neo4j', 'kuzudb', or 'ladybugdb'")
|
|
596
|
+
console.print("Must be 'falkordb', 'falkordb-remote', 'neo4j', 'kuzudb', 'nornic', or 'ladybugdb'")
|
|
594
597
|
raise typer.Exit(code=1)
|
|
595
598
|
|
|
596
599
|
updated = config_manager.set_config_value("DEFAULT_DATABASE", backend)
|
|
@@ -600,6 +603,99 @@ def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j'
|
|
|
600
603
|
|
|
601
604
|
console.print(f"[green]✔ Default database switched to {backend}[/green]")
|
|
602
605
|
|
|
606
|
+
# ============================================================================
|
|
607
|
+
# PROMPT COMMAND GROUP - Custom LLM Prompts
|
|
608
|
+
# ============================================================================
|
|
609
|
+
|
|
610
|
+
prompt_app = typer.Typer(help="Manage custom LLM prompt files")
|
|
611
|
+
app.add_typer(prompt_app, name="prompt")
|
|
612
|
+
|
|
613
|
+
@prompt_app.command("add")
|
|
614
|
+
def prompt_add(
|
|
615
|
+
path: str = typer.Argument(..., help="Path to the prompt file to register")
|
|
616
|
+
):
|
|
617
|
+
"""
|
|
618
|
+
Add a custom prompt file to the project.
|
|
619
|
+
|
|
620
|
+
Registers a prompt file that will be injected into the LLM system prompt.
|
|
621
|
+
The file path is stored relative to the project root.
|
|
622
|
+
|
|
623
|
+
Examples:
|
|
624
|
+
cgc prompt add skills.md
|
|
625
|
+
cgc prompt add docs/custom-instructions.txt
|
|
626
|
+
cgc prompt add /absolute/path/to/prompt.md
|
|
627
|
+
"""
|
|
628
|
+
try:
|
|
629
|
+
success = project_config.add_prompt_file(path)
|
|
630
|
+
if not success:
|
|
631
|
+
raise typer.Exit(code=1)
|
|
632
|
+
except Exception as e:
|
|
633
|
+
console.print(f"[red]Error adding prompt file: {e}[/red]")
|
|
634
|
+
raise typer.Exit(code=1)
|
|
635
|
+
|
|
636
|
+
@prompt_app.command("list")
|
|
637
|
+
def prompt_list():
|
|
638
|
+
"""
|
|
639
|
+
List all registered prompt files.
|
|
640
|
+
|
|
641
|
+
Shows all custom prompt files that will be injected into the LLM system prompt.
|
|
642
|
+
Files are shown in the order they will be prepended.
|
|
643
|
+
"""
|
|
644
|
+
try:
|
|
645
|
+
prompts = project_config.list_prompt_files()
|
|
646
|
+
|
|
647
|
+
if not prompts:
|
|
648
|
+
console.print("[yellow]No custom prompt files registered.[/yellow]")
|
|
649
|
+
console.print("\nUse [cyan]cgc prompt add <path>[/cyan] to register a prompt file.")
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
console.print("[bold cyan]Registered Prompt Files:[/bold cyan]\n")
|
|
653
|
+
|
|
654
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
655
|
+
table.add_column("#", style="dim", width=3)
|
|
656
|
+
table.add_column("Path", style="green")
|
|
657
|
+
table.add_column("Status", style="cyan", width=10)
|
|
658
|
+
|
|
659
|
+
project_root = project_config.get_project_root()
|
|
660
|
+
for i, prompt_path in enumerate(prompts, 1):
|
|
661
|
+
# Check if file exists
|
|
662
|
+
prompt_file = Path(prompt_path)
|
|
663
|
+
if not prompt_file.is_absolute():
|
|
664
|
+
prompt_file = project_root / prompt_file
|
|
665
|
+
|
|
666
|
+
status = "✅ Found" if prompt_file.exists() else "⚠️ Missing"
|
|
667
|
+
table.add_row(str(i), prompt_path, status)
|
|
668
|
+
|
|
669
|
+
console.print(table)
|
|
670
|
+
|
|
671
|
+
config_file = project_config.get_project_config_file()
|
|
672
|
+
console.print(f"\n[dim]Config: {config_file}[/dim]")
|
|
673
|
+
|
|
674
|
+
except Exception as e:
|
|
675
|
+
console.print(f"[red]Error listing prompt files: {e}[/red]")
|
|
676
|
+
raise typer.Exit(code=1)
|
|
677
|
+
|
|
678
|
+
@prompt_app.command("remove")
|
|
679
|
+
def prompt_remove(
|
|
680
|
+
path: str = typer.Argument(..., help="Path to the prompt file to unregister")
|
|
681
|
+
):
|
|
682
|
+
"""
|
|
683
|
+
Remove a custom prompt file from the project.
|
|
684
|
+
|
|
685
|
+
Unregisters a prompt file so it will no longer be injected into the LLM system prompt.
|
|
686
|
+
|
|
687
|
+
Examples:
|
|
688
|
+
cgc prompt remove skills.md
|
|
689
|
+
cgc prompt remove docs/custom-instructions.txt
|
|
690
|
+
"""
|
|
691
|
+
try:
|
|
692
|
+
success = project_config.remove_prompt_file(path)
|
|
693
|
+
if not success:
|
|
694
|
+
raise typer.Exit(code=1)
|
|
695
|
+
except Exception as e:
|
|
696
|
+
console.print(f"[red]Error removing prompt file: {e}[/red]")
|
|
697
|
+
raise typer.Exit(code=1)
|
|
698
|
+
|
|
603
699
|
# ============================================================================
|
|
604
700
|
# BUNDLE COMMAND GROUP - Pre-indexed Graph Snapshots
|
|
605
701
|
# ============================================================================
|
|
@@ -735,6 +831,7 @@ def bundle_load(
|
|
|
735
831
|
bundle_name: str = typer.Argument(..., help="Bundle name or path to load (e.g., 'numpy' or 'numpy.cgc')"),
|
|
736
832
|
clear: bool = typer.Option(False, "--clear", help="Clear existing graph data before loading"),
|
|
737
833
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation when using --clear"),
|
|
834
|
+
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
738
835
|
):
|
|
739
836
|
"""
|
|
740
837
|
Load a pre-indexed bundle (download if needed, then import).
|
|
@@ -755,7 +852,7 @@ def bundle_load(
|
|
|
755
852
|
|
|
756
853
|
# If it's an absolute path or has .cgc extension and exists, use it directly
|
|
757
854
|
if bundle_path.is_absolute() or (bundle_path.suffix == '.cgc' and bundle_path.exists()):
|
|
758
|
-
bundle_import(str(bundle_path), clear=clear, yes=yes)
|
|
855
|
+
bundle_import(str(bundle_path), clear=clear, yes=yes, context=context)
|
|
759
856
|
return
|
|
760
857
|
|
|
761
858
|
# Add .cgc extension if not present
|
|
@@ -765,7 +862,7 @@ def bundle_load(
|
|
|
765
862
|
# Check if exists locally
|
|
766
863
|
if bundle_path.exists():
|
|
767
864
|
console.print(f"[dim]Found local bundle: {bundle_path}[/dim]")
|
|
768
|
-
bundle_import(str(bundle_path), clear=clear, yes=yes)
|
|
865
|
+
bundle_import(str(bundle_path), clear=clear, yes=yes, context=context)
|
|
769
866
|
return
|
|
770
867
|
|
|
771
868
|
# Try to download from registry
|
|
@@ -783,7 +880,7 @@ def bundle_load(
|
|
|
783
880
|
|
|
784
881
|
if downloaded_path:
|
|
785
882
|
# Import the downloaded bundle
|
|
786
|
-
bundle_import(downloaded_path, clear=clear, yes=yes)
|
|
883
|
+
bundle_import(downloaded_path, clear=clear, yes=yes, context=context)
|
|
787
884
|
else:
|
|
788
885
|
console.print(f"[bold red]Failed to download bundle '{name}'[/bold red]")
|
|
789
886
|
raise typer.Exit(code=1)
|
|
@@ -921,9 +1018,12 @@ def load_shortcut(
|
|
|
921
1018
|
bundle_name: str = typer.Argument(..., help="Bundle name or path to load"),
|
|
922
1019
|
clear: bool = typer.Option(False, "--clear", help="Clear existing graph data before loading"),
|
|
923
1020
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation when using --clear"),
|
|
1021
|
+
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
924
1022
|
):
|
|
925
1023
|
"""Shortcut for 'cgc bundle load'"""
|
|
926
|
-
|
|
1024
|
+
# Must pass `context` explicitly: invoked as a plain function, Typer does not
|
|
1025
|
+
# resolve defaults, so an omitted parameter keeps its OptionInfo sentinel.
|
|
1026
|
+
bundle_load(bundle_name, clear, yes=yes, context=context)
|
|
927
1027
|
|
|
928
1028
|
# ============================================================================
|
|
929
1029
|
# REGISTRY COMMAND GROUP - Browse and Download Bundles
|
|
@@ -1024,7 +1124,7 @@ def registry_download(
|
|
|
1024
1124
|
|
|
1025
1125
|
if load and bundle_path:
|
|
1026
1126
|
console.print("\n[cyan]Loading bundle...[/cyan]")
|
|
1027
|
-
bundle_import(bundle_path, clear=False)
|
|
1127
|
+
bundle_import(bundle_path, clear=False, yes=False, context=None)
|
|
1028
1128
|
|
|
1029
1129
|
@registry_app.command("request")
|
|
1030
1130
|
def registry_request(
|
|
@@ -1063,6 +1163,10 @@ def doctor():
|
|
|
1063
1163
|
console.print("[bold cyan]🏥 Running CodeGraphContext Diagnostics...[/bold cyan]\n")
|
|
1064
1164
|
|
|
1065
1165
|
all_checks_passed = True
|
|
1166
|
+
# Warnings are not failures, but printing "All diagnostics passed! System is
|
|
1167
|
+
# healthy." while a ⚠ is on screen is misleading — `doctor` is the command
|
|
1168
|
+
# people run when something is already wrong.
|
|
1169
|
+
warnings_found = False
|
|
1066
1170
|
|
|
1067
1171
|
config_manager.ensure_first_run_bootstrap()
|
|
1068
1172
|
config_manager.ensure_config_file()
|
|
@@ -1200,6 +1304,32 @@ def doctor():
|
|
|
1200
1304
|
|
|
1201
1305
|
if is_falkordb_usable():
|
|
1202
1306
|
console.print(" [green]✓[/green] FalkorDB Lite is installed")
|
|
1307
|
+
# An import probe is not a connection check. This section is
|
|
1308
|
+
# titled "Checking Database Connection" and the neo4j /
|
|
1309
|
+
# falkordb-remote branches genuinely connect, so the default
|
|
1310
|
+
# backend must too — otherwise `doctor` reports a healthy
|
|
1311
|
+
# system without ever touching the database.
|
|
1312
|
+
try:
|
|
1313
|
+
from codegraphcontext.core import get_database_manager
|
|
1314
|
+
|
|
1315
|
+
probe_manager = get_database_manager()
|
|
1316
|
+
try:
|
|
1317
|
+
with probe_manager.get_driver().session() as probe_session:
|
|
1318
|
+
probe_session.run("RETURN 1")
|
|
1319
|
+
console.print(" [green]✓[/green] FalkorDB Lite connection successful")
|
|
1320
|
+
backend_in_use = probe_manager.get_backend_type()
|
|
1321
|
+
if backend_in_use != "falkordb":
|
|
1322
|
+
console.print(
|
|
1323
|
+
f" [yellow]⚠[/yellow] Configured backend is 'falkordb' but "
|
|
1324
|
+
f"'{backend_in_use}' is actually active"
|
|
1325
|
+
)
|
|
1326
|
+
warnings_found = True
|
|
1327
|
+
finally:
|
|
1328
|
+
probe_manager.close_driver()
|
|
1329
|
+
except Exception as conn_error:
|
|
1330
|
+
console.print(" [red]✗[/red] FalkorDB Lite connection failed")
|
|
1331
|
+
console.print(f" Reason: {conn_error}")
|
|
1332
|
+
all_checks_passed = False
|
|
1203
1333
|
else:
|
|
1204
1334
|
raise ImportError("FalkorDB Lite is not available on this platform")
|
|
1205
1335
|
except ImportError:
|
|
@@ -1210,6 +1340,7 @@ def doctor():
|
|
|
1210
1340
|
all_checks_passed = False
|
|
1211
1341
|
else:
|
|
1212
1342
|
console.print(f" [yellow]⚠[/yellow] No connectivity probe for backend '{default_db}'")
|
|
1343
|
+
warnings_found = True
|
|
1213
1344
|
except Exception as e:
|
|
1214
1345
|
console.print(f" [red]✗[/red] Database check error: {e}")
|
|
1215
1346
|
all_checks_passed = False
|
|
@@ -1239,6 +1370,7 @@ def doctor():
|
|
|
1239
1370
|
console.print(f" [green]✓[/green] {len(available)}/{len(probe_langs)} probed parsers OK: {', '.join(available)}")
|
|
1240
1371
|
if unavailable:
|
|
1241
1372
|
console.print(f" [yellow]⚠[/yellow] Unavailable: {', '.join(unavailable)}")
|
|
1373
|
+
warnings_found = True
|
|
1242
1374
|
except ImportError:
|
|
1243
1375
|
console.print(" [red]✗[/red] tree-sitter-language-pack not installed")
|
|
1244
1376
|
all_checks_passed = False
|
|
@@ -1264,6 +1396,7 @@ def doctor():
|
|
|
1264
1396
|
all_checks_passed = False
|
|
1265
1397
|
else:
|
|
1266
1398
|
console.print(" [yellow]⚠[/yellow] Config directory doesn't exist, will be created on first use")
|
|
1399
|
+
warnings_found = True
|
|
1267
1400
|
except Exception as e:
|
|
1268
1401
|
console.print(f" [red]✗[/red] Permission check error: {e}")
|
|
1269
1402
|
all_checks_passed = False
|
|
@@ -1276,11 +1409,17 @@ def doctor():
|
|
|
1276
1409
|
console.print(f" [green]✓[/green] cgc command found at: {cgc_path}")
|
|
1277
1410
|
else:
|
|
1278
1411
|
console.print(" [yellow]⚠[/yellow] cgc command not in PATH (using python -m cgc)")
|
|
1412
|
+
warnings_found = True
|
|
1279
1413
|
|
|
1280
1414
|
# Final summary
|
|
1281
1415
|
console.print("\n" + "=" * 60)
|
|
1282
|
-
if all_checks_passed:
|
|
1416
|
+
if all_checks_passed and not warnings_found:
|
|
1283
1417
|
console.print("[bold green]✅ All diagnostics passed! System is healthy.[/bold green]")
|
|
1418
|
+
elif all_checks_passed:
|
|
1419
|
+
console.print(
|
|
1420
|
+
"[bold yellow]✅ No failures, but some checks reported warnings "
|
|
1421
|
+
"(⚠ above).[/bold yellow]"
|
|
1422
|
+
)
|
|
1284
1423
|
else:
|
|
1285
1424
|
console.print("[bold yellow]⚠️ Some issues detected. Please review the output above.[/bold yellow]")
|
|
1286
1425
|
console.print("\n[cyan]Common fixes:[/cyan]")
|
|
@@ -1295,7 +1434,6 @@ def doctor():
|
|
|
1295
1434
|
|
|
1296
1435
|
|
|
1297
1436
|
|
|
1298
|
-
@app.command()
|
|
1299
1437
|
@app.command()
|
|
1300
1438
|
def index(
|
|
1301
1439
|
path: Optional[str] = typer.Argument(None, help="Path to the directory or file to index. Defaults to the current directory."),
|
|
@@ -1320,9 +1458,18 @@ def index(
|
|
|
1320
1458
|
reindex_helper(path, context)
|
|
1321
1459
|
else:
|
|
1322
1460
|
index_helper(path, context)
|
|
1461
|
+
except typer.Exit:
|
|
1462
|
+
# typer.Exit subclasses RuntimeError and str() is empty, so the handler
|
|
1463
|
+
# below caught it, printed nothing, and returned 0 — every helper that
|
|
1464
|
+
# raised `typer.Exit(code=1)` (missing path, failed indexing) silently
|
|
1465
|
+
# became a success. Re-raise control flow before catching errors.
|
|
1466
|
+
raise
|
|
1323
1467
|
except Exception as e:
|
|
1324
1468
|
if str(e):
|
|
1325
1469
|
console.print(f"[red]An error occurred during indexing: {e}[/red]")
|
|
1470
|
+
else:
|
|
1471
|
+
console.print("[red]An error occurred during indexing.[/red]")
|
|
1472
|
+
raise typer.Exit(code=1)
|
|
1326
1473
|
|
|
1327
1474
|
if summarize:
|
|
1328
1475
|
import os
|
|
@@ -1411,6 +1558,7 @@ def setup_scip():
|
|
|
1411
1558
|
def delete(
|
|
1412
1559
|
path: Optional[str] = typer.Argument(None, help="Path of the repository to delete from the code graph."),
|
|
1413
1560
|
all_repos: bool = typer.Option(False, "--all", help="Delete all indexed repositories"),
|
|
1561
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt (for CI/non-interactive use)"),
|
|
1414
1562
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
|
|
1415
1563
|
):
|
|
1416
1564
|
"""
|
|
@@ -1508,6 +1656,22 @@ def delete(
|
|
|
1508
1656
|
)
|
|
1509
1657
|
raise typer.Exit(code=1)
|
|
1510
1658
|
|
|
1659
|
+
# `--all` demands a typer.confirm *and* typing "delete all", while a
|
|
1660
|
+
# single delete went straight through — irreversibly dropping a
|
|
1661
|
+
# repository's entire graph (potentially an hour of indexing) on a
|
|
1662
|
+
# typo, with no prompt, no --yes flag and no undo. The asymmetry was
|
|
1663
|
+
# actively misleading: anyone who had seen the heavy --all guardrails
|
|
1664
|
+
# would reasonably assume single deletes were guarded too.
|
|
1665
|
+
if not yes:
|
|
1666
|
+
resolved = Path(path).expanduser().resolve()
|
|
1667
|
+
console.print(
|
|
1668
|
+
f"[bold yellow]About to delete the graph for:[/bold yellow] {resolved}"
|
|
1669
|
+
)
|
|
1670
|
+
console.print("[dim]This is irreversible; re-indexing is the only way back.[/dim]")
|
|
1671
|
+
if not typer.confirm("Proceed?", default=False):
|
|
1672
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
1673
|
+
raise typer.Exit(code=1)
|
|
1674
|
+
|
|
1511
1675
|
delete_helper(path, context)
|
|
1512
1676
|
|
|
1513
1677
|
|
|
@@ -1515,6 +1679,7 @@ def delete(
|
|
|
1515
1679
|
def report(
|
|
1516
1680
|
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path. Defaults to CGC_REPORT.md in the current directory."),
|
|
1517
1681
|
java: bool = typer.Option(False, "--java", "-j", help="Include Spring/Maven Java sections."),
|
|
1682
|
+
repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Repository root to scope the report to. Defaults to auto-detection from the current directory."),
|
|
1518
1683
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
1519
1684
|
):
|
|
1520
1685
|
"""
|
|
@@ -1533,7 +1698,15 @@ def report(
|
|
|
1533
1698
|
db_manager, _, _, _ = _initialize_services(context)
|
|
1534
1699
|
try:
|
|
1535
1700
|
from codegraphcontext.tools.report_generator import generate_report
|
|
1536
|
-
|
|
1701
|
+
# Without --repo the generator silently picks the repo with the most
|
|
1702
|
+
# indexed files, which is disclosed only in the report body.
|
|
1703
|
+
scoped_repo = Path(repo).expanduser().resolve().as_posix() if repo else None
|
|
1704
|
+
report_text = generate_report(
|
|
1705
|
+
db_manager,
|
|
1706
|
+
output_path=output_path,
|
|
1707
|
+
include_java=java,
|
|
1708
|
+
repo_path=scoped_repo,
|
|
1709
|
+
)
|
|
1537
1710
|
console.print(f"[green]✓[/green] Report written to [bold]{output_path}[/bold]")
|
|
1538
1711
|
# Print a short preview (first ~40 lines)
|
|
1539
1712
|
preview_lines = report_text.splitlines()[:40]
|
|
@@ -1553,7 +1726,9 @@ def report(
|
|
|
1553
1726
|
@app.command()
|
|
1554
1727
|
def visualize(
|
|
1555
1728
|
repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Path to the repository to visualize."),
|
|
1556
|
-
|
|
1729
|
+
# `-h` is --help everywhere else in the CLI; use -H so `cgc visualize -h`
|
|
1730
|
+
# doesn't fail with "Option '-h' requires an argument".
|
|
1731
|
+
host: str = typer.Option("127.0.0.1", "--host", "-H", help="Host interface to bind to."),
|
|
1557
1732
|
port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on."),
|
|
1558
1733
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
|
|
1559
1734
|
):
|
|
@@ -1642,7 +1817,11 @@ def unwatch(
|
|
|
1642
1817
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
1643
1818
|
):
|
|
1644
1819
|
"""
|
|
1645
|
-
Stop watching a directory for changes.
|
|
1820
|
+
[MCP only] Stop watching a directory for changes.
|
|
1821
|
+
|
|
1822
|
+
Not supported from the CLI: this cannot reach a watcher running in another
|
|
1823
|
+
process. Press Ctrl+C in the 'cgc watch' terminal, or use the
|
|
1824
|
+
'unwatch_directory' MCP tool.
|
|
1646
1825
|
|
|
1647
1826
|
Note: This command is primarily for MCP server mode.
|
|
1648
1827
|
For CLI watch mode, simply press Ctrl+C in the watch terminal.
|
|
@@ -1658,7 +1837,10 @@ def watching(
|
|
|
1658
1837
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
1659
1838
|
):
|
|
1660
1839
|
"""
|
|
1661
|
-
List all directories currently being watched for changes.
|
|
1840
|
+
[MCP only] List all directories currently being watched for changes.
|
|
1841
|
+
|
|
1842
|
+
Not supported from the CLI: this cannot reach a watcher running in another
|
|
1843
|
+
process. Use the 'list_watched_paths' MCP tool.
|
|
1662
1844
|
|
|
1663
1845
|
Note: This command is primarily for MCP server mode.
|
|
1664
1846
|
For CLI watch mode, check the terminal where you ran 'cgc watch'.
|
|
@@ -2867,10 +3049,13 @@ def cypher_legacy(
|
|
|
2867
3049
|
def index_abbrev(
|
|
2868
3050
|
path: Optional[str] = typer.Argument(None, help="Path to index"),
|
|
2869
3051
|
force: bool = typer.Option(False, "--force", "-f", help="Force re-index (delete existing and rebuild)"),
|
|
3052
|
+
summarize: bool = typer.Option(False, "--summarize", "-s", help="Display a codebase summary after indexing"),
|
|
2870
3053
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
|
|
2871
3054
|
):
|
|
2872
3055
|
"""Shortcut for 'cgc index'"""
|
|
2873
|
-
|
|
3056
|
+
# `summarize` must be passed explicitly: omitted, it keeps its OptionInfo
|
|
3057
|
+
# sentinel, which is truthy — so `cgc i` always printed the summary.
|
|
3058
|
+
index(path, force=force, summarize=summarize, context=context)
|
|
2874
3059
|
|
|
2875
3060
|
@app.command("ls", rich_help_panel="Shortcuts")
|
|
2876
3061
|
def list_abbrev(
|
|
@@ -2883,10 +3068,13 @@ def list_abbrev(
|
|
|
2883
3068
|
def delete_abbrev(
|
|
2884
3069
|
path: Optional[str] = typer.Argument(None, help="Path to delete"),
|
|
2885
3070
|
all_repos: bool = typer.Option(False, "--all", help="Delete all indexed repositories"),
|
|
3071
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt (for CI/non-interactive use)"),
|
|
2886
3072
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
|
|
2887
3073
|
):
|
|
2888
3074
|
"""Shortcut for 'cgc delete'"""
|
|
2889
|
-
|
|
3075
|
+
# `yes` must be forwarded explicitly: omitted, it keeps its OptionInfo
|
|
3076
|
+
# sentinel, which is truthy — silently skipping the confirmation.
|
|
3077
|
+
delete(path, all_repos, yes=yes, context=context)
|
|
2890
3078
|
|
|
2891
3079
|
@app.command("v", rich_help_panel="Shortcuts")
|
|
2892
3080
|
def visualize_abbrev(
|
|
@@ -3161,5 +3349,9 @@ def _write_datasource_graph(ingested: dict, context: Optional[str] = None) -> No
|
|
|
3161
3349
|
db_manager.close_driver()
|
|
3162
3350
|
|
|
3163
3351
|
|
|
3352
|
+
from codegraphcontext.cli.simulator import simulate_app
|
|
3353
|
+
app.add_typer(simulate_app, name="simulate")
|
|
3354
|
+
|
|
3355
|
+
|
|
3164
3356
|
if __name__ == "__main__":
|
|
3165
3357
|
app()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project-level configuration management for CodeGraphContext.
|
|
3
|
+
Handles managing .cgc directory and project-specific settings like custom prompts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional, Dict, Any
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Project config directory and file
|
|
16
|
+
PROJECT_CONFIG_DIR_NAME = ".cgc"
|
|
17
|
+
PROJECT_CONFIG_FILE_NAME = "config.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_project_root() -> Path:
|
|
21
|
+
"""Get the current working directory as project root."""
|
|
22
|
+
return Path.cwd()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_project_config_dir(project_root: Optional[Path] = None) -> Path:
|
|
26
|
+
"""Get the .cgc directory path for the project."""
|
|
27
|
+
if project_root is None:
|
|
28
|
+
project_root = get_project_root()
|
|
29
|
+
return project_root / PROJECT_CONFIG_DIR_NAME
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_project_config_file(project_root: Optional[Path] = None) -> Path:
|
|
33
|
+
"""Get the config.json file path within .cgc directory."""
|
|
34
|
+
return get_project_config_dir(project_root) / PROJECT_CONFIG_FILE_NAME
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_project_config_dir(project_root: Optional[Path] = None) -> Path:
|
|
38
|
+
"""Ensure .cgc directory exists and return its path."""
|
|
39
|
+
config_dir = get_project_config_dir(project_root)
|
|
40
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
return config_dir
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_project_config(project_root: Optional[Path] = None) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Load project configuration from .cgc/config.json.
|
|
47
|
+
Returns an empty config structure if file doesn't exist.
|
|
48
|
+
"""
|
|
49
|
+
config_file = get_project_config_file(project_root)
|
|
50
|
+
|
|
51
|
+
if not config_file.exists():
|
|
52
|
+
return {"prompts": []}
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
56
|
+
config = json.load(f)
|
|
57
|
+
# Ensure prompts field exists
|
|
58
|
+
if "prompts" not in config:
|
|
59
|
+
config["prompts"] = []
|
|
60
|
+
return config
|
|
61
|
+
except json.JSONDecodeError as e:
|
|
62
|
+
console.print(f"[yellow]Warning: Invalid JSON in {config_file}: {e}[/yellow]")
|
|
63
|
+
return {"prompts": []}
|
|
64
|
+
except Exception as e:
|
|
65
|
+
console.print(f"[yellow]Warning: Could not load config: {e}[/yellow]")
|
|
66
|
+
return {"prompts": []}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def save_project_config(config: Dict[str, Any], project_root: Optional[Path] = None):
|
|
70
|
+
"""Save project configuration to .cgc/config.json."""
|
|
71
|
+
ensure_project_config_dir(project_root)
|
|
72
|
+
config_file = get_project_config_file(project_root)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
76
|
+
json.dump(config, f, indent=2)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
console.print(f"[red]Error saving project config: {e}[/red]")
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def add_prompt_file(prompt_path: str, project_root: Optional[Path] = None) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Add a custom prompt file to the project configuration.
|
|
85
|
+
Validates that the file exists and prevents duplicates.
|
|
86
|
+
Stores paths relative to project root.
|
|
87
|
+
|
|
88
|
+
Returns True if successful, False otherwise.
|
|
89
|
+
"""
|
|
90
|
+
if project_root is None:
|
|
91
|
+
project_root = get_project_root()
|
|
92
|
+
|
|
93
|
+
# Convert to Path object and resolve
|
|
94
|
+
prompt_file = Path(prompt_path)
|
|
95
|
+
|
|
96
|
+
# Check if file exists
|
|
97
|
+
if not prompt_file.exists():
|
|
98
|
+
console.print(f"[red]❌ File not found: {prompt_path}[/red]")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
if not prompt_file.is_file():
|
|
102
|
+
console.print(f"[red]❌ Not a file: {prompt_path}[/red]")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# Make path relative to project root if it's absolute and within project
|
|
106
|
+
try:
|
|
107
|
+
if prompt_file.is_absolute():
|
|
108
|
+
try:
|
|
109
|
+
relative_path = prompt_file.relative_to(project_root)
|
|
110
|
+
prompt_path_str = str(relative_path).replace("\\", "/")
|
|
111
|
+
except ValueError:
|
|
112
|
+
# File is outside project root, store as absolute
|
|
113
|
+
prompt_path_str = str(prompt_file.resolve()).replace("\\", "/")
|
|
114
|
+
else:
|
|
115
|
+
prompt_path_str = str(prompt_file).replace("\\", "/")
|
|
116
|
+
except Exception:
|
|
117
|
+
prompt_path_str = str(prompt_file).replace("\\", "/")
|
|
118
|
+
|
|
119
|
+
# Load current config
|
|
120
|
+
config = load_project_config(project_root)
|
|
121
|
+
|
|
122
|
+
# Check for duplicates
|
|
123
|
+
if prompt_path_str in config["prompts"]:
|
|
124
|
+
console.print(f"[yellow]⚠ Prompt file already registered: {prompt_path_str}[/yellow]")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
# Add to config
|
|
128
|
+
config["prompts"].append(prompt_path_str)
|
|
129
|
+
save_project_config(config, project_root)
|
|
130
|
+
|
|
131
|
+
console.print(f"[green]✅ Added prompt file: {prompt_path_str}[/green]")
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def list_prompt_files(project_root: Optional[Path] = None) -> List[str]:
|
|
136
|
+
"""
|
|
137
|
+
Get list of registered prompt files.
|
|
138
|
+
Returns list of prompt file paths.
|
|
139
|
+
"""
|
|
140
|
+
config = load_project_config(project_root)
|
|
141
|
+
return config.get("prompts", [])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def remove_prompt_file(prompt_path: str, project_root: Optional[Path] = None) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Remove a prompt file from the project configuration.
|
|
147
|
+
|
|
148
|
+
Returns True if successful, False otherwise.
|
|
149
|
+
"""
|
|
150
|
+
if project_root is None:
|
|
151
|
+
project_root = get_project_root()
|
|
152
|
+
|
|
153
|
+
# Normalize the path for comparison
|
|
154
|
+
prompt_file = Path(prompt_path)
|
|
155
|
+
|
|
156
|
+
# Try both relative and absolute versions
|
|
157
|
+
try:
|
|
158
|
+
if prompt_file.is_absolute():
|
|
159
|
+
try:
|
|
160
|
+
relative_path = prompt_file.relative_to(project_root)
|
|
161
|
+
prompt_path_normalized = str(relative_path).replace("\\", "/")
|
|
162
|
+
except ValueError:
|
|
163
|
+
prompt_path_normalized = str(prompt_file.resolve()).replace("\\", "/")
|
|
164
|
+
else:
|
|
165
|
+
prompt_path_normalized = str(prompt_file).replace("\\", "/")
|
|
166
|
+
except Exception:
|
|
167
|
+
prompt_path_normalized = str(prompt_file).replace("\\", "/")
|
|
168
|
+
|
|
169
|
+
# Load current config
|
|
170
|
+
config = load_project_config(project_root)
|
|
171
|
+
|
|
172
|
+
# Try to remove - check both the normalized path and original
|
|
173
|
+
original_length = len(config["prompts"])
|
|
174
|
+
config["prompts"] = [p for p in config["prompts"] if p not in (prompt_path, prompt_path_normalized)]
|
|
175
|
+
|
|
176
|
+
if len(config["prompts"]) == original_length:
|
|
177
|
+
console.print(f"[yellow]⚠ Prompt file not found in config: {prompt_path}[/yellow]")
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
save_project_config(config, project_root)
|
|
181
|
+
console.print(f"[green]✅ Removed prompt file: {prompt_path}[/green]")
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_prompt_file_contents(project_root: Optional[Path] = None) -> List[str]:
|
|
186
|
+
"""
|
|
187
|
+
Load contents of all registered prompt files.
|
|
188
|
+
Skips files that don't exist and logs warnings.
|
|
189
|
+
|
|
190
|
+
Returns list of prompt file contents in order.
|
|
191
|
+
"""
|
|
192
|
+
if project_root is None:
|
|
193
|
+
project_root = get_project_root()
|
|
194
|
+
|
|
195
|
+
prompt_files = list_prompt_files(project_root)
|
|
196
|
+
contents = []
|
|
197
|
+
|
|
198
|
+
for prompt_path in prompt_files:
|
|
199
|
+
# Resolve path relative to project root
|
|
200
|
+
prompt_file = Path(prompt_path)
|
|
201
|
+
if not prompt_file.is_absolute():
|
|
202
|
+
prompt_file = project_root / prompt_file
|
|
203
|
+
|
|
204
|
+
# Try to read file
|
|
205
|
+
try:
|
|
206
|
+
if not prompt_file.exists():
|
|
207
|
+
logger.warning(f"Skipping missing prompt file: {prompt_path}")
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
with open(prompt_file, "r", encoding="utf-8") as f:
|
|
211
|
+
content = f.read().strip()
|
|
212
|
+
if content:
|
|
213
|
+
contents.append(content)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.warning(f"Error reading prompt file {prompt_path}: {e}")
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
return contents
|
|
@@ -112,7 +112,7 @@ def list_bundles(verbose: bool = False, unique: bool = False):
|
|
|
112
112
|
table.add_column("Download URL", style="blue", no_wrap=False)
|
|
113
113
|
|
|
114
114
|
# Sort by full_name to group versions together
|
|
115
|
-
bundles.sort(key=lambda b: (b.get('name'
|
|
115
|
+
bundles.sort(key=lambda b: ((b.get('name') or ''), (b.get('full_name') or '')))
|
|
116
116
|
|
|
117
117
|
for bundle in bundles:
|
|
118
118
|
# Use full_name for display (includes version info)
|
|
@@ -163,10 +163,10 @@ def search_bundles(query: str):
|
|
|
163
163
|
query_lower = query.lower()
|
|
164
164
|
matching_bundles = [
|
|
165
165
|
b for b in bundles
|
|
166
|
-
if query_lower in b.get('name'
|
|
167
|
-
query_lower in b.get('full_name'
|
|
168
|
-
query_lower in b.get('repo'
|
|
169
|
-
query_lower in b.get('description'
|
|
166
|
+
if query_lower in (b.get('name') or '').lower() or
|
|
167
|
+
query_lower in (b.get('full_name') or '').lower() or
|
|
168
|
+
query_lower in (b.get('repo') or '').lower() or
|
|
169
|
+
query_lower in (b.get('description') or '').lower()
|
|
170
170
|
]
|
|
171
171
|
|
|
172
172
|
if not matching_bundles:
|
|
@@ -407,7 +407,7 @@ def load_bundle_command(bundle_name: str, clear_existing: bool = False):
|
|
|
407
407
|
if not all(services):
|
|
408
408
|
return (False, "Failed to initialize database services", {})
|
|
409
409
|
|
|
410
|
-
db_manager, _, _ = services
|
|
410
|
+
db_manager, _, _, _ = services
|
|
411
411
|
|
|
412
412
|
# Check if bundle exists locally
|
|
413
413
|
bundle_path = Path(bundle_name)
|