codegraph-cli 2.1.0__py3-none-any.whl → 2.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. codegraph_cli/__init__.py +1 -1
  2. codegraph_cli/agents.py +59 -3
  3. codegraph_cli/chat_agent.py +58 -11
  4. codegraph_cli/cli.py +569 -54
  5. codegraph_cli/cli_chat.py +204 -94
  6. codegraph_cli/cli_diagnose.py +13 -2
  7. codegraph_cli/cli_docs.py +207 -0
  8. codegraph_cli/cli_explore.py +1053 -0
  9. codegraph_cli/cli_export.py +941 -0
  10. codegraph_cli/cli_groups.py +33 -0
  11. codegraph_cli/cli_health.py +316 -0
  12. codegraph_cli/cli_history.py +213 -0
  13. codegraph_cli/cli_onboard.py +380 -0
  14. codegraph_cli/cli_quickstart.py +256 -0
  15. codegraph_cli/cli_refactor.py +17 -3
  16. codegraph_cli/cli_setup.py +12 -12
  17. codegraph_cli/cli_suggestions.py +90 -0
  18. codegraph_cli/cli_test.py +17 -3
  19. codegraph_cli/cli_tui.py +210 -0
  20. codegraph_cli/cli_v2.py +24 -4
  21. codegraph_cli/cli_watch.py +158 -0
  22. codegraph_cli/cli_workflows.py +255 -0
  23. codegraph_cli/codegen_agent.py +15 -1
  24. codegraph_cli/config.py +18 -5
  25. codegraph_cli/context_manager.py +117 -15
  26. codegraph_cli/crew_agents.py +32 -8
  27. codegraph_cli/crew_chat.py +146 -13
  28. codegraph_cli/crew_tools.py +30 -2
  29. codegraph_cli/embeddings.py +95 -5
  30. codegraph_cli/llm.py +42 -55
  31. codegraph_cli/project_context.py +64 -1
  32. codegraph_cli/rag.py +282 -19
  33. codegraph_cli/storage.py +310 -14
  34. codegraph_cli/vector_store.py +110 -8
  35. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/METADATA +75 -21
  36. codegraph_cli-2.1.2.dist-info/RECORD +55 -0
  37. codegraph_cli-2.1.2.dist-info/entry_points.txt +2 -0
  38. codegraph_cli-2.1.0.dist-info/RECORD +0 -43
  39. codegraph_cli-2.1.0.dist-info/entry_points.txt +0 -2
  40. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/WHEEL +0 -0
  41. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/licenses/LICENSE +0 -0
  42. {codegraph_cli-2.1.0.dist-info → codegraph_cli-2.1.2.dist-info}/top_level.txt +0 -0
@@ -305,10 +305,10 @@ def set_llm(
305
305
  """Quickly switch LLM provider without full setup wizard.
306
306
 
307
307
  Examples:
308
- cg set-llm groq -k YOUR_API_KEY
309
- cg set-llm gemini -k YOUR_API_KEY -m gemini-2.0-flash
310
- cg set-llm openrouter -k YOUR_API_KEY -m google/gemini-2.0-flash-exp:free
311
- cg set-llm ollama -m qwen2.5-coder:7b
308
+ cg config set-llm groq -k YOUR_API_KEY
309
+ cg config set-llm gemini -k YOUR_API_KEY -m gemini-2.0-flash
310
+ cg config set-llm openrouter -k YOUR_API_KEY -m google/gemini-2.0-flash-exp:free
311
+ cg config set-llm ollama -m qwen2.5-coder:7b
312
312
  """
313
313
  provider = provider.lower().strip()
314
314
 
@@ -467,9 +467,9 @@ def show_llm():
467
467
  typer.echo("")
468
468
  typer.echo(typer.style(" Quick Commands", bold=True))
469
469
  typer.echo(typer.style(" ─────────────────────────────────────────", dim=True))
470
- typer.echo(f" {typer.style('cg setup', fg=typer.colors.YELLOW)} Full interactive wizard")
471
- typer.echo(f" {typer.style('cg set-llm <name>', fg=typer.colors.YELLOW)} Quick switch provider")
472
- typer.echo(f" {typer.style('cg unset-llm', fg=typer.colors.YELLOW)} Reset / clear config")
470
+ typer.echo(f" {typer.style('cg config setup', fg=typer.colors.YELLOW)} Full interactive wizard")
471
+ typer.echo(f" {typer.style('cg config set-llm <name>', fg=typer.colors.YELLOW)} Quick switch provider")
472
+ typer.echo(f" {typer.style('cg config unset-llm', fg=typer.colors.YELLOW)} Reset / clear config")
473
473
  typer.echo("")
474
474
 
475
475
 
@@ -550,9 +550,9 @@ def set_embedding(
550
550
  qodo-1.5b ~6.2 GB Best quality, code-optimized
551
551
 
552
552
  Examples:
553
- cg set-embedding minilm
554
- cg set-embedding jina-code
555
- cg set-embedding hash
553
+ cg config set-embedding minilm
554
+ cg config set-embedding jina-code
555
+ cg config set-embedding hash
556
556
  """
557
557
  model = model.lower().strip()
558
558
 
@@ -619,8 +619,8 @@ def show_embedding():
619
619
  typer.echo("")
620
620
  typer.echo(typer.style(" Quick Commands", bold=True))
621
621
  typer.echo(typer.style(" ─────────────────────────────────────────", dim=True))
622
- typer.echo(f" {typer.style('cg set-embedding <model>', fg=typer.colors.YELLOW)} Switch model")
623
- typer.echo(f" {typer.style('cg unset-embedding', fg=typer.colors.YELLOW)} Reset to hash")
622
+ typer.echo(f" {typer.style('cg config set-embedding <model>', fg=typer.colors.YELLOW)} Switch model")
623
+ typer.echo(f" {typer.style('cg config unset-embedding', fg=typer.colors.YELLOW)} Reset to hash")
624
624
  typer.echo("")
625
625
 
626
626
 
@@ -0,0 +1,90 @@
1
+ """Contextual next-step suggestions after command completion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ console = Console()
9
+
10
+ NEXT_STEPS: dict[str, list[str]] = {
11
+ "index": [
12
+ "[cyan]cg search[/cyan] 'your query' Search code semantically",
13
+ "[cyan]cg chat start[/cyan] Ask questions about your code",
14
+ "[cyan]cg impact[/cyan] 'symbol' See what depends on a function",
15
+ ],
16
+ "search": [
17
+ "[cyan]cg chat start[/cyan] Discuss results with AI",
18
+ "[cyan]cg v2 review[/cyan] <file> Get AI code review",
19
+ "[cyan]cg graph[/cyan] 'symbol' Visualize dependencies",
20
+ ],
21
+ "generate": [
22
+ "[cyan]cg v2 review[/cyan] <file> Review generated code",
23
+ "[cyan]cg v2 test unit[/cyan] <symbol> Generate tests for new code",
24
+ "[cyan]cg export-graph[/cyan] Visualize changes",
25
+ ],
26
+ "diagnose": [
27
+ "[cyan]cg v2 test unit[/cyan] <symbol> Add tests for fixed code",
28
+ "[cyan]cg v2 review[/cyan] <file> Review the fixes",
29
+ ],
30
+ "chat": [
31
+ "[cyan]cg search[/cyan] 'query' Find specific code",
32
+ "[cyan]cg v2 generate[/cyan] 'description' Generate new code",
33
+ ],
34
+ "review": [
35
+ "[cyan]cg v2 diagnose fix[/cyan] <path> Auto-fix detected issues",
36
+ "[cyan]cg v2 refactor rename[/cyan] <old> <new> Rename symbols safely",
37
+ "[cyan]cg v2 test unit[/cyan] <symbol> Generate tests",
38
+ ],
39
+ "refactor": [
40
+ "[cyan]cg v2 review[/cyan] <file> Review refactored code",
41
+ "[cyan]cg v2 test unit[/cyan] <symbol> Add tests after refactoring",
42
+ ],
43
+ "test": [
44
+ "[cyan]cg v2 review[/cyan] <file> Review generated tests",
45
+ "[cyan]cg chat start[/cyan] Discuss test strategy with AI",
46
+ ],
47
+ "impact": [
48
+ "[cyan]cg graph[/cyan] 'symbol' Visualize dependency graph",
49
+ "[cyan]cg search[/cyan] 'related query' Find related code",
50
+ "[cyan]cg chat start[/cyan] Deep-dive with AI",
51
+ ],
52
+ "quickstart": [
53
+ "[cyan]cg search[/cyan] 'your query' Search code semantically",
54
+ "[cyan]cg chat start[/cyan] Chat with AI about your code",
55
+ "[cyan]cg v2 generate[/cyan] 'description' Generate new code",
56
+ ],
57
+ }
58
+
59
+
60
+ def show_next_steps(command_name: str) -> None:
61
+ """Show contextual next steps after command completion.
62
+
63
+ Args:
64
+ command_name: The name of the command that just completed.
65
+ """
66
+ steps = NEXT_STEPS.get(command_name)
67
+ if not steps:
68
+ return
69
+
70
+ console.print(
71
+ "\n"
72
+ + Panel.fit(
73
+ "\n".join([f" {step}" for step in steps]),
74
+ title="[bold cyan]💡 Next steps:[/bold cyan]",
75
+ border_style="cyan",
76
+ padding=(0, 1),
77
+ ).markup # type: ignore[attr-defined]
78
+ if False
79
+ else ""
80
+ )
81
+ # Use direct Panel printing to avoid markup issues
82
+ console.print()
83
+ console.print(
84
+ Panel(
85
+ "\n".join([f" {step}" for step in steps]),
86
+ title="[bold cyan]💡 Next steps:[/bold cyan]",
87
+ border_style="cyan",
88
+ padding=(0, 1),
89
+ )
90
+ )
codegraph_cli/cli_test.py CHANGED
@@ -45,7 +45,12 @@ def generate_unit_tests(
45
45
  symbol: str = typer.Argument(..., help="Function name to generate tests for"),
46
46
  output: Optional[str] = typer.Option(None, "--output", "-o", help="Output test file path"),
47
47
  ):
48
- """Generate unit tests for a function."""
48
+ """🧪 Generate unit tests for a function.
49
+
50
+ Example:
51
+ cg v2 test unit "calculate_total"
52
+ cg v2 test unit "UserService.authenticate" --output tests/test_auth.py
53
+ """
49
54
  pm = ProjectManager()
50
55
  agent = _get_testgen_agent(pm)
51
56
 
@@ -106,7 +111,12 @@ def generate_integration_tests(
106
111
  flow: str = typer.Argument(..., help="User flow description"),
107
112
  output: Optional[str] = typer.Option(None, "--output", "-o", help="Output test file path"),
108
113
  ):
109
- """Generate integration tests for a user flow."""
114
+ """🧪 Generate integration tests for a user flow.
115
+
116
+ Example:
117
+ cg v2 test integration "user login and token refresh"
118
+ cg v2 test integration "payment processing" --output tests/test_payments.py
119
+ """
110
120
  pm = ProjectManager()
111
121
  agent = _get_testgen_agent(pm)
112
122
 
@@ -155,7 +165,11 @@ def generate_integration_tests(
155
165
  def show_coverage_prediction(
156
166
  symbol: str = typer.Argument(..., help="Function to analyze"),
157
167
  ):
158
- """Show predicted coverage impact of generating tests."""
168
+ """📊 Show predicted coverage impact of generating tests.
169
+
170
+ Example:
171
+ cg v2 test coverage "process_payment"
172
+ """
159
173
  pm = ProjectManager()
160
174
  agent = _get_testgen_agent(pm)
161
175
 
@@ -0,0 +1,210 @@
1
+ """Interactive TUI menu for CodeGraph CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.prompt import Prompt
9
+
10
+ console = Console()
11
+
12
+
13
+ def show_interactive_menu() -> None:
14
+ """Display the interactive TUI menu when no command is provided."""
15
+ console.print()
16
+ console.print(
17
+ Panel.fit(
18
+ "[bold cyan]🧠 CodeGraph CLI[/bold cyan]\n"
19
+ "[dim]AI-powered code intelligence & multi-agent assistant[/dim]",
20
+ border_style="cyan",
21
+ )
22
+ )
23
+
24
+ while True:
25
+ console.print("\n[bold]What would you like to do?[/bold]\n")
26
+
27
+ choices = [
28
+ "1. 🔍 Search my codebase",
29
+ "2. 💬 Chat with AI about my code",
30
+ "3. ✨ Generate new code",
31
+ "4. 📊 Analyze code impact",
32
+ "5. 🔧 Review and improve code",
33
+ "6. ⚙️ Configure settings",
34
+ "7. 📚 Learn more / tutorial",
35
+ "8. 🏥 Project health dashboard",
36
+ "0. Exit",
37
+ ]
38
+ for choice in choices:
39
+ console.print(f" {choice}")
40
+
41
+ selection = Prompt.ask(
42
+ "\nChoice",
43
+ choices=["0", "1", "2", "3", "4", "5", "6", "7", "8"],
44
+ default="0",
45
+ )
46
+
47
+ if selection == "0":
48
+ console.print("[cyan]Goodbye![/cyan]")
49
+ break
50
+
51
+ elif selection == "1":
52
+ query = Prompt.ask("Search query")
53
+ if query.strip():
54
+ _run_search(query.strip())
55
+
56
+ elif selection == "2":
57
+ _run_chat()
58
+
59
+ elif selection == "3":
60
+ desc = Prompt.ask("What code to generate")
61
+ if desc.strip():
62
+ _run_generate(desc.strip())
63
+
64
+ elif selection == "4":
65
+ symbol = Prompt.ask("Symbol to analyze")
66
+ if symbol.strip():
67
+ _run_impact(symbol.strip())
68
+
69
+ elif selection == "5":
70
+ path = Prompt.ask("File path to review")
71
+ if path.strip():
72
+ _run_review(path.strip())
73
+
74
+ elif selection == "6":
75
+ _run_setup()
76
+
77
+ elif selection == "7":
78
+ _show_tutorial()
79
+
80
+ elif selection == "8":
81
+ _run_health()
82
+
83
+
84
+ def _run_search(query: str) -> None:
85
+ """Run search from TUI."""
86
+ try:
87
+ from .storage import ProjectManager, GraphStore
88
+ from .orchestrator import MCPOrchestrator
89
+
90
+ pm = ProjectManager()
91
+ project = pm.get_current_project()
92
+ if not project:
93
+ console.print("[red]✗[/red] No project loaded. Run 'cg index <path>' first.")
94
+ return
95
+ store = GraphStore(pm.project_dir(project))
96
+ orchestrator = MCPOrchestrator(store)
97
+ results = orchestrator.search(query, top_k=5)
98
+ if not results:
99
+ console.print("[yellow]No results found.[/yellow]")
100
+ else:
101
+ for item in results:
102
+ console.print(
103
+ f" [{item.node_type}] {item.qualname} "
104
+ f"[dim]score={item.score:.3f}[/dim]"
105
+ )
106
+ console.print(f" [dim]{item.file_path}:{item.start_line}-{item.end_line}[/dim]")
107
+ store.close()
108
+ except Exception as e:
109
+ console.print(f"[red]Error: {e}[/red]")
110
+
111
+
112
+ def _run_chat() -> None:
113
+ """Start chat from TUI."""
114
+ try:
115
+ # Import and invoke via typer to handle all the setup
116
+ console.print("[cyan]Starting chat... (use /exit to return)[/cyan]")
117
+ from .cli_chat import start_chat
118
+ start_chat()
119
+ except (typer.Exit, SystemExit):
120
+ pass
121
+ except Exception as e:
122
+ console.print(f"[red]Error: {e}[/red]")
123
+
124
+
125
+ def _run_generate(description: str) -> None:
126
+ """Run code generation from TUI."""
127
+ try:
128
+ from .cli_v2 import generate_code
129
+ generate_code(description)
130
+ except (typer.Exit, SystemExit):
131
+ pass
132
+ except Exception as e:
133
+ console.print(f"[red]Error: {e}[/red]")
134
+
135
+
136
+ def _run_impact(symbol: str) -> None:
137
+ """Run impact analysis from TUI."""
138
+ try:
139
+ from .storage import ProjectManager, GraphStore
140
+ from .orchestrator import MCPOrchestrator
141
+
142
+ pm = ProjectManager()
143
+ project = pm.get_current_project()
144
+ if not project:
145
+ console.print("[red]✗[/red] No project loaded.")
146
+ return
147
+ store = GraphStore(pm.project_dir(project))
148
+ orchestrator = MCPOrchestrator(store)
149
+ report = orchestrator.impact(symbol, hops=2)
150
+ console.print(f"Root: {report.root}")
151
+ if report.impacted:
152
+ console.print("Impacted symbols:")
153
+ for imp in report.impacted:
154
+ console.print(f" • {imp}")
155
+ console.print(f"\n{report.explanation}")
156
+ store.close()
157
+ except Exception as e:
158
+ console.print(f"[red]Error: {e}[/red]")
159
+
160
+
161
+ def _run_review(path: str) -> None:
162
+ """Run code review from TUI."""
163
+ try:
164
+ from .cli_v2 import review_code
165
+ review_code(path)
166
+ except (typer.Exit, SystemExit):
167
+ pass
168
+ except Exception as e:
169
+ console.print(f"[red]Error: {e}[/red]")
170
+
171
+
172
+ def _run_setup() -> None:
173
+ """Run setup wizard from TUI."""
174
+ try:
175
+ from .cli_setup import setup
176
+ setup()
177
+ except (typer.Exit, SystemExit):
178
+ pass
179
+ except Exception as e:
180
+ console.print(f"[red]Error: {e}[/red]")
181
+
182
+
183
+ def _run_health() -> None:
184
+ """Run health dashboard from TUI."""
185
+ try:
186
+ from .cli_health import health_dashboard
187
+ health_dashboard()
188
+ except (typer.Exit, SystemExit):
189
+ pass
190
+ except Exception as e:
191
+ console.print(f"[red]Error: {e}[/red]")
192
+
193
+
194
+ def _show_tutorial() -> None:
195
+ """Show interactive tutorial."""
196
+ console.print(
197
+ Panel(
198
+ "📚 [bold]Quick Tutorial[/bold]\n\n"
199
+ "1. [cyan]Index your project[/cyan]: cg index ./my-project\n"
200
+ "2. [cyan]Search code[/cyan]: cg search 'authentication'\n"
201
+ "3. [cyan]Chat with AI[/cyan]: cg chat start\n"
202
+ "4. [cyan]Generate code[/cyan]: cg v2 generate 'add API endpoint'\n"
203
+ "5. [cyan]Impact analysis[/cyan]: cg impact 'symbol_name'\n"
204
+ "6. [cyan]Code review[/cyan]: cg v2 review path/to/file.py\n"
205
+ "7. [cyan]Project health[/cyan]: cg health dashboard\n\n"
206
+ "For full reference: cg cheatsheet\n"
207
+ "For documentation: cg learn",
208
+ border_style="blue",
209
+ )
210
+ )
codegraph_cli/cli_v2.py CHANGED
@@ -56,7 +56,13 @@ def generate_code(
56
56
  llm_api_key: Optional[str] = typer.Option(config.LLM_API_KEY, help="API key for LLM"),
57
57
  llm_model: str = typer.Option(config.LLM_MODEL, help="LLM model"),
58
58
  ):
59
- """Generate code from natural language description (v2.0 experimental)."""
59
+ """Generate code from natural language description (v2.0 experimental).
60
+
61
+ Example:
62
+ cg v2 generate 'add REST API endpoint for users'
63
+ cg v2 generate 'add login function' --file auth.py
64
+ cg v2 generate 'create data model' --output models/ --auto-apply
65
+ """
60
66
  pm = ProjectManager()
61
67
  agent = _get_codegen_agent(pm)
62
68
 
@@ -116,7 +122,11 @@ def generate_code(
116
122
  def rollback_changes(
117
123
  backup_id: str = typer.Argument(..., help="Backup ID to rollback to"),
118
124
  ):
119
- """Rollback to a previous backup."""
125
+ """Rollback to a previous backup.
126
+
127
+ Example:
128
+ cg v2 rollback backup_20240101_120000
129
+ """
120
130
  diff_engine = DiffEngine()
121
131
 
122
132
  typer.echo(f"🔄 Rolling back to backup: {backup_id}")
@@ -130,7 +140,11 @@ def rollback_changes(
130
140
 
131
141
  @v2_app.command("list-backups")
132
142
  def list_backups():
133
- """List all available backups."""
143
+ """📦 List all available backups.
144
+
145
+ Example:
146
+ cg v2 list-backups
147
+ """
134
148
  diff_engine = DiffEngine()
135
149
  backups = diff_engine.list_backups()
136
150
 
@@ -156,7 +170,13 @@ def review_code(
156
170
  use_llm: bool = typer.Option(False, "--llm", help="Use LLM for deeper analysis"),
157
171
  show_fixes: bool = typer.Option(False, "--fix", help="Show auto-fix suggestions"),
158
172
  ):
159
- """Run AI-powered code review on a file."""
173
+ """🔍 Run AI-powered code review on a file.
174
+
175
+ Example:
176
+ cg v2 review src/auth.py
177
+ cg v2 review src/models.py --check security --llm
178
+ cg v2 review src/api.py --fix --verbose
179
+ """
160
180
  from .bug_detector import BugDetector
161
181
  from .security_scanner import SecurityScanner
162
182
  from .performance_analyzer import PerformanceAnalyzer
@@ -0,0 +1,158 @@
1
+ """Watch mode for auto-reindexing on file changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ console = Console()
12
+
13
+ watch_app = typer.Typer(help="👀 Watch mode for auto-reindexing")
14
+
15
+ # Supported code file extensions
16
+ WATCHED_EXTENSIONS = {
17
+ ".py", ".js", ".ts", ".tsx", ".jsx",
18
+ ".go", ".rs", ".java", ".kt", ".kts",
19
+ ".rb", ".php", ".ex", ".exs",
20
+ ".c", ".cpp", ".h", ".hpp", ".cs",
21
+ ".swift", ".dart",
22
+ }
23
+
24
+
25
+ class CodeChangeHandler:
26
+ """Handle file system events and trigger re-indexing."""
27
+
28
+ def __init__(self, reindex_callback, debounce_seconds: float = 2.0):
29
+ from watchdog.events import FileSystemEventHandler
30
+ self._base_class = FileSystemEventHandler
31
+ self.reindex_callback = reindex_callback
32
+ self.last_reindex = 0.0
33
+ self.debounce_seconds = debounce_seconds
34
+ self._pending_files: set[str] = set()
35
+
36
+ def dispatch(self, event):
37
+ """Route events to handler methods."""
38
+ if event.is_directory:
39
+ return
40
+ if hasattr(event, "src_path"):
41
+ self._handle_change(event.src_path)
42
+
43
+ def _handle_change(self, src_path: str):
44
+ file_path = Path(src_path)
45
+ if file_path.suffix not in WATCHED_EXTENSIONS:
46
+ return
47
+
48
+ # Skip hidden/temp files
49
+ if any(part.startswith(".") for part in file_path.parts):
50
+ return
51
+
52
+ now = time.time()
53
+ self._pending_files.add(str(file_path))
54
+
55
+ if now - self.last_reindex < self.debounce_seconds:
56
+ return
57
+
58
+ # Flush pending
59
+ files = list(self._pending_files)
60
+ self._pending_files.clear()
61
+ self.last_reindex = now
62
+
63
+ for f in files:
64
+ self.reindex_callback(Path(f))
65
+
66
+
67
+ @watch_app.command("start")
68
+ def watch(
69
+ path: str = typer.Argument(".", help="Path to watch for changes."),
70
+ interval: float = typer.Option(2.0, "--interval", "-i", help="Debounce interval in seconds."),
71
+ full_reindex: bool = typer.Option(False, "--full", help="Re-index entire project on each change."),
72
+ ):
73
+ """👀 Watch mode — auto-reindex on file changes.
74
+
75
+ Monitors your project directory for file changes and automatically
76
+ re-indexes modified files to keep the code graph up to date.
77
+
78
+ Example:
79
+ cg watch
80
+ cg watch ./src --interval 5
81
+ cg watch --full
82
+ """
83
+ try:
84
+ from watchdog.observers import Observer
85
+ from watchdog.events import FileSystemEventHandler
86
+ except ImportError:
87
+ console.print("[red]✗[/red] watchdog is not installed.")
88
+ console.print("[dim]Install with: pip install watchdog[/dim]")
89
+ raise typer.Exit(1)
90
+
91
+ watch_path = Path(path).resolve()
92
+ if not watch_path.exists():
93
+ console.print(f"[red]✗[/red] Path not found: {path}")
94
+ raise typer.Exit(1)
95
+
96
+ from .storage import ProjectManager, GraphStore
97
+ from .orchestrator import MCPOrchestrator
98
+
99
+ pm = ProjectManager()
100
+ project = pm.get_current_project()
101
+ if not project:
102
+ console.print("[red]✗[/red] No project loaded. Run 'cg index <path>' first.")
103
+ raise typer.Exit(1)
104
+
105
+ project_dir = pm.project_dir(project)
106
+ reindex_count = 0
107
+
108
+ def reindex_file(file_path: Path):
109
+ nonlocal reindex_count
110
+ try:
111
+ if full_reindex:
112
+ store = GraphStore(project_dir)
113
+ orchestrator = MCPOrchestrator(store)
114
+ stats = orchestrator.index(watch_path)
115
+ store.close()
116
+ console.print(
117
+ f" [green]✓[/green] Full re-index: {stats['nodes']} nodes, {stats['edges']} edges"
118
+ )
119
+ else:
120
+ # Incremental: re-index the single changed file
121
+ store = GraphStore(project_dir)
122
+ orchestrator = MCPOrchestrator(store)
123
+ # For now, do a full re-index (single-file not yet supported in orchestrator)
124
+ stats = orchestrator.index(watch_path)
125
+ store.close()
126
+ console.print(f" [green]✓[/green] Re-indexed ({file_path.name} changed)")
127
+ reindex_count += 1
128
+ except Exception as e:
129
+ console.print(f" [red]✗[/red] Re-index failed: {e}")
130
+
131
+ console.print(f"\n[bold green]👀 Watching[/bold green] [cyan]{watch_path}[/cyan] for changes...")
132
+ console.print(f"[dim] Debounce: {interval}s")
133
+ console.print(f" Mode: {'full re-index' if full_reindex else 'incremental'}")
134
+ console.print(f" Project: {project}")
135
+ console.print(f" Press Ctrl+C to stop[/dim]\n")
136
+
137
+ handler = CodeChangeHandler(reindex_file, debounce_seconds=interval)
138
+
139
+ # Wrap as proper watchdog handler
140
+ class WatchdogAdapter(FileSystemEventHandler):
141
+ def on_modified(self, event):
142
+ handler.dispatch(event)
143
+
144
+ def on_created(self, event):
145
+ handler.dispatch(event)
146
+
147
+ observer = Observer()
148
+ observer.schedule(WatchdogAdapter(), str(watch_path), recursive=True)
149
+ observer.start()
150
+
151
+ try:
152
+ while True:
153
+ time.sleep(1)
154
+ except KeyboardInterrupt:
155
+ observer.stop()
156
+ console.print(f"\n[yellow]Stopped watching.[/yellow] Re-indexed {reindex_count} time(s).")
157
+
158
+ observer.join()