codegraphcontext 0.4.16__py3-none-any.whl → 0.4.17__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/router.py +78 -23
- codegraphcontext/cli/cli_helpers.py +65 -46
- codegraphcontext/cli/config_manager.py +56 -21
- codegraphcontext/cli/main.py +156 -130
- codegraphcontext/cli/registry_commands.py +34 -11
- codegraphcontext/cli/visualizer.py +154 -40
- codegraphcontext/core/__init__.py +33 -19
- codegraphcontext/core/bundle_registry.py +18 -4
- codegraphcontext/core/cgc_bundle.py +241 -182
- codegraphcontext/core/cgcignore.py +3 -3
- codegraphcontext/core/database_falkordb.py +45 -21
- codegraphcontext/core/database_kuzu.py +38 -23
- codegraphcontext/core/database_ladybug.py +25 -14
- codegraphcontext/core/falkor_worker.py +39 -3
- codegraphcontext/core/jobs.py +1 -0
- codegraphcontext/core/watcher.py +45 -4
- codegraphcontext/server.py +57 -21
- codegraphcontext/tools/code_finder.py +16 -13
- codegraphcontext/tools/graph_builder.py +10 -7
- codegraphcontext/tools/handlers/analysis_handlers.py +4 -2
- codegraphcontext/tools/handlers/indexing_handlers.py +13 -36
- codegraphcontext/tools/handlers/management_handlers.py +68 -16
- codegraphcontext/tools/handlers/query_handlers.py +24 -37
- codegraphcontext/tools/handlers/watcher_handlers.py +38 -27
- codegraphcontext/tools/indexing/constants.py +4 -0
- codegraphcontext/tools/indexing/persistence/writer.py +72 -133
- codegraphcontext/tools/indexing/pipeline.py +20 -0
- codegraphcontext/tools/indexing/resolution/calls.py +14 -1
- codegraphcontext/tools/languages/c.py +13 -3
- codegraphcontext/tools/languages/dart.py +13 -3
- codegraphcontext/tools/languages/elisp.py +13 -3
- codegraphcontext/tools/languages/elixir.py +13 -3
- codegraphcontext/tools/languages/go.py +13 -3
- codegraphcontext/tools/languages/javascript.py +13 -3
- codegraphcontext/tools/languages/lua.py +13 -3
- codegraphcontext/tools/languages/perl.py +13 -3
- codegraphcontext/tools/languages/python.py +77 -35
- codegraphcontext/tools/languages/ruby.py +13 -3
- codegraphcontext/tools/languages/swift.py +13 -3
- codegraphcontext/tools/languages/typescript.py +13 -3
- codegraphcontext/tools/report_generator.py +80 -15
- codegraphcontext/tools/scip_indexer.py +6 -1
- codegraphcontext/utils/cypher_readonly.py +65 -0
- codegraphcontext/utils/path_sandbox.py +90 -0
- codegraphcontext/viz/server.py +57 -37
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.17.dist-info}/METADATA +82 -2
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.17.dist-info}/RECORD +51 -49
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.17.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.17.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.17.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.17.dist-info}/top_level.txt +0 -0
codegraphcontext/api/router.py
CHANGED
|
@@ -13,6 +13,8 @@ from .schemas import (
|
|
|
13
13
|
)
|
|
14
14
|
from codegraphcontext.server import MCPServer
|
|
15
15
|
|
|
16
|
+
import socket
|
|
17
|
+
|
|
16
18
|
router = APIRouter()
|
|
17
19
|
|
|
18
20
|
# Global server instance (initialized on startup)
|
|
@@ -25,9 +27,19 @@ def get_server() -> MCPServer:
|
|
|
25
27
|
_server_instance = MCPServer(cwd=Path.cwd())
|
|
26
28
|
return _server_instance
|
|
27
29
|
|
|
30
|
+
def raise_service_unavailable(exc: Exception):
|
|
31
|
+
raise HTTPException(
|
|
32
|
+
status_code=503,
|
|
33
|
+
detail="Database service unavailable",
|
|
34
|
+
) from exc
|
|
35
|
+
|
|
28
36
|
@router.get("/status", response_model=ApiResponse)
|
|
29
37
|
async def get_status(server: MCPServer = Depends(get_server)):
|
|
30
|
-
|
|
38
|
+
try:
|
|
39
|
+
status = server.db_manager.is_connected()
|
|
40
|
+
except (OSError, socket.error) as exc:
|
|
41
|
+
raise_service_unavailable(exc)
|
|
42
|
+
|
|
31
43
|
return ApiResponse(
|
|
32
44
|
status="ok",
|
|
33
45
|
message="Connected" if status else "Disconnected",
|
|
@@ -43,16 +55,35 @@ async def list_tools(server: MCPServer = Depends(get_server)):
|
|
|
43
55
|
|
|
44
56
|
@router.post("/tools/call", response_model=ApiResponse)
|
|
45
57
|
async def call_tool(
|
|
46
|
-
request: ToolCallRequest,
|
|
58
|
+
request: ToolCallRequest,
|
|
47
59
|
server: MCPServer = Depends(get_server)
|
|
48
60
|
):
|
|
49
61
|
try:
|
|
50
|
-
result = await server.handle_tool_call(
|
|
62
|
+
result = await server.handle_tool_call(
|
|
63
|
+
request.name,
|
|
64
|
+
request.arguments
|
|
65
|
+
)
|
|
66
|
+
|
|
51
67
|
if "error" in result:
|
|
52
|
-
return ApiResponse(
|
|
53
|
-
|
|
68
|
+
return ApiResponse(
|
|
69
|
+
status="error",
|
|
70
|
+
error=result["error"]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return ApiResponse(
|
|
74
|
+
status="ok",
|
|
75
|
+
data=result
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
except (OSError, socket.error) as exc:
|
|
79
|
+
raise_service_unavailable(exc)
|
|
80
|
+
|
|
54
81
|
except Exception as e:
|
|
55
|
-
return ApiResponse(
|
|
82
|
+
return ApiResponse(
|
|
83
|
+
status="error",
|
|
84
|
+
error=str(e)
|
|
85
|
+
)
|
|
86
|
+
|
|
56
87
|
|
|
57
88
|
@router.post("/index", response_model=ApiResponse)
|
|
58
89
|
async def index_repository(
|
|
@@ -60,21 +91,27 @@ async def index_repository(
|
|
|
60
91
|
background_tasks: BackgroundTasks,
|
|
61
92
|
server: MCPServer = Depends(get_server)
|
|
62
93
|
):
|
|
63
|
-
# Map to add_code_to_graph tool
|
|
64
94
|
args = {
|
|
65
95
|
"path": request.path,
|
|
66
96
|
"repo_name": request.repo_name,
|
|
67
97
|
"branch": request.branch,
|
|
68
98
|
"force": request.force
|
|
69
99
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
result = await server.handle_tool_call(
|
|
103
|
+
"add_code_to_graph",
|
|
104
|
+
args
|
|
105
|
+
)
|
|
106
|
+
except (OSError, socket.error) as exc:
|
|
107
|
+
raise_service_unavailable(exc)
|
|
108
|
+
|
|
75
109
|
if "error" in result:
|
|
76
|
-
raise HTTPException(
|
|
77
|
-
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=400,
|
|
112
|
+
detail=result["error"]
|
|
113
|
+
)
|
|
114
|
+
|
|
78
115
|
return ApiResponse(
|
|
79
116
|
status="ok",
|
|
80
117
|
message="Indexing job started",
|
|
@@ -86,17 +123,35 @@ async def execute_query(
|
|
|
86
123
|
request: QueryRequest,
|
|
87
124
|
server: MCPServer = Depends(get_server)
|
|
88
125
|
):
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
try:
|
|
127
|
+
result = await server.handle_tool_call(
|
|
128
|
+
"execute_cypher_query",
|
|
129
|
+
{
|
|
130
|
+
"cypher_query": request.query,
|
|
131
|
+
"params": request.params,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
except (OSError, socket.error) as exc:
|
|
135
|
+
raise_service_unavailable(exc)
|
|
136
|
+
|
|
94
137
|
if "error" in result:
|
|
95
138
|
return ApiResponse(status="error", error=result["error"])
|
|
96
|
-
|
|
139
|
+
|
|
97
140
|
return ApiResponse(status="ok", data=result)
|
|
98
141
|
|
|
99
142
|
@router.get("/repositories", response_model=ApiResponse)
|
|
100
|
-
async def list_repositories(
|
|
101
|
-
|
|
102
|
-
|
|
143
|
+
async def list_repositories(
|
|
144
|
+
server: MCPServer = Depends(get_server)
|
|
145
|
+
):
|
|
146
|
+
try:
|
|
147
|
+
result = await server.handle_tool_call(
|
|
148
|
+
"list_indexed_repositories",
|
|
149
|
+
{}
|
|
150
|
+
)
|
|
151
|
+
except (OSError, socket.error) as exc:
|
|
152
|
+
raise_service_unavailable(exc)
|
|
153
|
+
|
|
154
|
+
return ApiResponse(
|
|
155
|
+
status="ok",
|
|
156
|
+
data=result
|
|
157
|
+
)
|
|
@@ -29,7 +29,13 @@ from ..tools.package_resolver import get_local_package_path
|
|
|
29
29
|
from ..utils.debug_log import info_logger, warning_logger
|
|
30
30
|
from ..core.database import Neo4jConnectionError
|
|
31
31
|
from ..utils.repo_path import any_repo_matches_path
|
|
32
|
-
from .config_manager import
|
|
32
|
+
from .config_manager import (
|
|
33
|
+
resolve_context,
|
|
34
|
+
ResolvedContext,
|
|
35
|
+
register_repo_in_context,
|
|
36
|
+
ensure_first_run_bootstrap,
|
|
37
|
+
ContextNotFoundError,
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
console = Console()
|
|
35
41
|
|
|
@@ -41,6 +47,9 @@ def _fail_services_init() -> None:
|
|
|
41
47
|
|
|
42
48
|
def _kuzu_fallback_path(ctx: ResolvedContext) -> Optional[str]:
|
|
43
49
|
"""Derive a KùzuDB directory when falling back from another backend."""
|
|
50
|
+
runtime = os.getenv("CGC_RUNTIME_DB_PATH")
|
|
51
|
+
if runtime:
|
|
52
|
+
return str(Path(runtime).expanduser().resolve())
|
|
44
53
|
if ctx.db_path:
|
|
45
54
|
return str(Path(ctx.db_path).parent / "kuzudb")
|
|
46
55
|
try:
|
|
@@ -85,7 +94,11 @@ def _initialize_services(
|
|
|
85
94
|
"""
|
|
86
95
|
ensure_first_run_bootstrap()
|
|
87
96
|
console.print("[dim]Resolving context...[/dim]")
|
|
88
|
-
|
|
97
|
+
try:
|
|
98
|
+
ctx = resolve_context(cli_context_flag, cwd=cwd)
|
|
99
|
+
except ContextNotFoundError as exc:
|
|
100
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
101
|
+
raise typer.Exit(code=1)
|
|
89
102
|
|
|
90
103
|
# Let the user know what context we're operating in
|
|
91
104
|
if ctx.mode == "named":
|
|
@@ -120,6 +133,8 @@ def _initialize_services(
|
|
|
120
133
|
# Check if this is a FalkorDB failure that should trigger a KùzuDB fallback
|
|
121
134
|
from ..core.database_falkordb import FalkorDBUnavailableError
|
|
122
135
|
if isinstance(e, FalkorDBUnavailableError):
|
|
136
|
+
from ..core import mark_falkordb_unavailable
|
|
137
|
+
mark_falkordb_unavailable()
|
|
123
138
|
console.print(f"[yellow]⚠ FalkorDB Lite is not functional in this environment: {e}[/yellow]")
|
|
124
139
|
console.print("[cyan]Falling back to KùzuDB for a reliable experience...[/cyan]")
|
|
125
140
|
|
|
@@ -219,8 +234,8 @@ async def _run_index_with_progress(graph_builder: GraphBuilder, path_obj: Path,
|
|
|
219
234
|
if job.total_files > 0:
|
|
220
235
|
progress.update(task_id, total=job.total_files, completed=job.processed_files)
|
|
221
236
|
|
|
222
|
-
#
|
|
223
|
-
current_file = job.current_file or ""
|
|
237
|
+
# Prefer post-processing status over the last parsed file path
|
|
238
|
+
current_file = job.status_message or job.current_file or ""
|
|
224
239
|
if len(current_file) > 40:
|
|
225
240
|
current_file = "..." + current_file[-37:]
|
|
226
241
|
progress.update(task_id, filename=current_file)
|
|
@@ -283,9 +298,10 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
283
298
|
except Exception as e:
|
|
284
299
|
console.print(f"[yellow]Warning: Could not check file count: {e}. Proceeding with indexing...[/yellow]")
|
|
285
300
|
|
|
286
|
-
# Auto-register the repo into the named context (auto-creates if needed)
|
|
287
301
|
if context and ctx.mode == "named":
|
|
288
|
-
register_repo_in_context(context, str(path_obj), auto_create=
|
|
302
|
+
if not register_repo_in_context(context, str(path_obj), auto_create=False):
|
|
303
|
+
db_manager.close_driver()
|
|
304
|
+
raise typer.Exit(code=1)
|
|
289
305
|
|
|
290
306
|
console.print(f"Starting indexing for: {path_obj}")
|
|
291
307
|
|
|
@@ -327,7 +343,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
327
343
|
if not package_path_str:
|
|
328
344
|
console.print(f"[red]Error: Could not find package '{package_name}' for language '{language}'.[/red]")
|
|
329
345
|
db_manager.close_driver()
|
|
330
|
-
|
|
346
|
+
raise typer.Exit(code=1)
|
|
331
347
|
|
|
332
348
|
package_path = Path(package_path_str)
|
|
333
349
|
|
|
@@ -345,6 +361,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
345
361
|
console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
|
|
346
362
|
except Exception as e:
|
|
347
363
|
console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
|
|
364
|
+
raise typer.Exit(code=1)
|
|
348
365
|
finally:
|
|
349
366
|
db_manager.close_driver()
|
|
350
367
|
|
|
@@ -406,23 +423,26 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
406
423
|
_fail_services_init()
|
|
407
424
|
|
|
408
425
|
db_manager, _, _, ctx = services
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if re.search(pattern, query, re.IGNORECASE):
|
|
415
|
-
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
426
|
+
|
|
427
|
+
from ..utils.cypher_readonly import is_read_only_cypher, read_only_rejection_message
|
|
428
|
+
|
|
429
|
+
if not is_read_only_cypher(query):
|
|
430
|
+
console.print(f"[bold red]Error:[/bold red] {read_only_rejection_message()}")
|
|
416
431
|
db_manager.close_driver()
|
|
417
432
|
raise typer.Exit(code=1)
|
|
418
433
|
|
|
434
|
+
backend = getattr(db_manager, "get_backend_type", lambda: "neo4j")()
|
|
435
|
+
session_kwargs = {"default_access_mode": "READ"} if backend == "neo4j" else {}
|
|
436
|
+
|
|
419
437
|
try:
|
|
420
|
-
with db_manager.get_driver().session() as session:
|
|
438
|
+
with db_manager.get_driver().session(**session_kwargs) as session:
|
|
421
439
|
result = session.run(query)
|
|
422
440
|
records = [record.data() for record in result]
|
|
423
441
|
console.print(json.dumps(records, indent=2))
|
|
424
442
|
except Exception as e:
|
|
425
443
|
console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
|
|
444
|
+
db_manager.close_driver()
|
|
445
|
+
raise typer.Exit(code=1)
|
|
426
446
|
finally:
|
|
427
447
|
db_manager.close_driver()
|
|
428
448
|
|
|
@@ -430,34 +450,25 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
430
450
|
def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
431
451
|
"""Executes a read-only Cypher query and visualizes the results."""
|
|
432
452
|
from .visualizer import visualize_cypher_results
|
|
433
|
-
|
|
453
|
+
from ..utils.cypher_readonly import is_read_only_cypher, read_only_rejection_message
|
|
454
|
+
|
|
434
455
|
services = _initialize_services(context)
|
|
435
456
|
if not all(services[:3]):
|
|
436
457
|
_fail_services_init()
|
|
437
458
|
|
|
438
459
|
db_manager, _, _, ctx = services
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
443
|
-
pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
|
|
444
|
-
if re.search(pattern, query, re.IGNORECASE):
|
|
445
|
-
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
460
|
+
|
|
461
|
+
if not is_read_only_cypher(query):
|
|
462
|
+
console.print(f"[bold red]Error:[/bold red] {read_only_rejection_message()}")
|
|
446
463
|
db_manager.close_driver()
|
|
447
464
|
raise typer.Exit(code=1)
|
|
448
465
|
|
|
449
466
|
try:
|
|
450
|
-
|
|
451
|
-
result = session.run(query)
|
|
452
|
-
records = [record.data() for record in result]
|
|
453
|
-
|
|
454
|
-
if not records:
|
|
455
|
-
console.print("[yellow]No results to visualize.[/yellow]")
|
|
456
|
-
return # finally block will close driver
|
|
457
|
-
|
|
458
|
-
visualize_cypher_results(records, query)
|
|
467
|
+
visualize_cypher_results(query)
|
|
459
468
|
except Exception as e:
|
|
460
469
|
console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
|
|
470
|
+
db_manager.close_driver()
|
|
471
|
+
raise typer.Exit(code=1)
|
|
461
472
|
finally:
|
|
462
473
|
db_manager.close_driver()
|
|
463
474
|
|
|
@@ -466,7 +477,12 @@ import uvicorn
|
|
|
466
477
|
import urllib.parse
|
|
467
478
|
from ..viz.server import run_server, set_db_manager
|
|
468
479
|
|
|
469
|
-
def visualize_helper(
|
|
480
|
+
def visualize_helper(
|
|
481
|
+
repo_path: Optional[str] = None,
|
|
482
|
+
port: int = 8000,
|
|
483
|
+
context: Optional[str] = None,
|
|
484
|
+
cypher_query: Optional[str] = None,
|
|
485
|
+
):
|
|
470
486
|
"""Generates an interactive visualization using the Playground UI."""
|
|
471
487
|
services = _initialize_services(context)
|
|
472
488
|
if not all(services[:3]):
|
|
@@ -537,7 +553,9 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
537
553
|
params = {"backend": backend_url}
|
|
538
554
|
if repo_path:
|
|
539
555
|
params["repo_path"] = str(Path(repo_path).resolve())
|
|
540
|
-
|
|
556
|
+
if cypher_query:
|
|
557
|
+
params["cypher_query"] = cypher_query
|
|
558
|
+
|
|
541
559
|
query_string = urllib.parse.urlencode(params)
|
|
542
560
|
visualization_url = f"{backend_url}/explore?{query_string}"
|
|
543
561
|
|
|
@@ -558,6 +576,7 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
558
576
|
run_server(host="127.0.0.1", port=port, static_dir=str(static_dir))
|
|
559
577
|
except Exception as e:
|
|
560
578
|
console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
|
|
579
|
+
raise typer.Exit(code=1)
|
|
561
580
|
finally:
|
|
562
581
|
db_manager.close_driver()
|
|
563
582
|
|
|
@@ -565,17 +584,18 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
565
584
|
def reindex_helper(path: str, context: Optional[str] = None):
|
|
566
585
|
"""Force re-index by deleting and rebuilding the repository."""
|
|
567
586
|
time_start = time.time()
|
|
568
|
-
|
|
587
|
+
path_obj = Path(path).resolve()
|
|
588
|
+
index_cwd = path_obj if path_obj.is_dir() else path_obj.parent
|
|
589
|
+
services = _initialize_services(context, cwd=index_cwd)
|
|
569
590
|
if not all(services[:3]):
|
|
570
591
|
_fail_services_init()
|
|
571
592
|
|
|
572
593
|
db_manager, graph_builder, code_finder, ctx = services
|
|
573
|
-
path_obj = Path(path).resolve()
|
|
574
594
|
|
|
575
595
|
if not path_obj.exists():
|
|
576
596
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
577
597
|
db_manager.close_driver()
|
|
578
|
-
|
|
598
|
+
raise typer.Exit(code=1)
|
|
579
599
|
|
|
580
600
|
# Check if already indexed
|
|
581
601
|
indexed_repos = code_finder.list_indexed_repositories()
|
|
@@ -589,7 +609,7 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
589
609
|
except Exception as e:
|
|
590
610
|
console.print(f"[red]Error deleting old index: {e}[/red]")
|
|
591
611
|
db_manager.close_driver()
|
|
592
|
-
|
|
612
|
+
raise typer.Exit(code=1)
|
|
593
613
|
|
|
594
614
|
console.print(f"[cyan]Re-indexing: {path_obj}[/cyan]")
|
|
595
615
|
|
|
@@ -627,14 +647,13 @@ def clean_helper(context: Optional[str] = None):
|
|
|
627
647
|
batch_size = 500
|
|
628
648
|
|
|
629
649
|
with db_manager.get_driver().session() as session:
|
|
630
|
-
#
|
|
631
|
-
#
|
|
632
|
-
# Repository → File → Class/Function → Variable hierarchy.
|
|
650
|
+
# Delete nodes with no incoming relationships (true orphans).
|
|
651
|
+
# Parameters (HAS_PARAMETER), import Modules (IMPORTS), etc. are kept.
|
|
633
652
|
while True:
|
|
634
653
|
result = session.run("""
|
|
635
654
|
MATCH (n)
|
|
636
655
|
WHERE NOT n:Repository
|
|
637
|
-
AND NOT ()-[
|
|
656
|
+
AND NOT ()-[]->(n)
|
|
638
657
|
WITH n LIMIT $batch_size
|
|
639
658
|
DETACH DELETE n
|
|
640
659
|
RETURN count(n) as deleted
|
|
@@ -762,7 +781,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
762
781
|
db_manager.close_driver()
|
|
763
782
|
|
|
764
783
|
|
|
765
|
-
def watch_helper(path: str, context: Optional[str] = None):
|
|
784
|
+
def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional[bool] = None):
|
|
766
785
|
"""Watch a directory for changes and auto-update the graph (blocking mode)."""
|
|
767
786
|
import logging
|
|
768
787
|
from ..core.watcher import CodeWatcher
|
|
@@ -782,12 +801,12 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
782
801
|
if not path_obj.exists():
|
|
783
802
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
784
803
|
db_manager.close_driver()
|
|
785
|
-
|
|
804
|
+
raise typer.Exit(code=1)
|
|
786
805
|
|
|
787
806
|
if not path_obj.is_dir():
|
|
788
807
|
console.print(f"[red]Error: Path must be a directory: {path_obj}[/red]")
|
|
789
808
|
db_manager.close_driver()
|
|
790
|
-
|
|
809
|
+
raise typer.Exit(code=1)
|
|
791
810
|
|
|
792
811
|
console.print(f"[bold cyan]🔍 Watching {path_obj} for changes...[/bold cyan]")
|
|
793
812
|
|
|
@@ -817,7 +836,7 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
817
836
|
|
|
818
837
|
# Create watcher instance
|
|
819
838
|
job_manager = JobManager()
|
|
820
|
-
watcher = CodeWatcher(graph_builder, job_manager)
|
|
839
|
+
watcher = CodeWatcher(graph_builder, job_manager, use_polling=use_polling)
|
|
821
840
|
|
|
822
841
|
try:
|
|
823
842
|
# Start the observer thread
|
|
@@ -204,11 +204,12 @@ def normalize_config_path(value: str, *, absolute: bool = False, base_dir: Optio
|
|
|
204
204
|
return str(path_obj)
|
|
205
205
|
|
|
206
206
|
|
|
207
|
-
def ensure_config_dir(path: Path =
|
|
207
|
+
def ensure_config_dir(path: Optional[Path] = None):
|
|
208
208
|
"""
|
|
209
209
|
Ensure that the configuration directory exists.
|
|
210
210
|
Creates the directory and a logs subdirectory if they do not already exist.
|
|
211
211
|
"""
|
|
212
|
+
path = path or CONFIG_DIR
|
|
212
213
|
path.mkdir(parents=True, exist_ok=True)
|
|
213
214
|
(path / "logs").mkdir(parents=True, exist_ok=True)
|
|
214
215
|
|
|
@@ -275,11 +276,31 @@ def load_config() -> Dict[str, str]:
|
|
|
275
276
|
return config
|
|
276
277
|
|
|
277
278
|
|
|
279
|
+
def should_apply_project_dotenv() -> bool:
|
|
280
|
+
"""True when cwd-local ``.codegraphcontext/.env`` should merge with global config.
|
|
281
|
+
|
|
282
|
+
Skips project env when ``HOME`` is isolated (e.g. E2E) but ``cwd`` is an unrelated
|
|
283
|
+
checkout, unless ``CGC_LOAD_PROJECT_ENV=1``. Set ``CGC_IGNORE_PROJECT_ENV=1`` to force skip.
|
|
284
|
+
"""
|
|
285
|
+
if os.getenv("CGC_IGNORE_PROJECT_ENV", "").strip().lower() in ("1", "true", "yes"):
|
|
286
|
+
return False
|
|
287
|
+
if os.getenv("CGC_LOAD_PROJECT_ENV", "").strip().lower() in ("1", "true", "yes"):
|
|
288
|
+
return True
|
|
289
|
+
try:
|
|
290
|
+
Path.cwd().resolve().relative_to(Path.home().resolve())
|
|
291
|
+
return True
|
|
292
|
+
except ValueError:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
278
296
|
def find_local_env() -> Optional[Path]:
|
|
279
297
|
"""
|
|
280
298
|
Find a local .env file by searching current directory and parents.
|
|
281
299
|
Returns the first .env file found, or None.
|
|
282
300
|
"""
|
|
301
|
+
if not should_apply_project_dotenv():
|
|
302
|
+
return None
|
|
303
|
+
|
|
283
304
|
current = Path.cwd()
|
|
284
305
|
|
|
285
306
|
# Search up to 5 levels up
|
|
@@ -484,6 +505,11 @@ def get_config_value(key: str) -> Optional[str]:
|
|
|
484
505
|
return config.get(key)
|
|
485
506
|
|
|
486
507
|
|
|
508
|
+
def is_db_deletion_allowed() -> bool:
|
|
509
|
+
"""True when destructive delete/clear operations are permitted."""
|
|
510
|
+
return str(get_config_value("ALLOW_DB_DELETION") or "false").strip().lower() == "true"
|
|
511
|
+
|
|
512
|
+
|
|
487
513
|
def set_config_value(key: str, value: str) -> bool:
|
|
488
514
|
"""Set a configuration value. Returns True if successful.
|
|
489
515
|
|
|
@@ -513,7 +539,15 @@ def set_config_value(key: str, value: str) -> bool:
|
|
|
513
539
|
|
|
514
540
|
def reset_config():
|
|
515
541
|
"""Reset configuration to defaults (preserves database credentials)."""
|
|
542
|
+
import shutil
|
|
543
|
+
from datetime import datetime
|
|
544
|
+
|
|
516
545
|
ensure_config_dir()
|
|
546
|
+
if CONFIG_FILE.exists():
|
|
547
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
548
|
+
backup = CONFIG_FILE.with_name(f"{CONFIG_FILE.name}.{stamp}.bak")
|
|
549
|
+
shutil.copy2(CONFIG_FILE, backup)
|
|
550
|
+
console.print(f"[dim]Backed up current config to {backup}[/dim]")
|
|
517
551
|
save_config(DEFAULT_CONFIG.copy(), preserve_db_credentials=True)
|
|
518
552
|
console.print("[green]✅ Configuration reset to defaults[/green]")
|
|
519
553
|
console.print("[cyan]Note: Database credentials were preserved[/cyan]")
|
|
@@ -538,22 +572,21 @@ def _print_welcome_banner() -> None:
|
|
|
538
572
|
console.print()
|
|
539
573
|
|
|
540
574
|
|
|
541
|
-
def ensure_first_run_bootstrap() -> bool:
|
|
575
|
+
def ensure_first_run_bootstrap(show_welcome: bool = False) -> bool:
|
|
542
576
|
"""Run one-time setup for brand-new installs.
|
|
543
577
|
|
|
544
|
-
Creates default config files
|
|
545
|
-
|
|
578
|
+
Creates default config files and the global .cgcignore silently.
|
|
579
|
+
Returns True when bootstrap was performed.
|
|
546
580
|
"""
|
|
547
581
|
if _FIRST_RUN_MARKER.exists():
|
|
548
582
|
return False
|
|
549
583
|
|
|
550
584
|
ensure_config_dir()
|
|
551
585
|
ensure_global_cgcignore()
|
|
552
|
-
# Ensure config.yaml exists (triggers creation with defaults)
|
|
553
586
|
load_context_config()
|
|
554
|
-
|
|
587
|
+
if show_welcome:
|
|
588
|
+
_print_welcome_banner()
|
|
555
589
|
|
|
556
|
-
# Stamp so we don't repeat
|
|
557
590
|
_FIRST_RUN_MARKER.parent.mkdir(parents=True, exist_ok=True)
|
|
558
591
|
_FIRST_RUN_MARKER.write_text("1")
|
|
559
592
|
return True
|
|
@@ -792,6 +825,10 @@ def find_local_cgc_dir(start: Optional[Path] = None) -> Optional[Path]:
|
|
|
792
825
|
return None
|
|
793
826
|
|
|
794
827
|
|
|
828
|
+
class ContextNotFoundError(ValueError):
|
|
829
|
+
"""Raised when --context names an unregistered workspace."""
|
|
830
|
+
|
|
831
|
+
|
|
795
832
|
def resolve_context(
|
|
796
833
|
cli_context: Optional[str] = None,
|
|
797
834
|
cwd: Optional[Path] = None,
|
|
@@ -811,23 +848,21 @@ def resolve_context(
|
|
|
811
848
|
# --- 1. Explicit CLI flag ---
|
|
812
849
|
if cli_context:
|
|
813
850
|
ctx = cfg.contexts.get(cli_context)
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
else str(CONFIG_DIR / "contexts" / cli_context / ".cgcignore")
|
|
820
|
-
)
|
|
851
|
+
if ctx is None:
|
|
852
|
+
raise ContextNotFoundError(
|
|
853
|
+
f"Context '{cli_context}' is not registered. "
|
|
854
|
+
f"Create it with: cgc context create {cli_context}"
|
|
855
|
+
)
|
|
821
856
|
return ResolvedContext(
|
|
822
857
|
mode="named",
|
|
823
858
|
context_name=cli_context,
|
|
824
|
-
database=
|
|
825
|
-
db_path=db_path,
|
|
826
|
-
cgcignore_path=
|
|
859
|
+
database=ctx.database,
|
|
860
|
+
db_path=ctx.db_path,
|
|
861
|
+
cgcignore_path=ctx.cgcignore_path,
|
|
827
862
|
)
|
|
828
863
|
|
|
829
|
-
# --- 2. Local .codegraphcontext/ in repo ---
|
|
830
|
-
local_cgc = find_local_cgc_dir(cwd)
|
|
864
|
+
# --- 2. Local .codegraphcontext/ in repo (per-repo mode only) ---
|
|
865
|
+
local_cgc = find_local_cgc_dir(cwd) if cfg.mode == "per-repo" else None
|
|
831
866
|
|
|
832
867
|
# If we are in per-repo mode and no local folder was found, create it in CWD
|
|
833
868
|
if local_cgc is None and cfg.mode == "per-repo":
|
|
@@ -864,8 +899,8 @@ def resolve_context(
|
|
|
864
899
|
is_local=True,
|
|
865
900
|
)
|
|
866
901
|
|
|
867
|
-
# --- 2b. Saved workspace mapping (
|
|
868
|
-
mapping = get_workspace_mapping(cwd)
|
|
902
|
+
# --- 2b. Saved workspace mapping (per-repo mode only) ---
|
|
903
|
+
mapping = get_workspace_mapping(cwd) if cfg.mode == "per-repo" else None
|
|
869
904
|
if mapping:
|
|
870
905
|
mapped_ctx_path = Path(mapping["context_path"])
|
|
871
906
|
if mapped_ctx_path.exists() and mapped_ctx_path.is_dir():
|