codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,2386 @@
1
+ # src/codegraphcontext/cli/main.py
2
+ """
3
+ This module defines the command-line interface (CLI) for the CodeGraphContext application.
4
+ It uses the Typer library to create a user-friendly and well-documented CLI.
5
+
6
+ Commands:
7
+ - mcp setup: Runs an interactive wizard to configure the MCP client.
8
+ - mcp start: Launches the main MCP server.
9
+ - help: Displays help information.
10
+ - version: Show the installed version.
11
+ """
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+ from rich import box
16
+ from typing import Optional
17
+ import asyncio
18
+ import logging
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+ from importlib.metadata import version as pkg_version, PackageNotFoundError
23
+
24
+ from codegraphcontext.server import MCPServer
25
+ from codegraphcontext.core.database import DatabaseManager
26
+ from .setup_wizard import run_neo4j_setup_wizard, configure_mcp_client
27
+ from . import config_manager
28
+ # Import the new helper functions
29
+ from .cli_helpers import (
30
+ index_helper,
31
+ add_package_helper,
32
+ list_repos_helper,
33
+ delete_helper,
34
+ cypher_helper,
35
+ cypher_helper_visual,
36
+ visualize_helper,
37
+ reindex_helper,
38
+ clean_helper,
39
+ stats_helper,
40
+ _initialize_services,
41
+ watch_helper,
42
+ unwatch_helper,
43
+ list_watching_helper,
44
+ )
45
+
46
+ # Set the log level for the noisy neo4j, asyncio, and urllib3 loggers to keep the output clean.
47
+ # Get the log level from config, defaulting to WARNING
48
+ def _configure_library_loggers():
49
+ """Configure third-party library loggers based on config setting."""
50
+ try:
51
+ log_level_str = config_manager.get_config_value('LIBRARY_LOG_LEVEL')
52
+ if log_level_str is None:
53
+ log_level_str = 'WARNING'
54
+ log_level_str = str(log_level_str).upper()
55
+ log_level = getattr(logging, log_level_str, logging.WARNING)
56
+ except (AttributeError, Exception):
57
+ log_level = logging.WARNING
58
+
59
+ logging.getLogger("neo4j").setLevel(log_level)
60
+ logging.getLogger("asyncio").setLevel(log_level)
61
+ logging.getLogger("urllib3").setLevel(log_level)
62
+
63
+ _configure_library_loggers()
64
+
65
+
66
+ # Import visualization module
67
+ from .visualizer import (
68
+ visualize_call_graph,
69
+ visualize_call_chain,
70
+ visualize_dependencies,
71
+ visualize_inheritance_tree,
72
+ visualize_overrides,
73
+ visualize_search_results,
74
+ check_visual_flag,
75
+ )
76
+
77
+ # Initialize the Typer app and Rich console for formatted output.
78
+ app = typer.Typer(
79
+ name="cgc",
80
+ help="CodeGraphContext: An MCP server for AI-powered code analysis.\n\n[DEPRECATED] 'cgc start' is deprecated. Use 'cgc mcp start' instead.",
81
+ add_completion=True,
82
+ )
83
+ console = Console(stderr=True)
84
+
85
+ # Configure basic logging for the application.
86
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
87
+
88
+
89
+ def get_version() -> str:
90
+ """
91
+ Try to read version from the installed package metadata.
92
+ Fallback to a dev version if not installed.
93
+ """
94
+ try:
95
+ return pkg_version("codegraphcontext") # must match [project].name in pyproject.toml
96
+ except PackageNotFoundError:
97
+ return "0.0.0 (dev)"
98
+
99
+
100
+ # Create MCP command group
101
+ mcp_app = typer.Typer(help="MCP client configuration commands")
102
+ app.add_typer(mcp_app, name="mcp")
103
+
104
+ @mcp_app.command("setup")
105
+ def mcp_setup():
106
+ """
107
+ Configure MCP Client (IDE/CLI Integration).
108
+
109
+ Sets up CodeGraphContext integration with your IDE or CLI tool:
110
+ - VS Code, Cursor, Windsurf
111
+ - Claude Desktop, Gemini CLI
112
+ - Cline, RooCode, Amazon Q Developer
113
+ - OpenCode (prints stdio config + link to vendor docs)
114
+
115
+ Works with FalkorDB by default (no database setup needed).
116
+ """
117
+ console.print("\n[bold cyan]MCP Client Setup[/bold cyan]")
118
+ console.print("Configure your IDE or CLI tool to use CodeGraphContext.\n")
119
+ configure_mcp_client()
120
+
121
+ @mcp_app.command("start")
122
+ def mcp_start():
123
+ """
124
+ Start the CodeGraphContext MCP server.
125
+
126
+ Starts the server which listens for JSON-RPC requests from stdin.
127
+ This is used by IDE integrations (VS Code, Cursor, etc.).
128
+ """
129
+ console.print("[bold green]Starting CodeGraphContext Server...[/bold green]")
130
+ _load_credentials()
131
+
132
+ server = None
133
+ loop = asyncio.new_event_loop()
134
+ asyncio.set_event_loop(loop)
135
+ try:
136
+ server = MCPServer(loop=loop, cwd=Path.cwd())
137
+ loop.run_until_complete(server.run())
138
+ except ValueError as e:
139
+ # This typically happens if credentials are still not found after all checks.
140
+ console.print(f"[bold red]Configuration Error:[/bold red] {e}")
141
+ console.print("Please run `cgc neo4j setup` or use FalkorDB (default).")
142
+ except KeyboardInterrupt:
143
+ # Handle graceful shutdown on Ctrl+C.
144
+ console.print("\n[bold yellow]Server stopped by user.[/bold yellow]")
145
+ finally:
146
+ # Ensure server and event loop are properly closed.
147
+ if server:
148
+ server.shutdown()
149
+ loop.close()
150
+
151
+ @mcp_app.command("tools")
152
+ def mcp_tools():
153
+ """
154
+ List all available MCP tools.
155
+
156
+ Shows all tools that can be called by AI assistants through the MCP interface.
157
+ """
158
+ _load_credentials()
159
+ console.print("[bold green]Available MCP Tools:[/bold green]")
160
+ try:
161
+ # Instantiate the server to access the tool definitions.
162
+ server = MCPServer()
163
+ tools = server.tools.values()
164
+
165
+ table = Table(show_header=True, header_style="bold magenta")
166
+ table.add_column("Tool Name", style="dim", width=30)
167
+ table.add_column("Description")
168
+
169
+ for tool in sorted(tools, key=lambda t: t['name']):
170
+ table.add_row(tool['name'], tool['description'])
171
+
172
+ console.print(table)
173
+
174
+ except ValueError as e:
175
+ console.print(f"[bold red]Error loading tools:[/bold red] {e}")
176
+ console.print("Please ensure your database is configured correctly.")
177
+ except Exception as e:
178
+ console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
179
+
180
+ # Abbreviation for mcp setup
181
+ @app.command("m", rich_help_panel="Shortcuts")
182
+ def mcp_setup_alias():
183
+ """Shortcut for 'cgc mcp setup'"""
184
+ mcp_setup()
185
+
186
+
187
+ # Create Neo4j command group
188
+ neo4j_app = typer.Typer(help="Neo4j database configuration commands")
189
+ app.add_typer(neo4j_app, name="neo4j")
190
+
191
+ @neo4j_app.command("setup")
192
+ def neo4j_setup():
193
+ """
194
+ Configure Neo4j Database Connection.
195
+
196
+ Choose from multiple setup options:
197
+ - Local (Docker-based, recommended)
198
+ - Local (Binary installation on Linux)
199
+ - Hosted (Neo4j AuraDB or remote instance)
200
+ - Connect to existing Neo4j instance
201
+
202
+ Note: This is optional. CodeGraphContext works with FalkorDB by default.
203
+ """
204
+ console.print("\n[bold cyan]Neo4j Database Setup[/bold cyan]")
205
+ console.print("Configure Neo4j database connection for CodeGraphContext.\n")
206
+ run_neo4j_setup_wizard()
207
+
208
+ # Abbreviation for neo4j setup
209
+ @app.command("n", rich_help_panel="Shortcuts")
210
+ def neo4j_setup_alias():
211
+ """Shortcut for 'cgc neo4j setup'"""
212
+ neo4j_setup()
213
+
214
+
215
+ # Create Context command group
216
+ context_app = typer.Typer(help="Manage Contexts (logical workspaces) and Mode")
217
+ app.add_typer(context_app, name="context")
218
+
219
+ @context_app.command("list")
220
+ def context_list():
221
+ """List all available contexts and current mode."""
222
+ cfg = config_manager.load_context_config()
223
+ console.print(f"\n[bold]Current Mode:[/bold] {cfg.mode}")
224
+ if cfg.mode == "named":
225
+ console.print(f"[bold]Default Context:[/bold] {cfg.default_context or '<none>'}")
226
+
227
+ contexts = config_manager.list_contexts()
228
+ if not contexts:
229
+ console.print("\n[yellow]No custom contexts defined. Using global default.[/yellow]")
230
+ return
231
+
232
+ table = Table(title="\nConfigured Contexts")
233
+ table.add_column("Name", style="cyan")
234
+ table.add_column("Database", style="green")
235
+ table.add_column("DB Path", style="dim")
236
+ table.add_column("Repos Linked", justify="right")
237
+
238
+ for ctx in contexts:
239
+ marker = " *" if ctx.name == cfg.default_context else ""
240
+ table.add_row(
241
+ f"{ctx.name}{marker}",
242
+ ctx.database,
243
+ ctx.db_path,
244
+ str(len(ctx.repos))
245
+ )
246
+ console.print(table)
247
+ console.print("[dim] * indicates default context[/dim]\n")
248
+
249
+ @context_app.command("create")
250
+ def context_create(
251
+ name: str = typer.Argument(..., help="Name of the new context"),
252
+ database: str = typer.Option(None, "--database", "-d", help="Database backend (falkordb, kuzudb, neo4j). Defaults to DEFAULT_DATABASE from config."),
253
+ db_path: str = typer.Option(None, "--db-path", help="Explicit path for the DB (defaults to ~/.codegraphcontext/contexts/<name>/db)"),
254
+ ):
255
+ """Create a new logical context."""
256
+ if database is None:
257
+ database = config_manager.get_config_value("DEFAULT_DATABASE") or "falkordb"
258
+ config_manager.create_context(name, database, db_path)
259
+
260
+ @context_app.command("delete")
261
+ def context_delete(
262
+ name: str = typer.Argument(..., help="Name of the context to delete")
263
+ ):
264
+ """Delete a context from the registry."""
265
+ if typer.confirm(f"Are you sure you want to delete context '{name}'? DB files will remain on disk."):
266
+ config_manager.delete_context(name)
267
+
268
+ @context_app.command("mode")
269
+ def context_mode(
270
+ mode: str = typer.Argument(..., help="Mode to switch to (global, per-repo, named)"),
271
+ ):
272
+ """Set the system-wide context mode."""
273
+ config_manager.set_context_mode(mode.lower())
274
+
275
+ @context_app.command("default")
276
+ def context_default(
277
+ name: str = typer.Argument(..., help="Name of the context to set as default"),
278
+ ):
279
+ """Set the default named context (used when --context is omitted in named mode)."""
280
+ config_manager.set_default_context(name)
281
+
282
+
283
+ # ============================================================================
284
+ # CREDENTIALS LOADING PRECEDENCE
285
+ # ============================================================================
286
+
287
+ def _load_credentials():
288
+ """
289
+ Loads configuration and credentials from various sources into environment variables.
290
+ Uses per-variable precedence - each variable is loaded from the highest priority source.
291
+ Priority order (highest to lowest):
292
+ 1. Local `mcp.json` env vars (highest - explicit MCP server config)
293
+ 2. ``<cwd>/.codegraphcontext/.env`` only (no parent-directory walk)
294
+ 3. Global `~/.codegraphcontext/.env` (lowest - user defaults)
295
+
296
+ Step 2 skips duplicate loading when that file is the same path as the global file.
297
+ Arbitrary repo-root `.env` files are not loaded—only CodeGraphContext config paths.
298
+ """
299
+ from dotenv import dotenv_values
300
+ from codegraphcontext.cli.config_manager import (
301
+ ensure_config_dir,
302
+ codegraphcontext_dotenv_at_cwd,
303
+ )
304
+
305
+ # Ensure config directory exists (lazy initialization)
306
+ ensure_config_dir()
307
+
308
+ # Collect all config sources in reverse priority order (lowest to highest)
309
+ config_sources = []
310
+ config_source_names = []
311
+
312
+ # 3. Global .env file (lowest priority - user defaults)
313
+ global_env_path = Path.home() / ".codegraphcontext" / ".env"
314
+ if global_env_path.exists():
315
+ try:
316
+ config_sources.append(dotenv_values(str(global_env_path)))
317
+ config_source_names.append(str(global_env_path))
318
+ except Exception as e:
319
+ console.print(f"[yellow]Warning: Could not load global .env: {e}[/yellow]")
320
+
321
+ # 2. <cwd>/.codegraphcontext/.env only (overrides global when distinct)
322
+ try:
323
+ local_cgc_env = codegraphcontext_dotenv_at_cwd(Path.cwd())
324
+ if local_cgc_env and local_cgc_env.resolve() != global_env_path.resolve():
325
+ config_sources.append(dotenv_values(str(local_cgc_env)))
326
+ config_source_names.append(str(local_cgc_env))
327
+ except Exception as e:
328
+ console.print(
329
+ f"[yellow]Warning: Could not load .codegraphcontext/.env at cwd: {e}[/yellow]"
330
+ )
331
+
332
+ # 1. Local mcp.json (highest priority - explicit MCP server config)
333
+ mcp_file_path = Path.cwd() / "mcp.json"
334
+ if mcp_file_path.exists():
335
+ try:
336
+ with open(mcp_file_path, "r") as f:
337
+ mcp_config = json.load(f)
338
+ server_env = mcp_config.get("mcpServers", {}).get("CodeGraphContext", {}).get("env", {})
339
+ if server_env:
340
+ config_sources.append(server_env)
341
+ config_source_names.append("mcp.json")
342
+ except Exception as e:
343
+ console.print(f"[yellow]Warning: Could not load mcp.json: {e}[/yellow]")
344
+
345
+ # Merge all configs with proper precedence (later sources override earlier ones)
346
+ merged_config = {}
347
+ for config in config_sources:
348
+ merged_config.update(config)
349
+
350
+ # Apply merged config to environment.
351
+ # IMPORTANT: DB-selection keys set in the shell must win over .env defaults.
352
+ # E.g. `DEFAULT_DATABASE=falkordb cgc index …` must not be overridden by
353
+ # DEFAULT_DATABASE=neo4j sitting in ~/.codegraphcontext/.env
354
+ DB_OVERRIDE_KEYS = {"CGC_RUNTIME_DB_TYPE", "DEFAULT_DATABASE"}
355
+ for key, value in merged_config.items():
356
+ if value is not None: # Only set non-None values
357
+ # Never let .env clobber a DB-type key that the user already set in the shell
358
+ if key in DB_OVERRIDE_KEYS and key in os.environ:
359
+ continue
360
+ os.environ[key] = str(value)
361
+
362
+ # Report what was loaded
363
+ if config_source_names:
364
+ if len(config_source_names) == 1:
365
+ console.print(f"[dim]Loaded configuration from: {config_source_names[-1]}[/dim]")
366
+ else:
367
+ console.print(f"[dim]Loaded configuration from: {', '.join(config_source_names)} (highest priority: {config_source_names[-1]})[/dim]")
368
+ else:
369
+ console.print("[yellow]No configuration file found. Using defaults.[/yellow]")
370
+
371
+
372
+ # Show which database is actually being used.
373
+ # When CGC_RUNTIME_DB_TYPE or DEFAULT_DATABASE is set, trust it. Otherwise
374
+ # call get_database_manager() so the banner matches factory fallbacks.
375
+ runtime_db = os.environ.get("CGC_RUNTIME_DB_TYPE")
376
+ explicit_db = runtime_db or os.environ.get("DEFAULT_DATABASE")
377
+
378
+ if explicit_db:
379
+ default_db = explicit_db.lower()
380
+ else:
381
+ # No explicit choice — ask the factory which backend it will use
382
+ try:
383
+ from codegraphcontext.core import get_database_manager
384
+ _mgr = get_database_manager()
385
+ default_db = _mgr.get_backend_type() # e.g. 'falkordb' / 'kuzudb'
386
+ except Exception:
387
+ # Factory failed entirely — still show a best-guess
388
+ from codegraphcontext.core import _is_falkordb_available
389
+ default_db = "falkordb" if _is_falkordb_available() else "kuzudb"
390
+
391
+ if default_db == "neo4j":
392
+ has_neo4j_creds = all([
393
+ os.environ.get("NEO4J_URI"),
394
+ os.environ.get("NEO4J_USERNAME"),
395
+ os.environ.get("NEO4J_PASSWORD")
396
+ ])
397
+ if has_neo4j_creds:
398
+ neo4j_db = os.environ.get("NEO4J_DATABASE")
399
+ if neo4j_db:
400
+ console.print(f"[cyan]Using database: Neo4j (database: {neo4j_db})[/cyan]")
401
+ else:
402
+ console.print("[cyan]Using database: Neo4j[/cyan]")
403
+ else:
404
+ console.print("[yellow]⚠ DEFAULT_DATABASE=neo4j but credentials not found. Falling back to default.[/yellow]")
405
+ elif default_db == "falkordb":
406
+ console.print("[cyan]Using database: FalkorDB Lite[/cyan]")
407
+ elif default_db == "kuzudb":
408
+ console.print("[cyan]Using database: KùzuDB[/cyan]")
409
+ elif default_db == "falkordb-remote":
410
+ host = os.environ.get("FALKORDB_HOST")
411
+ if host:
412
+ console.print(f"[cyan]Using database: FalkorDB Remote ({host})[/cyan]")
413
+ else:
414
+ console.print(
415
+ "[yellow]⚠ DEFAULT_DATABASE=falkordb-remote but FALKORDB_HOST not set.[/yellow]"
416
+ )
417
+ else:
418
+ console.print(f"[cyan]Using database: {default_db}[/cyan]")
419
+
420
+
421
+
422
+ # ============================================================================
423
+ # CONFIG COMMAND GROUP
424
+ # ============================================================================
425
+
426
+ config_app = typer.Typer(help="Manage configuration settings")
427
+ app.add_typer(config_app, name="config")
428
+
429
+ @config_app.command("show")
430
+ def config_show():
431
+ """
432
+ Display current configuration settings.
433
+
434
+ Shows all configuration values including database, indexing options,
435
+ logging settings, and performance tuning parameters.
436
+ """
437
+ config_manager.show_config()
438
+
439
+ @config_app.command("set")
440
+ def config_set(
441
+ key: str = typer.Argument(..., help="Configuration key to set"),
442
+ value: str = typer.Argument(..., help="Value to set")
443
+ ):
444
+ """
445
+ Set a configuration value.
446
+
447
+ Examples:
448
+ cgc config set DEFAULT_DATABASE neo4j
449
+ cgc config set INDEX_VARIABLES false
450
+ cgc config set MAX_FILE_SIZE_MB 20
451
+ cgc config set DEBUG_LOGS true
452
+ """
453
+ config_manager.set_config_value(key, value)
454
+
455
+ @config_app.command("reset")
456
+ def config_reset():
457
+ """
458
+ Reset all configuration to default values.
459
+
460
+ This will restore all settings to their defaults.
461
+ Your current configuration will be backed up.
462
+ """
463
+ if typer.confirm("Are you sure you want to reset all configuration to defaults?", default=False):
464
+ config_manager.reset_config()
465
+ else:
466
+ console.print("[yellow]Reset cancelled[/yellow]")
467
+
468
+ @config_app.command("db")
469
+ def config_db(backend: str = typer.Argument(..., help="Database backend: 'neo4j', 'falkordb', 'falkordb-remote', or 'kuzudb'")):
470
+ """
471
+ Quickly switch the default database backend.
472
+
473
+ Shortcut for 'cgc config set DEFAULT_DATABASE <backend>'.
474
+
475
+ Examples:
476
+ cgc config db neo4j
477
+ cgc config db falkordb
478
+ cgc config db kuzudb
479
+ """
480
+ backend = backend.lower()
481
+ if backend not in ['falkordb', 'falkordb-remote', 'neo4j', 'kuzudb']:
482
+ console.print(f"[bold red]Invalid backend: {backend}[/bold red]")
483
+ console.print("Must be 'falkordb', 'falkordb-remote', 'neo4j', or 'kuzudb'")
484
+ raise typer.Exit(code=1)
485
+
486
+ updated = config_manager.set_config_value("DEFAULT_DATABASE", backend)
487
+ if not updated:
488
+ console.print(f"[bold red]Failed to switch default database to {backend}[/bold red]")
489
+ raise typer.Exit(code=1)
490
+
491
+ console.print(f"[green]✔ Default database switched to {backend}[/green]")
492
+
493
+ # ============================================================================
494
+ # BUNDLE COMMAND GROUP - Pre-indexed Graph Snapshots
495
+ # ============================================================================
496
+
497
+ bundle_app = typer.Typer(help="Create and load pre-indexed graph bundles")
498
+ app.add_typer(bundle_app, name="bundle")
499
+
500
+ @bundle_app.command("export")
501
+ def bundle_export(
502
+ output: str = typer.Argument(..., help="Output path for the .cgc bundle file"),
503
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Specific repository path to export (default: export all)"),
504
+ no_stats: bool = typer.Option(False, "--no-stats", help="Skip statistics generation"),
505
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
506
+ ):
507
+ """
508
+ Export the current graph to a portable .cgc bundle.
509
+
510
+ Creates a pre-indexed graph snapshot that can be distributed and loaded
511
+ instantly without re-indexing. Perfect for sharing famous repositories.
512
+
513
+ Examples:
514
+ cgc bundle export numpy.cgc --repo /path/to/numpy
515
+ cgc bundle export my-project.cgc
516
+ cgc bundle export all-repos.cgc --no-stats
517
+ """
518
+ _load_credentials()
519
+ from codegraphcontext.core.cgc_bundle import CGCBundle
520
+
521
+ services = _initialize_services(context)
522
+ if not all(services[:3]):
523
+ return
524
+ db_manager, graph_builder, code_finder = services[:3]
525
+
526
+ try:
527
+ output_path = Path(output)
528
+ repo_path = Path(repo).resolve() if repo else None
529
+
530
+ console.print(f"[cyan]Exporting graph to {output_path}...[/cyan]")
531
+ if repo_path:
532
+ console.print(f"[dim]Repository: {repo_path}[/dim]")
533
+ else:
534
+ console.print(f"[dim]Exporting all repositories[/dim]")
535
+
536
+ bundle = CGCBundle(db_manager)
537
+ success, message = bundle.export_to_bundle(
538
+ output_path,
539
+ repo_path=repo_path,
540
+ include_stats=not no_stats
541
+ )
542
+
543
+ if success:
544
+ console.print(f"[bold green]{message}[/bold green]")
545
+ else:
546
+ console.print(f"[bold red]Export failed: {message}[/bold red]")
547
+ raise typer.Exit(code=1)
548
+
549
+ finally:
550
+ db_manager.close_driver()
551
+
552
+ @bundle_app.command("import")
553
+ def bundle_import(
554
+ bundle_file: str = typer.Argument(..., help="Path to the .cgc bundle file to import"),
555
+ clear: bool = typer.Option(False, "--clear", help="Clear existing graph data before importing"),
556
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
557
+ ):
558
+ """
559
+ Import a .cgc bundle into the current database.
560
+
561
+ Loads a pre-indexed graph snapshot into your database. Use --clear to
562
+ replace all existing data with the bundle contents.
563
+
564
+ Examples:
565
+ cgc bundle import numpy.cgc
566
+ cgc bundle import my-project.cgc --clear
567
+ """
568
+ _load_credentials()
569
+ from codegraphcontext.core.cgc_bundle import CGCBundle
570
+
571
+ services = _initialize_services(context)
572
+ if not all(services[:3]):
573
+ return
574
+ db_manager, graph_builder, code_finder = services[:3]
575
+
576
+ try:
577
+ bundle_path = Path(bundle_file)
578
+
579
+ if not bundle_path.exists():
580
+ console.print(f"[bold red]Bundle file not found: {bundle_path}[/bold red]")
581
+ raise typer.Exit(code=1)
582
+
583
+ if clear:
584
+ console.print("[yellow]⚠️ Warning: This will clear all existing graph data![/yellow]")
585
+ if not typer.confirm("Are you sure you want to continue?", default=False):
586
+ console.print("[yellow]Import cancelled[/yellow]")
587
+ return
588
+
589
+ console.print(f"[cyan]Importing bundle from {bundle_path}...[/cyan]")
590
+
591
+ bundle = CGCBundle(db_manager)
592
+ success, message = bundle.import_from_bundle(
593
+ bundle_path,
594
+ clear_existing=clear
595
+ )
596
+
597
+ if success:
598
+ console.print(f"[bold green]{message}[/bold green]")
599
+ else:
600
+ console.print(f"[bold red]Import failed: {message}[/bold red]")
601
+ raise typer.Exit(code=1)
602
+
603
+ finally:
604
+ db_manager.close_driver()
605
+
606
+ @bundle_app.command("load")
607
+ def bundle_load(
608
+ bundle_name: str = typer.Argument(..., help="Bundle name or path to load (e.g., 'numpy' or 'numpy.cgc')"),
609
+ clear: bool = typer.Option(False, "--clear", help="Clear existing graph data before loading")
610
+ ):
611
+ """
612
+ Load a pre-indexed bundle (download if needed, then import).
613
+
614
+ This is a convenience command that will:
615
+ 1. Check if the bundle exists locally
616
+ 2. Download from registry if not found
617
+ 3. Import the bundle into the database
618
+
619
+ Examples:
620
+ cgc load numpy
621
+ cgc load numpy.cgc --clear
622
+ cgc load /path/to/bundle.cgc
623
+ """
624
+ _load_credentials()
625
+
626
+ bundle_path = Path(bundle_name)
627
+
628
+ # If it's an absolute path or has .cgc extension and exists, use it directly
629
+ if bundle_path.is_absolute() or (bundle_path.suffix == '.cgc' and bundle_path.exists()):
630
+ bundle_import(str(bundle_path), clear=clear)
631
+ return
632
+
633
+ # Add .cgc extension if not present
634
+ if not bundle_path.suffix:
635
+ bundle_path = Path(f"{bundle_name}.cgc")
636
+
637
+ # Check if exists locally
638
+ if bundle_path.exists():
639
+ console.print(f"[dim]Found local bundle: {bundle_path}[/dim]")
640
+ bundle_import(str(bundle_path), clear=clear)
641
+ return
642
+
643
+ # Try to download from registry
644
+ console.print(f"[yellow]Bundle '{bundle_name}' not found locally.[/yellow]")
645
+ console.print(f"[cyan]Attempting to download from registry...[/cyan]")
646
+
647
+ try:
648
+ from .registry_commands import download_bundle
649
+
650
+ # Extract just the name (without .cgc extension)
651
+ name = bundle_path.stem
652
+
653
+ # Download the bundle
654
+ downloaded_path = download_bundle(name, output_dir=None, auto_load=True)
655
+
656
+ if downloaded_path:
657
+ # Import the downloaded bundle
658
+ bundle_import(downloaded_path, clear=clear)
659
+ else:
660
+ console.print(f"[bold red]Failed to download bundle '{name}'[/bold red]")
661
+ raise typer.Exit(code=1)
662
+
663
+ except Exception as e:
664
+ console.print(f"[bold red]Error: {e}[/bold red]")
665
+ console.print(f"[dim]Use 'cgc registry list' to see available bundles[/dim]")
666
+ raise typer.Exit(code=1)
667
+
668
+ # Shortcut commands at root level
669
+ @app.command("export", rich_help_panel="Bundle Shortcuts")
670
+ def export_shortcut(
671
+ output: str = typer.Argument(..., help="Output path for the .cgc bundle file"),
672
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Specific repository path to export"),
673
+ no_stats: bool = typer.Option(False, "--no-stats", help="Skip generating statistics in the bundle"),
674
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
675
+ ):
676
+ """Shortcut for 'cgc bundle export'"""
677
+ bundle_export(output, repo, no_stats, context)
678
+
679
+ @app.command("load", rich_help_panel="Bundle Shortcuts")
680
+ def load_shortcut(
681
+ bundle_name: str = typer.Argument(..., help="Bundle name or path to load"),
682
+ clear: bool = typer.Option(False, "--clear", help="Clear existing graph data before loading")
683
+ ):
684
+ """Shortcut for 'cgc bundle load'"""
685
+ bundle_load(bundle_name, clear)
686
+
687
+ # ============================================================================
688
+ # REGISTRY COMMAND GROUP - Browse and Download Bundles
689
+ # ============================================================================
690
+
691
+ registry_app = typer.Typer(help="Browse and download bundles from the registry")
692
+ app.add_typer(registry_app, name="registry")
693
+
694
+ @registry_app.command("list")
695
+ def registry_list(
696
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information including download URLs"),
697
+ unique: bool = typer.Option(False, "--unique", "-u", help="Show only one version per package (most recent)")
698
+ ):
699
+ """
700
+ List all available bundles in the registry.
701
+
702
+ Shows bundles from both weekly pre-indexed releases and on-demand generations.
703
+ By default, shows all versions. Use --unique to see only the most recent version per package.
704
+
705
+ Examples:
706
+ cgc registry list
707
+ cgc registry list --verbose
708
+ cgc registry list --unique
709
+ """
710
+ from .registry_commands import list_bundles
711
+ list_bundles(verbose=verbose, unique=unique)
712
+
713
+ @registry_app.command("search")
714
+ def registry_search(
715
+ query: str = typer.Argument(..., help="Search query (matches name, repository, or description)")
716
+ ):
717
+ """
718
+ Search for bundles in the registry.
719
+
720
+ Searches bundle names, repositories, and descriptions for matches.
721
+
722
+ Examples:
723
+ cgc registry search numpy
724
+ cgc registry search web
725
+ cgc registry search http
726
+ """
727
+ from .registry_commands import search_bundles
728
+ search_bundles(query)
729
+
730
+ @registry_app.command("download")
731
+ def registry_download(
732
+ name: str = typer.Argument(..., help="Bundle name to download (e.g., 'numpy')"),
733
+ output_dir: Optional[str] = typer.Option(None, "--output", "-o", help="Output directory (default: current directory)"),
734
+ load: bool = typer.Option(False, "--load", "-l", help="Automatically load the bundle after downloading")
735
+ ):
736
+ """
737
+ Download a bundle from the registry.
738
+
739
+ Downloads the specified bundle to the current directory or specified output directory.
740
+ Use --load to automatically import the bundle after downloading.
741
+
742
+ Examples:
743
+ cgc registry download numpy
744
+ cgc registry download pandas --output ./bundles
745
+ cgc registry download fastapi --load
746
+ """
747
+ from .registry_commands import download_bundle
748
+
749
+ bundle_path = download_bundle(name, output_dir, auto_load=load)
750
+
751
+ if load and bundle_path:
752
+ console.print(f"\n[cyan]Loading bundle...[/cyan]")
753
+ bundle_import(bundle_path, clear=False)
754
+
755
+ @registry_app.command("request")
756
+ def registry_request(
757
+ repo_url: str = typer.Argument(..., help="GitHub repository URL to index"),
758
+ wait: bool = typer.Option(False, "--wait", "-w", help="Wait for generation to complete (not yet implemented)")
759
+ ):
760
+ """
761
+ Request on-demand generation of a bundle.
762
+
763
+ Submits a request to generate a bundle for the specified GitHub repository.
764
+ The bundle will be available in the registry after 5-10 minutes.
765
+
766
+ Examples:
767
+ cgc registry request https://github.com/encode/httpx
768
+ cgc registry request https://github.com/pallets/flask
769
+ """
770
+ from .registry_commands import request_bundle
771
+ request_bundle(repo_url, wait=wait)
772
+
773
+ # ============================================================================
774
+ # DOCTOR DIAGNOSTIC COMMAND
775
+ # ============================================================================
776
+
777
+ @app.command()
778
+ def doctor():
779
+ """
780
+ Run diagnostics to check system health and configuration.
781
+
782
+ Checks:
783
+ - Configuration validity
784
+ - Database connectivity
785
+ - Tree-sitter installation
786
+ - Required dependencies
787
+ - File permissions
788
+ """
789
+ console.print("[bold cyan]🏥 Running CodeGraphContext Diagnostics...[/bold cyan]\n")
790
+
791
+ all_checks_passed = True
792
+
793
+ config_manager.ensure_first_run_bootstrap()
794
+ config_manager.ensure_config_file()
795
+
796
+ # 1. Check configuration
797
+ console.print("[bold]1. Checking Configuration...[/bold]")
798
+ config = {}
799
+ try:
800
+ config = config_manager.load_config()
801
+
802
+ if config_manager.CONFIG_FILE.exists():
803
+ console.print(f" [green]✓[/green] Config loaded from {config_manager.CONFIG_FILE}")
804
+ else:
805
+ console.print(f" [yellow]ℹ[/yellow] No .env config found, using defaults")
806
+ console.print(f" [dim]Config will be created at: {config_manager.CONFIG_FILE}[/dim]")
807
+
808
+ if config_manager.CONTEXT_CONFIG_FILE.exists():
809
+ console.print(f" [green]✓[/green] Context config loaded from {config_manager.CONTEXT_CONFIG_FILE}")
810
+ else:
811
+ console.print(f" [yellow]ℹ[/yellow] No Context config found")
812
+ console.print(f" [dim]Context config will be auto-generated at: {config_manager.CONTEXT_CONFIG_FILE}[/dim]")
813
+
814
+ # Validate each config value
815
+ invalid_configs = []
816
+ for key, value in config.items():
817
+ is_valid, error_msg = config_manager.validate_config_value(key, value)
818
+ if not is_valid:
819
+ invalid_configs.append(f"{key}: {error_msg}")
820
+
821
+ if invalid_configs:
822
+ console.print(f" [red]✗[/red] Invalid configuration values found:")
823
+ for err in invalid_configs:
824
+ console.print(f" - {err}")
825
+ all_checks_passed = False
826
+ else:
827
+ console.print(f" [green]✓[/green] All configuration values are valid")
828
+ except Exception as e:
829
+ console.print(f" [red]✗[/red] Configuration error: {e}")
830
+ all_checks_passed = False
831
+
832
+ # 2. Check database connectivity
833
+ console.print("\n[bold]2. Checking Database Connection...[/bold]")
834
+ try:
835
+ _load_credentials()
836
+ default_db = config.get("DEFAULT_DATABASE", "falkordb")
837
+ console.print(f" Default database: {default_db}")
838
+
839
+ if default_db == "neo4j":
840
+ uri = os.environ.get("NEO4J_URI")
841
+ username = os.environ.get("NEO4J_USERNAME")
842
+ password = os.environ.get("NEO4J_PASSWORD")
843
+
844
+ if uri and username and password:
845
+ console.print(f" [cyan]Testing Neo4j connection to {uri}...[/cyan]")
846
+ is_connected, error_msg = DatabaseManager.test_connection(uri, username, password, database=os.environ.get("NEO4J_DATABASE"))
847
+ if is_connected:
848
+ console.print(f" [green]✓[/green] Neo4j connection successful")
849
+ else:
850
+ console.print(f"[red]✗[/red] Neo4j connection failed: {error_msg}")
851
+ all_checks_passed = False
852
+ else:
853
+ console.print(f" [yellow]⚠[/yellow] Neo4j credentials not set. Run 'cgc neo4j setup'")
854
+ elif default_db == "kuzudb":
855
+ from importlib.util import find_spec
856
+
857
+ if find_spec("kuzu") is not None:
858
+ console.print(f" [green]✓[/green] KuzuDB is installed")
859
+ else:
860
+ console.print(f" [red]✗[/red] KuzuDB is not installed")
861
+ console.print(f" Run: pip install kuzu")
862
+ all_checks_passed = False
863
+ else:
864
+ # FalkorDB
865
+ try:
866
+ import falkordb
867
+ console.print(f" [green]✓[/green] FalkorDB Lite is installed")
868
+ except ImportError:
869
+ console.print(f" [yellow]⚠[/yellow] FalkorDB Lite not installed (Python 3.12+ only)")
870
+ console.print(f" Run: pip install falkordblite")
871
+ except Exception as e:
872
+ console.print(f" [red]✗[/red] Database check error: {e}")
873
+ all_checks_passed = False
874
+
875
+ # 3. Check tree-sitter installation
876
+ console.print("\n[bold]3. Checking Tree-Sitter Installation...[/bold]")
877
+ try:
878
+ from tree_sitter import Language, Parser
879
+ console.print(f" [green]✓[/green] tree-sitter is installed")
880
+
881
+ try:
882
+ from tree_sitter_language_pack import get_language
883
+ console.print(f" [green]✓[/green] tree-sitter-language-pack is installed")
884
+
885
+ from codegraphcontext.utils.tree_sitter_manager import LANGUAGE_ALIASES, LANGUAGE_PACK_NAMES
886
+ all_langs = sorted(set(LANGUAGE_ALIASES.values()))
887
+ console.print(f" [dim]Supported languages ({len(all_langs)}): {', '.join(all_langs)}[/dim]")
888
+ probe_langs = ["python", "javascript", "typescript", "java", "go", "rust", "c", "cpp"]
889
+ available, unavailable = [], []
890
+ for lang in probe_langs:
891
+ try:
892
+ pack_name = LANGUAGE_PACK_NAMES.get(lang, lang)
893
+ get_language(pack_name)
894
+ available.append(lang)
895
+ except Exception:
896
+ unavailable.append(lang)
897
+ console.print(f" [green]✓[/green] {len(available)}/{len(probe_langs)} probed parsers OK: {', '.join(available)}")
898
+ if unavailable:
899
+ console.print(f" [yellow]⚠[/yellow] Unavailable: {', '.join(unavailable)}")
900
+ except ImportError:
901
+ console.print(f" [red]✗[/red] tree-sitter-language-pack not installed")
902
+ all_checks_passed = False
903
+ except ImportError as e:
904
+ console.print(f" [red]✗[/red] tree-sitter not installed: {e}")
905
+ all_checks_passed = False
906
+
907
+ # 4. Check file permissions
908
+ console.print("\n[bold]4. Checking File Permissions...[/bold]")
909
+ try:
910
+ config_dir = config_manager.CONFIG_DIR
911
+ if config_dir.exists():
912
+ console.print(f" [green]✓[/green] Config directory exists: {config_dir}")
913
+
914
+ # Check if writable
915
+ test_file = config_dir / ".test_write"
916
+ try:
917
+ test_file.touch()
918
+ test_file.unlink()
919
+ console.print(f" [green]✓[/green] Config directory is writable")
920
+ except Exception as e:
921
+ console.print(f" [red]✗[/red] Config directory not writable: {e}")
922
+ all_checks_passed = False
923
+ else:
924
+ console.print(f" [yellow]⚠[/yellow] Config directory doesn't exist, will be created on first use")
925
+ except Exception as e:
926
+ console.print(f" [red]✗[/red] Permission check error: {e}")
927
+ all_checks_passed = False
928
+
929
+ # 5. Check cgc command availability
930
+ console.print("\n[bold]5. Checking CGC Command...[/bold]")
931
+ import shutil
932
+ cgc_path = shutil.which("cgc")
933
+ if cgc_path:
934
+ console.print(f" [green]✓[/green] cgc command found at: {cgc_path}")
935
+ else:
936
+ console.print(f" [yellow]⚠[/yellow] cgc command not in PATH (using python -m cgc)")
937
+
938
+ # Final summary
939
+ console.print("\n" + "=" * 60)
940
+ if all_checks_passed:
941
+ console.print("[bold green]✅ All diagnostics passed! System is healthy.[/bold green]")
942
+ else:
943
+ console.print("[bold yellow]⚠️ Some issues detected. Please review the output above.[/bold yellow]")
944
+ console.print("\n[cyan]Common fixes:[/cyan]")
945
+ console.print(" • For Neo4j issues: Run 'cgc neo4j setup'")
946
+ console.print(" • For missing packages: pip install codegraphcontext")
947
+ console.print(" • For config issues: Run 'cgc config reset'")
948
+ console.print("=" * 60 + "\n")
949
+
950
+
951
+
952
+
953
+ @app.command()
954
+ def start():
955
+ """
956
+ [DEPRECATED] Use 'cgc mcp start' instead. This command will be removed in a future version.
957
+ """
958
+ console.print("[yellow]⚠️ 'cgc start' is deprecated. Use 'cgc mcp start' instead.[/yellow]")
959
+ mcp_start()
960
+
961
+
962
+ @app.command()
963
+ def index(
964
+ path: Optional[str] = typer.Argument(None, help="Path to the directory or file to index. Defaults to the current directory."),
965
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-index (delete existing and rebuild)"),
966
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use (overrides mode/default)"),
967
+ ):
968
+ """
969
+ Indexes a directory or file by adding it to the code graph.
970
+ If no path is provided, it indexes the current directory.
971
+
972
+ Use --force to delete the existing index and rebuild from scratch.
973
+ """
974
+ _load_credentials()
975
+ if path is None:
976
+ path = str(Path.cwd())
977
+
978
+ if force:
979
+ console.print("[yellow]Force re-indexing (--force flag detected)[/yellow]")
980
+ reindex_helper(path, context)
981
+ else:
982
+ index_helper(path, context)
983
+
984
+ @app.command()
985
+ def clean(
986
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
987
+ ):
988
+ """
989
+ Remove orphaned nodes and relationships from the database.
990
+
991
+ This will clean up nodes that are not connected to any repository,
992
+ helping to keep your database tidy and performant.
993
+ """
994
+ _load_credentials()
995
+ clean_helper(context)
996
+
997
+ @app.command()
998
+ def stats(
999
+ path: Optional[str] = typer.Argument(None, help="Path to show stats for. Omit for overall stats."),
1000
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
1001
+ ):
1002
+ """
1003
+ Show indexing statistics.
1004
+
1005
+ If a path is provided, shows stats for that specific repository.
1006
+ Otherwise, shows overall database statistics.
1007
+ """
1008
+ _load_credentials()
1009
+ if path:
1010
+ path = str(Path(path).resolve())
1011
+ stats_helper(path, context)
1012
+
1013
+ @app.command()
1014
+ def delete(
1015
+ path: Optional[str] = typer.Argument(None, help="Path of the repository to delete from the code graph."),
1016
+ all_repos: bool = typer.Option(False, "--all", help="Delete all indexed repositories"),
1017
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
1018
+ ):
1019
+ """
1020
+ Deletes a repository from the code graph.
1021
+
1022
+ Use --all to delete all repositories at once (requires confirmation).
1023
+
1024
+ Examples:
1025
+ cgc delete ./my-project # Delete specific repository
1026
+ cgc delete --all # Delete all repositories
1027
+ """
1028
+ _load_credentials()
1029
+
1030
+ if all_repos:
1031
+ # Delete all repositories
1032
+ services = _initialize_services(context)
1033
+ if not all(services[:3]):
1034
+ return
1035
+ db_manager, graph_builder, code_finder = services[:3]
1036
+
1037
+ try:
1038
+ # Get list of repositories
1039
+ repos = code_finder.list_indexed_repositories()
1040
+
1041
+ if not repos:
1042
+ console.print("[yellow]No repositories to delete.[/yellow]")
1043
+ return
1044
+
1045
+ # Show what will be deleted
1046
+ console.print(f"\n[bold red]⚠️ WARNING: You are about to delete ALL {len(repos)} repositories![/bold red]\n")
1047
+
1048
+ table = Table(show_header=True, header_style="bold magenta")
1049
+ table.add_column("Name", style="cyan")
1050
+ table.add_column("Path", style="dim")
1051
+
1052
+ for repo in repos:
1053
+ table.add_row(repo.get("name", ""), repo.get("path", ""))
1054
+
1055
+ console.print(table)
1056
+ console.print()
1057
+
1058
+ # Double confirmation
1059
+ if not typer.confirm("Are you sure you want to delete ALL repositories?", default=False):
1060
+ console.print("[yellow]Deletion cancelled.[/yellow]")
1061
+ return
1062
+
1063
+ console.print("[yellow]Please type 'delete all' to confirm:[/yellow] ", end="")
1064
+ confirmation = input()
1065
+
1066
+ if confirmation.strip().lower() != "delete all":
1067
+ console.print("[yellow]Deletion cancelled. Confirmation text did not match.[/yellow]")
1068
+ return
1069
+
1070
+ # Delete all repositories
1071
+ console.print("\n[cyan]Deleting all repositories...[/cyan]")
1072
+ deleted_count = 0
1073
+
1074
+ for repo in repos:
1075
+ repo_path = repo.get("path", "")
1076
+ try:
1077
+ graph_builder.delete_repository_from_graph(repo_path)
1078
+ console.print(f"[green]✓[/green] Deleted: {repo.get('name', '')}")
1079
+ deleted_count += 1
1080
+ except Exception as e:
1081
+ console.print(f"[red]✗[/red] Failed to delete {repo.get('name', '')}: {e}")
1082
+
1083
+ console.print(f"\n[bold green]Successfully deleted {deleted_count}/{len(repos)} repositories![/bold green]")
1084
+
1085
+ finally:
1086
+ db_manager.close_driver()
1087
+ else:
1088
+ # Delete specific repository
1089
+ if not path:
1090
+ console.print("[red]Error: Please provide a path or use --all to delete all repositories[/red]")
1091
+ console.print("Usage: cgc delete <path> or cgc delete --all")
1092
+ raise typer.Exit(code=1)
1093
+
1094
+ delete_helper(path, context)
1095
+
1096
+ @app.command()
1097
+ def visualize(
1098
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Path to the repository to visualize."),
1099
+ port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on."),
1100
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
1101
+ ):
1102
+ """
1103
+ Launches the interactive UI to visualize the code graph.
1104
+ """
1105
+ _load_credentials()
1106
+ visualize_helper(repo, port, context)
1107
+
1108
+ @app.command("list")
1109
+ def list_repositories(
1110
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
1111
+ ):
1112
+ """
1113
+ List all indexed repositories.
1114
+
1115
+ Shows all projects and packages that have been indexed in the code graph.
1116
+ """
1117
+ _load_credentials()
1118
+ list_repos_helper(context)
1119
+
1120
+ @app.command(name="add-package")
1121
+ def add_package(
1122
+ package_name: str = typer.Argument(..., help="Name of the package to add."),
1123
+ language: str = typer.Argument(..., help="Language of the package."),
1124
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1125
+ ):
1126
+ """
1127
+ Adds a package to the code graph.
1128
+ """
1129
+ _load_credentials()
1130
+ add_package_helper(package_name, language, context)
1131
+
1132
+ # ============================================================================
1133
+ # WATCH COMMAND GROUP - Live File Monitoring
1134
+ # ============================================================================
1135
+
1136
+ @app.command()
1137
+ def watch(
1138
+ path: str = typer.Argument(".", help="Path to the directory to watch. Defaults to current directory."),
1139
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1140
+ ):
1141
+ """
1142
+ Watch a directory for file changes and automatically update the code graph.
1143
+
1144
+ This command runs in the foreground and monitors the specified directory
1145
+ for any file changes. When changes are detected, the code graph is
1146
+ automatically updated.
1147
+
1148
+ The watcher will:
1149
+ - Perform an initial scan if the directory is not yet indexed
1150
+ - Monitor for file creation, modification, deletion, and moves
1151
+ - Automatically re-index affected files and update relationships
1152
+
1153
+ Press Ctrl+C to stop watching.
1154
+
1155
+ Examples:
1156
+ cgc watch . # Watch current directory
1157
+ cgc watch /path/to/project # Watch specific directory
1158
+ cgc w . # Using shortcut alias
1159
+ """
1160
+ _load_credentials()
1161
+ watch_helper(path, context)
1162
+
1163
+ @app.command()
1164
+ def unwatch(
1165
+ path: str = typer.Argument(..., help="Path to stop watching"),
1166
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1167
+ ):
1168
+ """
1169
+ Stop watching a directory for changes.
1170
+
1171
+ Note: This command is primarily for MCP server mode.
1172
+ For CLI watch mode, simply press Ctrl+C in the watch terminal.
1173
+
1174
+ Examples:
1175
+ cgc unwatch /path/to/project
1176
+ """
1177
+ _load_credentials()
1178
+ unwatch_helper(path)
1179
+
1180
+ @app.command()
1181
+ def watching(
1182
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1183
+ ):
1184
+ """
1185
+ List all directories currently being watched for changes.
1186
+
1187
+ Note: This command is primarily for MCP server mode.
1188
+ For CLI watch mode, check the terminal where you ran 'cgc watch'.
1189
+
1190
+ Examples:
1191
+ cgc watching
1192
+ """
1193
+ _load_credentials()
1194
+ list_watching_helper()
1195
+
1196
+
1197
+
1198
+ # ============================================================================
1199
+ # FIND COMMAND GROUP - Code Search & Discovery
1200
+ # ============================================================================
1201
+
1202
+ find_app = typer.Typer(help="Find and search code elements")
1203
+ app.add_typer(find_app, name="find")
1204
+
1205
+ @find_app.command("name")
1206
+ def find_by_name(
1207
+ ctx: typer.Context,
1208
+ name: str = typer.Argument(..., help="Exact name to search for"),
1209
+ type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type (function, class, file, module)"),
1210
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1211
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1212
+ ):
1213
+ """
1214
+ Find code elements by exact name.
1215
+
1216
+ Examples:
1217
+ cgc find name MyClass
1218
+ cgc find name calculate --type function
1219
+ cgc find name MyClass --visual
1220
+ """
1221
+ _load_credentials()
1222
+ services = _initialize_services(context)
1223
+ if not all(services[:3]):
1224
+ return
1225
+ db_manager, graph_builder, code_finder = services[:3]
1226
+
1227
+ try:
1228
+ results = []
1229
+
1230
+ # Search based on type filter
1231
+ if type is None or type.lower() == 'all':
1232
+ funcs = code_finder.find_by_function_name(name, fuzzy_search=False)
1233
+ classes = code_finder.find_by_class_name(name, fuzzy_search=False)
1234
+ variables = code_finder.find_by_variable_name(name)
1235
+ modules = code_finder.find_by_module_name(name)
1236
+ imports = code_finder.find_imports(name)
1237
+
1238
+ for f in funcs: f['type'] = 'Function'
1239
+ for c in classes: c['type'] = 'Class'
1240
+ for v in variables: v['type'] = 'Variable'
1241
+ for m in modules: m['type'] = 'Module'; m['path'] = m.get('name', 'External') # Modules might differ
1242
+ for i in imports:
1243
+ i['type'] = 'Import'
1244
+ i['name'] = i.get('alias') or i.get('imported_name')
1245
+
1246
+ results.extend(funcs)
1247
+ results.extend(classes)
1248
+ results.extend(variables)
1249
+ results.extend(modules)
1250
+ results.extend(imports)
1251
+
1252
+ elif type.lower() == 'function':
1253
+ results = code_finder.find_by_function_name(name, fuzzy_search=False)
1254
+ for r in results: r['type'] = 'Function'
1255
+
1256
+ elif type.lower() == 'class':
1257
+ results = code_finder.find_by_class_name(name, fuzzy_search=False)
1258
+ for r in results: r['type'] = 'Class'
1259
+
1260
+ elif type.lower() == 'variable':
1261
+ results = code_finder.find_by_variable_name(name)
1262
+ for r in results: r['type'] = 'Variable'
1263
+
1264
+ elif type.lower() == 'module':
1265
+ results = code_finder.find_by_module_name(name)
1266
+ for r in results:
1267
+ r['type'] = 'Module'
1268
+ r['path'] = r.get('name')
1269
+
1270
+ elif type.lower() == 'file':
1271
+ # Quick query for file
1272
+ with db_manager.get_driver().session() as session:
1273
+ res = session.run("MATCH (n:File) WHERE n.name = $name RETURN n.name as name, n.path as path, n.is_dependency as is_dependency", name=name)
1274
+ results = [dict(record) for record in res]
1275
+ for r in results: r['type'] = 'File'
1276
+
1277
+ if not results:
1278
+ console.print(f"[yellow]No code elements found with name '{name}'[/yellow]")
1279
+ return
1280
+
1281
+ # Check if visual mode is enabled
1282
+ if check_visual_flag(ctx, visual):
1283
+ visualize_search_results(results, name, search_type="name")
1284
+ return
1285
+
1286
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1287
+ table.add_column("Name", style="cyan")
1288
+ table.add_column("Type", style="bold blue")
1289
+ table.add_column("Location", style="dim", overflow="fold")
1290
+
1291
+ for res in results:
1292
+ path = res.get('path', '') or ''
1293
+ line_str = str(res.get('line_number', ''))
1294
+ location_str = f"{path}:{line_str}" if line_str else path
1295
+
1296
+ table.add_row(
1297
+ res.get('name', ''),
1298
+ res.get('type', 'Unknown'),
1299
+ location_str
1300
+ )
1301
+
1302
+ console.print(f"[cyan]Found {len(results)} matches for '{name}':[/cyan]")
1303
+ console.print(table)
1304
+ finally:
1305
+ db_manager.close_driver()
1306
+
1307
+ @find_app.command("pattern")
1308
+ def find_by_pattern(
1309
+ ctx: typer.Context,
1310
+ pattern: str = typer.Argument(..., help="Substring pattern to search (fuzzy search fallback)"),
1311
+ case_sensitive: bool = typer.Option(False, "--case-sensitive", "-C", help="Case-sensitive search"),
1312
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1313
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1314
+ ):
1315
+ """
1316
+ Find code elements using substring matching.
1317
+
1318
+ Examples:
1319
+ cgc find pattern "Auth" # Finds Auth, Authentication, Authorize...
1320
+ cgc find pattern "process_" # Finds process_data, process_request...
1321
+ cgc find pattern "Auth" --visual
1322
+ """
1323
+ _load_credentials()
1324
+ services = _initialize_services(context)
1325
+ if not all(services[:3]):
1326
+ return
1327
+ db_manager, graph_builder, code_finder = services[:3]
1328
+
1329
+ try:
1330
+ with db_manager.get_driver().session() as session:
1331
+ # Search Functions, Classes, and Modules
1332
+ # Note: FalkorDB Lite might not support regex, using CONTAINS
1333
+
1334
+ if not case_sensitive:
1335
+ query = """
1336
+ MATCH (n)
1337
+ WHERE (n:Function OR n:Class OR n:Module OR n:Variable) AND toLower(n.name) CONTAINS toLower($pattern)
1338
+ RETURN
1339
+ labels(n)[0] as type,
1340
+ n.name as name,
1341
+ n.path as path,
1342
+ n.line_number as line_number,
1343
+ n.is_dependency as is_dependency
1344
+ ORDER BY n.is_dependency ASC, n.name
1345
+ LIMIT 50
1346
+ """
1347
+ else:
1348
+ query = """
1349
+ MATCH (n)
1350
+ WHERE (n:Function OR n:Class OR n:Module OR n:Variable) AND n.name CONTAINS $pattern
1351
+ RETURN
1352
+ labels(n)[0] as type,
1353
+ n.name as name,
1354
+ n.path as path,
1355
+ n.line_number as line_number,
1356
+ n.is_dependency as is_dependency
1357
+ ORDER BY n.is_dependency ASC, n.name
1358
+ LIMIT 50
1359
+ """
1360
+
1361
+ result = session.run(query, pattern=pattern)
1362
+
1363
+ results = [dict(record) for record in result]
1364
+
1365
+ if not results:
1366
+ console.print(f"[yellow]No matches found for pattern '{pattern}'[/yellow]")
1367
+ return
1368
+
1369
+ # Check if visual mode is enabled
1370
+ if check_visual_flag(ctx, visual):
1371
+ visualize_search_results(results, pattern, search_type="pattern")
1372
+ return
1373
+
1374
+ if not case_sensitive and any(c in pattern for c in "*?["):
1375
+ console.print("[yellow]Note: Wildcards/Regex are not fully supported in this mode. Performing substring search.[/yellow]")
1376
+
1377
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1378
+ table.add_column("Name", style="cyan")
1379
+ table.add_column("Type", style="blue")
1380
+ table.add_column("Location", style="dim", overflow="fold")
1381
+ table.add_column("Source", style="yellow")
1382
+
1383
+ for res in results:
1384
+ path = res.get('path', '') or ''
1385
+ line_str = str(res.get('line_number', '') if res.get('line_number') is not None else '')
1386
+ location_str = f"{path}:{line_str}" if line_str else path
1387
+
1388
+ table.add_row(
1389
+ res.get('name', ''),
1390
+ res.get('type', 'Unknown'),
1391
+ location_str,
1392
+ "📦 Dependency" if res.get('is_dependency') else "📝 Project"
1393
+ )
1394
+
1395
+ console.print(f"[cyan]Found {len(results)} matches for pattern '{pattern}':[/cyan]")
1396
+ console.print(table)
1397
+ finally:
1398
+ db_manager.close_driver()
1399
+
1400
+ @find_app.command("type")
1401
+ def find_by_type(
1402
+ ctx: typer.Context,
1403
+ element_type: str = typer.Argument(..., help="Type to search for (function, class, file, module)"),
1404
+ limit: int = typer.Option(50, "--limit", "-l", help="Maximum results to return"),
1405
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1406
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1407
+ ):
1408
+ """
1409
+ Find all elements of a specific type.
1410
+
1411
+ Examples:
1412
+ cgc find type class
1413
+ cgc find type function --limit 100
1414
+ cgc find type class --visual
1415
+ """
1416
+ _load_credentials()
1417
+ services = _initialize_services(context)
1418
+ if not all(services[:3]):
1419
+ return
1420
+ db_manager, graph_builder, code_finder = services[:3]
1421
+
1422
+ try:
1423
+ results = code_finder.find_by_type(element_type, limit)
1424
+
1425
+ if not results:
1426
+ console.print(f"[yellow]No elements found of type '{element_type}'[/yellow]")
1427
+ return
1428
+
1429
+ # Add type to results for visualization
1430
+ for r in results:
1431
+ r['type'] = element_type.capitalize()
1432
+
1433
+ # Check if visual mode is enabled
1434
+ if check_visual_flag(ctx, visual):
1435
+ visualize_search_results(results, element_type, search_type="type")
1436
+ return
1437
+
1438
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1439
+ table.add_column("Name", style="cyan")
1440
+ table.add_column("Location", style="dim", overflow="fold")
1441
+ table.add_column("Source", style="yellow")
1442
+
1443
+ for res in results:
1444
+ path = res.get('path', '') or ''
1445
+ line_str = str(res.get('line_number', ''))
1446
+ location_str = f"{path}:{line_str}" if line_str else path
1447
+
1448
+ table.add_row(
1449
+ res.get('name', ''),
1450
+ location_str,
1451
+ "📦 Dependency" if res.get('is_dependency') else "📝 Project"
1452
+ )
1453
+
1454
+ console.print(f"[cyan]Found {len(results)} {element_type}s:[/cyan]")
1455
+ console.print(table)
1456
+ finally:
1457
+ db_manager.close_driver()
1458
+
1459
+ @find_app.command("variable")
1460
+ def find_by_variable(
1461
+ name: str = typer.Argument(..., help="Variable name to search for"),
1462
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1463
+ ):
1464
+ """
1465
+ Find variables by name.
1466
+
1467
+ Examples:
1468
+ cgc find variable MAX_RETRIES
1469
+ cgc find variable config
1470
+ """
1471
+ _load_credentials()
1472
+ services = _initialize_services(context)
1473
+ if not all(services[:3]):
1474
+ return
1475
+ db_manager, graph_builder, code_finder = services[:3]
1476
+
1477
+ try:
1478
+ results = code_finder.find_by_variable_name(name)
1479
+
1480
+ if not results:
1481
+ console.print(f"[yellow]No variables found with name '{name}'[/yellow]")
1482
+ return
1483
+
1484
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1485
+ table.add_column("Name", style="cyan")
1486
+ table.add_column("Location", style="dim", overflow="fold")
1487
+ table.add_column("Context", style="yellow")
1488
+
1489
+ for res in results:
1490
+ path = res.get('path', '') or ''
1491
+ line_str = str(res.get('line_number', ''))
1492
+ location_str = f"{path}:{line_str}" if line_str else path
1493
+
1494
+ table.add_row(
1495
+ res.get('name', ''),
1496
+ location_str,
1497
+ res.get('context', '') or 'module'
1498
+ )
1499
+
1500
+ console.print(f"[cyan]Found {len(results)} variable(s) named '{name}':[/cyan]")
1501
+ console.print(table)
1502
+ finally:
1503
+ db_manager.close_driver()
1504
+
1505
+ @find_app.command("content")
1506
+ def find_by_content_search(
1507
+ query: str = typer.Argument(..., help="Text to search for in source code and docstrings"),
1508
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1509
+ ):
1510
+ """
1511
+ Search code content (source and docstrings) using full-text index.
1512
+
1513
+ Examples:
1514
+ cgc find content "error 503"
1515
+ cgc find content "TODO: refactor"
1516
+ """
1517
+ _load_credentials()
1518
+ services = _initialize_services(context)
1519
+ if not all(services[:3]):
1520
+ return
1521
+ db_manager, graph_builder, code_finder = services[:3]
1522
+
1523
+ try:
1524
+ try:
1525
+ results = code_finder.find_by_content(query)
1526
+ except Exception as e:
1527
+ error_msg = str(e).lower()
1528
+ if ('fulltext' in error_msg or 'db.index.fulltext' in error_msg) and "Falkor" in db_manager.__class__.__name__:
1529
+ console.print("\n[bold red]❌ Full-text search is not supported on FalkorDB[/bold red]\n")
1530
+ console.print("[yellow]💡 You have two options:[/yellow]\n")
1531
+ console.print(" 1. [cyan]Switch to Neo4j:[/cyan]")
1532
+ console.print(f" [dim]cgc --database neo4j find content \"{query}\"[/dim]\n")
1533
+ console.print(" 2. [cyan]Use pattern search instead:[/cyan]")
1534
+ console.print(f" [dim]cgc find pattern \"{query}\"[/dim]")
1535
+ console.print(" [dim](searches in names only, not source code)[/dim]\n")
1536
+ return
1537
+ else:
1538
+ # Re-raise if it's a different error
1539
+ raise
1540
+
1541
+ if not results:
1542
+ console.print(f"[yellow]No content matches found for '{query}'[/yellow]")
1543
+ return
1544
+
1545
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1546
+ table.add_column("Name", style="cyan")
1547
+ table.add_column("Type", style="blue")
1548
+ table.add_column("Location", style="dim", overflow="fold")
1549
+
1550
+ for res in results:
1551
+ path = res.get('path', '') or ''
1552
+ line_str = str(res.get('line_number', ''))
1553
+ location_str = f"{path}:{line_str}" if line_str else path
1554
+
1555
+ table.add_row(
1556
+ res.get('name', ''),
1557
+ res.get('type', 'Unknown'),
1558
+ location_str
1559
+ )
1560
+
1561
+ console.print(f"[cyan]Found {len(results)} content match(es) for '{query}':[/cyan]")
1562
+ console.print(table)
1563
+ finally:
1564
+ db_manager.close_driver()
1565
+
1566
+ @find_app.command("decorator")
1567
+ def find_by_decorator_search(
1568
+ decorator: str = typer.Argument(..., help="Decorator name to search for"),
1569
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
1570
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1571
+ ):
1572
+ """
1573
+ Find functions with a specific decorator.
1574
+
1575
+ Examples:
1576
+ cgc find decorator app.route
1577
+ cgc find decorator test --file tests/test_main.py
1578
+ """
1579
+ _load_credentials()
1580
+ services = _initialize_services(context)
1581
+ if not all(services[:3]):
1582
+ return
1583
+ db_manager, graph_builder, code_finder = services[:3]
1584
+
1585
+ try:
1586
+ results = code_finder.find_functions_by_decorator(decorator, file)
1587
+
1588
+ if not results:
1589
+ console.print(f"[yellow]No functions found with decorator '@{decorator}'[/yellow]")
1590
+ return
1591
+
1592
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1593
+ table.add_column("Function", style="cyan")
1594
+ table.add_column("Location", style="dim", overflow="fold")
1595
+ table.add_column("Decorators", style="yellow")
1596
+
1597
+ for res in results:
1598
+ decorators_str = ", ".join(res.get('decorators', []))
1599
+ path = res.get('path', '') or ''
1600
+ line_str = str(res.get('line_number', ''))
1601
+ location_str = f"{path}:{line_str}" if line_str else path
1602
+
1603
+ table.add_row(
1604
+ res.get('function_name', ''),
1605
+ location_str,
1606
+ decorators_str
1607
+ )
1608
+
1609
+ console.print(f"[cyan]Found {len(results)} function(s) with decorator '@{decorator}':[/cyan]")
1610
+ console.print(table)
1611
+ finally:
1612
+ db_manager.close_driver()
1613
+
1614
+ @find_app.command("argument")
1615
+ def find_by_argument_search(
1616
+ argument: str = typer.Argument(..., help="Argument/parameter name to search for"),
1617
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
1618
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1619
+ ):
1620
+ """
1621
+ Find functions that take a specific argument/parameter.
1622
+
1623
+ Examples:
1624
+ cgc find argument password
1625
+ cgc find argument user_id --file src/auth.py
1626
+ """
1627
+ _load_credentials()
1628
+ services = _initialize_services(context)
1629
+ if not all(services[:3]):
1630
+ return
1631
+ db_manager, graph_builder, code_finder = services[:3]
1632
+
1633
+ try:
1634
+ results = code_finder.find_functions_by_argument(argument, file)
1635
+
1636
+ if not results:
1637
+ console.print(f"[yellow]No functions found with argument '{argument}'[/yellow]")
1638
+ return
1639
+
1640
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1641
+ table.add_column("Function", style="cyan")
1642
+ table.add_column("Location", style="dim", overflow="fold")
1643
+
1644
+ for res in results:
1645
+ path = res.get('path', '') or ''
1646
+ line_str = str(res.get('line_number', ''))
1647
+ location_str = f"{path}:{line_str}" if line_str else path
1648
+
1649
+ table.add_row(
1650
+ res.get('function_name', ''),
1651
+ location_str
1652
+ )
1653
+
1654
+ console.print(f"[cyan]Found {len(results)} function(s) with argument '{argument}':[/cyan]")
1655
+ console.print(table)
1656
+ finally:
1657
+ db_manager.close_driver()
1658
+
1659
+
1660
+ # ============================================================================
1661
+ # ANALYZE COMMAND GROUP - Code Analysis & Relationships
1662
+ # ============================================================================
1663
+
1664
+ analyze_app = typer.Typer(help="Analyze code relationships, dependencies, and quality")
1665
+ app.add_typer(analyze_app, name="analyze")
1666
+
1667
+ @analyze_app.command("calls")
1668
+ def analyze_calls(
1669
+ ctx: typer.Context,
1670
+ function: str = typer.Argument(..., help="Function name to analyze"),
1671
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
1672
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1673
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1674
+ ):
1675
+ """
1676
+ Show what functions this function calls (callees).
1677
+
1678
+ Example:
1679
+ cgc analyze calls process_data
1680
+ cgc analyze calls process_data --file src/main.py
1681
+ cgc analyze calls process_data --visual
1682
+ """
1683
+ _load_credentials()
1684
+ services = _initialize_services(context)
1685
+ if not all(services[:3]):
1686
+ return
1687
+ db_manager, graph_builder, code_finder = services[:3]
1688
+
1689
+ try:
1690
+ results = code_finder.what_does_function_call(function, file)
1691
+
1692
+ if not results:
1693
+ console.print(f"[yellow]No function calls found for '{function}'[/yellow]")
1694
+ return
1695
+
1696
+ # Check if visual mode is enabled
1697
+ if check_visual_flag(ctx, visual):
1698
+ visualize_call_graph(results, function, direction="outgoing")
1699
+ return
1700
+
1701
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1702
+ table.add_column("Called Function", style="cyan")
1703
+ table.add_column("Location", style="dim", overflow="fold")
1704
+ table.add_column("Type", style="yellow")
1705
+
1706
+ for result in results:
1707
+ path = result.get("called_file_path", "")
1708
+ line_str = str(result.get("called_line_number", ""))
1709
+ location_str = f"{path}:{line_str}" if line_str else path
1710
+
1711
+ table.add_row(
1712
+ result.get("called_function", ""),
1713
+ location_str,
1714
+ "📦 Dependency" if result.get("called_is_dependency") else "📝 Project"
1715
+ )
1716
+
1717
+ console.print(f"\n[bold cyan]Function '{function}' calls:[/bold cyan]")
1718
+ console.print(table)
1719
+ console.print(f"\n[dim]Total: {len(results)} function(s)[/dim]")
1720
+ finally:
1721
+ db_manager.close_driver()
1722
+
1723
+ @analyze_app.command("callers")
1724
+ def analyze_callers(
1725
+ ctx: typer.Context,
1726
+ function: str = typer.Argument(..., help="Function name to analyze"),
1727
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
1728
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1729
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1730
+ ):
1731
+ """
1732
+ Show what functions call this function.
1733
+
1734
+ Example:
1735
+ cgc analyze callers process_data
1736
+ cgc analyze callers process_data --file src/main.py
1737
+ cgc analyze callers process_data --visual
1738
+ """
1739
+ _load_credentials()
1740
+ services = _initialize_services(context)
1741
+ if not all(services[:3]):
1742
+ return
1743
+ db_manager, graph_builder, code_finder = services[:3]
1744
+
1745
+ try:
1746
+ results = code_finder.who_calls_function(function, file)
1747
+
1748
+ if not results:
1749
+ console.print(f"[yellow]No callers found for '{function}'[/yellow]")
1750
+ return
1751
+
1752
+ # Check if visual mode is enabled
1753
+ if check_visual_flag(ctx, visual):
1754
+ visualize_call_graph(results, function, direction="incoming")
1755
+ return
1756
+
1757
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1758
+ table.add_column("Caller Function", style="cyan")
1759
+ table.add_column("Location", style="green")
1760
+ table.add_column("Call Type", style="yellow")
1761
+
1762
+
1763
+ for result in results:
1764
+ path = result.get("caller_file_path", "")
1765
+ line_number = result.get("caller_line_number")
1766
+
1767
+ location = f"{path}:{line_number}" if line_number else path
1768
+
1769
+ table.add_row(
1770
+ result.get("caller_function", ""),
1771
+ location,
1772
+ "📦 Dependency" if result.get("caller_is_dependency") else "📝 Project"
1773
+ )
1774
+
1775
+ console.print(f"\n[bold cyan]Functions that call '{function}':[/bold cyan]")
1776
+ console.print(table)
1777
+ console.print(f"\n[dim]Total: {len(results)} caller(s)[/dim]")
1778
+ finally:
1779
+ db_manager.close_driver()
1780
+
1781
+ @analyze_app.command("chain")
1782
+ def analyze_chain(
1783
+ ctx: typer.Context,
1784
+ from_func: str = typer.Argument(..., help="Starting function"),
1785
+ to_func: str = typer.Argument(..., help="Target function"),
1786
+ max_depth: int = typer.Option(5, "--depth", "-d", help="Maximum call chain depth"),
1787
+ from_file: Optional[str] = typer.Option(None, "--from-file", help="File for starting function"),
1788
+ to_file: Optional[str] = typer.Option(None, "--to-file", help="File for target function"),
1789
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1790
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1791
+ ):
1792
+ """
1793
+ Show call chain between two functions.
1794
+
1795
+ Example:
1796
+ cgc analyze chain main process_data --depth 10
1797
+ cgc analyze chain main process --from-file main.py --to-file utils.py
1798
+ cgc analyze chain main process_data --visual
1799
+ """
1800
+ _load_credentials()
1801
+ services = _initialize_services(context)
1802
+ if not all(services[:3]):
1803
+ return
1804
+ db_manager, graph_builder, code_finder = services[:3]
1805
+
1806
+ try:
1807
+ results = code_finder.find_function_call_chain(from_func, to_func, max_depth, from_file, to_file)
1808
+
1809
+ if not results:
1810
+ console.print(f"[yellow]No call chain found between '{from_func}' and '{to_func}' within depth {max_depth}[/yellow]")
1811
+ return
1812
+
1813
+ # Check if visual mode is enabled
1814
+ if check_visual_flag(ctx, visual):
1815
+ visualize_call_chain(results, from_func, to_func)
1816
+ return
1817
+
1818
+ for idx, chain in enumerate(results, 1):
1819
+ console.print(f"\n[bold cyan]Call Chain #{idx} (length: {chain.get('chain_length', 0)}):[/bold cyan]")
1820
+
1821
+ functions = chain.get('function_chain', [])
1822
+ call_details = chain.get('call_details', [])
1823
+
1824
+ for i, func in enumerate(functions):
1825
+ indent = " " * i
1826
+
1827
+ # Print function
1828
+ console.print(f"{indent}[cyan]{func.get('name', 'Unknown')}[/cyan] [dim]({func.get('path', '')}:{func.get('line_number', '')})[/dim]")
1829
+
1830
+ # If there is a next step, print the connecting call detail
1831
+ if i < len(functions) - 1 and i < len(call_details):
1832
+ detail = call_details[i]
1833
+ line = detail.get('call_line', '?')
1834
+
1835
+ # Format args for display
1836
+ args_info = ""
1837
+ args_val = detail.get('args', [])
1838
+ if args_val:
1839
+ if isinstance(args_val, list):
1840
+ # Filter legacy punctuation just in case
1841
+ clean_args = [str(a) for a in args_val if str(a) not in ('(', ')', ',')]
1842
+ args_str = ", ".join(clean_args)
1843
+ else:
1844
+ args_str = str(args_val)
1845
+
1846
+ # Truncate if too long
1847
+ if len(args_str) > 50:
1848
+ args_str = args_str[:47] + "..."
1849
+ args_info = f" [dim]({args_str})[/dim]"
1850
+
1851
+ console.print(f"{indent} ⬇ [dim]calls at line {line}[/dim]{args_info}")
1852
+ finally:
1853
+ db_manager.close_driver()
1854
+
1855
+ @analyze_app.command("deps")
1856
+ def analyze_dependencies(
1857
+ ctx: typer.Context,
1858
+ target: str = typer.Argument(..., help="Module name"),
1859
+ show_external: bool = typer.Option(True, "--external/--no-external", help="Show external dependencies"),
1860
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1861
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1862
+ ):
1863
+ """
1864
+ Show dependencies and imports for a module.
1865
+
1866
+ Example:
1867
+ cgc analyze deps numpy
1868
+ cgc analyze deps mymodule --no-external
1869
+ cgc analyze deps mymodule --visual
1870
+ """
1871
+ _load_credentials()
1872
+ services = _initialize_services(context)
1873
+ if not all(services[:3]):
1874
+ return
1875
+ db_manager, graph_builder, code_finder = services[:3]
1876
+
1877
+ try:
1878
+ results = code_finder.find_module_dependencies(target)
1879
+
1880
+ if not results.get('importers') and not results.get('imports'):
1881
+ console.print(f"[yellow]No dependency information found for '{target}'[/yellow]")
1882
+ return
1883
+
1884
+ # Check if visual mode is enabled
1885
+ if check_visual_flag(ctx, visual):
1886
+ visualize_dependencies(results, target)
1887
+ return
1888
+
1889
+ # Show who imports this module
1890
+ if results.get('importers'):
1891
+ console.print(f"\n[bold cyan]Files that import '{target}':[/bold cyan]")
1892
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
1893
+ table.add_column("Location", style="cyan", overflow="fold")
1894
+
1895
+ for imp in results['importers']:
1896
+ path = imp.get('importer_file_path', '')
1897
+ line_str = str(imp.get('import_line_number', ''))
1898
+ location_str = f"{path}:{line_str}" if line_str else path
1899
+
1900
+ table.add_row(
1901
+ location_str
1902
+ )
1903
+ console.print(table)
1904
+ finally:
1905
+ db_manager.close_driver()
1906
+
1907
+ @analyze_app.command("tree")
1908
+ def analyze_inheritance_tree(
1909
+ ctx: typer.Context,
1910
+ class_name: str = typer.Argument(..., help="Class name"),
1911
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
1912
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
1913
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1914
+ ):
1915
+ """
1916
+ Show inheritance hierarchy for a class.
1917
+
1918
+ Example:
1919
+ cgc analyze tree MyClass
1920
+ cgc analyze tree MyClass --file src/models.py
1921
+ cgc analyze tree MyClass --visual
1922
+ """
1923
+ _load_credentials()
1924
+ services = _initialize_services(context)
1925
+ if not all(services[:3]):
1926
+ return
1927
+ db_manager, graph_builder, code_finder = services[:3]
1928
+
1929
+ try:
1930
+ results = code_finder.find_class_hierarchy(class_name, file)
1931
+
1932
+ # Check if visual mode is enabled (check for any hierarchy data)
1933
+ has_hierarchy = results.get('parent_classes') or results.get('child_classes')
1934
+ if check_visual_flag(ctx, visual):
1935
+ if has_hierarchy:
1936
+ visualize_inheritance_tree(results, class_name)
1937
+ else:
1938
+ console.print(f"[yellow]No inheritance hierarchy to visualize for '{class_name}'[/yellow]")
1939
+ return
1940
+
1941
+ console.print(f"\n[bold cyan]Class Hierarchy for '{class_name}':[/bold cyan]\n")
1942
+
1943
+ # Show parent classes
1944
+ if results.get('parent_classes'):
1945
+ console.print("[bold yellow]Parents (inherits from):[/bold yellow]")
1946
+ for parent in results['parent_classes']:
1947
+ console.print(f" ⬆ [cyan]{parent.get('parent_class', '')}[/cyan] [dim]({parent.get('parent_file_path', '')}:{parent.get('parent_line_number', '')})[/dim]")
1948
+ else:
1949
+ console.print("[dim]No parent classes found[/dim]")
1950
+
1951
+ console.print()
1952
+
1953
+ # Show child classes
1954
+ if results.get('child_classes'):
1955
+ console.print("[bold yellow]Children (classes that inherit from this):[/bold yellow]")
1956
+ for child in results['child_classes']:
1957
+ console.print(f" ⬇ [cyan]{child.get('child_class', '')}[/cyan] [dim]({child.get('child_file_path', '')}:{child.get('child_line_number', '')})[/dim]")
1958
+ else:
1959
+ console.print("[dim]No child classes found[/dim]")
1960
+
1961
+ console.print()
1962
+
1963
+ # Show methods
1964
+ if results.get('methods'):
1965
+ console.print(f"[bold yellow]Methods ({len(results['methods'])}):[/bold yellow]")
1966
+ for method in results['methods'][:10]: # Limit to 10
1967
+ console.print(f" • [green]{method.get('method_name', '')}[/green]({method.get('method_args', '')})")
1968
+ if len(results['methods']) > 10:
1969
+ console.print(f" [dim]... and {len(results['methods']) - 10} more[/dim]")
1970
+ finally:
1971
+ db_manager.close_driver()
1972
+
1973
+ @analyze_app.command("complexity")
1974
+ def analyze_complexity(
1975
+ path: Optional[str] = typer.Argument(None, help="Specific function name to analyze"),
1976
+ threshold: int = typer.Option(10, "--threshold", "-t", help="Complexity threshold for warnings"),
1977
+ limit: int = typer.Option(20, "--limit", "-l", help="Maximum results to show"),
1978
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path (only used when function name is provided)"),
1979
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
1980
+ ):
1981
+ """
1982
+ Show cyclomatic complexity for functions.
1983
+
1984
+ Example:
1985
+ cgc analyze complexity # Most complex functions
1986
+ cgc analyze complexity --threshold 15 # Functions over threshold
1987
+ cgc analyze complexity my_function # Specific function
1988
+ cgc analyze complexity my_function -f file.py # Specific function in file
1989
+ """
1990
+ _load_credentials()
1991
+ services = _initialize_services(context)
1992
+ if not all(services[:3]):
1993
+ return
1994
+ db_manager, graph_builder, code_finder = services[:3]
1995
+
1996
+ try:
1997
+ if path:
1998
+ # Specific function
1999
+ result = code_finder.get_cyclomatic_complexity(path, file)
2000
+ if result:
2001
+ console.print(f"\n[bold cyan]Complexity for '{path}':[/bold cyan]")
2002
+ console.print(f" Cyclomatic Complexity: [yellow]{result.get('complexity', 'N/A')}[/yellow]")
2003
+ console.print(f" File: [dim]{result.get('path', '')}[/dim]")
2004
+ console.print(f" Line: [dim]{result.get('line_number', '')}[/dim]")
2005
+ else:
2006
+ console.print(f"[yellow]Function '{path}' not found or has no complexity data[/yellow]")
2007
+ else:
2008
+ # Most complex functions
2009
+ results = code_finder.find_most_complex_functions(limit)
2010
+
2011
+ if not results:
2012
+ console.print("[yellow]No complexity data available[/yellow]")
2013
+ return
2014
+
2015
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2016
+ table.add_column("Function", style="cyan")
2017
+ table.add_column("Complexity", style="yellow", justify="right")
2018
+ table.add_column("Location", style="dim", overflow="fold")
2019
+
2020
+ for func in results:
2021
+ complexity = func.get('complexity', 0)
2022
+ color = "red" if complexity > threshold else "yellow" if complexity > threshold/2 else "green"
2023
+ path = func.get('path', '')
2024
+ line_str = str(func.get('line_number', ''))
2025
+ location_str = f"{path}:{line_str}" if line_str else path
2026
+
2027
+ table.add_row(
2028
+ func.get('function_name', ''),
2029
+ f"[{color}]{complexity}[/{color}]",
2030
+ location_str
2031
+ )
2032
+
2033
+ console.print(f"\n[bold cyan]Most Complex Functions (threshold: {threshold}):[/bold cyan]")
2034
+ console.print(table)
2035
+ console.print(f"\n[dim]{len([f for f in results if f.get('complexity', 0) > threshold])} function(s) exceed threshold[/dim]")
2036
+ finally:
2037
+ db_manager.close_driver()
2038
+
2039
+ @analyze_app.command("dead-code")
2040
+ def analyze_dead_code(
2041
+ path: Optional[str] = typer.Argument(None, help="Path to analyze (not yet implemented)"),
2042
+ exclude_decorators: Optional[str] = typer.Option(None, "--exclude", "-e", help="Comma-separated decorators to exclude"),
2043
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2044
+ ):
2045
+ """
2046
+ Find potentially unused functions and classes.
2047
+
2048
+ Example:
2049
+ cgc analyze dead-code
2050
+ cgc analyze dead-code --exclude route,task,api
2051
+ """
2052
+ _load_credentials()
2053
+ services = _initialize_services(context)
2054
+ if not all(services[:3]):
2055
+ return
2056
+ db_manager, graph_builder, code_finder = services[:3]
2057
+
2058
+ try:
2059
+ exclude_list = exclude_decorators.split(',') if exclude_decorators else []
2060
+ results = code_finder.find_dead_code(exclude_list)
2061
+
2062
+ unused_funcs = results.get('potentially_unused_functions', [])
2063
+
2064
+ if not unused_funcs:
2065
+ console.print("[green]✓ No dead code found![/green]")
2066
+ return
2067
+
2068
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2069
+ table.add_column("Function", style="cyan")
2070
+ table.add_column("Location", style="dim", overflow="fold")
2071
+
2072
+ for func in unused_funcs:
2073
+ path = func.get('path', '')
2074
+ line_str = str(func.get('line_number', ''))
2075
+ location_str = f"{path}:{line_str}" if line_str else path
2076
+
2077
+ table.add_row(
2078
+ func.get('function_name', ''),
2079
+ location_str
2080
+ )
2081
+
2082
+ console.print(f"\n[bold yellow]⚠️ Potentially Unused Functions:[/bold yellow]")
2083
+ console.print(table)
2084
+ console.print(f"\n[dim]Total: {len(unused_funcs)} function(s)[/dim]")
2085
+ console.print(f"[dim]Note: {results.get('note', '')}[/dim]")
2086
+ finally:
2087
+ db_manager.close_driver()
2088
+
2089
+ @analyze_app.command("overrides")
2090
+ def analyze_overrides(
2091
+ ctx: typer.Context,
2092
+ function_name: str = typer.Argument(..., help="Function/method name to find implementations of"),
2093
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
2094
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2095
+ ):
2096
+ """
2097
+ Find all implementations of a function across different classes.
2098
+
2099
+ Useful for finding polymorphic implementations and method overrides.
2100
+
2101
+ Example:
2102
+ cgc analyze overrides area
2103
+ cgc analyze overrides process
2104
+ cgc analyze overrides area --visual
2105
+ """
2106
+ _load_credentials()
2107
+ services = _initialize_services(context)
2108
+ if not all(services[:3]):
2109
+ return
2110
+ db_manager, graph_builder, code_finder = services[:3]
2111
+
2112
+ try:
2113
+ results = code_finder.find_function_overrides(function_name)
2114
+
2115
+ if not results:
2116
+ console.print(f"[yellow]No implementations found for function '{function_name}'[/yellow]")
2117
+ return
2118
+
2119
+ # Check if visual mode is enabled
2120
+ if check_visual_flag(ctx, visual):
2121
+ visualize_overrides(results, function_name)
2122
+ return
2123
+
2124
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2125
+ table.add_column("Class", style="cyan")
2126
+ table.add_column("Function", style="green")
2127
+ table.add_column("Location", style="dim", overflow="fold")
2128
+
2129
+ for res in results:
2130
+ path = res.get('class_file_path', '')
2131
+ line_str = str(res.get('function_line_number', ''))
2132
+ location_str = f"{path}:{line_str}" if line_str else path
2133
+
2134
+ table.add_row(
2135
+ res.get('class_name', ''),
2136
+ res.get('function_name', ''),
2137
+ location_str
2138
+ )
2139
+
2140
+ console.print(f"\n[bold cyan]Found {len(results)} implementation(s) of '{function_name}':[/bold cyan]")
2141
+ console.print(table)
2142
+ finally:
2143
+ db_manager.close_driver()
2144
+
2145
+ @analyze_app.command("variable")
2146
+ def analyze_variable_usage(
2147
+ variable_name: str = typer.Argument(..., help="Variable name to analyze"),
2148
+ file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
2149
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2150
+ ):
2151
+ """
2152
+ Analyze where a variable is defined and used across the codebase.
2153
+
2154
+ Shows all instances of the variable and their scope (function, class, module).
2155
+
2156
+ Example:
2157
+ cgc analyze variable MAX_RETRIES
2158
+ cgc analyze variable config
2159
+ cgc analyze variable x --file math_utils.py
2160
+ """
2161
+ _load_credentials()
2162
+ services = _initialize_services(context)
2163
+ if not all(services[:3]):
2164
+ return
2165
+ db_manager, graph_builder, code_finder = services[:3]
2166
+
2167
+ try:
2168
+ # Get variable usage scope
2169
+ scope_results = code_finder.find_variable_usage_scope(variable_name, file)
2170
+ instances = scope_results.get('instances', [])
2171
+
2172
+ if not instances:
2173
+ console.print(f"[yellow]No instances found for variable '{variable_name}'[/yellow]")
2174
+ return
2175
+
2176
+ console.print(f"\n[bold cyan]Variable '{variable_name}' Usage Analysis:[/bold cyan]\n")
2177
+
2178
+ # Group by scope type
2179
+ by_scope = {}
2180
+ for inst in instances:
2181
+ scope_type = inst.get('scope_type', 'unknown')
2182
+ if scope_type not in by_scope:
2183
+ by_scope[scope_type] = []
2184
+ by_scope[scope_type].append(inst)
2185
+
2186
+ # Display by scope
2187
+ for scope_type, items in by_scope.items():
2188
+ console.print(f"[bold yellow]{scope_type.upper()} Scope ({len(items)} instance(s)):[/bold yellow]")
2189
+
2190
+ table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
2191
+ table.add_column("Scope Name", style="cyan")
2192
+ table.add_column("Location", style="dim", overflow="fold")
2193
+ table.add_column("Value", style="yellow")
2194
+
2195
+ for item in items:
2196
+ path = item.get('path', '')
2197
+ line_str = str(item.get('line_number', ''))
2198
+ location_str = f"{path}:{line_str}" if line_str else path
2199
+
2200
+ table.add_row(
2201
+ item.get('scope_name', ''),
2202
+ location_str,
2203
+ str(item.get('variable_value', ''))[:50] if item.get('variable_value') else '-'
2204
+ )
2205
+
2206
+ console.print(table)
2207
+ console.print()
2208
+
2209
+ console.print(f"[dim]Total: {len(instances)} instance(s) across {len(by_scope)} scope type(s)[/dim]")
2210
+ finally:
2211
+ db_manager.close_driver()
2212
+
2213
+
2214
+ # ============================================================================
2215
+ # QUERY COMMAND - Raw Cypher Queries
2216
+ # ============================================================================
2217
+
2218
+ @app.command("query")
2219
+ def query_graph(
2220
+ ctx: typer.Context,
2221
+ query: str = typer.Argument(..., help="Cypher query to execute (read-only)"),
2222
+ visual: bool = typer.Option(False, "--visual", "--viz", "-V", help="Show results as interactive graph visualization"),
2223
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2224
+ ):
2225
+ """
2226
+ Execute a custom Cypher query on the code graph.
2227
+
2228
+ Examples:
2229
+ cgc query "MATCH (f:Function) RETURN f.name LIMIT 10"
2230
+ cgc query "MATCH (c:Class)-[:CONTAINS]->(m) RETURN c.name, count(m)"
2231
+ cgc query "MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 50" --visual
2232
+ """
2233
+ _load_credentials()
2234
+
2235
+ # Check if visual mode is enabled
2236
+ if check_visual_flag(ctx, visual):
2237
+ cypher_helper_visual(query, context)
2238
+ else:
2239
+ cypher_helper(query, context)
2240
+
2241
+ # Keep old 'cypher' as alias for backward compatibility
2242
+ @app.command("cypher", hidden=True)
2243
+ def cypher_legacy(
2244
+ query: str = typer.Argument(..., help="The read-only Cypher query to execute."),
2245
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2246
+ ):
2247
+ """[Deprecated] Use 'cgc query' instead."""
2248
+ console.print("[yellow]⚠️ 'cgc cypher' is deprecated. Use 'cgc query' instead.[/yellow]")
2249
+ cypher_helper(query, context)
2250
+
2251
+
2252
+
2253
+ # ============================================================================
2254
+ # ABBREVIATIONS / SHORTCUTS for common commands
2255
+ # ============================================================================
2256
+
2257
+ @app.command("i", rich_help_panel="Shortcuts")
2258
+ def index_abbrev(
2259
+ path: Optional[str] = typer.Argument(None, help="Path to index"),
2260
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-index (delete existing and rebuild)"),
2261
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
2262
+ ):
2263
+ """Shortcut for 'cgc index'"""
2264
+ index(path, force=force, context=context)
2265
+
2266
+ @app.command("ls", rich_help_panel="Shortcuts")
2267
+ def list_abbrev(
2268
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
2269
+ ):
2270
+ """Shortcut for 'cgc list'"""
2271
+ list_repositories(context=context)
2272
+
2273
+ @app.command("rm", rich_help_panel="Shortcuts")
2274
+ def delete_abbrev(
2275
+ path: Optional[str] = typer.Argument(None, help="Path to delete"),
2276
+ all_repos: bool = typer.Option(False, "--all", help="Delete all indexed repositories"),
2277
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
2278
+ ):
2279
+ """Shortcut for 'cgc delete'"""
2280
+ delete(path, all_repos, context=context)
2281
+
2282
+ @app.command("v", rich_help_panel="Shortcuts")
2283
+ def visualize_abbrev(
2284
+ repo: Optional[str] = typer.Argument(None, help="Path to the repository to visualize."),
2285
+ port: int = typer.Option(8000, "--port", "-p", help="Port to run the visualizer server on."),
2286
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use")
2287
+ ):
2288
+ """Shortcut for 'cgc visualize'"""
2289
+ _load_credentials()
2290
+ visualize_helper(repo, port, context=context)
2291
+
2292
+ @app.command("w", rich_help_panel="Shortcuts")
2293
+ def watch_abbrev(
2294
+ path: str = typer.Argument(".", help="Path to watch"),
2295
+ context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2296
+ ):
2297
+ """Shortcut for 'cgc watch'"""
2298
+ watch(path, context=context)
2299
+
2300
+
2301
+ # ============================================================================
2302
+
2303
+
2304
+
2305
+ @app.command()
2306
+ def help(ctx: typer.Context):
2307
+ """Show the main help message and exit."""
2308
+ root_ctx = ctx.parent or ctx
2309
+ typer.echo(root_ctx.get_help())
2310
+
2311
+
2312
+ @app.command("version")
2313
+ def version_cmd():
2314
+ """Show the application version."""
2315
+ console.print(f"CodeGraphContext [bold cyan]{get_version()}[/bold cyan]")
2316
+
2317
+
2318
+ @app.callback(invoke_without_command=True)
2319
+ def main(
2320
+ ctx: typer.Context,
2321
+ database: Optional[str] = typer.Option(
2322
+ None,
2323
+ "--database",
2324
+ "-db",
2325
+ help="[Global] Temporarily override database backend (falkordb, falkordb-remote, neo4j, or kuzudb) for any command"
2326
+ ),
2327
+ visual: bool = typer.Option(
2328
+ False,
2329
+ "--visual",
2330
+ "--viz",
2331
+ "-V",
2332
+ help="[Global] Show results as interactive graph visualization in browser"
2333
+ ),
2334
+ version_: bool = typer.Option(
2335
+ None,
2336
+ "--version",
2337
+ "-v",
2338
+ help="[Root-level only] Show version and exit",
2339
+ is_eager=True,
2340
+ ),
2341
+ help_: bool = typer.Option(
2342
+ None,
2343
+ "--help",
2344
+ "-h",
2345
+ help="[Root-level only] Show help and exit",
2346
+ is_eager=True,
2347
+ ),
2348
+ ):
2349
+ """
2350
+ Main entry point for the cgc CLI application.
2351
+ If no subcommand is provided, it displays a welcome message with instructions.
2352
+ """
2353
+ # Initialize context object for sharing state with subcommands
2354
+ ctx.ensure_object(dict)
2355
+
2356
+ if database:
2357
+ os.environ["CGC_RUNTIME_DB_TYPE"] = database
2358
+
2359
+ # Store visual flag in context for subcommands to access
2360
+ if visual:
2361
+ ctx.obj["visual"] = True
2362
+
2363
+ if version_:
2364
+ console.print(f"CodeGraphContext [bold cyan]{get_version()}[/bold cyan]")
2365
+ raise typer.Exit()
2366
+
2367
+ if ctx.invoked_subcommand is None:
2368
+ console.print("[bold green]👋 Welcome to CodeGraphContext (cgc)![/bold green]\n")
2369
+ console.print("CodeGraphContext is both an [bold cyan]MCP server[/bold cyan] and a [bold cyan]CLI toolkit[/bold cyan] for code analysis.\n")
2370
+ console.print("🤖 [bold]For MCP Server Mode (AI assistants):[/bold]")
2371
+ console.print(" 1. Run [cyan]cgc mcp setup[/cyan] (or [cyan]cgc m[/cyan]) to configure your IDE")
2372
+ console.print(" 2. Run [cyan]cgc mcp start[/cyan] to launch the server\n")
2373
+ console.print("🛠️ [bold]For CLI Toolkit Mode (direct usage):[/bold]")
2374
+ console.print(" • [cyan]cgc index .[/cyan] - Index your current directory")
2375
+ console.print(" • [cyan]cgc list[/cyan] - List indexed repositories\n")
2376
+ console.print("📊 [bold]Using Neo4j instead of FalkorDB?[/bold]")
2377
+ console.print(" Run [cyan]cgc neo4j setup[/cyan] (or [cyan]cgc n[/cyan]) to configure Neo4j\n")
2378
+ console.print("📈 [bold]Want visual graph output?[/bold]")
2379
+ console.print(" Add [cyan]-V[/cyan] or [cyan]--visual[/cyan] to any analyze/find command\n")
2380
+ console.print("👉 Run [cyan]cgc help[/cyan] to see all available commands")
2381
+ console.print("👉 Run [cyan]cgc --version[/cyan] to check the version\n")
2382
+ console.print("👉 Running [green]codegraphcontext[/green] works the same as using [green]cgc[/green]")
2383
+
2384
+
2385
+ if __name__ == "__main__":
2386
+ app()