codegraphcontext 0.4.15__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 +111 -71
- codegraphcontext/cli/config_manager.py +68 -23
- codegraphcontext/cli/main.py +138 -65
- 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 +297 -201
- codegraphcontext/core/cgcignore.py +127 -4
- 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 +11 -8
- codegraphcontext/tools/handlers/analysis_handlers.py +4 -2
- codegraphcontext/tools/handlers/indexing_handlers.py +16 -38
- 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 +150 -136
- 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.15.dist-info → codegraphcontext-0.4.17.dist-info}/METADATA +83 -3
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.17.dist-info}/RECORD +51 -49
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.17.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.17.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.15.dist-info → codegraphcontext-0.4.17.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.15.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,11 +29,36 @@ 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
|
|
|
36
42
|
|
|
43
|
+
def _fail_services_init() -> None:
|
|
44
|
+
"""Abort the CLI command when database/services could not be initialized."""
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _kuzu_fallback_path(ctx: ResolvedContext) -> Optional[str]:
|
|
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())
|
|
53
|
+
if ctx.db_path:
|
|
54
|
+
return str(Path(ctx.db_path).parent / "kuzudb")
|
|
55
|
+
try:
|
|
56
|
+
from .config_manager import _default_global_db_path
|
|
57
|
+
return _default_global_db_path("kuzudb")
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
37
62
|
def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int = 5) -> None:
|
|
38
63
|
diagnostics = getattr(graph_builder, "last_call_resolution_diagnostics", [])
|
|
39
64
|
if not diagnostics:
|
|
@@ -59,14 +84,21 @@ def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int =
|
|
|
59
84
|
console.print(table)
|
|
60
85
|
|
|
61
86
|
|
|
62
|
-
def _initialize_services(
|
|
87
|
+
def _initialize_services(
|
|
88
|
+
cli_context_flag: Optional[str] = None,
|
|
89
|
+
cwd: Optional[Path] = None,
|
|
90
|
+
) -> tuple[Any, Any, Any, ResolvedContext]:
|
|
63
91
|
"""
|
|
64
92
|
Initializes and returns core service managers based on the resolved context.
|
|
65
93
|
Returns (db_manager, graph_builder, code_finder, resolved_context).
|
|
66
94
|
"""
|
|
67
95
|
ensure_first_run_bootstrap()
|
|
68
96
|
console.print("[dim]Resolving context...[/dim]")
|
|
69
|
-
|
|
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)
|
|
70
102
|
|
|
71
103
|
# Let the user know what context we're operating in
|
|
72
104
|
if ctx.mode == "named":
|
|
@@ -93,7 +125,7 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
93
125
|
db_manager = get_database_manager(db_path=runtime_path or ctx.db_path)
|
|
94
126
|
except ValueError as e:
|
|
95
127
|
console.print(f"[bold red]Database Configuration Error:[/bold red] {e}")
|
|
96
|
-
|
|
128
|
+
_fail_services_init()
|
|
97
129
|
|
|
98
130
|
try:
|
|
99
131
|
db_manager.get_driver()
|
|
@@ -101,6 +133,8 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
101
133
|
# Check if this is a FalkorDB failure that should trigger a KùzuDB fallback
|
|
102
134
|
from ..core.database_falkordb import FalkorDBUnavailableError
|
|
103
135
|
if isinstance(e, FalkorDBUnavailableError):
|
|
136
|
+
from ..core import mark_falkordb_unavailable
|
|
137
|
+
mark_falkordb_unavailable()
|
|
104
138
|
console.print(f"[yellow]⚠ FalkorDB Lite is not functional in this environment: {e}[/yellow]")
|
|
105
139
|
console.print("[cyan]Falling back to KùzuDB for a reliable experience...[/cyan]")
|
|
106
140
|
|
|
@@ -110,15 +144,16 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
110
144
|
except Exception:
|
|
111
145
|
pass
|
|
112
146
|
|
|
113
|
-
# Re-initialize explicitly with KùzuDB
|
|
147
|
+
# Re-initialize explicitly with KùzuDB (never reuse the FalkorDB directory)
|
|
114
148
|
from ..core.database_kuzu import KuzuDBManager
|
|
115
|
-
|
|
149
|
+
kuzu_path = _kuzu_fallback_path(ctx)
|
|
150
|
+
db_manager = KuzuDBManager(db_path=kuzu_path)
|
|
116
151
|
try:
|
|
117
152
|
db_manager.get_driver()
|
|
118
153
|
console.print("[green]✓[/green] Successfully switched to KùzuDB fallback")
|
|
119
154
|
except Exception as kuzu_e:
|
|
120
155
|
console.print(f"[bold red]Critical Error:[/bold red] Both FalkorDB and KùzuDB failed: {kuzu_e}")
|
|
121
|
-
|
|
156
|
+
_fail_services_init()
|
|
122
157
|
else:
|
|
123
158
|
selected_db = (
|
|
124
159
|
os.environ.get("CGC_RUNTIME_DB_TYPE")
|
|
@@ -135,20 +170,20 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
135
170
|
console.print("[cyan]Neo4j failed and CGC_ALLOW_NEO4J_FALLBACK=true. Falling back to KuzuDB...[/cyan]")
|
|
136
171
|
try:
|
|
137
172
|
from ..core.database_kuzu import KuzuDBManager
|
|
138
|
-
db_manager = KuzuDBManager()
|
|
173
|
+
db_manager = KuzuDBManager(db_path=_kuzu_fallback_path(ctx))
|
|
139
174
|
db_manager.get_driver()
|
|
140
175
|
console.print("[green]✓[/green] Successfully switched to KuzuDB fallback")
|
|
141
176
|
except Exception as kuzu_e:
|
|
142
177
|
console.print(f"[bold red]Critical Error:[/bold red] Neo4j failed and KuzuDB fallback failed: {kuzu_e}")
|
|
143
|
-
|
|
178
|
+
_fail_services_init()
|
|
144
179
|
else:
|
|
145
180
|
if selected_db == "neo4j":
|
|
146
181
|
console.print("[yellow]Tip:[/yellow] To continue without Neo4j, rerun with --db kuzudb")
|
|
147
|
-
|
|
182
|
+
_fail_services_init()
|
|
148
183
|
else:
|
|
149
184
|
console.print(f"[bold red]Database Connection Error:[/bold red] {e}")
|
|
150
185
|
console.print("Please ensure your database is configured correctly or run 'cgc doctor'.")
|
|
151
|
-
|
|
186
|
+
_fail_services_init()
|
|
152
187
|
|
|
153
188
|
# The GraphBuilder requires an event loop, even for synchronous-style execution
|
|
154
189
|
try:
|
|
@@ -199,8 +234,8 @@ async def _run_index_with_progress(graph_builder: GraphBuilder, path_obj: Path,
|
|
|
199
234
|
if job.total_files > 0:
|
|
200
235
|
progress.update(task_id, total=job.total_files, completed=job.processed_files)
|
|
201
236
|
|
|
202
|
-
#
|
|
203
|
-
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 ""
|
|
204
239
|
if len(current_file) > 40:
|
|
205
240
|
current_file = "..." + current_file[-37:]
|
|
206
241
|
progress.update(task_id, filename=current_file)
|
|
@@ -224,17 +259,18 @@ async def _run_index_with_progress(graph_builder: GraphBuilder, path_obj: Path,
|
|
|
224
259
|
def index_helper(path: str, context: Optional[str] = None):
|
|
225
260
|
"""Synchronously indexes a repository in a given context."""
|
|
226
261
|
time_start = time.time()
|
|
227
|
-
|
|
262
|
+
path_obj = Path(path).resolve()
|
|
263
|
+
index_cwd = path_obj if path_obj.is_dir() else path_obj.parent
|
|
264
|
+
services = _initialize_services(context, cwd=index_cwd)
|
|
228
265
|
if not all(services[:3]):
|
|
229
|
-
|
|
266
|
+
_fail_services_init()
|
|
230
267
|
|
|
231
268
|
db_manager, graph_builder, code_finder, ctx = services
|
|
232
|
-
path_obj = Path(path).resolve()
|
|
233
269
|
|
|
234
270
|
if not path_obj.exists():
|
|
235
271
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
236
272
|
db_manager.close_driver()
|
|
237
|
-
|
|
273
|
+
raise typer.Exit(code=1)
|
|
238
274
|
|
|
239
275
|
indexed_repos = code_finder.list_indexed_repositories()
|
|
240
276
|
repo_exists = any_repo_matches_path(indexed_repos, path_obj)
|
|
@@ -262,9 +298,10 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
262
298
|
except Exception as e:
|
|
263
299
|
console.print(f"[yellow]Warning: Could not check file count: {e}. Proceeding with indexing...[/yellow]")
|
|
264
300
|
|
|
265
|
-
# Auto-register the repo into the named context (auto-creates if needed)
|
|
266
301
|
if context and ctx.mode == "named":
|
|
267
|
-
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)
|
|
268
305
|
|
|
269
306
|
console.print(f"Starting indexing for: {path_obj}")
|
|
270
307
|
|
|
@@ -298,7 +335,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
298
335
|
"""Synchronously indexes a package."""
|
|
299
336
|
services = _initialize_services(context)
|
|
300
337
|
if not all(services[:3]):
|
|
301
|
-
|
|
338
|
+
_fail_services_init()
|
|
302
339
|
|
|
303
340
|
db_manager, graph_builder, code_finder, ctx = services
|
|
304
341
|
|
|
@@ -306,7 +343,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
306
343
|
if not package_path_str:
|
|
307
344
|
console.print(f"[red]Error: Could not find package '{package_name}' for language '{language}'.[/red]")
|
|
308
345
|
db_manager.close_driver()
|
|
309
|
-
|
|
346
|
+
raise typer.Exit(code=1)
|
|
310
347
|
|
|
311
348
|
package_path = Path(package_path_str)
|
|
312
349
|
|
|
@@ -324,6 +361,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
324
361
|
console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
|
|
325
362
|
except Exception as e:
|
|
326
363
|
console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
|
|
364
|
+
raise typer.Exit(code=1)
|
|
327
365
|
finally:
|
|
328
366
|
db_manager.close_driver()
|
|
329
367
|
|
|
@@ -332,7 +370,7 @@ def list_repos_helper(context: Optional[str] = None):
|
|
|
332
370
|
"""Lists all indexed repositories."""
|
|
333
371
|
services = _initialize_services(context)
|
|
334
372
|
if not all(services[:3]):
|
|
335
|
-
|
|
373
|
+
_fail_services_init()
|
|
336
374
|
|
|
337
375
|
db_manager, _, code_finder, ctx = services
|
|
338
376
|
|
|
@@ -362,7 +400,7 @@ def delete_helper(repo_path: str, context: Optional[str] = None):
|
|
|
362
400
|
"""Deletes a repository from the graph."""
|
|
363
401
|
services = _initialize_services(context)
|
|
364
402
|
if not all(services[:3]):
|
|
365
|
-
|
|
403
|
+
_fail_services_init()
|
|
366
404
|
|
|
367
405
|
db_manager, graph_builder, _, ctx = services
|
|
368
406
|
|
|
@@ -382,26 +420,29 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
382
420
|
"""Executes a read-only Cypher query."""
|
|
383
421
|
services = _initialize_services(context)
|
|
384
422
|
if not all(services[:3]):
|
|
385
|
-
|
|
423
|
+
_fail_services_init()
|
|
386
424
|
|
|
387
425
|
db_manager, _, _, ctx = services
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if re.search(pattern, query, re.IGNORECASE):
|
|
394
|
-
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()}")
|
|
395
431
|
db_manager.close_driver()
|
|
396
|
-
|
|
432
|
+
raise typer.Exit(code=1)
|
|
433
|
+
|
|
434
|
+
backend = getattr(db_manager, "get_backend_type", lambda: "neo4j")()
|
|
435
|
+
session_kwargs = {"default_access_mode": "READ"} if backend == "neo4j" else {}
|
|
397
436
|
|
|
398
437
|
try:
|
|
399
|
-
with db_manager.get_driver().session() as session:
|
|
438
|
+
with db_manager.get_driver().session(**session_kwargs) as session:
|
|
400
439
|
result = session.run(query)
|
|
401
440
|
records = [record.data() for record in result]
|
|
402
441
|
console.print(json.dumps(records, indent=2))
|
|
403
442
|
except Exception as e:
|
|
404
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)
|
|
405
446
|
finally:
|
|
406
447
|
db_manager.close_driver()
|
|
407
448
|
|
|
@@ -409,34 +450,25 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
409
450
|
def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
410
451
|
"""Executes a read-only Cypher query and visualizes the results."""
|
|
411
452
|
from .visualizer import visualize_cypher_results
|
|
412
|
-
|
|
453
|
+
from ..utils.cypher_readonly import is_read_only_cypher, read_only_rejection_message
|
|
454
|
+
|
|
413
455
|
services = _initialize_services(context)
|
|
414
456
|
if not all(services[:3]):
|
|
415
|
-
|
|
457
|
+
_fail_services_init()
|
|
416
458
|
|
|
417
459
|
db_manager, _, _, ctx = services
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
422
|
-
pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
|
|
423
|
-
if re.search(pattern, query, re.IGNORECASE):
|
|
424
|
-
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()}")
|
|
425
463
|
db_manager.close_driver()
|
|
426
|
-
|
|
464
|
+
raise typer.Exit(code=1)
|
|
427
465
|
|
|
428
466
|
try:
|
|
429
|
-
|
|
430
|
-
result = session.run(query)
|
|
431
|
-
records = [record.data() for record in result]
|
|
432
|
-
|
|
433
|
-
if not records:
|
|
434
|
-
console.print("[yellow]No results to visualize.[/yellow]")
|
|
435
|
-
return # finally block will close driver
|
|
436
|
-
|
|
437
|
-
visualize_cypher_results(records, query)
|
|
467
|
+
visualize_cypher_results(query)
|
|
438
468
|
except Exception as e:
|
|
439
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)
|
|
440
472
|
finally:
|
|
441
473
|
db_manager.close_driver()
|
|
442
474
|
|
|
@@ -445,11 +477,16 @@ import uvicorn
|
|
|
445
477
|
import urllib.parse
|
|
446
478
|
from ..viz.server import run_server, set_db_manager
|
|
447
479
|
|
|
448
|
-
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
|
+
):
|
|
449
486
|
"""Generates an interactive visualization using the Playground UI."""
|
|
450
487
|
services = _initialize_services(context)
|
|
451
488
|
if not all(services[:3]):
|
|
452
|
-
|
|
489
|
+
_fail_services_init()
|
|
453
490
|
|
|
454
491
|
db_manager, _, _, ctx = services
|
|
455
492
|
|
|
@@ -516,7 +553,9 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
516
553
|
params = {"backend": backend_url}
|
|
517
554
|
if repo_path:
|
|
518
555
|
params["repo_path"] = str(Path(repo_path).resolve())
|
|
519
|
-
|
|
556
|
+
if cypher_query:
|
|
557
|
+
params["cypher_query"] = cypher_query
|
|
558
|
+
|
|
520
559
|
query_string = urllib.parse.urlencode(params)
|
|
521
560
|
visualization_url = f"{backend_url}/explore?{query_string}"
|
|
522
561
|
|
|
@@ -537,6 +576,7 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
537
576
|
run_server(host="127.0.0.1", port=port, static_dir=str(static_dir))
|
|
538
577
|
except Exception as e:
|
|
539
578
|
console.print(f"[bold red]An error occurred while running the server:[/bold red] {e}")
|
|
579
|
+
raise typer.Exit(code=1)
|
|
540
580
|
finally:
|
|
541
581
|
db_manager.close_driver()
|
|
542
582
|
|
|
@@ -544,17 +584,18 @@ def visualize_helper(repo_path: Optional[str] = None, port: int = 8000, context:
|
|
|
544
584
|
def reindex_helper(path: str, context: Optional[str] = None):
|
|
545
585
|
"""Force re-index by deleting and rebuilding the repository."""
|
|
546
586
|
time_start = time.time()
|
|
547
|
-
|
|
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)
|
|
548
590
|
if not all(services[:3]):
|
|
549
|
-
|
|
591
|
+
_fail_services_init()
|
|
550
592
|
|
|
551
593
|
db_manager, graph_builder, code_finder, ctx = services
|
|
552
|
-
path_obj = Path(path).resolve()
|
|
553
594
|
|
|
554
595
|
if not path_obj.exists():
|
|
555
596
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
556
597
|
db_manager.close_driver()
|
|
557
|
-
|
|
598
|
+
raise typer.Exit(code=1)
|
|
558
599
|
|
|
559
600
|
# Check if already indexed
|
|
560
601
|
indexed_repos = code_finder.list_indexed_repositories()
|
|
@@ -568,7 +609,7 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
568
609
|
except Exception as e:
|
|
569
610
|
console.print(f"[red]Error deleting old index: {e}[/red]")
|
|
570
611
|
db_manager.close_driver()
|
|
571
|
-
|
|
612
|
+
raise typer.Exit(code=1)
|
|
572
613
|
|
|
573
614
|
console.print(f"[cyan]Re-indexing: {path_obj}[/cyan]")
|
|
574
615
|
|
|
@@ -595,7 +636,7 @@ def clean_helper(context: Optional[str] = None):
|
|
|
595
636
|
"""Remove orphaned nodes and relationships from the database."""
|
|
596
637
|
services = _initialize_services(context)
|
|
597
638
|
if not all(services[:3]):
|
|
598
|
-
|
|
639
|
+
_fail_services_init()
|
|
599
640
|
|
|
600
641
|
db_manager, _, _, ctx = services
|
|
601
642
|
|
|
@@ -606,14 +647,13 @@ def clean_helper(context: Optional[str] = None):
|
|
|
606
647
|
batch_size = 500
|
|
607
648
|
|
|
608
649
|
with db_manager.get_driver().session() as session:
|
|
609
|
-
#
|
|
610
|
-
#
|
|
611
|
-
# 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.
|
|
612
652
|
while True:
|
|
613
653
|
result = session.run("""
|
|
614
654
|
MATCH (n)
|
|
615
655
|
WHERE NOT n:Repository
|
|
616
|
-
AND NOT ()-[
|
|
656
|
+
AND NOT ()-[]->(n)
|
|
617
657
|
WITH n LIMIT $batch_size
|
|
618
658
|
DETACH DELETE n
|
|
619
659
|
RETURN count(n) as deleted
|
|
@@ -643,7 +683,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
643
683
|
"""Show indexing statistics for a repository or overall."""
|
|
644
684
|
services = _initialize_services(context)
|
|
645
685
|
if not all(services[:3]):
|
|
646
|
-
|
|
686
|
+
_fail_services_init()
|
|
647
687
|
|
|
648
688
|
db_manager, _, code_finder, ctx = services
|
|
649
689
|
|
|
@@ -741,7 +781,7 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
741
781
|
db_manager.close_driver()
|
|
742
782
|
|
|
743
783
|
|
|
744
|
-
def watch_helper(path: str, context: Optional[str] = None):
|
|
784
|
+
def watch_helper(path: str, context: Optional[str] = None, use_polling: Optional[bool] = None):
|
|
745
785
|
"""Watch a directory for changes and auto-update the graph (blocking mode)."""
|
|
746
786
|
import logging
|
|
747
787
|
from ..core.watcher import CodeWatcher
|
|
@@ -753,7 +793,7 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
753
793
|
|
|
754
794
|
services = _initialize_services(context)
|
|
755
795
|
if not all(services[:3]):
|
|
756
|
-
|
|
796
|
+
_fail_services_init()
|
|
757
797
|
|
|
758
798
|
db_manager, graph_builder, code_finder, ctx = services
|
|
759
799
|
path_obj = Path(path).resolve()
|
|
@@ -761,12 +801,12 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
761
801
|
if not path_obj.exists():
|
|
762
802
|
console.print(f"[red]Error: Path does not exist: {path_obj}[/red]")
|
|
763
803
|
db_manager.close_driver()
|
|
764
|
-
|
|
804
|
+
raise typer.Exit(code=1)
|
|
765
805
|
|
|
766
806
|
if not path_obj.is_dir():
|
|
767
807
|
console.print(f"[red]Error: Path must be a directory: {path_obj}[/red]")
|
|
768
808
|
db_manager.close_driver()
|
|
769
|
-
|
|
809
|
+
raise typer.Exit(code=1)
|
|
770
810
|
|
|
771
811
|
console.print(f"[bold cyan]🔍 Watching {path_obj} for changes...[/bold cyan]")
|
|
772
812
|
|
|
@@ -796,7 +836,7 @@ def watch_helper(path: str, context: Optional[str] = None):
|
|
|
796
836
|
|
|
797
837
|
# Create watcher instance
|
|
798
838
|
job_manager = JobManager()
|
|
799
|
-
watcher = CodeWatcher(graph_builder, job_manager)
|
|
839
|
+
watcher = CodeWatcher(graph_builder, job_manager, use_polling=use_polling)
|
|
800
840
|
|
|
801
841
|
try:
|
|
802
842
|
# Start the observer thread
|