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.
Files changed (58) hide show
  1. codegraphcontext/api/app.py +5 -0
  2. codegraphcontext/api/router.py +78 -23
  3. codegraphcontext/cli/cli_helpers.py +117 -50
  4. codegraphcontext/cli/config_manager.py +86 -26
  5. codegraphcontext/cli/hook_manager.py +220 -0
  6. codegraphcontext/cli/main.py +273 -141
  7. codegraphcontext/cli/registry_commands.py +34 -11
  8. codegraphcontext/cli/setup_wizard.py +96 -10
  9. codegraphcontext/cli/visualizer.py +154 -40
  10. codegraphcontext/core/__init__.py +33 -19
  11. codegraphcontext/core/bundle_registry.py +18 -4
  12. codegraphcontext/core/cgc_bundle.py +254 -185
  13. codegraphcontext/core/cgcignore.py +3 -3
  14. codegraphcontext/core/database_falkordb.py +45 -21
  15. codegraphcontext/core/database_kuzu.py +47 -22
  16. codegraphcontext/core/database_ladybug.py +25 -14
  17. codegraphcontext/core/falkor_worker.py +39 -3
  18. codegraphcontext/core/jobs.py +1 -0
  19. codegraphcontext/core/watcher.py +92 -8
  20. codegraphcontext/server.py +57 -21
  21. codegraphcontext/tools/code_finder.py +17 -14
  22. codegraphcontext/tools/graph_builder.py +47 -184
  23. codegraphcontext/tools/handlers/analysis_handlers.py +4 -2
  24. codegraphcontext/tools/handlers/indexing_handlers.py +13 -36
  25. codegraphcontext/tools/handlers/management_handlers.py +74 -18
  26. codegraphcontext/tools/handlers/query_handlers.py +24 -37
  27. codegraphcontext/tools/handlers/watcher_handlers.py +45 -29
  28. codegraphcontext/tools/indexing/constants.py +4 -0
  29. codegraphcontext/tools/indexing/discovery.py +1 -2
  30. codegraphcontext/tools/indexing/persistence/utils.py +46 -0
  31. codegraphcontext/tools/indexing/persistence/writer.py +335 -225
  32. codegraphcontext/tools/indexing/pipeline.py +47 -13
  33. codegraphcontext/tools/indexing/resolution/calls.py +34 -10
  34. codegraphcontext/tools/indexing/resolution/post_resolution.py +18 -6
  35. codegraphcontext/tools/indexing/scip_pipeline.py +38 -3
  36. codegraphcontext/tools/languages/c.py +13 -3
  37. codegraphcontext/tools/languages/dart.py +13 -3
  38. codegraphcontext/tools/languages/elisp.py +13 -3
  39. codegraphcontext/tools/languages/elixir.py +13 -3
  40. codegraphcontext/tools/languages/go.py +13 -3
  41. codegraphcontext/tools/languages/javascript.py +13 -3
  42. codegraphcontext/tools/languages/lua.py +13 -3
  43. codegraphcontext/tools/languages/perl.py +13 -3
  44. codegraphcontext/tools/languages/python.py +87 -46
  45. codegraphcontext/tools/languages/ruby.py +13 -3
  46. codegraphcontext/tools/languages/swift.py +13 -3
  47. codegraphcontext/tools/languages/typescript.py +13 -3
  48. codegraphcontext/tools/report_generator.py +80 -15
  49. codegraphcontext/tools/scip_indexer.py +6 -1
  50. codegraphcontext/utils/cypher_readonly.py +65 -0
  51. codegraphcontext/utils/path_sandbox.py +91 -0
  52. codegraphcontext/viz/server.py +57 -37
  53. {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/METADATA +85 -5
  54. {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/RECORD +58 -54
  55. {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/WHEEL +0 -0
  56. {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/entry_points.txt +0 -0
  57. {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/licenses/LICENSE +0 -0
  58. {codegraphcontext-0.4.16.dist-info → codegraphcontext-0.4.18.dist-info}/top_level.txt +0 -0
@@ -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"])
@@ -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
- status = server.db_manager.is_connected()
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(request.name, request.arguments)
62
+ result = await server.handle_tool_call(
63
+ request.name,
64
+ request.arguments
65
+ )
66
+
51
67
  if "error" in result:
52
- return ApiResponse(status="error", error=result["error"])
53
- return ApiResponse(status="ok", data=result)
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(status="error", error=str(e))
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
- # We call handle_tool_call which is async
72
- # But add_code_to_graph starts a background job anyway
73
- result = await server.handle_tool_call("add_code_to_graph", args)
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(status_code=400, detail=result["error"])
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
- result = await server.handle_tool_call("execute_cypher_query", {
90
- "cypher_query": request.query,
91
- "params": request.params
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(server: MCPServer = Depends(get_server)):
101
- result = await server.handle_tool_call("list_indexed_repositories", {})
102
- return ApiResponse(status="ok", data=result)
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 resolve_context, ResolvedContext, register_repo_in_context, ensure_first_run_bootstrap
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
- ctx = resolve_context(cli_context_flag, cwd=cwd)
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
- # Update the current filename in the UI
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=str(path_obj)
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=True)
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
- return
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
- # Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
411
- import re
412
- forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
413
- pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
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
- console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
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
- # Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
441
- import re
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
- with db_manager.get_driver().session() as session:
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
- console.print(f"[bold red]An error occurred while executing query:[/bold red] {e}")
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(repo_path: Optional[str] = None, port: int = 8000, context: Optional[str] = None):
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
- services = _initialize_services(context)
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
- return
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
- return
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
- # Layer-by-layer deletion: iteratively delete nodes that lost
631
- # their CONTAINS parent. Each pass peels one layer of the
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 ()-[:CONTAINS]->(n)
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
- return
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
- return
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 (no initial scan needed)")
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: