omnimancer-cli 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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,628 @@
1
+ """
2
+ User Interface for Approval System.
3
+
4
+ This module provides interactive user interfaces for handling approval requests,
5
+ including CLI prompts, formatted displays, and batch approval workflows.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from ..security.approval_workflow import ApprovalRequest, RiskLevel
12
+ from .approval_manager import (
13
+ BatchApprovalRequest,
14
+ ChangePreview,
15
+ EnhancedApprovalManager,
16
+ )
17
+ from .types import Operation, OperationType
18
+
19
+
20
+ class ApprovalChoice(Enum):
21
+ """User approval choices."""
22
+
23
+ APPROVE = "approve"
24
+ DENY = "deny"
25
+ SKIP = "skip"
26
+ APPROVE_ALL = "approve_all"
27
+ DENY_ALL = "deny_all"
28
+ VIEW_DETAILS = "details"
29
+ QUIT = "quit"
30
+
31
+
32
+ class ApprovalInterface:
33
+ """
34
+ Interactive user interface for handling approval requests.
35
+
36
+ Provides CLI-based approval interface with formatted previews,
37
+ diff visualization, and batch approval capabilities.
38
+ """
39
+
40
+ def __init__(self, approval_manager: EnhancedApprovalManager):
41
+ """
42
+ Initialize approval interface.
43
+
44
+ Args:
45
+ approval_manager: Enhanced approval manager instance
46
+ """
47
+ self.approval_manager = approval_manager
48
+ self.approval_manager.set_approval_callback(self.handle_single_approval)
49
+ self.approval_manager.set_batch_approval_callback(self.handle_batch_approval)
50
+
51
+ # Interface configuration
52
+ self.show_colors = True
53
+ self.auto_show_diff = True
54
+ self.max_diff_lines = 50
55
+ self.page_size = 10
56
+
57
+ async def handle_single_approval(self, approval_data: Dict[str, Any]) -> bool:
58
+ """
59
+ Handle approval request for a single operation.
60
+
61
+ Args:
62
+ approval_data: Dictionary containing operation, preview, and approval request
63
+
64
+ Returns:
65
+ True if approved, False if denied
66
+ """
67
+ operation = approval_data["operation"]
68
+ preview = approval_data["preview"]
69
+ approval_request = approval_data["approval_request"]
70
+ risk_level = approval_data["risk_level"]
71
+
72
+ # Display operation summary
73
+ self._print_header(
74
+ f"Approval Request - {operation.type.value.replace('_', ' ').title()}"
75
+ )
76
+
77
+ # Show operation details
78
+ self._print_operation_info(operation, risk_level)
79
+
80
+ # Show preview
81
+ self._print_preview(preview)
82
+
83
+ # Get user decision
84
+ while True:
85
+ choice = await self._get_user_choice(
86
+ [
87
+ ApprovalChoice.APPROVE,
88
+ ApprovalChoice.DENY,
89
+ ApprovalChoice.VIEW_DETAILS,
90
+ ApprovalChoice.QUIT,
91
+ ]
92
+ )
93
+
94
+ if choice == ApprovalChoice.APPROVE:
95
+ self._print_success("✓ Operation approved")
96
+ return True
97
+ elif choice == ApprovalChoice.DENY:
98
+ self._print_warning("✗ Operation denied")
99
+ return False
100
+ elif choice == ApprovalChoice.VIEW_DETAILS:
101
+ await self._show_detailed_view(operation, preview, approval_request)
102
+ elif choice == ApprovalChoice.QUIT:
103
+ self._print_info("Approval cancelled by user")
104
+ return False
105
+
106
+ async def handle_batch_approval(
107
+ self, batch_request: BatchApprovalRequest
108
+ ) -> Dict[str, Any]:
109
+ """
110
+ Handle approval request for a batch of operations.
111
+
112
+ Args:
113
+ batch_request: Batch approval request
114
+
115
+ Returns:
116
+ Dictionary with approval decisions
117
+ """
118
+ self._print_header(
119
+ f"Batch Approval Request - {len(batch_request.operations)} Operations"
120
+ )
121
+
122
+ # Show batch summary
123
+ self._print_batch_summary(batch_request)
124
+
125
+ # Handle different batch approval modes
126
+ approval_mode = await self._get_batch_approval_mode()
127
+
128
+ if approval_mode == "all_at_once":
129
+ return await self._handle_bulk_approval(batch_request)
130
+ elif approval_mode == "individual":
131
+ return await self._handle_individual_approval(batch_request)
132
+ elif approval_mode == "filtered":
133
+ return await self._handle_filtered_approval(batch_request)
134
+ else:
135
+ return {
136
+ "deny_all": True,
137
+ "reason": "User cancelled batch approval",
138
+ }
139
+
140
+ async def _handle_bulk_approval(
141
+ self, batch_request: BatchApprovalRequest
142
+ ) -> Dict[str, Any]:
143
+ """Handle bulk approval/denial of all operations."""
144
+ self._print_info("Choose action for all operations:")
145
+
146
+ choice = await self._get_user_choice(
147
+ [
148
+ ApprovalChoice.APPROVE_ALL,
149
+ ApprovalChoice.DENY_ALL,
150
+ ApprovalChoice.QUIT,
151
+ ]
152
+ )
153
+
154
+ if choice == ApprovalChoice.APPROVE_ALL:
155
+ self._print_success(
156
+ f"✓ All {len(batch_request.operations)} operations approved"
157
+ )
158
+ return {"approve_all": True}
159
+ elif choice == ApprovalChoice.DENY_ALL:
160
+ reason = input("Reason for denial (optional): ").strip()
161
+ self._print_warning(
162
+ f"✗ All {len(batch_request.operations)} operations denied"
163
+ )
164
+ return {
165
+ "deny_all": True,
166
+ "reason": reason or "User denied all operations",
167
+ }
168
+ else:
169
+ return {"deny_all": True, "reason": "User cancelled"}
170
+
171
+ async def _handle_individual_approval(
172
+ self, batch_request: BatchApprovalRequest
173
+ ) -> Dict[str, Any]:
174
+ """Handle individual approval of each operation in batch."""
175
+ approved_indices = []
176
+
177
+ for i, (operation, preview) in enumerate(
178
+ zip(batch_request.operations, batch_request.previews)
179
+ ):
180
+ self._print_separator()
181
+ self._print_info(f"Operation {i + 1} of {len(batch_request.operations)}")
182
+
183
+ # Show operation details
184
+ self._print_operation_info(operation)
185
+ self._print_preview(preview, compact=True)
186
+
187
+ # Get decision for this operation
188
+ while True:
189
+ choice = await self._get_user_choice(
190
+ [
191
+ ApprovalChoice.APPROVE,
192
+ ApprovalChoice.DENY,
193
+ ApprovalChoice.SKIP,
194
+ ApprovalChoice.VIEW_DETAILS,
195
+ ApprovalChoice.APPROVE_ALL,
196
+ ApprovalChoice.DENY_ALL,
197
+ ApprovalChoice.QUIT,
198
+ ]
199
+ )
200
+
201
+ if choice == ApprovalChoice.APPROVE:
202
+ approved_indices.append(i)
203
+ self._print_success("✓ Approved")
204
+ break
205
+ elif choice == ApprovalChoice.DENY:
206
+ self._print_warning("✗ Denied")
207
+ break
208
+ elif choice == ApprovalChoice.SKIP:
209
+ self._print_info("→ Skipped")
210
+ break
211
+ elif choice == ApprovalChoice.VIEW_DETAILS:
212
+ await self._show_detailed_view(operation, preview)
213
+ elif choice == ApprovalChoice.APPROVE_ALL:
214
+ # Approve remaining operations
215
+ approved_indices.extend(range(i, len(batch_request.operations)))
216
+ self._print_success(f"✓ Approved all remaining operations")
217
+ return {"approved_indices": approved_indices}
218
+ elif choice == ApprovalChoice.DENY_ALL:
219
+ self._print_warning("✗ Denied all remaining operations")
220
+ return {"approved_indices": approved_indices}
221
+ elif choice == ApprovalChoice.QUIT:
222
+ return {"approved_indices": approved_indices}
223
+
224
+ return {"approved_indices": approved_indices}
225
+
226
+ async def _handle_filtered_approval(
227
+ self, batch_request: BatchApprovalRequest
228
+ ) -> Dict[str, Any]:
229
+ """Handle filtered approval based on operation types or risk levels."""
230
+ self._print_info("Filter options:")
231
+ print("1. Approve all low-risk operations")
232
+ print("2. Approve all read-only operations")
233
+ print("3. Approve by operation type")
234
+ print("4. Custom selection")
235
+ print("5. Cancel")
236
+
237
+ choice = input("Choose filter option (1-5): ").strip()
238
+ approved_indices = []
239
+
240
+ if choice == "1":
241
+ # Approve low-risk operations
242
+ for i, preview in enumerate(batch_request.previews):
243
+ if preview.risk_assessment and "low" in preview.risk_assessment.lower():
244
+ approved_indices.append(i)
245
+ self._print_success(
246
+ f"✓ Approved {len(approved_indices)} low-risk operations"
247
+ )
248
+
249
+ elif choice == "2":
250
+ # Approve read-only operations
251
+ for i, operation in enumerate(batch_request.operations):
252
+ if operation.type in [OperationType.FILE_READ]:
253
+ approved_indices.append(i)
254
+ self._print_success(
255
+ f"✓ Approved {len(approved_indices)} read-only operations"
256
+ )
257
+
258
+ elif choice == "3":
259
+ # Approve by operation type
260
+ operation_types = list(set(op.type for op in batch_request.operations))
261
+ print("Available operation types:")
262
+ for idx, op_type in enumerate(operation_types, 1):
263
+ print(f"{idx}. {op_type.value}")
264
+
265
+ type_choice = input(
266
+ "Select operation types to approve (comma-separated numbers): "
267
+ ).strip()
268
+ try:
269
+ selected_indices = [int(x.strip()) - 1 for x in type_choice.split(",")]
270
+ selected_types = [
271
+ operation_types[i]
272
+ for i in selected_indices
273
+ if 0 <= i < len(operation_types)
274
+ ]
275
+
276
+ for i, operation in enumerate(batch_request.operations):
277
+ if operation.type in selected_types:
278
+ approved_indices.append(i)
279
+
280
+ self._print_success(
281
+ f"✓ Approved {len(approved_indices)} operations of selected types"
282
+ )
283
+ except (ValueError, IndexError):
284
+ self._print_error("Invalid selection")
285
+ return {"deny_all": True, "reason": "Invalid filter selection"}
286
+
287
+ elif choice == "4":
288
+ # Custom selection
289
+ print("Enter operation indices to approve (comma-separated, 1-based):")
290
+ indices_input = input(
291
+ f"Operations 1-{len(batch_request.operations)}: "
292
+ ).strip()
293
+ try:
294
+ approved_indices = [
295
+ int(x.strip()) - 1 for x in indices_input.split(",")
296
+ ]
297
+ approved_indices = [
298
+ i
299
+ for i in approved_indices
300
+ if 0 <= i < len(batch_request.operations)
301
+ ]
302
+ self._print_success(
303
+ f"✓ Approved {len(approved_indices)} selected operations"
304
+ )
305
+ except ValueError:
306
+ self._print_error("Invalid indices")
307
+ return {"deny_all": True, "reason": "Invalid custom selection"}
308
+
309
+ else:
310
+ return {"deny_all": True, "reason": "User cancelled"}
311
+
312
+ return {"approved_indices": approved_indices}
313
+
314
+ async def _get_batch_approval_mode(self) -> str:
315
+ """Get user preference for batch approval mode."""
316
+ self._print_info("Batch approval modes:")
317
+ print("1. All at once (approve/deny all)")
318
+ print("2. Individual review")
319
+ print("3. Filtered approval")
320
+ print("0. Cancel")
321
+
322
+ while True:
323
+ choice = input("Select mode (0-3): ").strip()
324
+ if choice == "1":
325
+ return "all_at_once"
326
+ elif choice == "2":
327
+ return "individual"
328
+ elif choice == "3":
329
+ return "filtered"
330
+ elif choice == "0":
331
+ return "cancel"
332
+ else:
333
+ self._print_error("Invalid choice. Please select 0-3.")
334
+
335
+ async def _get_user_choice(
336
+ self, available_choices: List[ApprovalChoice]
337
+ ) -> ApprovalChoice:
338
+ """Get user choice from available options."""
339
+ choice_map = {
340
+ "y": ApprovalChoice.APPROVE,
341
+ "yes": ApprovalChoice.APPROVE,
342
+ "a": ApprovalChoice.APPROVE,
343
+ "approve": ApprovalChoice.APPROVE,
344
+ "n": ApprovalChoice.DENY,
345
+ "no": ApprovalChoice.DENY,
346
+ "d": ApprovalChoice.DENY,
347
+ "deny": ApprovalChoice.DENY,
348
+ "s": ApprovalChoice.SKIP,
349
+ "skip": ApprovalChoice.SKIP,
350
+ "aa": ApprovalChoice.APPROVE_ALL,
351
+ "all": ApprovalChoice.APPROVE_ALL,
352
+ "approve_all": ApprovalChoice.APPROVE_ALL,
353
+ "dd": ApprovalChoice.DENY_ALL,
354
+ "deny_all": ApprovalChoice.DENY_ALL,
355
+ "v": ApprovalChoice.VIEW_DETAILS,
356
+ "details": ApprovalChoice.VIEW_DETAILS,
357
+ "view": ApprovalChoice.VIEW_DETAILS,
358
+ "q": ApprovalChoice.QUIT,
359
+ "quit": ApprovalChoice.QUIT,
360
+ "exit": ApprovalChoice.QUIT,
361
+ }
362
+
363
+ # Build prompt with available choices
364
+ prompt_parts = []
365
+ for choice in available_choices:
366
+ if choice == ApprovalChoice.APPROVE:
367
+ prompt_parts.append("[Y]es/[A]pprove")
368
+ elif choice == ApprovalChoice.DENY:
369
+ prompt_parts.append("[N]o/[D]eny")
370
+ elif choice == ApprovalChoice.SKIP:
371
+ prompt_parts.append("[S]kip")
372
+ elif choice == ApprovalChoice.APPROVE_ALL:
373
+ prompt_parts.append("Approve [A]ll")
374
+ elif choice == ApprovalChoice.DENY_ALL:
375
+ prompt_parts.append("[D]eny All")
376
+ elif choice == ApprovalChoice.VIEW_DETAILS:
377
+ prompt_parts.append("[V]iew Details")
378
+ elif choice == ApprovalChoice.QUIT:
379
+ prompt_parts.append("[Q]uit")
380
+
381
+ prompt = f"Choose: {'/'.join(prompt_parts)}: "
382
+
383
+ while True:
384
+ user_input = input(prompt).strip().lower()
385
+
386
+ if user_input in choice_map:
387
+ choice = choice_map[user_input]
388
+ if choice in available_choices:
389
+ return choice
390
+ else:
391
+ self._print_error(
392
+ f"'{user_input}' is not available in this context"
393
+ )
394
+ else:
395
+ self._print_error(f"Invalid choice: '{user_input}'")
396
+
397
+ async def _show_detailed_view(
398
+ self,
399
+ operation: Operation,
400
+ preview: ChangePreview,
401
+ approval_request: Optional[ApprovalRequest] = None,
402
+ ):
403
+ """Show detailed view of operation and preview."""
404
+ self._print_header("Detailed View")
405
+
406
+ # Operation details
407
+ print("OPERATION DETAILS:")
408
+ print(f" Type: {operation.type.value}")
409
+ print(f" Description: {operation.description}")
410
+ print(f" Requires Approval: {operation.requires_approval}")
411
+ print(f" Reversible: {operation.reversible}")
412
+
413
+ if operation.data:
414
+ print(" Data:")
415
+ for key, value in operation.data.items():
416
+ print(f" {key}: {value}")
417
+
418
+ print()
419
+
420
+ # Preview details
421
+ print("PREVIEW DETAILS:")
422
+ print(f" Change Type: {preview.change_type.value}")
423
+ print(f" Risk Assessment: {preview.risk_assessment}")
424
+ print(f" Reversible: {preview.reversible}")
425
+
426
+ if preview.metadata:
427
+ print(" Metadata:")
428
+ for key, value in preview.metadata.items():
429
+ print(f" {key}: {value}")
430
+
431
+ print()
432
+
433
+ # Show diff if available
434
+ if preview.diff and self.auto_show_diff:
435
+ print("CHANGES:")
436
+ diff_lines = preview.diff.split("\n")
437
+ if len(diff_lines) > self.max_diff_lines:
438
+ print(f"Showing first {self.max_diff_lines} lines of diff:")
439
+ for line in diff_lines[: self.max_diff_lines]:
440
+ self._print_diff_line(line)
441
+ print(f"... ({len(diff_lines) - self.max_diff_lines} more lines)")
442
+ else:
443
+ for line in diff_lines:
444
+ self._print_diff_line(line)
445
+
446
+ # Approval request details
447
+ if approval_request:
448
+ print()
449
+ print("APPROVAL REQUEST:")
450
+ print(f" Request ID: {approval_request.id}")
451
+ print(f" Risk Level: {approval_request.risk_level.value}")
452
+ print(f" Requested At: {approval_request.requested_at}")
453
+ if approval_request.expires_at:
454
+ remaining = approval_request.time_remaining()
455
+ print(f" Time Remaining: {remaining}")
456
+
457
+ input("\\nPress Enter to continue...")
458
+
459
+ def _print_operation_info(
460
+ self, operation: Operation, risk_level: Optional[RiskLevel] = None
461
+ ):
462
+ """Print formatted operation information."""
463
+ print(f"Operation: {operation.type.value.replace('_', ' ').title()}")
464
+ print(f"Description: {operation.description}")
465
+
466
+ if risk_level:
467
+ risk_color = self._get_risk_color(risk_level)
468
+ print(f"Risk Level: {self._colorize(risk_level.value.upper(), risk_color)}")
469
+
470
+ print(f"Reversible: {'Yes' if operation.reversible else 'No'}")
471
+ print()
472
+
473
+ def _print_preview(self, preview: ChangePreview, compact: bool = False):
474
+ """Print formatted preview information."""
475
+ if compact:
476
+ print(f"Preview: {preview.description}")
477
+ if preview.risk_assessment:
478
+ print(f"Risk: {preview.risk_assessment}")
479
+ else:
480
+ print("PREVIEW:")
481
+ print(f" {preview.description}")
482
+ if preview.risk_assessment:
483
+ print(f" Risk: {preview.risk_assessment}")
484
+
485
+ if preview.diff and len(preview.diff.split("\n")) <= 10:
486
+ print(" Changes:")
487
+ for line in preview.diff.split("\n")[:5]: # Show first 5 lines
488
+ print(f" {line}")
489
+ if len(preview.diff.split("\n")) > 5:
490
+ print(" ... (use 'details' to see full diff)")
491
+ print()
492
+
493
+ def _print_batch_summary(self, batch_request: BatchApprovalRequest):
494
+ """Print summary of batch request."""
495
+ print(f"Total Operations: {len(batch_request.operations)}")
496
+ print(f"Created: {batch_request.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
497
+
498
+ if batch_request.expires_at:
499
+ print(f"Expires: {batch_request.expires_at.strftime('%Y-%m-%d %H:%M:%S')}")
500
+
501
+ # Operation type breakdown
502
+ type_counts = {}
503
+ for operation in batch_request.operations:
504
+ type_counts[operation.type] = type_counts.get(operation.type, 0) + 1
505
+
506
+ print("\\nOperation Types:")
507
+ for op_type, count in type_counts.items():
508
+ print(f" {op_type.value}: {count}")
509
+
510
+ # Risk level breakdown
511
+ risk_counts = {}
512
+ for preview in batch_request.previews:
513
+ if preview.risk_assessment:
514
+ # Extract risk level from assessment
515
+ if "low" in preview.risk_assessment.lower():
516
+ risk = "low"
517
+ elif "medium" in preview.risk_assessment.lower():
518
+ risk = "medium"
519
+ elif "high" in preview.risk_assessment.lower():
520
+ risk = "high"
521
+ elif "critical" in preview.risk_assessment.lower():
522
+ risk = "critical"
523
+ else:
524
+ risk = "unknown"
525
+ risk_counts[risk] = risk_counts.get(risk, 0) + 1
526
+
527
+ if risk_counts:
528
+ print("\\nRisk Distribution:")
529
+ for risk, count in risk_counts.items():
530
+ color = self._get_risk_color_by_name(risk)
531
+ print(f" {self._colorize(risk.title(), color)}: {count}")
532
+
533
+ print()
534
+
535
+ def _print_diff_line(self, line: str):
536
+ """Print a diff line with appropriate coloring."""
537
+ if not self.show_colors:
538
+ print(line)
539
+ return
540
+
541
+ if line.startswith("+"):
542
+ print(self._colorize(line, "green"))
543
+ elif line.startswith("-"):
544
+ print(self._colorize(line, "red"))
545
+ elif line.startswith("@@"):
546
+ print(self._colorize(line, "cyan"))
547
+ else:
548
+ print(line)
549
+
550
+ def _get_risk_color(self, risk_level: RiskLevel) -> str:
551
+ """Get color for risk level."""
552
+ colors = {
553
+ RiskLevel.LOW: "green",
554
+ RiskLevel.MEDIUM: "yellow",
555
+ RiskLevel.HIGH: "red",
556
+ RiskLevel.CRITICAL: "magenta",
557
+ }
558
+ return colors.get(risk_level, "white")
559
+
560
+ def _get_risk_color_by_name(self, risk_name: str) -> str:
561
+ """Get color for risk level by name."""
562
+ colors = {
563
+ "low": "green",
564
+ "medium": "yellow",
565
+ "high": "red",
566
+ "critical": "magenta",
567
+ }
568
+ return colors.get(risk_name.lower(), "white")
569
+
570
+ def _colorize(self, text: str, color: str) -> str:
571
+ """Add color to text if colors are enabled."""
572
+ if not self.show_colors:
573
+ return text
574
+
575
+ colors = {
576
+ "red": "\033[91m",
577
+ "green": "\033[92m",
578
+ "yellow": "\033[93m",
579
+ "blue": "\033[94m",
580
+ "magenta": "\033[95m",
581
+ "cyan": "\033[96m",
582
+ "white": "\033[97m",
583
+ "reset": "\033[0m",
584
+ }
585
+
586
+ color_code = colors.get(color.lower(), colors["white"])
587
+ reset_code = colors["reset"]
588
+ return f"{color_code}{text}{reset_code}"
589
+
590
+ def _print_header(self, title: str):
591
+ """Print formatted header."""
592
+ print()
593
+ print("=" * 60)
594
+ print(f" {title}")
595
+ print("=" * 60)
596
+ print()
597
+
598
+ def _print_separator(self):
599
+ """Print separator line."""
600
+ print("-" * 40)
601
+
602
+ def _print_success(self, message: str):
603
+ """Print success message."""
604
+ print(self._colorize(message, "green"))
605
+
606
+ def _print_warning(self, message: str):
607
+ """Print warning message."""
608
+ print(self._colorize(message, "yellow"))
609
+
610
+ def _print_error(self, message: str):
611
+ """Print error message."""
612
+ print(self._colorize(message, "red"))
613
+
614
+ def _print_info(self, message: str):
615
+ """Print info message."""
616
+ print(self._colorize(message, "cyan"))
617
+
618
+ def set_colors_enabled(self, enabled: bool):
619
+ """Enable or disable colored output."""
620
+ self.show_colors = enabled
621
+
622
+ def set_auto_show_diff(self, enabled: bool):
623
+ """Enable or disable automatic diff display."""
624
+ self.auto_show_diff = enabled
625
+
626
+ def set_max_diff_lines(self, max_lines: int):
627
+ """Set maximum number of diff lines to display."""
628
+ self.max_diff_lines = max_lines