codegraphcontext 0.4.15__py3-none-any.whl → 0.4.16__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/cli/cli_helpers.py +47 -26
- codegraphcontext/cli/config_manager.py +12 -2
- codegraphcontext/cli/main.py +71 -24
- codegraphcontext/core/cgc_bundle.py +62 -25
- codegraphcontext/core/cgcignore.py +127 -4
- codegraphcontext/tools/graph_builder.py +1 -1
- codegraphcontext/tools/handlers/indexing_handlers.py +3 -2
- codegraphcontext/tools/indexing/persistence/writer.py +83 -8
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.16.dist-info}/METADATA +3 -3
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.16.dist-info}/RECORD +14 -14
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.16.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.16.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.16.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.16.dist-info}/top_level.txt +0 -0
|
@@ -34,6 +34,22 @@ from .config_manager import resolve_context, ResolvedContext, register_repo_in_c
|
|
|
34
34
|
console = Console()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _fail_services_init() -> None:
|
|
38
|
+
"""Abort the CLI command when database/services could not be initialized."""
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _kuzu_fallback_path(ctx: ResolvedContext) -> Optional[str]:
|
|
43
|
+
"""Derive a KùzuDB directory when falling back from another backend."""
|
|
44
|
+
if ctx.db_path:
|
|
45
|
+
return str(Path(ctx.db_path).parent / "kuzudb")
|
|
46
|
+
try:
|
|
47
|
+
from .config_manager import _default_global_db_path
|
|
48
|
+
return _default_global_db_path("kuzudb")
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int = 5) -> None:
|
|
38
54
|
diagnostics = getattr(graph_builder, "last_call_resolution_diagnostics", [])
|
|
39
55
|
if not diagnostics:
|
|
@@ -59,14 +75,17 @@ def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int =
|
|
|
59
75
|
console.print(table)
|
|
60
76
|
|
|
61
77
|
|
|
62
|
-
def _initialize_services(
|
|
78
|
+
def _initialize_services(
|
|
79
|
+
cli_context_flag: Optional[str] = None,
|
|
80
|
+
cwd: Optional[Path] = None,
|
|
81
|
+
) -> tuple[Any, Any, Any, ResolvedContext]:
|
|
63
82
|
"""
|
|
64
83
|
Initializes and returns core service managers based on the resolved context.
|
|
65
84
|
Returns (db_manager, graph_builder, code_finder, resolved_context).
|
|
66
85
|
"""
|
|
67
86
|
ensure_first_run_bootstrap()
|
|
68
87
|
console.print("[dim]Resolving context...[/dim]")
|
|
69
|
-
ctx = resolve_context(cli_context_flag)
|
|
88
|
+
ctx = resolve_context(cli_context_flag, cwd=cwd)
|
|
70
89
|
|
|
71
90
|
# Let the user know what context we're operating in
|
|
72
91
|
if ctx.mode == "named":
|
|
@@ -93,7 +112,7 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
93
112
|
db_manager = get_database_manager(db_path=runtime_path or ctx.db_path)
|
|
94
113
|
except ValueError as e:
|
|
95
114
|
console.print(f"[bold red]Database Configuration Error:[/bold red] {e}")
|
|
96
|
-
|
|
115
|
+
_fail_services_init()
|
|
97
116
|
|
|
98
117
|
try:
|
|
99
118
|
db_manager.get_driver()
|
|
@@ -110,15 +129,16 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
110
129
|
except Exception:
|
|
111
130
|
pass
|
|
112
131
|
|
|
113
|
-
# Re-initialize explicitly with KùzuDB
|
|
132
|
+
# Re-initialize explicitly with KùzuDB (never reuse the FalkorDB directory)
|
|
114
133
|
from ..core.database_kuzu import KuzuDBManager
|
|
115
|
-
|
|
134
|
+
kuzu_path = _kuzu_fallback_path(ctx)
|
|
135
|
+
db_manager = KuzuDBManager(db_path=kuzu_path)
|
|
116
136
|
try:
|
|
117
137
|
db_manager.get_driver()
|
|
118
138
|
console.print("[green]✓[/green] Successfully switched to KùzuDB fallback")
|
|
119
139
|
except Exception as kuzu_e:
|
|
120
140
|
console.print(f"[bold red]Critical Error:[/bold red] Both FalkorDB and KùzuDB failed: {kuzu_e}")
|
|
121
|
-
|
|
141
|
+
_fail_services_init()
|
|
122
142
|
else:
|
|
123
143
|
selected_db = (
|
|
124
144
|
os.environ.get("CGC_RUNTIME_DB_TYPE")
|
|
@@ -135,20 +155,20 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
135
155
|
console.print("[cyan]Neo4j failed and CGC_ALLOW_NEO4J_FALLBACK=true. Falling back to KuzuDB...[/cyan]")
|
|
136
156
|
try:
|
|
137
157
|
from ..core.database_kuzu import KuzuDBManager
|
|
138
|
-
db_manager = KuzuDBManager()
|
|
158
|
+
db_manager = KuzuDBManager(db_path=_kuzu_fallback_path(ctx))
|
|
139
159
|
db_manager.get_driver()
|
|
140
160
|
console.print("[green]✓[/green] Successfully switched to KuzuDB fallback")
|
|
141
161
|
except Exception as kuzu_e:
|
|
142
162
|
console.print(f"[bold red]Critical Error:[/bold red] Neo4j failed and KuzuDB fallback failed: {kuzu_e}")
|
|
143
|
-
|
|
163
|
+
_fail_services_init()
|
|
144
164
|
else:
|
|
145
165
|
if selected_db == "neo4j":
|
|
146
166
|
console.print("[yellow]Tip:[/yellow] To continue without Neo4j, rerun with --db kuzudb")
|
|
147
|
-
|
|
167
|
+
_fail_services_init()
|
|
148
168
|
else:
|
|
149
169
|
console.print(f"[bold red]Database Connection Error:[/bold red] {e}")
|
|
150
170
|
console.print("Please ensure your database is configured correctly or run 'cgc doctor'.")
|
|
151
|
-
|
|
171
|
+
_fail_services_init()
|
|
152
172
|
|
|
153
173
|
# The GraphBuilder requires an event loop, even for synchronous-style execution
|
|
154
174
|
try:
|
|
@@ -224,17 +244,18 @@ async def _run_index_with_progress(graph_builder: GraphBuilder, path_obj: Path,
|
|
|
224
244
|
def index_helper(path: str, context: Optional[str] = None):
|
|
225
245
|
"""Synchronously indexes a repository in a given context."""
|
|
226
246
|
time_start = time.time()
|
|
227
|
-
|
|
247
|
+
path_obj = Path(path).resolve()
|
|
248
|
+
index_cwd = path_obj if path_obj.is_dir() else path_obj.parent
|
|
249
|
+
services = _initialize_services(context, cwd=index_cwd)
|
|
228
250
|
if not all(services[:3]):
|
|
229
|
-
|
|
251
|
+
_fail_services_init()
|
|
230
252
|
|
|
231
253
|
db_manager, graph_builder, code_finder, ctx = services
|
|
232
|
-
path_obj = Path(path).resolve()
|
|
233
254
|
|
|
234
255
|
if not path_obj.exists():
|
|
235
256
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
236
257
|
db_manager.close_driver()
|
|
237
|
-
|
|
258
|
+
raise typer.Exit(code=1)
|
|
238
259
|
|
|
239
260
|
indexed_repos = code_finder.list_indexed_repositories()
|
|
240
261
|
repo_exists = any_repo_matches_path(indexed_repos, path_obj)
|
|
@@ -298,7 +319,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
298
319
|
"""Synchronously indexes a package."""
|
|
299
320
|
services = _initialize_services(context)
|
|
300
321
|
if not all(services[:3]):
|
|
301
|
-
|
|
322
|
+
_fail_services_init()
|
|
302
323
|
|
|
303
324
|
db_manager, graph_builder, code_finder, ctx = services
|
|
304
325
|
|
|
@@ -332,7 +353,7 @@ def list_repos_helper(context: Optional[str] = None):
|
|
|
332
353
|
"""Lists all indexed repositories."""
|
|
333
354
|
services = _initialize_services(context)
|
|
334
355
|
if not all(services[:3]):
|
|
335
|
-
|
|
356
|
+
_fail_services_init()
|
|
336
357
|
|
|
337
358
|
db_manager, _, code_finder, ctx = services
|
|
338
359
|
|
|
@@ -362,7 +383,7 @@ def delete_helper(repo_path: str, context: Optional[str] = None):
|
|
|
362
383
|
"""Deletes a repository from the graph."""
|
|
363
384
|
services = _initialize_services(context)
|
|
364
385
|
if not all(services[:3]):
|
|
365
|
-
|
|
386
|
+
_fail_services_init()
|
|
366
387
|
|
|
367
388
|
db_manager, graph_builder, _, ctx = services
|
|
368
389
|
|
|
@@ -382,7 +403,7 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
382
403
|
"""Executes a read-only Cypher query."""
|
|
383
404
|
services = _initialize_services(context)
|
|
384
405
|
if not all(services[:3]):
|
|
385
|
-
|
|
406
|
+
_fail_services_init()
|
|
386
407
|
|
|
387
408
|
db_manager, _, _, ctx = services
|
|
388
409
|
|
|
@@ -393,7 +414,7 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
393
414
|
if re.search(pattern, query, re.IGNORECASE):
|
|
394
415
|
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
395
416
|
db_manager.close_driver()
|
|
396
|
-
|
|
417
|
+
raise typer.Exit(code=1)
|
|
397
418
|
|
|
398
419
|
try:
|
|
399
420
|
with db_manager.get_driver().session() as session:
|
|
@@ -412,7 +433,7 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
|
412
433
|
|
|
413
434
|
services = _initialize_services(context)
|
|
414
435
|
if not all(services[:3]):
|
|
415
|
-
|
|
436
|
+
_fail_services_init()
|
|
416
437
|
|
|
417
438
|
db_manager, _, _, ctx = services
|
|
418
439
|
|
|
@@ -423,7 +444,7 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
|
423
444
|
if re.search(pattern, query, re.IGNORECASE):
|
|
424
445
|
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
425
446
|
db_manager.close_driver()
|
|
426
|
-
|
|
447
|
+
raise typer.Exit(code=1)
|
|
427
448
|
|
|
428
449
|
try:
|
|
429
450
|
with db_manager.get_driver().session() as session:
|
|
@@ -449,7 +470,7 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
449
470
|
"""Generates an interactive visualization using the Playground UI."""
|
|
450
471
|
services = _initialize_services(context)
|
|
451
472
|
if not all(services[:3]):
|
|
452
|
-
|
|
473
|
+
_fail_services_init()
|
|
453
474
|
|
|
454
475
|
db_manager, _, _, ctx = services
|
|
455
476
|
|
|
@@ -546,7 +567,7 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
546
567
|
time_start = time.time()
|
|
547
568
|
services = _initialize_services(context)
|
|
548
569
|
if not all(services[:3]):
|
|
549
|
-
|
|
570
|
+
_fail_services_init()
|
|
550
571
|
|
|
551
572
|
db_manager, graph_builder, code_finder, ctx = services
|
|
552
573
|
path_obj = Path(path).resolve()
|
|
@@ -595,7 +616,7 @@ def clean_helper(context: Optional[str] = None):
|
|
|
595
616
|
"""Remove orphaned nodes and relationships from the database."""
|
|
596
617
|
services = _initialize_services(context)
|
|
597
618
|
if not all(services[:3]):
|
|
598
|
-
|
|
619
|
+
_fail_services_init()
|
|
599
620
|
|
|
600
621
|
db_manager, _, _, ctx = services
|
|
601
622
|
|
|
@@ -643,7 +664,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
643
664
|
"""Show indexing statistics for a repository or overall."""
|
|
644
665
|
services = _initialize_services(context)
|
|
645
666
|
if not all(services[:3]):
|
|
646
|
-
|
|
667
|
+
_fail_services_init()
|
|
647
668
|
|
|
648
669
|
db_manager, _, code_finder, ctx = services
|
|
649
670
|
|
|
@@ -753,7 +774,7 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
753
774
|
|
|
754
775
|
services = _initialize_services(context)
|
|
755
776
|
if not all(services[:3]):
|
|
756
|
-
|
|
777
|
+
_fail_services_init()
|
|
757
778
|
|
|
758
779
|
db_manager, graph_builder, code_finder, ctx = services
|
|
759
780
|
path_obj = Path(path).resolve()
|
|
@@ -19,10 +19,17 @@ console = Console()
|
|
|
19
19
|
CONFIG_DIR = Path.home() / ".codegraphcontext"
|
|
20
20
|
CONFIG_FILE = CONFIG_DIR / ".env"
|
|
21
21
|
|
|
22
|
+
# Keys that pin embedded DB directories; must not bleed across profiles via local .env
|
|
23
|
+
DB_PATH_ENV_KEYS = frozenset({
|
|
24
|
+
"FALKORDB_PATH", "FALKORDB_SOCKET_PATH", "KUZUDB_PATH", "LADYBUGDB_PATH",
|
|
25
|
+
})
|
|
26
|
+
|
|
22
27
|
# Database credential keys (stored in same .env file but not managed as config)
|
|
23
28
|
DATABASE_CREDENTIAL_KEYS = {
|
|
24
29
|
"NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE",
|
|
25
|
-
"NORNIC_URI", "NORNIC_USERNAME", "NORNIC_PASSWORD", "NORNIC_DATABASE"
|
|
30
|
+
"NORNIC_URI", "NORNIC_USERNAME", "NORNIC_PASSWORD", "NORNIC_DATABASE",
|
|
31
|
+
"FALKORDB_HOST", "FALKORDB_PORT", "FALKORDB_PASSWORD", "FALKORDB_SSL",
|
|
32
|
+
"FALKORDB_GRAPH_NAME",
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
# Default configuration values
|
|
@@ -669,7 +676,10 @@ def _default_global_db_path(database: str) -> str:
|
|
|
669
676
|
if database == "falkordb":
|
|
670
677
|
custom_path = load_config().get("FALKORDB_PATH")
|
|
671
678
|
if custom_path:
|
|
672
|
-
|
|
679
|
+
resolved = Path(custom_path).resolve()
|
|
680
|
+
# Ignore paths from another profile/repo that leaked via local .env
|
|
681
|
+
if str(resolved).startswith(str(CONFIG_DIR.resolve())):
|
|
682
|
+
return str(resolved)
|
|
673
683
|
if _LEGACY_FALKORDB_PATH.exists():
|
|
674
684
|
return str(_LEGACY_FALKORDB_PATH)
|
|
675
685
|
return str(CONFIG_DIR / "global" / "db" / database)
|
codegraphcontext/cli/main.py
CHANGED
|
@@ -302,6 +302,7 @@ def _load_credentials(cli_context_flag: Optional[str] = None):
|
|
|
302
302
|
ensure_config_dir,
|
|
303
303
|
codegraphcontext_dotenv_at_cwd,
|
|
304
304
|
normalize_config_path,
|
|
305
|
+
DB_PATH_ENV_KEYS,
|
|
305
306
|
)
|
|
306
307
|
|
|
307
308
|
# Ensure config directory exists (lazy initialization)
|
|
@@ -371,6 +372,11 @@ def _load_credentials(cli_context_flag: Optional[str] = None):
|
|
|
371
372
|
if local_cgc_env and local_cgc_env.resolve() != global_env_path.resolve():
|
|
372
373
|
with open(local_cgc_env, "r", encoding="utf-8", errors="replace") as f:
|
|
373
374
|
vals = dotenv_values(stream=f)
|
|
375
|
+
# Do not let a repo-local profile override another user's global DB paths.
|
|
376
|
+
vals = {
|
|
377
|
+
k: v for k, v in (vals or {}).items()
|
|
378
|
+
if k not in DB_PATH_ENV_KEYS
|
|
379
|
+
}
|
|
374
380
|
_append_source(str(local_cgc_env), vals)
|
|
375
381
|
except Exception as e:
|
|
376
382
|
console.print(
|
|
@@ -622,7 +628,8 @@ def bundle_export(
|
|
|
622
628
|
|
|
623
629
|
services = _initialize_services(context)
|
|
624
630
|
if not all(services[:3]):
|
|
625
|
-
|
|
631
|
+
from .cli_helpers import _fail_services_init
|
|
632
|
+
_fail_services_init()
|
|
626
633
|
db_manager, _, code_finder = services[:3]
|
|
627
634
|
|
|
628
635
|
try:
|
|
@@ -655,6 +662,7 @@ def bundle_export(
|
|
|
655
662
|
def bundle_import(
|
|
656
663
|
bundle_file: str = typer.Argument(..., help="Path to the .cgc bundle file to import"),
|
|
657
664
|
clear: bool = typer.Option(False, "--clear", help="Clear existing graph data before importing"),
|
|
665
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation when using --clear"),
|
|
658
666
|
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
659
667
|
):
|
|
660
668
|
"""
|
|
@@ -672,7 +680,8 @@ def bundle_import(
|
|
|
672
680
|
|
|
673
681
|
services = _initialize_services(context)
|
|
674
682
|
if not all(services[:3]):
|
|
675
|
-
|
|
683
|
+
from .cli_helpers import _fail_services_init
|
|
684
|
+
_fail_services_init()
|
|
676
685
|
db_manager, graph_builder, code_finder = services[:3]
|
|
677
686
|
|
|
678
687
|
try:
|
|
@@ -684,9 +693,9 @@ def bundle_import(
|
|
|
684
693
|
|
|
685
694
|
if clear:
|
|
686
695
|
console.print("[yellow]⚠️ Warning: This will clear all existing graph data![/yellow]")
|
|
687
|
-
if not typer.confirm("Are you sure you want to continue?", default=False):
|
|
696
|
+
if not yes and not typer.confirm("Are you sure you want to continue?", default=False):
|
|
688
697
|
console.print("[yellow]Import cancelled[/yellow]")
|
|
689
|
-
|
|
698
|
+
raise typer.Exit(code=1)
|
|
690
699
|
|
|
691
700
|
console.print(f"[cyan]Importing bundle from {bundle_path}...[/cyan]")
|
|
692
701
|
|
|
@@ -1026,6 +1035,26 @@ def doctor():
|
|
|
1026
1035
|
console.print(" [red]✗[/red] LadybugDB core (ladybug) is not installed")
|
|
1027
1036
|
console.print(" Run: pip install ladybug")
|
|
1028
1037
|
all_checks_passed = False
|
|
1038
|
+
elif default_db == "falkordb-remote":
|
|
1039
|
+
host = os.environ.get("FALKORDB_HOST")
|
|
1040
|
+
if host:
|
|
1041
|
+
port = os.environ.get("FALKORDB_PORT", "6379")
|
|
1042
|
+
console.print(f" [cyan]Endpoint:[/cyan] {host}:{port}")
|
|
1043
|
+
try:
|
|
1044
|
+
from codegraphcontext.core.database_falkordb_remote import FalkorDBRemoteManager
|
|
1045
|
+
ok, msg = FalkorDBRemoteManager.test_connection()
|
|
1046
|
+
if ok:
|
|
1047
|
+
console.print(" [green]✓[/green] FalkorDB remote connection successful")
|
|
1048
|
+
else:
|
|
1049
|
+
console.print(f" [red]✗[/red] FalkorDB remote connection failed: {msg}")
|
|
1050
|
+
all_checks_passed = False
|
|
1051
|
+
except Exception as exc:
|
|
1052
|
+
console.print(f" [red]✗[/red] FalkorDB remote check error: {exc}")
|
|
1053
|
+
all_checks_passed = False
|
|
1054
|
+
else:
|
|
1055
|
+
console.print(" [red]✗[/red] FALKORDB_HOST is not configured")
|
|
1056
|
+
console.print(" Run: cgc config set FALKORDB_HOST 127.0.0.1")
|
|
1057
|
+
all_checks_passed = False
|
|
1029
1058
|
else:
|
|
1030
1059
|
# FalkorDB
|
|
1031
1060
|
try:
|
|
@@ -1200,7 +1229,8 @@ def delete(
|
|
|
1200
1229
|
# Delete all repositories
|
|
1201
1230
|
services = _initialize_services(context)
|
|
1202
1231
|
if not all(services[:3]):
|
|
1203
|
-
|
|
1232
|
+
from .cli_helpers import _fail_services_init
|
|
1233
|
+
_fail_services_init()
|
|
1204
1234
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1205
1235
|
|
|
1206
1236
|
try:
|
|
@@ -1435,7 +1465,8 @@ def find_by_name(
|
|
|
1435
1465
|
_load_credentials()
|
|
1436
1466
|
services = _initialize_services(context)
|
|
1437
1467
|
if not all(services[:3]):
|
|
1438
|
-
|
|
1468
|
+
from .cli_helpers import _fail_services_init
|
|
1469
|
+
_fail_services_init()
|
|
1439
1470
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1440
1471
|
|
|
1441
1472
|
# Resolve effective fuzzy setting: CLI flag wins, else config, else true.
|
|
@@ -1557,7 +1588,8 @@ def find_by_pattern(
|
|
|
1557
1588
|
_load_credentials()
|
|
1558
1589
|
services = _initialize_services(context)
|
|
1559
1590
|
if not all(services[:3]):
|
|
1560
|
-
|
|
1591
|
+
from .cli_helpers import _fail_services_init
|
|
1592
|
+
_fail_services_init()
|
|
1561
1593
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1562
1594
|
|
|
1563
1595
|
try:
|
|
@@ -1650,7 +1682,8 @@ def find_by_type(
|
|
|
1650
1682
|
_load_credentials()
|
|
1651
1683
|
services = _initialize_services(context)
|
|
1652
1684
|
if not all(services[:3]):
|
|
1653
|
-
|
|
1685
|
+
from .cli_helpers import _fail_services_init
|
|
1686
|
+
_fail_services_init()
|
|
1654
1687
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1655
1688
|
|
|
1656
1689
|
try:
|
|
@@ -1705,7 +1738,8 @@ def find_by_variable(
|
|
|
1705
1738
|
_load_credentials()
|
|
1706
1739
|
services = _initialize_services(context)
|
|
1707
1740
|
if not all(services[:3]):
|
|
1708
|
-
|
|
1741
|
+
from .cli_helpers import _fail_services_init
|
|
1742
|
+
_fail_services_init()
|
|
1709
1743
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1710
1744
|
|
|
1711
1745
|
try:
|
|
@@ -1751,7 +1785,8 @@ def find_by_content_search(
|
|
|
1751
1785
|
_load_credentials()
|
|
1752
1786
|
services = _initialize_services(context)
|
|
1753
1787
|
if not all(services[:3]):
|
|
1754
|
-
|
|
1788
|
+
from .cli_helpers import _fail_services_init
|
|
1789
|
+
_fail_services_init()
|
|
1755
1790
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1756
1791
|
|
|
1757
1792
|
try:
|
|
@@ -1813,7 +1848,8 @@ def find_by_decorator_search(
|
|
|
1813
1848
|
_load_credentials()
|
|
1814
1849
|
services = _initialize_services(context)
|
|
1815
1850
|
if not all(services[:3]):
|
|
1816
|
-
|
|
1851
|
+
from .cli_helpers import _fail_services_init
|
|
1852
|
+
_fail_services_init()
|
|
1817
1853
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1818
1854
|
|
|
1819
1855
|
try:
|
|
@@ -1861,7 +1897,8 @@ def find_by_argument_search(
|
|
|
1861
1897
|
_load_credentials()
|
|
1862
1898
|
services = _initialize_services(context)
|
|
1863
1899
|
if not all(services[:3]):
|
|
1864
|
-
|
|
1900
|
+
from .cli_helpers import _fail_services_init
|
|
1901
|
+
_fail_services_init()
|
|
1865
1902
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1866
1903
|
|
|
1867
1904
|
try:
|
|
@@ -1917,7 +1954,8 @@ def analyze_calls(
|
|
|
1917
1954
|
_load_credentials()
|
|
1918
1955
|
services = _initialize_services(context)
|
|
1919
1956
|
if not all(services[:3]):
|
|
1920
|
-
|
|
1957
|
+
from .cli_helpers import _fail_services_init
|
|
1958
|
+
_fail_services_init()
|
|
1921
1959
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1922
1960
|
|
|
1923
1961
|
try:
|
|
@@ -1973,7 +2011,8 @@ def analyze_callers(
|
|
|
1973
2011
|
_load_credentials()
|
|
1974
2012
|
services = _initialize_services(context)
|
|
1975
2013
|
if not all(services[:3]):
|
|
1976
|
-
|
|
2014
|
+
from .cli_helpers import _fail_services_init
|
|
2015
|
+
_fail_services_init()
|
|
1977
2016
|
db_manager, graph_builder, code_finder = services[:3]
|
|
1978
2017
|
|
|
1979
2018
|
try:
|
|
@@ -2034,7 +2073,8 @@ def analyze_chain(
|
|
|
2034
2073
|
_load_credentials()
|
|
2035
2074
|
services = _initialize_services(context)
|
|
2036
2075
|
if not all(services[:3]):
|
|
2037
|
-
|
|
2076
|
+
from .cli_helpers import _fail_services_init
|
|
2077
|
+
_fail_services_init()
|
|
2038
2078
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2039
2079
|
|
|
2040
2080
|
try:
|
|
@@ -2104,7 +2144,8 @@ def analyze_kotlin_call_audit(
|
|
|
2104
2144
|
_load_credentials()
|
|
2105
2145
|
services = _initialize_services(context)
|
|
2106
2146
|
if not all(services[:3]):
|
|
2107
|
-
|
|
2147
|
+
from .cli_helpers import _fail_services_init
|
|
2148
|
+
_fail_services_init()
|
|
2108
2149
|
db_manager, _, code_finder = services[:3]
|
|
2109
2150
|
|
|
2110
2151
|
try:
|
|
@@ -2164,7 +2205,8 @@ def analyze_dependencies(
|
|
|
2164
2205
|
_load_credentials()
|
|
2165
2206
|
services = _initialize_services(context)
|
|
2166
2207
|
if not all(services[:3]):
|
|
2167
|
-
|
|
2208
|
+
from .cli_helpers import _fail_services_init
|
|
2209
|
+
_fail_services_init()
|
|
2168
2210
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2169
2211
|
|
|
2170
2212
|
try:
|
|
@@ -2216,7 +2258,8 @@ def analyze_inheritance_tree(
|
|
|
2216
2258
|
_load_credentials()
|
|
2217
2259
|
services = _initialize_services(context)
|
|
2218
2260
|
if not all(services[:3]):
|
|
2219
|
-
|
|
2261
|
+
from .cli_helpers import _fail_services_init
|
|
2262
|
+
_fail_services_init()
|
|
2220
2263
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2221
2264
|
|
|
2222
2265
|
try:
|
|
@@ -2286,7 +2329,8 @@ def analyze_complexity(
|
|
|
2286
2329
|
_load_credentials()
|
|
2287
2330
|
services = _initialize_services(context)
|
|
2288
2331
|
if not all(services[:3]):
|
|
2289
|
-
|
|
2332
|
+
from .cli_helpers import _fail_services_init
|
|
2333
|
+
_fail_services_init()
|
|
2290
2334
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2291
2335
|
|
|
2292
2336
|
_FILE_EXTENSIONS = ('.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.rb',
|
|
@@ -2363,7 +2407,8 @@ def analyze_dead_code(
|
|
|
2363
2407
|
_load_credentials()
|
|
2364
2408
|
services = _initialize_services(context)
|
|
2365
2409
|
if not all(services[:3]):
|
|
2366
|
-
|
|
2410
|
+
from .cli_helpers import _fail_services_init
|
|
2411
|
+
_fail_services_init()
|
|
2367
2412
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2368
2413
|
|
|
2369
2414
|
try:
|
|
@@ -2417,7 +2462,8 @@ def analyze_overrides(
|
|
|
2417
2462
|
_load_credentials()
|
|
2418
2463
|
services = _initialize_services(context)
|
|
2419
2464
|
if not all(services[:3]):
|
|
2420
|
-
|
|
2465
|
+
from .cli_helpers import _fail_services_init
|
|
2466
|
+
_fail_services_init()
|
|
2421
2467
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2422
2468
|
|
|
2423
2469
|
try:
|
|
@@ -2472,7 +2518,8 @@ def analyze_variable_usage(
|
|
|
2472
2518
|
_load_credentials()
|
|
2473
2519
|
services = _initialize_services(context)
|
|
2474
2520
|
if not all(services[:3]):
|
|
2475
|
-
|
|
2521
|
+
from .cli_helpers import _fail_services_init
|
|
2522
|
+
_fail_services_init()
|
|
2476
2523
|
db_manager, graph_builder, code_finder = services[:3]
|
|
2477
2524
|
|
|
2478
2525
|
try:
|
|
@@ -2634,7 +2681,7 @@ def main(
|
|
|
2634
2681
|
"--database",
|
|
2635
2682
|
"--db",
|
|
2636
2683
|
"-db",
|
|
2637
|
-
help="[Global] Temporarily override database backend (falkordb, falkordb-remote, neo4j, or
|
|
2684
|
+
help="[Global] Temporarily override database backend (falkordb, falkordb-remote, kuzudb, ladybugdb, neo4j, or nornic) for any command"
|
|
2638
2685
|
),
|
|
2639
2686
|
visual: bool = typer.Option(
|
|
2640
2687
|
False,
|
|
@@ -2849,7 +2896,7 @@ def _write_datasource_graph(ingested: dict) -> None:
|
|
|
2849
2896
|
raise typer.Exit(1)
|
|
2850
2897
|
|
|
2851
2898
|
from codegraphcontext.tools.indexing.persistence.writer import GraphWriter
|
|
2852
|
-
GraphWriter(driver).write_datasource_graph(ingested)
|
|
2899
|
+
GraphWriter(driver, db_manager=dm).write_datasource_graph(ingested)
|
|
2853
2900
|
|
|
2854
2901
|
|
|
2855
2902
|
if __name__ == "__main__":
|
|
@@ -71,12 +71,25 @@ class CGCBundle:
|
|
|
71
71
|
Returns:
|
|
72
72
|
str: 'elementId' for Neo4j, 'id' for FalkorDB
|
|
73
73
|
"""
|
|
74
|
-
# Check if we're using Neo4j or FalkorDB
|
|
75
74
|
backend = self.db_manager.get_backend_type()
|
|
76
75
|
if backend == 'neo4j':
|
|
77
76
|
return 'elementId'
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
return 'id'
|
|
78
|
+
|
|
79
|
+
def _uses_pk_edge_matching(self) -> bool:
|
|
80
|
+
"""Kùzu/Ladybug internal IDs are not comparable via id() in MATCH."""
|
|
81
|
+
return self.db_manager.get_backend_type() in {'kuzudb', 'ladybugdb'}
|
|
82
|
+
|
|
83
|
+
def _node_lookup_key(self, labels, properties: Dict) -> Optional[tuple]:
|
|
84
|
+
if not labels:
|
|
85
|
+
return None
|
|
86
|
+
if isinstance(labels, str):
|
|
87
|
+
labels = [labels]
|
|
88
|
+
primary_label = labels[0]
|
|
89
|
+
pk_field = self._PK_MAP.get(primary_label)
|
|
90
|
+
if pk_field and pk_field in properties:
|
|
91
|
+
return (primary_label, pk_field, properties[pk_field])
|
|
92
|
+
return None
|
|
80
93
|
|
|
81
94
|
|
|
82
95
|
def export_to_bundle(
|
|
@@ -139,7 +152,7 @@ class CGCBundle:
|
|
|
139
152
|
from importlib.metadata import version as get_version
|
|
140
153
|
py_version = get_version("codegraphcontext")
|
|
141
154
|
except Exception:
|
|
142
|
-
py_version = "0.4.
|
|
155
|
+
py_version = "0.4.16"
|
|
143
156
|
|
|
144
157
|
metadata["format_version"] = "1.0.0"
|
|
145
158
|
metadata["generator"] = f"PYv{py_version}"
|
|
@@ -368,18 +381,24 @@ class CGCBundle:
|
|
|
368
381
|
}
|
|
369
382
|
|
|
370
383
|
with self.db_manager.get_driver().session() as session:
|
|
371
|
-
# Get node labels
|
|
384
|
+
# Get node labels (backend-aware)
|
|
385
|
+
backend = getattr(self.db_manager, "get_backend_type", lambda: "neo4j")()
|
|
372
386
|
try:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
387
|
+
if backend in ("kuzudb", "ladybugdb"):
|
|
388
|
+
# KuzuDB/LadybugDB: SHOW TABLES not available in ≤ 0.11
|
|
389
|
+
result = session.run("MATCH (n) RETURN DISTINCT label(n) AS lbl")
|
|
390
|
+
labels = sorted({record[0] for record in result if record[0] is not None})
|
|
391
|
+
else:
|
|
392
|
+
result = session.run("CALL db.labels()")
|
|
393
|
+
labels = []
|
|
394
|
+
for record in result:
|
|
395
|
+
try:
|
|
396
|
+
labels.append(record[0])
|
|
397
|
+
except (KeyError, TypeError):
|
|
398
|
+
if hasattr(record, 'values'):
|
|
399
|
+
vals = list(record.values())
|
|
400
|
+
if vals:
|
|
401
|
+
labels.append(vals[0])
|
|
383
402
|
schema["node_labels"] = labels
|
|
384
403
|
except Exception:
|
|
385
404
|
schema["node_labels"] = []
|
|
@@ -908,7 +927,12 @@ cgc import <bundle-file>.cgc
|
|
|
908
927
|
|
|
909
928
|
record = result.single()
|
|
910
929
|
if record and old_id:
|
|
911
|
-
|
|
930
|
+
if self._uses_pk_edge_matching():
|
|
931
|
+
lookup = self._node_lookup_key(labels, properties)
|
|
932
|
+
if lookup:
|
|
933
|
+
id_mapping[old_id] = lookup
|
|
934
|
+
else:
|
|
935
|
+
id_mapping[old_id] = record['new_id']
|
|
912
936
|
|
|
913
937
|
return len(batch)
|
|
914
938
|
|
|
@@ -959,14 +983,27 @@ cgc import <bundle-file>.cgc
|
|
|
959
983
|
warning_logger(f"Skipping edge: node IDs not found in mapping")
|
|
960
984
|
continue
|
|
961
985
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
986
|
+
if self._uses_pk_edge_matching():
|
|
987
|
+
from_label, from_pk, from_val = new_from
|
|
988
|
+
to_label, to_pk, to_val = new_to
|
|
989
|
+
query = f"""
|
|
990
|
+
MATCH (a:{from_label} {{{from_pk}: $from_val}}), (b:{to_label} {{{to_pk}: $to_val}})
|
|
991
|
+
CREATE (a)-[r:{rel_type}]->(b)
|
|
992
|
+
SET r = $props
|
|
993
|
+
"""
|
|
994
|
+
session.run(
|
|
995
|
+
query,
|
|
996
|
+
from_val=from_val,
|
|
997
|
+
to_val=to_val,
|
|
998
|
+
props=properties,
|
|
999
|
+
)
|
|
1000
|
+
else:
|
|
1001
|
+
query = f"""
|
|
1002
|
+
MATCH (a), (b)
|
|
1003
|
+
WHERE {id_function}(a) = $from_id AND {id_function}(b) = $to_id
|
|
1004
|
+
CREATE (a)-[r:{rel_type}]->(b)
|
|
1005
|
+
SET r = $props
|
|
1006
|
+
"""
|
|
1007
|
+
session.run(query, from_id=new_from, to_id=new_to, props=properties)
|
|
971
1008
|
|
|
972
1009
|
return len(batch)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# src/codegraphcontext/core/cgcignore.py
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Iterable, Optional, Tuple
|
|
4
|
-
|
|
4
|
+
import re
|
|
5
|
+
from ..utils.debug_log import warning_logger
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def _resolve_explicit_path(ignore_root: Path, explicit_path: Optional[str]) -> Optional[Path]:
|
|
@@ -85,12 +86,134 @@ def read_cgcignore_patterns(path: Path, default_patterns: list[str]) -> list[str
|
|
|
85
86
|
# User patterns first so explicit repo rules take precedence.
|
|
86
87
|
return user_patterns + list(default_patterns)
|
|
87
88
|
|
|
89
|
+
class CGCIgnoreMatcher:
|
|
90
|
+
def __init__(self, patterns: list[str], root_dir: Path):
|
|
91
|
+
self.root_dir = root_dir
|
|
92
|
+
self.rules = self._compile_patterns(patterns)
|
|
93
|
+
|
|
94
|
+
def _translate_segment(self, seg: str) -> str:
|
|
95
|
+
"""Translates a single gitwildmatch segment into regex, completely avoiding fnmatch."""
|
|
96
|
+
i, n = 0, len(seg)
|
|
97
|
+
res = ""
|
|
98
|
+
while i < n:
|
|
99
|
+
c = seg[i]
|
|
100
|
+
i += 1
|
|
101
|
+
if c == '*':
|
|
102
|
+
res += '[^/]*'
|
|
103
|
+
elif c == '?':
|
|
104
|
+
res += '[^/]'
|
|
105
|
+
elif c == '\\':
|
|
106
|
+
# Handle gitignore escape sequence (e.g. \! matches a literal !)
|
|
107
|
+
if i < n:
|
|
108
|
+
res += re.escape(seg[i])
|
|
109
|
+
i += 1
|
|
110
|
+
else:
|
|
111
|
+
res += re.escape('\\')
|
|
112
|
+
elif c == '[':
|
|
113
|
+
j = i
|
|
114
|
+
if j < n and seg[j] == '!':
|
|
115
|
+
j += 1
|
|
116
|
+
if j < n and seg[j] == ']':
|
|
117
|
+
j += 1
|
|
118
|
+
while j < n and seg[j] != ']':
|
|
119
|
+
j += 1
|
|
120
|
+
if j >= n:
|
|
121
|
+
res += '\\['
|
|
122
|
+
else:
|
|
123
|
+
stuff = seg[i:j].replace('\\', '\\\\')
|
|
124
|
+
i = j + 1
|
|
125
|
+
if stuff[0] == '!':
|
|
126
|
+
stuff = '^' + stuff[1:]
|
|
127
|
+
elif stuff[0] == '^':
|
|
128
|
+
stuff = '\\' + stuff
|
|
129
|
+
res += '[' + stuff + ']'
|
|
130
|
+
else:
|
|
131
|
+
res += re.escape(c)
|
|
132
|
+
return res
|
|
133
|
+
|
|
134
|
+
def _compile_patterns(self, patterns: list[str]) -> list[Tuple[bool, re.Pattern]]:
|
|
135
|
+
rules = []
|
|
136
|
+
for raw_pat in patterns:
|
|
137
|
+
pat = raw_pat.strip()
|
|
138
|
+
if not pat or pat.startswith('#'):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
is_negation = pat.startswith('!')
|
|
142
|
+
if is_negation:
|
|
143
|
+
pat = pat[1:]
|
|
144
|
+
|
|
145
|
+
is_dir_only = pat.endswith('/')
|
|
146
|
+
if is_dir_only:
|
|
147
|
+
pat = pat[:-1]
|
|
148
|
+
|
|
149
|
+
is_root_anchored = '/' in pat
|
|
150
|
+
if pat.startswith('/'):
|
|
151
|
+
pat = pat[1:]
|
|
152
|
+
|
|
153
|
+
segments = pat.split('**')
|
|
154
|
+
translated_segments = []
|
|
155
|
+
for seg in segments:
|
|
156
|
+
if seg:
|
|
157
|
+
translated_segments.append(self._translate_segment(seg))
|
|
158
|
+
else:
|
|
159
|
+
translated_segments.append('')
|
|
160
|
+
|
|
161
|
+
regex_str = '.*'.join(translated_segments)
|
|
162
|
+
|
|
163
|
+
if is_root_anchored:
|
|
164
|
+
regex_str = r'\A' + regex_str
|
|
165
|
+
else:
|
|
166
|
+
regex_str = r'(?:.*/)?' + regex_str
|
|
167
|
+
|
|
168
|
+
if is_dir_only:
|
|
169
|
+
# Must match a directory (so it must have trailing slash, or children)
|
|
170
|
+
regex_str = regex_str + r'(?:/.*)\Z'
|
|
171
|
+
else:
|
|
172
|
+
regex_str = regex_str + r'(?:/.*)?\Z'
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
compiled = re.compile(regex_str, re.DOTALL)
|
|
176
|
+
rules.append((is_negation, compiled))
|
|
177
|
+
except re.error as e:
|
|
178
|
+
warning_logger(f"Failed to compile .cgcignore pattern '{raw_pat}': {e}")
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
return rules
|
|
182
|
+
|
|
183
|
+
def match_file(self, file_path) -> bool:
|
|
184
|
+
"""Returns True if the file should be ignored.
|
|
185
|
+
|
|
186
|
+
Note on directory-only patterns (e.g. `build/`):
|
|
187
|
+
Since we match on the path string, a directory-only pattern requires a
|
|
188
|
+
trailing slash in the input to be matched accurately. If the caller checks
|
|
189
|
+
a directory without appending a trailing slash (e.g. `match_file("build")`),
|
|
190
|
+
it will not match `build/` because we cannot distinguish it from a file named `build`.
|
|
191
|
+
Callers (like `safe_walk`) should append `/` to directory paths before calling this.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
if isinstance(file_path, Path) and file_path.is_absolute():
|
|
195
|
+
rel_path = str(file_path.relative_to(self.root_dir)).replace("\\", "/")
|
|
196
|
+
else:
|
|
197
|
+
rel_path = str(file_path).replace("\\", "/")
|
|
198
|
+
except ValueError:
|
|
199
|
+
# If not relative to root, just use absolute
|
|
200
|
+
rel_path = str(file_path).replace("\\", "/")
|
|
201
|
+
|
|
202
|
+
rel_path = rel_path.lstrip('/')
|
|
203
|
+
|
|
204
|
+
ignored = False
|
|
205
|
+
for is_neg, rule in self.rules:
|
|
206
|
+
if rule.search(rel_path):
|
|
207
|
+
ignored = not is_neg
|
|
208
|
+
|
|
209
|
+
return ignored
|
|
210
|
+
|
|
88
211
|
def build_ignore_spec(
|
|
89
212
|
ignore_root: Path,
|
|
90
213
|
default_patterns: list[str],
|
|
91
214
|
explicit_path: Optional[str] = None,
|
|
92
|
-
) -> Tuple[
|
|
93
|
-
"""Build
|
|
215
|
+
) -> Tuple[CGCIgnoreMatcher, Optional[Path]]:
|
|
216
|
+
"""Build CGCIgnoreMatcher using merged default + user .cgcignore patterns.
|
|
94
217
|
|
|
95
218
|
Returns the compiled spec and the discovered/created .cgcignore path.
|
|
96
219
|
"""
|
|
@@ -116,4 +239,4 @@ def build_ignore_spec(
|
|
|
116
239
|
)
|
|
117
240
|
|
|
118
241
|
all_patterns = merged_user_patterns + list(default_patterns)
|
|
119
|
-
return
|
|
242
|
+
return CGCIgnoreMatcher(all_patterns, ignore_root), local_cgcignore_path
|
|
@@ -34,7 +34,7 @@ class GraphBuilder:
|
|
|
34
34
|
self.job_manager = job_manager
|
|
35
35
|
self.loop = loop
|
|
36
36
|
self.driver = self.db_manager.get_driver()
|
|
37
|
-
self._writer = GraphWriter(self.driver)
|
|
37
|
+
self._writer = GraphWriter(self.driver, db_manager=self.db_manager)
|
|
38
38
|
self.last_call_resolution_diagnostics: list[Dict[str, Any]] = []
|
|
39
39
|
self.parsers = {
|
|
40
40
|
".py": "python",
|
|
@@ -65,9 +65,10 @@ def add_code_to_graph(graph_builder, job_manager, loop, list_repos_func, **args)
|
|
|
65
65
|
|
|
66
66
|
if not path_obj.exists():
|
|
67
67
|
return {
|
|
68
|
-
"success":
|
|
68
|
+
"success": False,
|
|
69
69
|
"status": "path_not_found",
|
|
70
|
-
"
|
|
70
|
+
"error": f"Path '{path}' does not exist.",
|
|
71
|
+
"message": f"Path '{path}' does not exist.",
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
# Prevent re-indexing the same repository.
|
|
@@ -11,6 +11,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
11
11
|
from ....utils.debug_log import info_logger, warning_logger
|
|
12
12
|
from ....utils.git_utils import get_repo_commit_hash
|
|
13
13
|
from ..sanitize import sanitize_props
|
|
14
|
+
from ..schema_contract import NODE_LABELS
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _is_binder_exception(e: Exception) -> bool:
|
|
@@ -22,8 +23,83 @@ def _is_binder_exception(e: Exception) -> bool:
|
|
|
22
23
|
class GraphWriter:
|
|
23
24
|
"""Persists repository/file/symbol nodes and relationships via the Neo4j-like driver API."""
|
|
24
25
|
|
|
25
|
-
def __init__(self, driver: Any):
|
|
26
|
+
def __init__(self, driver: Any, db_manager: Any = None):
|
|
26
27
|
self.driver = driver
|
|
28
|
+
self._db_manager = db_manager
|
|
29
|
+
if db_manager is None:
|
|
30
|
+
warning_logger(
|
|
31
|
+
"[GraphWriter] db_manager not provided; "
|
|
32
|
+
"backend detection will default to 'neo4j'"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _get_all_node_labels(self) -> list[str]:
|
|
36
|
+
"""Discover all node labels in the database, backend-aware.
|
|
37
|
+
|
|
38
|
+
Neo4j / Nornic use ``CALL db.labels()``.
|
|
39
|
+
KuzuDB / LadybugDB use ``MATCH (n) RETURN DISTINCT label(n)``
|
|
40
|
+
(``SHOW TABLES`` is not supported in KuzuDB Python bindings ≤ 0.11).
|
|
41
|
+
FalkorDB uses ``CALL db.labels()`` without YIELD.
|
|
42
|
+
All backends fall back to :data:`schema_contract.NODE_LABELS`
|
|
43
|
+
plus supplementary labels on failure.
|
|
44
|
+
"""
|
|
45
|
+
# Prefer db_manager.get_backend_type(); fall back to driver, then neo4j
|
|
46
|
+
backend = (
|
|
47
|
+
getattr(self._db_manager, "get_backend_type", None)
|
|
48
|
+
or getattr(self.driver, "get_backend_type", None)
|
|
49
|
+
or (lambda: "neo4j")
|
|
50
|
+
)()
|
|
51
|
+
|
|
52
|
+
if backend in ("kuzudb", "ladybugdb"):
|
|
53
|
+
# NOTE: Full node scan required because SHOW TABLES is unavailable
|
|
54
|
+
# in KuzuDB ≤ 0.11. Acceptable for delete_repository (low-frequency).
|
|
55
|
+
try:
|
|
56
|
+
with self.driver.session() as session:
|
|
57
|
+
result = session.run(
|
|
58
|
+
"MATCH (n) RETURN DISTINCT label(n) AS lbl"
|
|
59
|
+
)
|
|
60
|
+
labels = sorted(
|
|
61
|
+
{record[0] for record in result if record[0] is not None}
|
|
62
|
+
)
|
|
63
|
+
if labels:
|
|
64
|
+
return labels
|
|
65
|
+
except Exception as e:
|
|
66
|
+
info_logger(
|
|
67
|
+
f"[DELETE] label discovery failed for {backend} "
|
|
68
|
+
f"({e}), using fallback list"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif backend in ("neo4j", "nornic"):
|
|
72
|
+
try:
|
|
73
|
+
with self.driver.session() as session:
|
|
74
|
+
label_records = session.run(
|
|
75
|
+
"CALL db.labels() YIELD label RETURN label"
|
|
76
|
+
)
|
|
77
|
+
return sorted({record["label"] for record in label_records})
|
|
78
|
+
except Exception as e:
|
|
79
|
+
info_logger(
|
|
80
|
+
f"[DELETE] CALL db.labels() failed for {backend} "
|
|
81
|
+
f"({e}), using fallback list"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
elif backend in ("falkordb", "falkordb-remote"):
|
|
85
|
+
try:
|
|
86
|
+
with self.driver.session() as session:
|
|
87
|
+
label_records = session.run("CALL db.labels()")
|
|
88
|
+
return sorted({record["label"] for record in label_records})
|
|
89
|
+
except Exception as e:
|
|
90
|
+
info_logger(
|
|
91
|
+
f"[DELETE] CALL db.labels() failed for {backend} "
|
|
92
|
+
f"({e}), using fallback list"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Fallback: canonical NODE_LABELS from schema_contract + supplementary
|
|
96
|
+
# labels that may exist in the graph from dynamic indexing paths.
|
|
97
|
+
return sorted(NODE_LABELS | {
|
|
98
|
+
"ExternalClass", "ExternalFunction",
|
|
99
|
+
"EnumValue", "Namespace", "TypeAlias", "Decorator",
|
|
100
|
+
"Method", "Endpoint", "OrmMapping", "Query",
|
|
101
|
+
"SpringDataRepository", "Mixin", "Extension", "Object",
|
|
102
|
+
})
|
|
27
103
|
|
|
28
104
|
def add_repository_to_graph(self, repo_path: Path, is_dependency: bool = False) -> None:
|
|
29
105
|
repo_name = repo_path.name
|
|
@@ -1364,17 +1440,16 @@ class GraphWriter:
|
|
|
1364
1440
|
# list. Every time the indexer learned a new node type (Variable,
|
|
1365
1441
|
# Parameter, Directory, ExternalClass, DbTable, ...) the hardcoded
|
|
1366
1442
|
# tuple here had to be kept in lockstep, and every miss leaked
|
|
1367
|
-
# orphan nodes on `delete_repository`.
|
|
1368
|
-
#
|
|
1369
|
-
#
|
|
1370
|
-
#
|
|
1443
|
+
# orphan nodes on `delete_repository`.
|
|
1444
|
+
#
|
|
1445
|
+
# Neo4j: `CALL db.labels()` returns exactly the set of labels.
|
|
1446
|
+
# KuzuDB: `MATCH (n) RETURN DISTINCT label(n)` discovers labels dynamically.
|
|
1447
|
+
# Other backends: comprehensive fallback list.
|
|
1371
1448
|
#
|
|
1372
1449
|
# Labels with no node matching the path prefix are cheap: the
|
|
1373
1450
|
# label-scoped scan returns 0 rows, the while-True loop exits
|
|
1374
1451
|
# immediately, and we move on.
|
|
1375
|
-
|
|
1376
|
-
label_records = session.run("CALL db.labels() YIELD label RETURN label")
|
|
1377
|
-
all_labels = sorted({record["label"] for record in label_records})
|
|
1452
|
+
all_labels = self._get_all_node_labels()
|
|
1378
1453
|
|
|
1379
1454
|
for label in all_labels:
|
|
1380
1455
|
while True:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraphcontext
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.16
|
|
4
4
|
Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
|
|
5
5
|
Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -155,7 +155,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
157
|
## 📍 Quick Navigation
|
|
158
|
-
* [🚀 Quick Start](
|
|
158
|
+
* [🚀 Quick Start](#-installation--quick-start)
|
|
159
159
|
* [🌐 Supported Programming Languages](#supported-programming-languages)
|
|
160
160
|
* [🛠️ CLI Toolkit](#for-cli-toolkit-mode)
|
|
161
161
|
* [🤖 MCP Server](#-for-mcp-server-mode)
|
|
@@ -183,7 +183,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
|
|
|
183
183
|
---
|
|
184
184
|
|
|
185
185
|
## Project Details
|
|
186
|
-
- **Version:** 0.4.
|
|
186
|
+
- **Version:** 0.4.16
|
|
187
187
|
- **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
|
|
188
188
|
- **License:** MIT License (See [LICENSE](LICENSE) for details)
|
|
189
189
|
- **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
|
|
@@ -9,17 +9,17 @@ codegraphcontext/api/mcp_sse.py,sha256=VVQ_5s438SOJWSNwRinXgAmCE2Qhiz5rTGafwfp-K
|
|
|
9
9
|
codegraphcontext/api/router.py,sha256=8iEhjERfGI5Udn801wMGpZmcmAuwbt5cMCwnK-Evg9Q,3229
|
|
10
10
|
codegraphcontext/api/schemas.py,sha256=pGLuKtdn55pf8mS8wv8MwehbT_3-4OZuSYjI5LJxzOc,1034
|
|
11
11
|
codegraphcontext/cli/__init__.py,sha256=2vUlnf7H09M9JbS2gS_LmMUBJWAWcGGHkkl4I2YrdSg,39
|
|
12
|
-
codegraphcontext/cli/cli_helpers.py,sha256=
|
|
13
|
-
codegraphcontext/cli/config_manager.py,sha256=
|
|
14
|
-
codegraphcontext/cli/main.py,sha256=
|
|
12
|
+
codegraphcontext/cli/cli_helpers.py,sha256=2TW5PWiaCo_htuZiqwCNYcjPKPKh9nDzMp82A3KDFOw,39748
|
|
13
|
+
codegraphcontext/cli/config_manager.py,sha256=3Bfco0sV07p7_WecvBru3Eim_VWdg5Ne9I-KCdsuUSE,45350
|
|
14
|
+
codegraphcontext/cli/main.py,sha256=y9fK-GVANefdCmvNS-TVTufB49I0FRwN6PZmbph_LZE,119777
|
|
15
15
|
codegraphcontext/cli/registry_commands.py,sha256=8irzcdPV53PtERuytFfzm8KO5nEUPrgy9ZzyKL5oZ_0,17029
|
|
16
16
|
codegraphcontext/cli/setup_macos.py,sha256=Xjlv_9jk9qv8Gh7stpH1pvlalzC0Fg176y7jc5G1zh0,3575
|
|
17
17
|
codegraphcontext/cli/setup_wizard.py,sha256=_aZxOe3q4qIEQeaghDVsNPxMRcxJsvibrfqf39rSshI,47746
|
|
18
18
|
codegraphcontext/cli/visualizer.py,sha256=md5HehQ9_8lBTejQqq2lxXtMRzDxCjjF9L9h2qu6KR0,1902
|
|
19
19
|
codegraphcontext/core/__init__.py,sha256=mXMX6gBJ6DMad6lTvQvrpTxaSt1QfPMn3t_x1pZF4cg,10492
|
|
20
20
|
codegraphcontext/core/bundle_registry.py,sha256=k3QZAGaLo2DV5p1xhqzgl_bf7faHCBfWT6ETYylSbKU,5711
|
|
21
|
-
codegraphcontext/core/cgc_bundle.py,sha256=
|
|
22
|
-
codegraphcontext/core/cgcignore.py,sha256=
|
|
21
|
+
codegraphcontext/core/cgc_bundle.py,sha256=o5L1GPxZ23IxUEvzv6XeNFHQhsZIQr0aSFq-dM5lJU8,42854
|
|
22
|
+
codegraphcontext/core/cgcignore.py,sha256=O8dDWAhixIeTzLnL5I9dEe28oCzvBhUEQ0mAx3AH_ik,8753
|
|
23
23
|
codegraphcontext/core/database.py,sha256=RWMdKKinoWewyTyTuIXX-fk7EHkba91LO18gZ12wwEg,14648
|
|
24
24
|
codegraphcontext/core/database_falkordb.py,sha256=iN7uSvXFVp1clSjF2q92Omh4k6TlgAlS1-1QL9FF50o,25989
|
|
25
25
|
codegraphcontext/core/database_falkordb_remote.py,sha256=aMiuJTB_-2rBUpbAmX8ElZiZrYZbOXK-On3ncgMCyhY,7118
|
|
@@ -32,7 +32,7 @@ codegraphcontext/core/watcher.py,sha256=EFjKYK3wUQG2pQter_A2xSe8yIrkZwfjZ9anITAj
|
|
|
32
32
|
codegraphcontext/tools/__init__.py,sha256=iqQPEkDlGXO7rAaLNQrS_ZAbLSklBSK-OowpARq0mig,41
|
|
33
33
|
codegraphcontext/tools/advanced_language_query_tool.py,sha256=sZ1_IjqyeIHW-KR4MjXQnEly8-B1hpsFctMdUCROODE,3914
|
|
34
34
|
codegraphcontext/tools/code_finder.py,sha256=rSoyezvOedIeejN9INyb4kbniRctO25DR3tczvKAImo,70586
|
|
35
|
-
codegraphcontext/tools/graph_builder.py,sha256=
|
|
35
|
+
codegraphcontext/tools/graph_builder.py,sha256=Sn7zZG7zbwNqc9noH29cV1CXDrnykEsOGEMgCvD3v6g,54682
|
|
36
36
|
codegraphcontext/tools/package_resolver.py,sha256=KtbdMReTezszjdsqYniL-Xb-QUsrAJWtf1NSiyIPkLI,18704
|
|
37
37
|
codegraphcontext/tools/report_generator.py,sha256=K4AROEMZU--fX217JT6y7P7LfiqRnG8olwrj0vO1kKo,12499
|
|
38
38
|
codegraphcontext/tools/scip_indexer.py,sha256=tHR362n_tVOhOykTKPnPpCdgk9iryu2DuxAfSNonvXM,36926
|
|
@@ -45,7 +45,7 @@ codegraphcontext/tools/datasources/cassandra_ingester.py,sha256=sgbskSkvE98qtOpr
|
|
|
45
45
|
codegraphcontext/tools/datasources/mysql_ingester.py,sha256=uWstOv7mOEws1pmT8uAsEBkoOGM1FXOWt_2VCE-tZeg,3991
|
|
46
46
|
codegraphcontext/tools/datasources/redis_ingester.py,sha256=mVUKYmr_v6g7wYaavmQscEHGi_CPZrxYLjfkldc6cWE,3788
|
|
47
47
|
codegraphcontext/tools/handlers/analysis_handlers.py,sha256=d09od3Cs2O7_a7OGLul-_KHObZqCQM3vjXClrLHdfWk,12821
|
|
48
|
-
codegraphcontext/tools/handlers/indexing_handlers.py,sha256=
|
|
48
|
+
codegraphcontext/tools/handlers/indexing_handlers.py,sha256=w6RsHrda9Tkit-OVFxawAddVNKrarzowUoXEGDTViPg,6963
|
|
49
49
|
codegraphcontext/tools/handlers/management_handlers.py,sha256=CbgvAAJlYc0yv5Jh0dDnqOYQoDKQORaDwzAw3fUZ1A8,14253
|
|
50
50
|
codegraphcontext/tools/handlers/query_handlers.py,sha256=1GGbH86vX-qjk9TzDkvi-QLmsT3kT0y9E41m8h3hBcQ,4466
|
|
51
51
|
codegraphcontext/tools/handlers/watcher_handlers.py,sha256=qsx1Qs_BXVEpKg8IG6Zxpseq_36z4WR3NjDYWGOc-vI,3479
|
|
@@ -61,7 +61,7 @@ codegraphcontext/tools/indexing/schema_contract.py,sha256=W8mU7HgvyVvNw3Mxrwc-tD
|
|
|
61
61
|
codegraphcontext/tools/indexing/scip_pipeline.py,sha256=Uc3kSLNN4G1epLle5BvIQ-jtYJDAC4k1_j_cVFf_Dd4,8988
|
|
62
62
|
codegraphcontext/tools/indexing/vector_resolver.py,sha256=RK2sj3n9_UVPVbfe9Isyn1GpfJTNrmPb830m9PF5yp4,5119
|
|
63
63
|
codegraphcontext/tools/indexing/persistence/__init__.py,sha256=hFRlNgiYS7uN9X4e4xfP3c1BvcYSxpmq0k_uaDRn2Fg,121
|
|
64
|
-
codegraphcontext/tools/indexing/persistence/writer.py,sha256=
|
|
64
|
+
codegraphcontext/tools/indexing/persistence/writer.py,sha256=vshr-cli3kSoEHiZgUeoweJ8FlAtL6VKvxX58tw43D4,71073
|
|
65
65
|
codegraphcontext/tools/indexing/resolution/__init__.py,sha256=SWaJlN5kpWPjLoLUTLC4VEDaubpf0_tl5g7igGCbWJM,310
|
|
66
66
|
codegraphcontext/tools/indexing/resolution/calls.py,sha256=R7o5ryWGCYL4h_DfnLaqdwpzSWYB6gTc8dbBWm-tAcM,107280
|
|
67
67
|
codegraphcontext/tools/indexing/resolution/inheritance.py,sha256=BOYw_-NiO4oX5dS0CIX-ebg2ZhQhJ1NRC6Vk_tnAX-M,4118
|
|
@@ -154,9 +154,9 @@ codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm,sha256=hRVATc7tOOHthq
|
|
|
154
154
|
codegraphcontext/viz/dist/wasm/tree-sitter.wasm,sha256=CCeVuI_hXktkBD-DW01xYPv5XoAYVRIqzJUJI5te-RY,196763
|
|
155
155
|
codegraphcontext/viz/dist/wasm/web-tree-sitter.js,sha256=DIaCNqRylrT_PBVw8g4ImeSnhP9uXNe_ycOlUiVGPko,153666
|
|
156
156
|
codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm,sha256=CCeVuI_hXktkBD-DW01xYPv5XoAYVRIqzJUJI5te-RY,196763
|
|
157
|
-
codegraphcontext-0.4.
|
|
158
|
-
codegraphcontext-0.4.
|
|
159
|
-
codegraphcontext-0.4.
|
|
160
|
-
codegraphcontext-0.4.
|
|
161
|
-
codegraphcontext-0.4.
|
|
162
|
-
codegraphcontext-0.4.
|
|
157
|
+
codegraphcontext-0.4.16.dist-info/licenses/LICENSE,sha256=rh8M-bJpQYJnw2vtRVgt0t7piMZXh5QzaKeNEI0vqqA,1061
|
|
158
|
+
codegraphcontext-0.4.16.dist-info/METADATA,sha256=6qMX2oZtd9IPsftIqBX98WdXyq8rhLa0kl26_GjRQSo,23919
|
|
159
|
+
codegraphcontext-0.4.16.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
160
|
+
codegraphcontext-0.4.16.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
|
|
161
|
+
codegraphcontext-0.4.16.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
|
|
162
|
+
codegraphcontext-0.4.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|