roma-debug 0.1.0__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.
roma_debug/main.py ADDED
@@ -0,0 +1,753 @@
1
+ """Interactive CLI for ROMA Debug.
2
+
3
+ Production-grade CLI with diff display and safe file patching.
4
+ Supports V1 (simple) and V2 (deep debugging) modes.
5
+ """
6
+
7
+ import difflib
8
+ import os
9
+ import shutil
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import click
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+ from rich.prompt import Prompt, Confirm
18
+ from rich.table import Table
19
+ from rich import print as rprint
20
+
21
+ from roma_debug import __version__
22
+ from roma_debug.config import GEMINI_API_KEY, get_api_key_status
23
+ from roma_debug.utils.context import get_file_context, get_primary_file
24
+ from roma_debug.core.engine import analyze_error, analyze_error_v2, FixResult, FixResultV2
25
+ from roma_debug.core.models import Language
26
+
27
+
28
+ console = Console()
29
+
30
+
31
+ # Language name mappings for CLI
32
+ LANGUAGE_CHOICES = {
33
+ "python": Language.PYTHON,
34
+ "py": Language.PYTHON,
35
+ "javascript": Language.JAVASCRIPT,
36
+ "js": Language.JAVASCRIPT,
37
+ "typescript": Language.TYPESCRIPT,
38
+ "ts": Language.TYPESCRIPT,
39
+ "go": Language.GO,
40
+ "golang": Language.GO,
41
+ "rust": Language.RUST,
42
+ "rs": Language.RUST,
43
+ "java": Language.JAVA,
44
+ }
45
+
46
+
47
+ def print_welcome():
48
+ """Print welcome banner."""
49
+ console.print()
50
+ console.print(Panel(
51
+ "[bold blue]ROMA Debug[/bold blue] - AI-Powered Code Debugger\n"
52
+ f"[dim]Version {__version__} | Powered by Gemini[/dim]",
53
+ border_style="blue",
54
+ ))
55
+ console.print()
56
+
57
+
58
+ def get_multiline_input() -> str:
59
+ """Get multi-line input from user (paste-friendly)."""
60
+ console.print("[yellow]Paste your error log below.[/yellow]")
61
+ console.print("[dim]Press Enter twice (empty line) when done:[/dim]")
62
+ console.print()
63
+
64
+ lines = []
65
+ empty_count = 0
66
+
67
+ while True:
68
+ try:
69
+ line = input()
70
+ if line == "":
71
+ empty_count += 1
72
+ if empty_count >= 1 and lines:
73
+ break
74
+ lines.append(line)
75
+ else:
76
+ empty_count = 0
77
+ lines.append(line)
78
+ except EOFError:
79
+ break
80
+ except KeyboardInterrupt:
81
+ console.print("\n[yellow]Cancelled.[/yellow]")
82
+ return ""
83
+
84
+ return "\n".join(lines).strip()
85
+
86
+
87
+ def compute_diff(original: str, fixed: str, filepath: str) -> str:
88
+ """Compute unified diff between original and fixed code.
89
+
90
+ Args:
91
+ original: Original file content
92
+ fixed: Fixed code from AI
93
+ filepath: Path for diff header
94
+
95
+ Returns:
96
+ Unified diff string
97
+ """
98
+ original_lines = original.splitlines(keepends=True)
99
+ fixed_lines = fixed.splitlines(keepends=True)
100
+
101
+ diff = difflib.unified_diff(
102
+ original_lines,
103
+ fixed_lines,
104
+ fromfile=f"a/{filepath}",
105
+ tofile=f"b/{filepath}",
106
+ lineterm=""
107
+ )
108
+
109
+ return "".join(diff)
110
+
111
+
112
+ def display_diff(diff_text: str):
113
+ """Display diff with rich formatting (green=add, red=delete).
114
+
115
+ Args:
116
+ diff_text: Unified diff string
117
+ """
118
+ if not diff_text.strip():
119
+ console.print("[yellow]No differences detected.[/yellow]")
120
+ return
121
+
122
+ console.print()
123
+ console.print("[bold]Proposed Changes:[/bold]")
124
+ console.print()
125
+
126
+ for line in diff_text.splitlines():
127
+ if line.startswith("+++") or line.startswith("---"):
128
+ console.print(f"[bold]{line}[/bold]")
129
+ elif line.startswith("@@"):
130
+ console.print(f"[cyan]{line}[/cyan]")
131
+ elif line.startswith("+"):
132
+ console.print(f"[green]{line}[/green]")
133
+ elif line.startswith("-"):
134
+ console.print(f"[red]{line}[/red]")
135
+ else:
136
+ console.print(line)
137
+
138
+ console.print()
139
+
140
+
141
+ def display_fix_result(result: FixResult, is_general: bool = False):
142
+ """Display the fix result in a panel.
143
+
144
+ Args:
145
+ result: FixResult from engine
146
+ is_general: If True, display as general advice (no file target)
147
+ """
148
+ console.print()
149
+
150
+ if is_general or result.filepath is None:
151
+ # General system error - no specific file
152
+ console.print(Panel(
153
+ f"[bold]Type:[/bold] General Advice\n"
154
+ f"[bold]Model:[/bold] {result.model_used}\n\n"
155
+ f"[bold]Explanation:[/bold]\n{result.explanation}",
156
+ title="[bold yellow]General Advice[/bold yellow]",
157
+ border_style="yellow",
158
+ ))
159
+ else:
160
+ # Specific file fix
161
+ console.print(Panel(
162
+ f"[bold]File:[/bold] {result.filepath}\n"
163
+ f"[bold]Model:[/bold] {result.model_used}\n\n"
164
+ f"[bold]Explanation:[/bold]\n{result.explanation}",
165
+ title="[bold green]Analysis Result[/bold green]",
166
+ border_style="green",
167
+ ))
168
+
169
+
170
+ def display_fix_result_v2(result: FixResultV2):
171
+ """Display V2 fix result with root cause info.
172
+
173
+ Args:
174
+ result: FixResultV2 from engine
175
+ """
176
+ console.print()
177
+
178
+ # Main result panel
179
+ if result.filepath is None:
180
+ console.print(Panel(
181
+ f"[bold]Type:[/bold] General Advice\n"
182
+ f"[bold]Model:[/bold] {result.model_used}\n\n"
183
+ f"[bold]Explanation:[/bold]\n{result.explanation}",
184
+ title="[bold yellow]General Advice[/bold yellow]",
185
+ border_style="yellow",
186
+ ))
187
+ else:
188
+ panel_content = f"[bold]File:[/bold] {result.filepath}\n"
189
+ panel_content += f"[bold]Model:[/bold] {result.model_used}\n\n"
190
+ panel_content += f"[bold]Explanation:[/bold]\n{result.explanation}"
191
+
192
+ console.print(Panel(
193
+ panel_content,
194
+ title="[bold green]Analysis Result[/bold green]",
195
+ border_style="green",
196
+ ))
197
+
198
+ # Root cause panel (if different file)
199
+ if result.has_root_cause:
200
+ console.print()
201
+ console.print(Panel(
202
+ f"[bold]Root Cause File:[/bold] {result.root_cause_file}\n\n"
203
+ f"[bold]Root Cause:[/bold]\n{result.root_cause_explanation}",
204
+ title="[bold magenta]Root Cause Analysis[/bold magenta]",
205
+ border_style="magenta",
206
+ ))
207
+
208
+ # Additional fixes
209
+ if result.additional_fixes:
210
+ console.print()
211
+ console.print("[bold cyan]Additional Files to Fix:[/bold cyan]")
212
+ for fix in result.additional_fixes:
213
+ console.print(f" • {fix.filepath}: {fix.explanation}")
214
+
215
+
216
+ def display_general_advice(result: FixResult):
217
+ """Display general advice for system errors (no file patching).
218
+
219
+ Args:
220
+ result: FixResult from engine
221
+ """
222
+ display_fix_result(result, is_general=True)
223
+
224
+ if result.full_code_block:
225
+ console.print("\n[bold]Suggested Code / Solution:[/bold]")
226
+ console.print(Panel(
227
+ Syntax(result.full_code_block, "python", theme="monokai", line_numbers=True),
228
+ border_style="yellow",
229
+ ))
230
+
231
+ console.print("\n[dim]This is general advice. No file will be modified.[/dim]")
232
+
233
+
234
+ def read_file_content(filepath: str) -> str | None:
235
+ """Read file content if it exists.
236
+
237
+ Args:
238
+ filepath: Path to file
239
+
240
+ Returns:
241
+ File content or None if not readable
242
+ """
243
+ # Try absolute path first
244
+ if os.path.isfile(filepath):
245
+ try:
246
+ with open(filepath, 'r', encoding='utf-8') as f:
247
+ return f.read()
248
+ except (IOError, OSError):
249
+ return None
250
+
251
+ # Try relative to cwd
252
+ cwd_path = os.path.join(os.getcwd(), filepath)
253
+ if os.path.isfile(cwd_path):
254
+ try:
255
+ with open(cwd_path, 'r', encoding='utf-8') as f:
256
+ return f.read()
257
+ except (IOError, OSError):
258
+ return None
259
+
260
+ return None
261
+
262
+
263
+ def resolve_filepath(filepath: str) -> str:
264
+ """Resolve filepath relative to cwd if not absolute.
265
+
266
+ Args:
267
+ filepath: Path from AI response
268
+
269
+ Returns:
270
+ Resolved absolute or cwd-relative path
271
+ """
272
+ if os.path.isabs(filepath):
273
+ return filepath
274
+
275
+ # Check if it exists relative to cwd
276
+ cwd_path = os.path.join(os.getcwd(), filepath)
277
+ if os.path.exists(cwd_path) or os.path.exists(os.path.dirname(cwd_path) or "."):
278
+ return cwd_path
279
+
280
+ return filepath
281
+
282
+
283
+ def create_backup(filepath: str) -> str | None:
284
+ """Create a backup of the file.
285
+
286
+ Args:
287
+ filepath: Path to file
288
+
289
+ Returns:
290
+ Backup path or None if failed
291
+ """
292
+ backup_path = f"{filepath}.bak"
293
+ try:
294
+ shutil.copy2(filepath, backup_path)
295
+ return backup_path
296
+ except (IOError, OSError) as e:
297
+ console.print(f"[yellow]Warning: Could not create backup: {e}[/yellow]")
298
+ return None
299
+
300
+
301
+ def apply_fix(filepath: str, new_content: str) -> bool:
302
+ """Apply the fix to the file.
303
+
304
+ Args:
305
+ filepath: Path to file
306
+ new_content: New file content
307
+
308
+ Returns:
309
+ True if successful
310
+ """
311
+ try:
312
+ # Ensure parent directory exists
313
+ Path(filepath).parent.mkdir(parents=True, exist_ok=True)
314
+
315
+ with open(filepath, 'w', encoding='utf-8') as f:
316
+ f.write(new_content)
317
+ return True
318
+ except (IOError, OSError) as e:
319
+ console.print(f"[red]Error writing file: {e}[/red]")
320
+ return False
321
+
322
+
323
+ def interactive_fix(result: FixResult):
324
+ """Interactive workflow to apply a fix.
325
+
326
+ Handles three cases:
327
+ 1. filepath is None -> Display as general advice (no file ops)
328
+ 2. filepath exists -> Show diff and offer to patch
329
+ 3. filepath doesn't exist -> Offer to create new file
330
+
331
+ Args:
332
+ result: FixResult from engine
333
+ """
334
+ # Case 1: No filepath - general system error
335
+ if result.filepath is None:
336
+ display_general_advice(result)
337
+ return
338
+
339
+ # Resolve filepath relative to cwd
340
+ filepath = resolve_filepath(result.filepath)
341
+ new_code = result.full_code_block
342
+
343
+ # Display the result
344
+ display_fix_result(result)
345
+
346
+ if not new_code:
347
+ console.print("[yellow]No code fix provided by AI.[/yellow]")
348
+ return
349
+
350
+ # Check if file exists
351
+ original_content = read_file_content(filepath)
352
+
353
+ if original_content is None:
354
+ # Case 3: File doesn't exist - offer to create
355
+ console.print(f"\n[yellow]File '{filepath}' does not exist locally.[/yellow]")
356
+
357
+ # Show the proposed new content
358
+ console.print("\n[bold]Proposed new file content:[/bold]")
359
+ console.print(Panel(
360
+ Syntax(new_code, "python", theme="monokai", line_numbers=True),
361
+ border_style="blue",
362
+ ))
363
+
364
+ if Confirm.ask(f"\n[bold]Create new file '{filepath}'?[/bold]", default=False):
365
+ if apply_fix(filepath, new_code):
366
+ console.print(f"[green]Created: {filepath}[/green]")
367
+ else:
368
+ console.print(f"[red]Failed to create file.[/red]")
369
+ else:
370
+ console.print("[dim]Skipped.[/dim]")
371
+ return
372
+
373
+ # Case 2: File exists - show diff and offer to patch
374
+ diff_text = compute_diff(original_content, new_code, filepath)
375
+
376
+ if not diff_text.strip():
377
+ console.print("[yellow]The AI suggested no changes to the file.[/yellow]")
378
+ return
379
+
380
+ # Display diff
381
+ display_diff(diff_text)
382
+
383
+ # Ask to apply
384
+ if Confirm.ask(f"[bold]Apply this fix to '{filepath}'?[/bold]", default=True):
385
+ # Create backup
386
+ backup_path = create_backup(filepath)
387
+ if backup_path:
388
+ console.print(f"[dim]Backup created: {backup_path}[/dim]")
389
+
390
+ # Apply fix
391
+ if apply_fix(filepath, new_code):
392
+ console.print(f"[green]Success! Fixed: {filepath}[/green]")
393
+ else:
394
+ console.print(f"[red]Failed to apply fix.[/red]")
395
+ else:
396
+ console.print("[dim]Skipped.[/dim]")
397
+
398
+
399
+ def interactive_fix_v2(result: FixResultV2):
400
+ """Interactive workflow for V2 fixes with multiple files.
401
+
402
+ Args:
403
+ result: FixResultV2 from engine
404
+ """
405
+ # No filepath - general advice
406
+ if result.filepath is None:
407
+ display_general_advice(result)
408
+ return
409
+
410
+ # Display the full analysis
411
+ display_fix_result_v2(result)
412
+
413
+ # Handle primary fix
414
+ _apply_single_fix(result.filepath, result.full_code_block, "primary fix")
415
+
416
+ # Handle additional fixes
417
+ for fix in result.additional_fixes:
418
+ console.print()
419
+ console.print(f"[bold cyan]Additional fix for {fix.filepath}:[/bold cyan]")
420
+ console.print(f"[dim]{fix.explanation}[/dim]")
421
+ _apply_single_fix(fix.filepath, fix.full_code_block, "this fix")
422
+
423
+
424
+ def _apply_single_fix(filepath: str, new_code: str, fix_name: str = "fix"):
425
+ """Apply a single fix interactively.
426
+
427
+ Args:
428
+ filepath: Path to the file
429
+ new_code: New code content
430
+ fix_name: Name for the fix in prompts
431
+ """
432
+ if not new_code:
433
+ console.print(f"[yellow]No code provided for {filepath}[/yellow]")
434
+ return
435
+
436
+ resolved = resolve_filepath(filepath)
437
+ original = read_file_content(resolved)
438
+
439
+ if original is None:
440
+ console.print(f"\n[yellow]File '{resolved}' does not exist.[/yellow]")
441
+ if Confirm.ask(f"Create new file?", default=False):
442
+ if apply_fix(resolved, new_code):
443
+ console.print(f"[green]Created: {resolved}[/green]")
444
+ return
445
+
446
+ diff_text = compute_diff(original, new_code, resolved)
447
+ if not diff_text.strip():
448
+ console.print(f"[yellow]No changes needed for {resolved}[/yellow]")
449
+ return
450
+
451
+ display_diff(diff_text)
452
+
453
+ if Confirm.ask(f"[bold]Apply {fix_name} to '{resolved}'?[/bold]", default=True):
454
+ backup = create_backup(resolved)
455
+ if backup:
456
+ console.print(f"[dim]Backup: {backup}[/dim]")
457
+ if apply_fix(resolved, new_code):
458
+ console.print(f"[green]Fixed: {resolved}[/green]")
459
+
460
+
461
+ def analyze_and_interact(
462
+ error_log: str,
463
+ deep: bool = False,
464
+ language_hint: str | None = None,
465
+ ):
466
+ """Analyze error and run interactive fix workflow.
467
+
468
+ Args:
469
+ error_log: The error log string
470
+ deep: Whether to use V2 deep debugging
471
+ language_hint: Optional language hint
472
+ """
473
+ if not error_log:
474
+ console.print("[red]No error provided.[/red]")
475
+ return
476
+
477
+ # Extract file context
478
+ context = ""
479
+ contexts = []
480
+ analysis_ctx = None
481
+
482
+ if deep:
483
+ # V2: Use context builder for deep debugging with project awareness
484
+ with console.status("[bold blue]Scanning project and analyzing error..."):
485
+ try:
486
+ from roma_debug.tracing.context_builder import ContextBuilder
487
+
488
+ language = LANGUAGE_CHOICES.get(language_hint.lower()) if language_hint else None
489
+ builder = ContextBuilder(project_root=os.getcwd(), scan_project=True)
490
+
491
+ # Show project info
492
+ project_info = builder.project_info
493
+ console.print(f"[cyan]Project: {project_info.project_type}[/cyan]")
494
+ if project_info.frameworks_detected:
495
+ console.print(f"[cyan]Frameworks: {', '.join(project_info.frameworks_detected)}[/cyan]")
496
+
497
+ # Build analysis context
498
+ analysis_ctx = builder.build_analysis_context(
499
+ error_log,
500
+ language_hint=language,
501
+ )
502
+
503
+ # Use deep context for comprehensive analysis
504
+ context = builder.get_deep_context(error_log, language_hint=language)
505
+ contexts = analysis_ctx.traceback_contexts
506
+
507
+ # Report what we found
508
+ if contexts:
509
+ console.print(f"[green]Found context from {len(contexts)} file(s)[/green]")
510
+ if analysis_ctx.upstream_context:
511
+ upstream_count = len(analysis_ctx.upstream_context.file_contexts)
512
+ if upstream_count > 0:
513
+ console.print(f"[green]Analyzed {upstream_count} upstream file(s)[/green]")
514
+
515
+ # Report error analysis findings
516
+ if analysis_ctx.error_analysis:
517
+ ea = analysis_ctx.error_analysis
518
+ console.print(f"[dim]Error type: {ea.error_type} ({ea.error_category})[/dim]")
519
+ if ea.relevant_files:
520
+ console.print(f"[green]Identified {len(ea.relevant_files)} relevant file(s):[/green]")
521
+ for rf in ea.relevant_files[:3]:
522
+ console.print(f" [dim]- {rf.path}[/dim]")
523
+
524
+ # Show entry points if no explicit files found
525
+ if not contexts and project_info.entry_points:
526
+ console.print(f"[yellow]No explicit traceback. Using entry points for context.[/yellow]")
527
+ for ep in project_info.entry_points[:2]:
528
+ console.print(f" [dim]- {ep.path}[/dim]")
529
+
530
+ except Exception as e:
531
+ console.print(f"[yellow]Deep analysis failed, falling back to basic: {e}[/yellow]")
532
+ import traceback
533
+ traceback.print_exc()
534
+ context, contexts = get_file_context(error_log)
535
+ else:
536
+ # V1: Basic context extraction - but upgrade to deep if no traceback found
537
+ with console.status("[bold blue]Reading source files..."):
538
+ context, contexts = get_file_context(error_log)
539
+ if context:
540
+ primary = get_primary_file(contexts)
541
+ if primary:
542
+ console.print(f"[green]Found context from {len(contexts)} file(s)[/green]")
543
+ if primary.function_name:
544
+ console.print(f"[dim]Primary: {primary.filepath} (function: {primary.function_name})[/dim]")
545
+ elif primary.class_name:
546
+ console.print(f"[dim]Primary: {primary.filepath} (class: {primary.class_name})[/dim]")
547
+ else:
548
+ # No traceback found - automatically use deep project awareness
549
+ console.print("[yellow]No traceback found. Activating project awareness...[/yellow]")
550
+ try:
551
+ from roma_debug.tracing.context_builder import ContextBuilder
552
+
553
+ language = LANGUAGE_CHOICES.get(language_hint.lower()) if language_hint else None
554
+ builder = ContextBuilder(project_root=os.getcwd(), scan_project=True)
555
+
556
+ # Show project info
557
+ project_info = builder.project_info
558
+ console.print(f"[cyan]Project: {project_info.project_type}[/cyan]")
559
+ if project_info.frameworks_detected:
560
+ console.print(f"[cyan]Frameworks: {', '.join(project_info.frameworks_detected)}[/cyan]")
561
+ if project_info.entry_points:
562
+ console.print(f"[cyan]Entry points: {', '.join(ep.path for ep in project_info.entry_points[:3])}[/cyan]")
563
+
564
+ # Build analysis context with project awareness
565
+ analysis_ctx = builder.build_analysis_context(
566
+ error_log,
567
+ language_hint=language,
568
+ )
569
+
570
+ # Get deep context
571
+ context = builder.get_deep_context(error_log, language_hint=language)
572
+
573
+ # Report error analysis findings
574
+ if analysis_ctx.error_analysis:
575
+ ea = analysis_ctx.error_analysis
576
+ console.print(f"[dim]Error type: {ea.error_type} ({ea.error_category})[/dim]")
577
+ if ea.relevant_files:
578
+ console.print(f"[green]Found {len(ea.relevant_files)} relevant file(s):[/green]")
579
+ for rf in ea.relevant_files[:3]:
580
+ console.print(f" [dim]- {rf.path}[/dim]")
581
+
582
+ # Switch to V2 analysis
583
+ deep = True
584
+
585
+ except Exception as e:
586
+ console.print(f"[yellow]Project scanning failed: {e}[/yellow]")
587
+
588
+ # Analyze with Gemini
589
+ with console.status("[bold green]Analyzing with Gemini..."):
590
+ try:
591
+ if deep:
592
+ result = analyze_error_v2(error_log, context)
593
+ else:
594
+ result = analyze_error(error_log, context)
595
+ except RuntimeError as e:
596
+ console.print(f"\n[red]Configuration Error:[/red] {e}")
597
+ return
598
+ except Exception as e:
599
+ console.print(f"\n[red]Analysis failed:[/red] {e}")
600
+ return
601
+
602
+ # Interactive fix workflow
603
+ if deep and isinstance(result, FixResultV2):
604
+ interactive_fix_v2(result)
605
+ else:
606
+ interactive_fix(result)
607
+
608
+
609
+ def interactive_mode(deep: bool = False, language_hint: str | None = None):
610
+ """Run interactive mode - paste errors, get fixes.
611
+
612
+ Args:
613
+ deep: Whether to use V2 deep debugging
614
+ language_hint: Optional language hint
615
+ """
616
+ print_welcome()
617
+
618
+ if deep:
619
+ console.print("[bold cyan]Deep Debugging Mode Enabled[/bold cyan]")
620
+ console.print("[dim]Analyzing imports and call chains for root cause analysis[/dim]")
621
+ console.print()
622
+
623
+ # Check for API key
624
+ status = get_api_key_status()
625
+ if status != "OK":
626
+ console.print("[red]Error: GEMINI_API_KEY not configured[/red]")
627
+ console.print("[dim]Set it in .env file or: export GEMINI_API_KEY=your-key[/dim]")
628
+ console.print("[dim]Get a key at: https://aistudio.google.com/apikey[/dim]")
629
+ sys.exit(1)
630
+
631
+ console.print()
632
+
633
+ while True:
634
+ # Get error input
635
+ error_log = get_multiline_input()
636
+
637
+ if not error_log:
638
+ break
639
+
640
+ # Analyze and offer to fix
641
+ analyze_and_interact(error_log, deep=deep, language_hint=language_hint)
642
+
643
+ # Ask to continue
644
+ console.print()
645
+ if not Confirm.ask("[bold]Debug another error?[/bold]", default=True):
646
+ break
647
+ console.print()
648
+
649
+ console.print("\n[blue]Goodbye![/blue]")
650
+
651
+
652
+ @click.command()
653
+ @click.option("--serve", is_flag=True, help="Start the web API server")
654
+ @click.option("--port", default=8080, help="Port for API server")
655
+ @click.option("--version", "-v", is_flag=True, help="Show version")
656
+ @click.option("--no-apply", is_flag=True, help="Show fixes without applying")
657
+ @click.option("--deep", is_flag=True, help="Enable deep debugging (V2) with root cause analysis")
658
+ @click.option(
659
+ "--language", "-l",
660
+ type=click.Choice(list(LANGUAGE_CHOICES.keys()), case_sensitive=False),
661
+ help="Language hint for the error (python, javascript, typescript, go, rust, java)"
662
+ )
663
+ @click.argument("error_input", required=False)
664
+ def cli(serve, port, version, no_apply, deep, language, error_input):
665
+ """ROMA Debug - AI-powered code debugger with auto-fix.
666
+
667
+ Just run 'roma' to start interactive mode and paste your errors.
668
+
669
+ Examples:
670
+
671
+ roma # Interactive mode
672
+
673
+ roma error.log # Analyze a file directly
674
+
675
+ roma --deep error.log # Deep debugging with root cause analysis
676
+
677
+ roma --language go error.log # Specify language hint
678
+
679
+ roma --serve # Start web API server
680
+
681
+ roma --no-apply error.log # Show fix without applying
682
+ """
683
+ if version:
684
+ console.print(f"roma-debug {__version__}")
685
+ return
686
+
687
+ if serve:
688
+ import uvicorn
689
+ console.print(f"[green]Starting ROMA Debug API on http://127.0.0.1:{port}[/green]")
690
+ console.print(f"[dim]API docs at http://127.0.0.1:{port}/docs[/dim]")
691
+ uvicorn.run("roma_debug.server:app", host="127.0.0.1", port=port)
692
+ return
693
+
694
+ if error_input:
695
+ # Direct file/string mode
696
+ if os.path.isfile(error_input):
697
+ with open(error_input, 'r') as f:
698
+ error_log = f.read()
699
+ else:
700
+ error_log = error_input
701
+
702
+ print_welcome()
703
+
704
+ if deep:
705
+ console.print("[bold cyan]Deep Debugging Mode[/bold cyan]")
706
+ console.print()
707
+
708
+ if no_apply:
709
+ # Just show fix without interactive apply
710
+ if deep:
711
+ # V2: Use ContextBuilder for deep analysis
712
+ try:
713
+ from roma_debug.tracing.context_builder import ContextBuilder
714
+
715
+ lang_hint = LANGUAGE_CHOICES.get(language.lower()) if language else None
716
+ builder = ContextBuilder(project_root=os.getcwd())
717
+ analysis_ctx = builder.build_analysis_context(
718
+ error_log,
719
+ language_hint=lang_hint,
720
+ )
721
+ context = builder.get_context_for_prompt(analysis_ctx)
722
+ except Exception as e:
723
+ console.print(f"[yellow]Deep analysis failed, using basic: {e}[/yellow]")
724
+ context, _ = get_file_context(error_log)
725
+
726
+ result = analyze_error_v2(error_log, context)
727
+ if isinstance(result, FixResultV2):
728
+ display_fix_result_v2(result)
729
+ else:
730
+ display_fix_result(result)
731
+ else:
732
+ context, _ = get_file_context(error_log)
733
+ result = analyze_error(error_log, context)
734
+ if result.filepath is None:
735
+ display_general_advice(result)
736
+ else:
737
+ display_fix_result(result)
738
+
739
+ console.print("\n[bold]Suggested Code:[/bold]")
740
+ console.print(Panel(
741
+ Syntax(result.full_code_block, "python", theme="monokai", line_numbers=True),
742
+ border_style="green",
743
+ ))
744
+ else:
745
+ analyze_and_interact(error_log, deep=deep, language_hint=language)
746
+ return
747
+
748
+ # Default: interactive mode
749
+ interactive_mode(deep=deep, language_hint=language)
750
+
751
+
752
+ if __name__ == "__main__":
753
+ cli()