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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Approval Dialog Formatter for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module provides terminal UI formatting for approval requests from
|
|
5
|
+
EnhancedApprovalManager, converting them into rich, readable terminal output
|
|
6
|
+
with proper formatting for action context, impact assessment, and risk scores.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from rich.console import Console, Group
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.progress import BarColumn, Progress, TextColumn
|
|
17
|
+
from rich.rule import Rule
|
|
18
|
+
from rich.syntax import Syntax
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
from ..core.agent.approval_manager import (
|
|
23
|
+
BatchApprovalRequest,
|
|
24
|
+
ChangePreview,
|
|
25
|
+
ChangeType,
|
|
26
|
+
)
|
|
27
|
+
from ..core.security.approval_workflow import (
|
|
28
|
+
ApprovalRequest,
|
|
29
|
+
RiskLevel,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CLIApprovalFormatter:
|
|
36
|
+
"""
|
|
37
|
+
Terminal UI formatter for approval requests.
|
|
38
|
+
|
|
39
|
+
Converts ApprovalRequest and ChangePreview objects into rich,
|
|
40
|
+
readable terminal output with color coding, visual indicators,
|
|
41
|
+
and structured data display.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, console: Optional[Console] = None):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the approval formatter.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
console: Rich Console instance (creates new if None)
|
|
50
|
+
"""
|
|
51
|
+
self.console = console or Console()
|
|
52
|
+
|
|
53
|
+
# Risk level color mapping
|
|
54
|
+
self.risk_colors = {
|
|
55
|
+
RiskLevel.LOW: "green",
|
|
56
|
+
RiskLevel.MEDIUM: "yellow",
|
|
57
|
+
RiskLevel.HIGH: "red",
|
|
58
|
+
RiskLevel.CRITICAL: "bold red",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Change type icons
|
|
62
|
+
self.change_type_icons = {
|
|
63
|
+
ChangeType.FILE_CREATE: "📄",
|
|
64
|
+
ChangeType.FILE_MODIFY: "✏️ ",
|
|
65
|
+
ChangeType.FILE_DELETE: "🗑️ ",
|
|
66
|
+
ChangeType.DIRECTORY_CREATE: "📁",
|
|
67
|
+
ChangeType.DIRECTORY_DELETE: "📂",
|
|
68
|
+
ChangeType.COMMAND_EXECUTE: "⚡",
|
|
69
|
+
ChangeType.WEB_REQUEST: "🌐",
|
|
70
|
+
ChangeType.MCP_TOOL_CALL: "🔧",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Risk level icons
|
|
74
|
+
self.risk_icons = {
|
|
75
|
+
RiskLevel.LOW: "✅",
|
|
76
|
+
RiskLevel.MEDIUM: "⚠️ ",
|
|
77
|
+
RiskLevel.HIGH: "⛔",
|
|
78
|
+
RiskLevel.CRITICAL: "🚨",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def format_approval_dialog(
|
|
82
|
+
self,
|
|
83
|
+
approval_request: ApprovalRequest,
|
|
84
|
+
preview: Optional[ChangePreview] = None,
|
|
85
|
+
operation_data: Optional[Dict[str, Any]] = None,
|
|
86
|
+
) -> Group:
|
|
87
|
+
"""
|
|
88
|
+
Format a complete approval dialog for terminal display.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
approval_request: The approval request from workflow
|
|
92
|
+
preview: Optional change preview with diff information
|
|
93
|
+
operation_data: Additional operation data for context
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Rich Group with formatted approval dialog components
|
|
97
|
+
"""
|
|
98
|
+
components = []
|
|
99
|
+
|
|
100
|
+
# Header with operation title and risk level
|
|
101
|
+
components.append(self._format_header(approval_request, preview))
|
|
102
|
+
|
|
103
|
+
# Separator
|
|
104
|
+
components.append(Rule(style="dim"))
|
|
105
|
+
|
|
106
|
+
# Operation details
|
|
107
|
+
components.append(
|
|
108
|
+
self._format_operation_details(approval_request, operation_data)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Change preview and diff if available
|
|
112
|
+
if preview:
|
|
113
|
+
components.append(self._format_change_preview(preview))
|
|
114
|
+
|
|
115
|
+
# Show diff if available
|
|
116
|
+
if preview.diff:
|
|
117
|
+
components.append(self._format_diff_display(preview))
|
|
118
|
+
|
|
119
|
+
# Risk assessment
|
|
120
|
+
components.append(self._format_risk_assessment(approval_request))
|
|
121
|
+
|
|
122
|
+
# Impact assessment
|
|
123
|
+
if preview and preview.metadata:
|
|
124
|
+
components.append(self._format_impact_assessment(preview))
|
|
125
|
+
|
|
126
|
+
# Metadata information
|
|
127
|
+
if approval_request.metadata:
|
|
128
|
+
components.append(self._format_metadata(approval_request.metadata))
|
|
129
|
+
|
|
130
|
+
# Time information
|
|
131
|
+
components.append(self._format_time_info(approval_request))
|
|
132
|
+
|
|
133
|
+
# Separator before options
|
|
134
|
+
components.append(Rule(style="dim"))
|
|
135
|
+
|
|
136
|
+
# Available options
|
|
137
|
+
components.append(self._format_approval_options())
|
|
138
|
+
|
|
139
|
+
return Group(*components)
|
|
140
|
+
|
|
141
|
+
def format_batch_approval_dialog(
|
|
142
|
+
self, batch_request: BatchApprovalRequest
|
|
143
|
+
) -> Group:
|
|
144
|
+
"""
|
|
145
|
+
Format a batch approval dialog for multiple operations.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
batch_request: BatchApprovalRequest containing multiple operations
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Rich Group with formatted batch approval dialog
|
|
152
|
+
"""
|
|
153
|
+
components = []
|
|
154
|
+
|
|
155
|
+
# Batch header
|
|
156
|
+
components.append(self._format_batch_header(batch_request))
|
|
157
|
+
components.append(Rule(style="dim"))
|
|
158
|
+
|
|
159
|
+
# Summary table
|
|
160
|
+
components.append(self._format_batch_summary(batch_request))
|
|
161
|
+
|
|
162
|
+
# Individual operations
|
|
163
|
+
components.append(Text("Operations:", style="bold"))
|
|
164
|
+
for i, (operation, preview) in enumerate(
|
|
165
|
+
zip(batch_request.operations, batch_request.previews)
|
|
166
|
+
):
|
|
167
|
+
# Create a mini approval request for consistent formatting
|
|
168
|
+
mini_request = ApprovalRequest(
|
|
169
|
+
operation_type=operation.type.value,
|
|
170
|
+
description=operation.description,
|
|
171
|
+
metadata=operation.data,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
operation_panel = Panel(
|
|
175
|
+
self._format_single_batch_operation(mini_request, preview, i + 1),
|
|
176
|
+
title=f"Operation {i + 1}",
|
|
177
|
+
border_style="dim",
|
|
178
|
+
)
|
|
179
|
+
components.append(operation_panel)
|
|
180
|
+
|
|
181
|
+
# Batch approval options
|
|
182
|
+
components.append(Rule(style="dim"))
|
|
183
|
+
components.append(
|
|
184
|
+
self._format_batch_approval_options(len(batch_request.operations))
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return Group(*components)
|
|
188
|
+
|
|
189
|
+
def _format_header(
|
|
190
|
+
self,
|
|
191
|
+
approval_request: ApprovalRequest,
|
|
192
|
+
preview: Optional[ChangePreview],
|
|
193
|
+
) -> Panel:
|
|
194
|
+
"""Format the approval dialog header."""
|
|
195
|
+
# Get icon for change type
|
|
196
|
+
icon = ""
|
|
197
|
+
if preview:
|
|
198
|
+
icon = self.change_type_icons.get(preview.change_type, "❓")
|
|
199
|
+
|
|
200
|
+
# Risk level with color and icon
|
|
201
|
+
risk_icon = self.risk_icons.get(approval_request.risk_level, "❓")
|
|
202
|
+
risk_color = self.risk_colors.get(approval_request.risk_level, "white")
|
|
203
|
+
|
|
204
|
+
title = f"{icon} {approval_request.operation_type.replace('_', ' ').title()}"
|
|
205
|
+
risk_text = Text(
|
|
206
|
+
f"{risk_icon} Risk Level: {approval_request.risk_level.value.upper()}",
|
|
207
|
+
style=risk_color,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
header_content = Group(
|
|
211
|
+
Text(title, style="bold white"),
|
|
212
|
+
Text(approval_request.description, style="dim white"),
|
|
213
|
+
Text(),
|
|
214
|
+
risk_text,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return Panel(
|
|
218
|
+
header_content,
|
|
219
|
+
title="🔐 Approval Required",
|
|
220
|
+
title_align="left",
|
|
221
|
+
border_style=risk_color,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def _format_operation_details(
|
|
225
|
+
self,
|
|
226
|
+
approval_request: ApprovalRequest,
|
|
227
|
+
operation_data: Optional[Dict[str, Any]],
|
|
228
|
+
) -> Panel:
|
|
229
|
+
"""Format operation details table."""
|
|
230
|
+
table = Table(show_header=False, box=None, pad_edge=False)
|
|
231
|
+
table.add_column("Field", style="cyan", min_width=15)
|
|
232
|
+
table.add_column("Value", style="white")
|
|
233
|
+
|
|
234
|
+
# Basic operation info
|
|
235
|
+
table.add_row("Operation Type", approval_request.operation_type)
|
|
236
|
+
table.add_row("Requested By", approval_request.requested_by)
|
|
237
|
+
table.add_row(
|
|
238
|
+
"Request Time", approval_request.requested_at.strftime("%H:%M:%S")
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Operation-specific details from metadata or operation data
|
|
242
|
+
data_source = operation_data or approval_request.metadata
|
|
243
|
+
if data_source:
|
|
244
|
+
# Common operation details
|
|
245
|
+
if "path" in data_source:
|
|
246
|
+
table.add_row("Target Path", str(data_source["path"]))
|
|
247
|
+
|
|
248
|
+
if "command" in data_source:
|
|
249
|
+
table.add_row("Command", str(data_source["command"]))
|
|
250
|
+
|
|
251
|
+
if "url" in data_source:
|
|
252
|
+
table.add_row("URL", str(data_source["url"]))
|
|
253
|
+
|
|
254
|
+
if "content_length" in data_source:
|
|
255
|
+
size_kb = int(data_source["content_length"]) / 1024
|
|
256
|
+
table.add_row("Content Size", f"{size_kb:.1f} KB")
|
|
257
|
+
|
|
258
|
+
return Panel(
|
|
259
|
+
table,
|
|
260
|
+
title="Operation Details",
|
|
261
|
+
title_align="left",
|
|
262
|
+
border_style="blue",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _format_change_preview(self, preview: ChangePreview) -> Panel:
|
|
266
|
+
"""Format change preview information."""
|
|
267
|
+
preview_content = []
|
|
268
|
+
|
|
269
|
+
# Change description
|
|
270
|
+
preview_content.append(Text(preview.description, style="white"))
|
|
271
|
+
preview_content.append(Text())
|
|
272
|
+
|
|
273
|
+
# Key attributes
|
|
274
|
+
attrs_table = Table(show_header=False, box=None, pad_edge=False)
|
|
275
|
+
attrs_table.add_column("Attribute", style="cyan", min_width=12)
|
|
276
|
+
attrs_table.add_column("Value", style="white")
|
|
277
|
+
|
|
278
|
+
attrs_table.add_row("Reversible", "✅ Yes" if preview.reversible else "❌ No")
|
|
279
|
+
|
|
280
|
+
if preview.risk_assessment:
|
|
281
|
+
attrs_table.add_row("Risk", preview.risk_assessment)
|
|
282
|
+
|
|
283
|
+
# File-specific information
|
|
284
|
+
if preview.change_type in [
|
|
285
|
+
ChangeType.FILE_CREATE,
|
|
286
|
+
ChangeType.FILE_MODIFY,
|
|
287
|
+
]:
|
|
288
|
+
if preview.proposed_state:
|
|
289
|
+
lines = len(preview.proposed_state.splitlines())
|
|
290
|
+
attrs_table.add_row("New Lines", str(lines))
|
|
291
|
+
|
|
292
|
+
if preview.change_type == ChangeType.FILE_DELETE:
|
|
293
|
+
if preview.current_state:
|
|
294
|
+
lines = len(preview.current_state.splitlines())
|
|
295
|
+
attrs_table.add_row("Lines Lost", str(lines))
|
|
296
|
+
|
|
297
|
+
# Directory-specific information
|
|
298
|
+
if preview.change_type == ChangeType.DIRECTORY_DELETE:
|
|
299
|
+
item_count = preview.metadata.get("item_count", 0)
|
|
300
|
+
attrs_table.add_row("Items Inside", str(item_count))
|
|
301
|
+
|
|
302
|
+
preview_content.append(attrs_table)
|
|
303
|
+
|
|
304
|
+
return Panel(
|
|
305
|
+
Group(*preview_content),
|
|
306
|
+
title=f"{self.change_type_icons.get(preview.change_type, '❓')} Change Preview",
|
|
307
|
+
title_align="left",
|
|
308
|
+
border_style="yellow",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _format_diff_display(self, preview: ChangePreview) -> Panel:
|
|
312
|
+
"""Format diff visualization with syntax highlighting."""
|
|
313
|
+
if not preview.diff:
|
|
314
|
+
return Panel(
|
|
315
|
+
Text("No diff available", style="dim"),
|
|
316
|
+
title="📋 Changes",
|
|
317
|
+
border_style="dim",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Try to detect file type for syntax highlighting
|
|
321
|
+
if "path" in preview.metadata:
|
|
322
|
+
path = Path(preview.metadata["path"])
|
|
323
|
+
path.suffix.lstrip(".")
|
|
324
|
+
|
|
325
|
+
# Limit diff length to prevent terminal overflow
|
|
326
|
+
diff_text = preview.diff
|
|
327
|
+
if len(diff_text) > 2000:
|
|
328
|
+
lines = diff_text.splitlines()
|
|
329
|
+
if len(lines) > 50:
|
|
330
|
+
truncated_lines = (
|
|
331
|
+
lines[:25]
|
|
332
|
+
+ ["...", "# Diff truncated - too long to display", "..."]
|
|
333
|
+
+ lines[-25:]
|
|
334
|
+
)
|
|
335
|
+
diff_text = "\n".join(truncated_lines)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
# Use syntax highlighting for diff
|
|
339
|
+
syntax = Syntax(
|
|
340
|
+
diff_text,
|
|
341
|
+
"diff",
|
|
342
|
+
theme="monokai",
|
|
343
|
+
line_numbers=False,
|
|
344
|
+
word_wrap=False,
|
|
345
|
+
)
|
|
346
|
+
except Exception:
|
|
347
|
+
# Fallback to plain text if syntax highlighting fails
|
|
348
|
+
syntax = Text(diff_text, style="white")
|
|
349
|
+
|
|
350
|
+
return Panel(
|
|
351
|
+
syntax,
|
|
352
|
+
title="📋 Changes (Diff)",
|
|
353
|
+
title_align="left",
|
|
354
|
+
border_style="magenta",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def _format_risk_assessment(self, approval_request: ApprovalRequest) -> Panel:
|
|
358
|
+
"""Format risk assessment with visual indicators."""
|
|
359
|
+
risk_level = approval_request.risk_level
|
|
360
|
+
risk_color = self.risk_colors[risk_level]
|
|
361
|
+
risk_icon = self.risk_icons[risk_level]
|
|
362
|
+
|
|
363
|
+
# Risk level bar
|
|
364
|
+
risk_value = {
|
|
365
|
+
RiskLevel.LOW: 25,
|
|
366
|
+
RiskLevel.MEDIUM: 50,
|
|
367
|
+
RiskLevel.HIGH: 75,
|
|
368
|
+
RiskLevel.CRITICAL: 100,
|
|
369
|
+
}[risk_level]
|
|
370
|
+
|
|
371
|
+
# Create visual risk meter
|
|
372
|
+
progress = Progress(
|
|
373
|
+
TextColumn("[bold blue]Risk Level"),
|
|
374
|
+
BarColumn(bar_width=20),
|
|
375
|
+
TextColumn(f"{risk_icon} {risk_level.value.upper()}"),
|
|
376
|
+
expand=False,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
progress.add_task("risk", total=100, completed=risk_value)
|
|
380
|
+
|
|
381
|
+
# Risk description
|
|
382
|
+
risk_descriptions = {
|
|
383
|
+
RiskLevel.LOW: "Low risk operation. Minimal security impact expected.",
|
|
384
|
+
RiskLevel.MEDIUM: "Medium risk operation. Review recommended before approval.",
|
|
385
|
+
RiskLevel.HIGH: "High risk operation. Careful consideration required.",
|
|
386
|
+
RiskLevel.CRITICAL: "Critical risk operation. High security impact possible.",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
description = Text(risk_descriptions[risk_level], style=risk_color)
|
|
390
|
+
|
|
391
|
+
risk_content = Group(progress, Text(), description)
|
|
392
|
+
|
|
393
|
+
return Panel(
|
|
394
|
+
risk_content,
|
|
395
|
+
title=f"{risk_icon} Risk Assessment",
|
|
396
|
+
title_align="left",
|
|
397
|
+
border_style=risk_color,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def _format_impact_assessment(self, preview: ChangePreview) -> Panel:
|
|
401
|
+
"""Format impact assessment from preview metadata."""
|
|
402
|
+
impact_items = []
|
|
403
|
+
|
|
404
|
+
# File system impact
|
|
405
|
+
if preview.change_type in [
|
|
406
|
+
ChangeType.FILE_CREATE,
|
|
407
|
+
ChangeType.FILE_MODIFY,
|
|
408
|
+
ChangeType.FILE_DELETE,
|
|
409
|
+
]:
|
|
410
|
+
if "path" in preview.metadata:
|
|
411
|
+
path = preview.metadata["path"]
|
|
412
|
+
impact_items.append(f"• File system: {path}")
|
|
413
|
+
|
|
414
|
+
if preview.change_type in [
|
|
415
|
+
ChangeType.DIRECTORY_CREATE,
|
|
416
|
+
ChangeType.DIRECTORY_DELETE,
|
|
417
|
+
]:
|
|
418
|
+
if "path" in preview.metadata:
|
|
419
|
+
path = preview.metadata["path"]
|
|
420
|
+
item_count = preview.metadata.get("item_count", 0)
|
|
421
|
+
impact_items.append(f"• Directory: {path} ({item_count} items)")
|
|
422
|
+
|
|
423
|
+
# Command execution impact
|
|
424
|
+
if preview.change_type == ChangeType.COMMAND_EXECUTE:
|
|
425
|
+
if "command" in preview.metadata:
|
|
426
|
+
command = preview.metadata["command"]
|
|
427
|
+
impact_items.append(f"• System command: {command}")
|
|
428
|
+
|
|
429
|
+
# Network impact
|
|
430
|
+
if preview.change_type == ChangeType.WEB_REQUEST:
|
|
431
|
+
if "url" in preview.metadata:
|
|
432
|
+
url = preview.metadata["url"]
|
|
433
|
+
method = preview.metadata.get("method", "GET")
|
|
434
|
+
impact_items.append(f"• Network {method}: {url}")
|
|
435
|
+
|
|
436
|
+
# Reversibility impact
|
|
437
|
+
reversible_text = "✅ Reversible" if preview.reversible else "❌ Not reversible"
|
|
438
|
+
impact_items.append(f"• Recovery: {reversible_text}")
|
|
439
|
+
|
|
440
|
+
if not impact_items:
|
|
441
|
+
impact_items.append("• No specific impacts identified")
|
|
442
|
+
|
|
443
|
+
impact_text = Text("\n".join(impact_items), style="white")
|
|
444
|
+
|
|
445
|
+
return Panel(
|
|
446
|
+
impact_text,
|
|
447
|
+
title="🎯 Impact Assessment",
|
|
448
|
+
title_align="left",
|
|
449
|
+
border_style="cyan",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def _format_metadata(self, metadata: Dict[str, Any]) -> Panel:
|
|
453
|
+
"""Format metadata information in a readable way."""
|
|
454
|
+
# Filter out large or sensitive metadata
|
|
455
|
+
filtered_metadata = {}
|
|
456
|
+
for key, value in metadata.items():
|
|
457
|
+
if key in ["preview", "operation_data"]:
|
|
458
|
+
continue # Skip large nested objects
|
|
459
|
+
|
|
460
|
+
# Limit string length
|
|
461
|
+
if isinstance(value, str) and len(value) > 100:
|
|
462
|
+
filtered_metadata[key] = value[:100] + "..."
|
|
463
|
+
else:
|
|
464
|
+
filtered_metadata[key] = value
|
|
465
|
+
|
|
466
|
+
if not filtered_metadata:
|
|
467
|
+
return Panel(
|
|
468
|
+
Text("No additional metadata", style="dim"),
|
|
469
|
+
title="📊 Additional Information",
|
|
470
|
+
border_style="dim",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Pretty print metadata as JSON
|
|
475
|
+
metadata_json = json.dumps(filtered_metadata, indent=2, default=str)
|
|
476
|
+
syntax = Syntax(metadata_json, "json", theme="monokai", line_numbers=False)
|
|
477
|
+
except Exception:
|
|
478
|
+
# Fallback to simple text display
|
|
479
|
+
lines = [f"{k}: {v}" for k, v in filtered_metadata.items()]
|
|
480
|
+
syntax = Text("\n".join(lines), style="white")
|
|
481
|
+
|
|
482
|
+
return Panel(
|
|
483
|
+
syntax,
|
|
484
|
+
title="📊 Additional Information",
|
|
485
|
+
title_align="left",
|
|
486
|
+
border_style="dim",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def _format_time_info(self, approval_request: ApprovalRequest) -> Panel:
|
|
490
|
+
"""Format timing information."""
|
|
491
|
+
time_content = []
|
|
492
|
+
|
|
493
|
+
# Request time
|
|
494
|
+
request_time = approval_request.requested_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
495
|
+
time_content.append(Text(f"Requested: {request_time}", style="white"))
|
|
496
|
+
|
|
497
|
+
# Expiration time
|
|
498
|
+
if approval_request.expires_at:
|
|
499
|
+
expires_time = approval_request.expires_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
500
|
+
time_remaining = approval_request.time_remaining()
|
|
501
|
+
|
|
502
|
+
if time_remaining and time_remaining.total_seconds() > 0:
|
|
503
|
+
minutes_left = int(time_remaining.total_seconds() / 60)
|
|
504
|
+
time_content.append(
|
|
505
|
+
Text(
|
|
506
|
+
f"Expires: {expires_time} ({minutes_left}m remaining)",
|
|
507
|
+
style="yellow",
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
else:
|
|
511
|
+
time_content.append(
|
|
512
|
+
Text(f"Expires: {expires_time} (EXPIRED)", style="red")
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
return Panel(
|
|
516
|
+
Group(*time_content),
|
|
517
|
+
title="⏰ Timing",
|
|
518
|
+
title_align="left",
|
|
519
|
+
border_style="blue",
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def _format_approval_options(self) -> Panel:
|
|
523
|
+
"""Format available approval options."""
|
|
524
|
+
options_text = [
|
|
525
|
+
Text("Available Options:", style="bold white"),
|
|
526
|
+
Text(),
|
|
527
|
+
Text(
|
|
528
|
+
"• [bold green]y[/bold green] or [bold green]yes[/bold green] - Approve this operation",
|
|
529
|
+
style="green",
|
|
530
|
+
),
|
|
531
|
+
Text(
|
|
532
|
+
"• [bold blue]r[/bold blue] or [bold blue]remember[/bold blue] - Approve and remember for similar operations",
|
|
533
|
+
style="blue",
|
|
534
|
+
),
|
|
535
|
+
Text(
|
|
536
|
+
"• [bold red]n[/bold red] or [bold red]no[/bold red] - Deny this operation",
|
|
537
|
+
style="red",
|
|
538
|
+
),
|
|
539
|
+
Text(
|
|
540
|
+
"• [bold yellow]q[/bold yellow] or [bold yellow]quit[/bold yellow] - Cancel and exit",
|
|
541
|
+
style="yellow",
|
|
542
|
+
),
|
|
543
|
+
Text(),
|
|
544
|
+
Text("Press Ctrl+C to cancel at any time.", style="dim"),
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
return Panel(
|
|
548
|
+
Group(*options_text),
|
|
549
|
+
title="⌨️ Options",
|
|
550
|
+
title_align="left",
|
|
551
|
+
border_style="white",
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def _format_batch_header(self, batch_request: BatchApprovalRequest) -> Panel:
|
|
555
|
+
"""Format batch approval header."""
|
|
556
|
+
summary = batch_request.get_approval_summary()
|
|
557
|
+
|
|
558
|
+
header_content = Group(
|
|
559
|
+
Text("Batch Operation Approval", style="bold white"),
|
|
560
|
+
Text(
|
|
561
|
+
f"Multiple operations require approval ({summary['total_operations']} operations)",
|
|
562
|
+
style="dim white",
|
|
563
|
+
),
|
|
564
|
+
Text(),
|
|
565
|
+
Text(f"🔢 Operations: {summary['total_operations']}", style="blue"),
|
|
566
|
+
Text(
|
|
567
|
+
f"⏰ Created: {batch_request.created_at.strftime('%H:%M:%S')}",
|
|
568
|
+
style="white",
|
|
569
|
+
),
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return Panel(
|
|
573
|
+
header_content,
|
|
574
|
+
title="🔐 Batch Approval Required",
|
|
575
|
+
title_align="left",
|
|
576
|
+
border_style="blue",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def _format_batch_summary(self, batch_request: BatchApprovalRequest) -> Panel:
|
|
580
|
+
"""Format batch operations summary table."""
|
|
581
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
582
|
+
table.add_column("Op", width=3)
|
|
583
|
+
table.add_column("Type", width=15)
|
|
584
|
+
table.add_column("Description", width=40)
|
|
585
|
+
table.add_column("Risk", width=8)
|
|
586
|
+
|
|
587
|
+
for i, (operation, preview) in enumerate(
|
|
588
|
+
zip(batch_request.operations, batch_request.previews)
|
|
589
|
+
):
|
|
590
|
+
# Determine risk level from preview or use medium as default
|
|
591
|
+
risk_level = RiskLevel.MEDIUM
|
|
592
|
+
if preview.risk_assessment:
|
|
593
|
+
if "high" in preview.risk_assessment.lower():
|
|
594
|
+
risk_level = RiskLevel.HIGH
|
|
595
|
+
elif "low" in preview.risk_assessment.lower():
|
|
596
|
+
risk_level = RiskLevel.LOW
|
|
597
|
+
elif "critical" in preview.risk_assessment.lower():
|
|
598
|
+
risk_level = RiskLevel.CRITICAL
|
|
599
|
+
|
|
600
|
+
risk_color = self.risk_colors[risk_level]
|
|
601
|
+
risk_icon = self.risk_icons[risk_level]
|
|
602
|
+
|
|
603
|
+
# Truncate description if too long
|
|
604
|
+
desc = operation.description
|
|
605
|
+
if len(desc) > 37:
|
|
606
|
+
desc = desc[:37] + "..."
|
|
607
|
+
|
|
608
|
+
table.add_row(
|
|
609
|
+
str(i + 1),
|
|
610
|
+
operation.type.value.replace("_", " ").title(),
|
|
611
|
+
desc,
|
|
612
|
+
Text(f"{risk_icon} {risk_level.value}", style=risk_color),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
return Panel(
|
|
616
|
+
table,
|
|
617
|
+
title="📋 Operations Summary",
|
|
618
|
+
title_align="left",
|
|
619
|
+
border_style="cyan",
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
def _format_single_batch_operation(
|
|
623
|
+
self,
|
|
624
|
+
approval_request: ApprovalRequest,
|
|
625
|
+
preview: ChangePreview,
|
|
626
|
+
operation_number: int,
|
|
627
|
+
) -> Group:
|
|
628
|
+
"""Format a single operation within a batch."""
|
|
629
|
+
components = []
|
|
630
|
+
|
|
631
|
+
# Operation header
|
|
632
|
+
icon = self.change_type_icons.get(preview.change_type, "❓")
|
|
633
|
+
components.append(
|
|
634
|
+
Text(f"{icon} {approval_request.description}", style="bold white")
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Key details in compact format
|
|
638
|
+
details_text = []
|
|
639
|
+
if "path" in approval_request.metadata:
|
|
640
|
+
details_text.append(f"Path: {approval_request.metadata['path']}")
|
|
641
|
+
|
|
642
|
+
if preview.risk_assessment:
|
|
643
|
+
details_text.append(f"Risk: {preview.risk_assessment}")
|
|
644
|
+
|
|
645
|
+
if details_text:
|
|
646
|
+
components.append(Text(" | ".join(details_text), style="dim white"))
|
|
647
|
+
|
|
648
|
+
return Group(*components)
|
|
649
|
+
|
|
650
|
+
def _format_batch_approval_options(self, operation_count: int) -> Panel:
|
|
651
|
+
"""Format batch approval options."""
|
|
652
|
+
options_text = [
|
|
653
|
+
Text("Batch Approval Options:", style="bold white"),
|
|
654
|
+
Text(),
|
|
655
|
+
Text(
|
|
656
|
+
"• [bold green]all[/bold green] - Approve all operations",
|
|
657
|
+
style="green",
|
|
658
|
+
),
|
|
659
|
+
Text(
|
|
660
|
+
"• [bold red]none[/bold red] - Deny all operations",
|
|
661
|
+
style="red",
|
|
662
|
+
),
|
|
663
|
+
Text(
|
|
664
|
+
f"• [bold blue]select[/bold blue] - Choose specific operations (1-{operation_count})",
|
|
665
|
+
style="blue",
|
|
666
|
+
),
|
|
667
|
+
Text(
|
|
668
|
+
"• [bold yellow]individual[/bold yellow] - Review each operation separately",
|
|
669
|
+
style="yellow",
|
|
670
|
+
),
|
|
671
|
+
Text(
|
|
672
|
+
"• [bold yellow]q[/bold yellow] or [bold yellow]quit[/bold yellow] - Cancel batch",
|
|
673
|
+
style="yellow",
|
|
674
|
+
),
|
|
675
|
+
Text(),
|
|
676
|
+
Text("Press Ctrl+C to cancel at any time.", style="dim"),
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
return Panel(
|
|
680
|
+
Group(*options_text),
|
|
681
|
+
title="⌨️ Batch Options",
|
|
682
|
+
title_align="left",
|
|
683
|
+
border_style="white",
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
def format_approval_denied(
|
|
687
|
+
self, approval_request: ApprovalRequest, reason: str = ""
|
|
688
|
+
) -> Panel:
|
|
689
|
+
"""Format approval denied message."""
|
|
690
|
+
content = [
|
|
691
|
+
Text("❌ Operation Denied", style="bold red"),
|
|
692
|
+
Text(),
|
|
693
|
+
Text(f"Operation: {approval_request.description}", style="white"),
|
|
694
|
+
Text(f"Type: {approval_request.operation_type}", style="dim"),
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
if reason:
|
|
698
|
+
content.extend([Text(), Text(f"Reason: {reason}", style="yellow")])
|
|
699
|
+
|
|
700
|
+
return Panel(
|
|
701
|
+
Group(*content),
|
|
702
|
+
title="🚫 Approval Denied",
|
|
703
|
+
title_align="left",
|
|
704
|
+
border_style="red",
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
def format_approval_approved(
|
|
708
|
+
self, approval_request: ApprovalRequest, remember: bool = False
|
|
709
|
+
) -> Panel:
|
|
710
|
+
"""Format approval approved message."""
|
|
711
|
+
content = [
|
|
712
|
+
Text("✅ Operation Approved", style="bold green"),
|
|
713
|
+
Text(),
|
|
714
|
+
Text(f"Operation: {approval_request.description}", style="white"),
|
|
715
|
+
Text(f"Type: {approval_request.operation_type}", style="dim"),
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
if remember:
|
|
719
|
+
content.extend(
|
|
720
|
+
[
|
|
721
|
+
Text(),
|
|
722
|
+
Text(
|
|
723
|
+
"🧠 Similar operations will be auto-approved in the future",
|
|
724
|
+
style="blue",
|
|
725
|
+
),
|
|
726
|
+
]
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
return Panel(
|
|
730
|
+
Group(*content),
|
|
731
|
+
title="✅ Approval Granted",
|
|
732
|
+
title_align="left",
|
|
733
|
+
border_style="green",
|
|
734
|
+
)
|