codegraphcontext 0.4.16__py3-none-any.whl → 0.4.18__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 +5 -0
- codegraphcontext/api/router.py +78 -23
- codegraphcontext/cli/cli_helpers.py +117 -50
- codegraphcontext/cli/config_manager.py +86 -26
- codegraphcontext/cli/hook_manager.py +220 -0
- codegraphcontext/cli/main.py +273 -141
- codegraphcontext/cli/registry_commands.py +34 -11
- codegraphcontext/cli/setup_wizard.py +96 -10
- codegraphcontext/cli/visualizer.py +154 -40
- codegraphcontext/core/__init__.py +33 -19
- codegraphcontext/core/bundle_registry.py +18 -4
- codegraphcontext/core/cgc_bundle.py +254 -185
- codegraphcontext/core/cgcignore.py +3 -3
- codegraphcontext/core/database_falkordb.py +45 -21
- codegraphcontext/core/database_kuzu.py +47 -22
- codegraphcontext/core/database_ladybug.py +25 -14
- codegraphcontext/core/falkor_worker.py +39 -3
- codegraphcontext/core/jobs.py +1 -0
- codegraphcontext/core/watcher.py +92 -8
- codegraphcontext/server.py +57 -21
- codegraphcontext/tools/code_finder.py +17 -14
- codegraphcontext/tools/graph_builder.py +47 -184
- codegraphcontext/tools/handlers/analysis_handlers.py +4 -2
- codegraphcontext/tools/handlers/indexing_handlers.py +13 -36
- codegraphcontext/tools/handlers/management_handlers.py +74 -18
- codegraphcontext/tools/handlers/query_handlers.py +24 -37
- codegraphcontext/tools/handlers/watcher_handlers.py +45 -29
- codegraphcontext/tools/indexing/constants.py +4 -0
- codegraphcontext/tools/indexing/discovery.py +1 -2
- codegraphcontext/tools/indexing/persistence/utils.py +46 -0
- codegraphcontext/tools/indexing/persistence/writer.py +335 -225
- codegraphcontext/tools/indexing/pipeline.py +47 -13
- codegraphcontext/tools/indexing/resolution/calls.py +34 -10
- codegraphcontext/tools/indexing/resolution/post_resolution.py +18 -6
- codegraphcontext/tools/indexing/scip_pipeline.py +38 -3
- 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 +87 -46
- 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 +91 -0
- codegraphcontext/viz/server.py +57 -37
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/METADATA +85 -5
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/RECORD +58 -54
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/top_level.txt +0 -0
codegraphcontext/api/app.py
CHANGED
|
@@ -24,6 +24,11 @@ def create_app() -> FastAPI:
|
|
|
24
24
|
|
|
25
25
|
app.include_router(router, prefix="/api/v1")
|
|
26
26
|
|
|
27
|
+
@app.get("/health")
|
|
28
|
+
async def health():
|
|
29
|
+
"""Liveness probe for load balancers and k8s."""
|
|
30
|
+
return {"status": "ok"}
|
|
31
|
+
|
|
27
32
|
# MCP-over-SSE Endpoints
|
|
28
33
|
app.add_api_route("/api/v1/mcp/sse", handle_sse, methods=["GET"])
|
|
29
34
|
app.add_api_route("/api/v1/mcp/messages", handle_messages, methods=["POST"])
|
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)
|
|
@@ -245,6 +260,10 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
245
260
|
"""Synchronously indexes a repository in a given context."""
|
|
246
261
|
time_start = time.time()
|
|
247
262
|
path_obj = Path(path).resolve()
|
|
263
|
+
# Normalize to forward slashes for cross-platform DB consistency.
|
|
264
|
+
# The graph DB always stores paths via Path.resolve().as_posix(),
|
|
265
|
+
# so Cypher queries must also use forward slashes on Windows.
|
|
266
|
+
repo_path_str = path_obj.as_posix()
|
|
248
267
|
index_cwd = path_obj if path_obj.is_dir() else path_obj.parent
|
|
249
268
|
services = _initialize_services(context, cwd=index_cwd)
|
|
250
269
|
if not all(services[:3]):
|
|
@@ -268,7 +287,7 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
268
287
|
with db_manager.get_driver().session() as session:
|
|
269
288
|
result = session.run(
|
|
270
289
|
"MATCH (r:Repository {path: $path})-[:CONTAINS*]->(f:File) RETURN count(DISTINCT f) as file_count",
|
|
271
|
-
path=
|
|
290
|
+
path=repo_path_str
|
|
272
291
|
)
|
|
273
292
|
record = result.single()
|
|
274
293
|
file_count = record["file_count"] if record else 0
|
|
@@ -283,9 +302,10 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
283
302
|
except Exception as e:
|
|
284
303
|
console.print(f"[yellow]Warning: Could not check file count: {e}. Proceeding with indexing...[/yellow]")
|
|
285
304
|
|
|
286
|
-
# Auto-register the repo into the named context (auto-creates if needed)
|
|
287
305
|
if context and ctx.mode == "named":
|
|
288
|
-
register_repo_in_context(context, str(path_obj), auto_create=
|
|
306
|
+
if not register_repo_in_context(context, str(path_obj), auto_create=False):
|
|
307
|
+
db_manager.close_driver()
|
|
308
|
+
raise typer.Exit(code=1)
|
|
289
309
|
|
|
290
310
|
console.print(f"Starting indexing for: {path_obj}")
|
|
291
311
|
|
|
@@ -327,7 +347,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
327
347
|
if not package_path_str:
|
|
328
348
|
console.print(f"[red]Error: Could not find package '{package_name}' for language '{language}'.[/red]")
|
|
329
349
|
db_manager.close_driver()
|
|
330
|
-
|
|
350
|
+
raise typer.Exit(code=1)
|
|
331
351
|
|
|
332
352
|
package_path = Path(package_path_str)
|
|
333
353
|
|
|
@@ -345,6 +365,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
345
365
|
console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
|
|
346
366
|
except Exception as e:
|
|
347
367
|
console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
|
|
368
|
+
raise typer.Exit(code=1)
|
|
348
369
|
finally:
|
|
349
370
|
db_manager.close_driver()
|
|
350
371
|
|
|
@@ -398,6 +419,49 @@ def delete_helper(repo_path: str, context: Optional[str] = None):
|
|
|
398
419
|
finally:
|
|
399
420
|
db_manager.close_driver()
|
|
400
421
|
|
|
422
|
+
def _print_query_exception(e: Exception, query: str) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Pretty-print a database query exception, surfacing the raw driver
|
|
425
|
+
error message so Cypher syntax problems are clearly visible.
|
|
426
|
+
"""
|
|
427
|
+
import traceback
|
|
428
|
+
|
|
429
|
+
error_type = type(e).__name__
|
|
430
|
+
error_module = type(e).__module__ or ""
|
|
431
|
+
|
|
432
|
+
# Neo4j: CypherSyntaxError and other ClientError subclasses carry
|
|
433
|
+
# a .message and .code attribute with the full server-side detail.
|
|
434
|
+
if "neo4j" in error_module:
|
|
435
|
+
code = getattr(e, "code", None)
|
|
436
|
+
msg = getattr(e, "message", None) or str(e)
|
|
437
|
+
console.print(f"[bold red]Query Error ({error_type}):[/bold red]")
|
|
438
|
+
if code:
|
|
439
|
+
console.print(f" [yellow]Code:[/yellow] {code}")
|
|
440
|
+
console.print(f" [yellow]Message:[/yellow] {msg}")
|
|
441
|
+
|
|
442
|
+
# FalkorDB: ResponseError / exceptions in falkordb or redis packages
|
|
443
|
+
elif "falkordb" in error_module or "redis" in error_module:
|
|
444
|
+
console.print(f"[bold red]Query Error ({error_type}):[/bold red]")
|
|
445
|
+
console.print(f" [yellow]Database message:[/yellow] {e}")
|
|
446
|
+
|
|
447
|
+
# KuzuDB: RuntimeError from the kuzu extension
|
|
448
|
+
# KuzuDB: RuntimeError from the kuzu extension or database_kuzu wrapper
|
|
449
|
+
elif "kuzu" in error_module or (
|
|
450
|
+
error_type == "RuntimeError" and "Parser exception" in str(e)
|
|
451
|
+
):
|
|
452
|
+
console.print(f"[bold red]Query Error ({error_type}):[/bold red]")
|
|
453
|
+
console.print(f" [yellow]Database message:[/yellow] {e}")
|
|
454
|
+
|
|
455
|
+
else:
|
|
456
|
+
# Fallback: unknown backend — print type + message + traceback
|
|
457
|
+
console.print(f"[bold red]An error occurred while executing query ({error_type}):[/bold red]")
|
|
458
|
+
console.print(f" [yellow]Message:[/yellow] {e}")
|
|
459
|
+
console.print("[dim]--- Traceback ---[/dim]")
|
|
460
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
461
|
+
|
|
462
|
+
console.print(f"\n[dim]Failed query:[/dim]")
|
|
463
|
+
console.print(f"[dim] {query}[/dim]")
|
|
464
|
+
|
|
401
465
|
|
|
402
466
|
def cypher_helper(query: str, context: Optional[str] = None):
|
|
403
467
|
"""Executes a read-only Cypher query."""
|
|
@@ -406,23 +470,26 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
406
470
|
_fail_services_init()
|
|
407
471
|
|
|
408
472
|
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]")
|
|
473
|
+
|
|
474
|
+
from ..utils.cypher_readonly import is_read_only_cypher, read_only_rejection_message
|
|
475
|
+
|
|
476
|
+
if not is_read_only_cypher(query):
|
|
477
|
+
console.print(f"[bold red]Error:[/bold red] {read_only_rejection_message()}")
|
|
416
478
|
db_manager.close_driver()
|
|
417
479
|
raise typer.Exit(code=1)
|
|
418
480
|
|
|
481
|
+
backend = getattr(db_manager, "get_backend_type", lambda: "neo4j")()
|
|
482
|
+
session_kwargs = {"default_access_mode": "READ"} if backend == "neo4j" else {}
|
|
483
|
+
|
|
419
484
|
try:
|
|
420
|
-
with db_manager.get_driver().session() as session:
|
|
485
|
+
with db_manager.get_driver().session(**session_kwargs) as session:
|
|
421
486
|
result = session.run(query)
|
|
422
487
|
records = [record.data() for record in result]
|
|
423
488
|
console.print(json.dumps(records, indent=2))
|
|
424
489
|
except Exception as e:
|
|
425
|
-
|
|
490
|
+
_print_query_exception(e, query)
|
|
491
|
+
db_manager.close_driver()
|
|
492
|
+
raise typer.Exit(code=1)
|
|
426
493
|
finally:
|
|
427
494
|
db_manager.close_driver()
|
|
428
495
|
|
|
@@ -430,34 +497,25 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
430
497
|
def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
431
498
|
"""Executes a read-only Cypher query and visualizes the results."""
|
|
432
499
|
from .visualizer import visualize_cypher_results
|
|
433
|
-
|
|
500
|
+
from ..utils.cypher_readonly import is_read_only_cypher, read_only_rejection_message
|
|
501
|
+
|
|
434
502
|
services = _initialize_services(context)
|
|
435
503
|
if not all(services[:3]):
|
|
436
504
|
_fail_services_init()
|
|
437
505
|
|
|
438
506
|
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]")
|
|
507
|
+
|
|
508
|
+
if not is_read_only_cypher(query):
|
|
509
|
+
console.print(f"[bold red]Error:[/bold red] {read_only_rejection_message()}")
|
|
446
510
|
db_manager.close_driver()
|
|
447
511
|
raise typer.Exit(code=1)
|
|
448
512
|
|
|
449
513
|
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)
|
|
514
|
+
visualize_cypher_results(query)
|
|
459
515
|
except Exception as e:
|
|
460
|
-
|
|
516
|
+
_print_query_exception(e, query)
|
|
517
|
+
db_manager.close_driver()
|
|
518
|
+
raise typer.Exit(code=1)
|
|
461
519
|
finally:
|
|
462
520
|
db_manager.close_driver()
|
|
463
521
|
|
|
@@ -466,7 +524,12 @@ import uvicorn
|
|
|
466
524
|
import urllib.parse
|
|
467
525
|
from ..viz.server import run_server, set_db_manager
|
|
468
526
|
|
|
469
|
-
def visualize_helper(
|
|
527
|
+
def visualize_helper(
|
|
528
|
+
repo_path: Optional[str] = None,
|
|
529
|
+
port: int = 8000,
|
|
530
|
+
context: Optional[str] = None,
|
|
531
|
+
cypher_query: Optional[str] = None,
|
|
532
|
+
):
|
|
470
533
|
"""Generates an interactive visualization using the Playground UI."""
|
|
471
534
|
services = _initialize_services(context)
|
|
472
535
|
if not all(services[:3]):
|
|
@@ -537,7 +600,9 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
537
600
|
params = {"backend": backend_url}
|
|
538
601
|
if repo_path:
|
|
539
602
|
params["repo_path"] = str(Path(repo_path).resolve())
|
|
540
|
-
|
|
603
|
+
if cypher_query:
|
|
604
|
+
params["cypher_query"] = cypher_query
|
|
605
|
+
|
|
541
606
|
query_string = urllib.parse.urlencode(params)
|
|
542
607
|
visualization_url = f"{backend_url}/explore?{query_string}"
|
|
543
608
|
|
|
@@ -558,6 +623,7 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
558
623
|
run_server(host="127.0.0.1", port=port, static_dir=str(static_dir))
|
|
559
624
|
except Exception as e:
|
|
560
625
|
console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
|
|
626
|
+
raise typer.Exit(code=1)
|
|
561
627
|
finally:
|
|
562
628
|
db_manager.close_driver()
|
|
563
629
|
|
|
@@ -565,17 +631,18 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
565
631
|
def reindex_helper(path: str, context: Optional[str] = None):
|
|
566
632
|
"""Force re-index by deleting and rebuilding the repository."""
|
|
567
633
|
time_start = time.time()
|
|
568
|
-
|
|
634
|
+
path_obj = Path(path).resolve()
|
|
635
|
+
index_cwd = path_obj if path_obj.is_dir() else path_obj.parent
|
|
636
|
+
services = _initialize_services(context, cwd=index_cwd)
|
|
569
637
|
if not all(services[:3]):
|
|
570
638
|
_fail_services_init()
|
|
571
639
|
|
|
572
640
|
db_manager, graph_builder, code_finder, ctx = services
|
|
573
|
-
path_obj = Path(path).resolve()
|
|
574
641
|
|
|
575
642
|
if not path_obj.exists():
|
|
576
643
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
577
644
|
db_manager.close_driver()
|
|
578
|
-
|
|
645
|
+
raise typer.Exit(code=1)
|
|
579
646
|
|
|
580
647
|
# Check if already indexed
|
|
581
648
|
indexed_repos = code_finder.list_indexed_repositories()
|
|
@@ -589,7 +656,7 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
589
656
|
except Exception as e:
|
|
590
657
|
console.print(f"[red]Error deleting old index: {e}[/red]")
|
|
591
658
|
db_manager.close_driver()
|
|
592
|
-
|
|
659
|
+
raise typer.Exit(code=1)
|
|
593
660
|
|
|
594
661
|
console.print(f"[cyan]Re-indexing: {path_obj}[/cyan]")
|
|
595
662
|
|
|
@@ -627,14 +694,13 @@ def clean_helper(context: Optional[str] = None):
|
|
|
627
694
|
batch_size = 500
|
|
628
695
|
|
|
629
696
|
with db_manager.get_driver().session() as session:
|
|
630
|
-
#
|
|
631
|
-
#
|
|
632
|
-
# Repository → File → Class/Function → Variable hierarchy.
|
|
697
|
+
# Delete nodes with no incoming relationships (true orphans).
|
|
698
|
+
# Parameters (HAS_PARAMETER), import Modules (IMPORTS), etc. are kept.
|
|
633
699
|
while True:
|
|
634
700
|
result = session.run("""
|
|
635
701
|
MATCH (n)
|
|
636
702
|
WHERE NOT n:Repository
|
|
637
|
-
AND NOT ()-[
|
|
703
|
+
AND NOT ()-[]->(n)
|
|
638
704
|
WITH n LIMIT $batch_size
|
|
639
705
|
DETACH DELETE n
|
|
640
706
|
RETURN count(n) as deleted
|
|
@@ -762,7 +828,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
762
828
|
db_manager.close_driver()
|
|
763
829
|
|
|
764
830
|
|
|
765
|
-
def watch_helper(path: str, context: Optional[str] = None):
|
|
831
|
+
def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional[bool] = None):
|
|
766
832
|
"""Watch a directory for changes and auto-update the graph (blocking mode)."""
|
|
767
833
|
import logging
|
|
768
834
|
from ..core.watcher import CodeWatcher
|
|
@@ -782,12 +848,12 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
782
848
|
if not path_obj.exists():
|
|
783
849
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
784
850
|
db_manager.close_driver()
|
|
785
|
-
|
|
851
|
+
raise typer.Exit(code=1)
|
|
786
852
|
|
|
787
853
|
if not path_obj.is_dir():
|
|
788
854
|
console.print(f"[red]Error: Path must be a directory: {path_obj}[/red]")
|
|
789
855
|
db_manager.close_driver()
|
|
790
|
-
|
|
856
|
+
raise typer.Exit(code=1)
|
|
791
857
|
|
|
792
858
|
console.print(f"[bold cyan]🔍 Watching {path_obj} for changes...[/bold cyan]")
|
|
793
859
|
|
|
@@ -817,7 +883,7 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
817
883
|
|
|
818
884
|
# Create watcher instance
|
|
819
885
|
job_manager = JobManager()
|
|
820
|
-
watcher = CodeWatcher(graph_builder, job_manager)
|
|
886
|
+
watcher = CodeWatcher(graph_builder, job_manager, use_polling=use_polling)
|
|
821
887
|
|
|
822
888
|
try:
|
|
823
889
|
# Start the observer thread
|
|
@@ -825,10 +891,11 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
825
891
|
|
|
826
892
|
# Add the directory to watch
|
|
827
893
|
if is_indexed:
|
|
828
|
-
console.print("[green]✓[/green] Already indexed
|
|
894
|
+
console.print("[green]✓[/green] Already indexed. Synchronizing current files...")
|
|
829
895
|
watcher.watch_directory(
|
|
830
896
|
str(path_obj),
|
|
831
897
|
perform_initial_scan=False,
|
|
898
|
+
sync_on_start=True,
|
|
832
899
|
cgcignore_path=ctx.cgcignore_path,
|
|
833
900
|
)
|
|
834
901
|
else:
|