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,757 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command models and types for the Omnimancer CLI.
|
|
3
|
+
|
|
4
|
+
This module defines the command structure, types, and parsing logic
|
|
5
|
+
for handling user input in the command-line interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from .dynamic_commands import CommandRegistry, DynamicCommand
|
|
13
|
+
|
|
14
|
+
# Global command registry instance
|
|
15
|
+
_command_registry: Optional[CommandRegistry] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_command_registry() -> CommandRegistry:
|
|
19
|
+
"""Get or create the global command registry."""
|
|
20
|
+
global _command_registry
|
|
21
|
+
if _command_registry is None:
|
|
22
|
+
_command_registry = CommandRegistry()
|
|
23
|
+
# Set built-in commands to prevent overriding
|
|
24
|
+
_command_registry.set_builtin_commands([cmd.value for cmd in SlashCommand])
|
|
25
|
+
# Load dynamic commands from default directory
|
|
26
|
+
_command_registry.load_commands_from_directory()
|
|
27
|
+
return _command_registry
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CommandType(Enum):
|
|
31
|
+
"""Types of commands that can be processed."""
|
|
32
|
+
|
|
33
|
+
CHAT_MESSAGE = "chat"
|
|
34
|
+
SLASH_COMMAND = "slash"
|
|
35
|
+
SYSTEM_COMMAND = "system"
|
|
36
|
+
DYNAMIC_COMMAND = "dynamic"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SlashCommand(Enum):
|
|
40
|
+
"""Available slash commands."""
|
|
41
|
+
|
|
42
|
+
HELP = "/help"
|
|
43
|
+
MODELS = "/models"
|
|
44
|
+
MODEL = "/model" # Alias for /models
|
|
45
|
+
SWITCH = "/switch"
|
|
46
|
+
CLEAR = "/clear"
|
|
47
|
+
SAVE = "/save"
|
|
48
|
+
LOAD = "/load"
|
|
49
|
+
QUIT = "/quit"
|
|
50
|
+
STATUS = "/status"
|
|
51
|
+
LIST = "/list"
|
|
52
|
+
PROVIDERS = "/providers"
|
|
53
|
+
TOOLS = "/tools"
|
|
54
|
+
MCP = "/mcp"
|
|
55
|
+
SETUP = "/setup"
|
|
56
|
+
VALIDATE = "/validate"
|
|
57
|
+
HEALTH = "/health"
|
|
58
|
+
REPAIR = "/repair"
|
|
59
|
+
DIAGNOSE = "/diagnose"
|
|
60
|
+
HISTORY = "/history"
|
|
61
|
+
ADD_MODEL = "/add-model"
|
|
62
|
+
REMOVE_MODEL = "/remove-model"
|
|
63
|
+
LIST_CUSTOM_MODELS = "/list-custom-models"
|
|
64
|
+
AGENT = "/agent"
|
|
65
|
+
AGENTS = "/agents"
|
|
66
|
+
AGENTS_CUSTOM = "/agents-custom"
|
|
67
|
+
AGENTSTATUS = "/agentstatus"
|
|
68
|
+
APPROVALS = "/approvals"
|
|
69
|
+
PERMISSIONS = "/permissions"
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_string(cls, command_str: str) -> Optional["SlashCommand"]:
|
|
73
|
+
"""Get SlashCommand from string, case-insensitive."""
|
|
74
|
+
command_str = command_str.lower().strip()
|
|
75
|
+
for cmd in cls:
|
|
76
|
+
if cmd.value == command_str:
|
|
77
|
+
return cmd
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_all_commands(cls) -> List[str]:
|
|
82
|
+
"""Get list of all available slash commands."""
|
|
83
|
+
return [cmd.value for cmd in cls]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Command:
|
|
88
|
+
"""Represents a parsed command from user input."""
|
|
89
|
+
|
|
90
|
+
type: CommandType
|
|
91
|
+
content: str
|
|
92
|
+
parameters: Dict[str, Any]
|
|
93
|
+
raw_input: str
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def create_chat_message(cls, content: str) -> "Command":
|
|
97
|
+
"""Create a chat message command."""
|
|
98
|
+
return cls(
|
|
99
|
+
type=CommandType.CHAT_MESSAGE,
|
|
100
|
+
content=content.strip(),
|
|
101
|
+
parameters={},
|
|
102
|
+
raw_input=content,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def create_slash_command(
|
|
107
|
+
cls, command: SlashCommand, args: List[str], raw_input: str
|
|
108
|
+
) -> "Command":
|
|
109
|
+
"""Create a slash command."""
|
|
110
|
+
return cls(
|
|
111
|
+
type=CommandType.SLASH_COMMAND,
|
|
112
|
+
content=command.value,
|
|
113
|
+
parameters={"args": args, "command": command},
|
|
114
|
+
raw_input=raw_input,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def create_system_command(cls, content: str) -> "Command":
|
|
119
|
+
"""Create a system command."""
|
|
120
|
+
return cls(
|
|
121
|
+
type=CommandType.SYSTEM_COMMAND,
|
|
122
|
+
content=content,
|
|
123
|
+
parameters={},
|
|
124
|
+
raw_input=content,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def create_dynamic_command(
|
|
129
|
+
cls, command: DynamicCommand, args: List[str], raw_input: str
|
|
130
|
+
) -> "Command":
|
|
131
|
+
"""Create a dynamic command."""
|
|
132
|
+
return cls(
|
|
133
|
+
type=CommandType.DYNAMIC_COMMAND,
|
|
134
|
+
content=command.slash_name,
|
|
135
|
+
parameters={"args": args, "command": command},
|
|
136
|
+
raw_input=raw_input,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def is_chat_message(self) -> bool:
|
|
141
|
+
"""Check if this is a chat message."""
|
|
142
|
+
return self.type == CommandType.CHAT_MESSAGE
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def is_slash_command(self) -> bool:
|
|
146
|
+
"""Check if this is a slash command."""
|
|
147
|
+
return self.type == CommandType.SLASH_COMMAND
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def is_system_command(self) -> bool:
|
|
151
|
+
"""Check if this is a system command."""
|
|
152
|
+
return self.type == CommandType.SYSTEM_COMMAND
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_dynamic_command(self) -> bool:
|
|
156
|
+
"""Check if this is a dynamic command."""
|
|
157
|
+
return self.type == CommandType.DYNAMIC_COMMAND
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def slash_command(self) -> Optional[SlashCommand]:
|
|
161
|
+
"""Get the slash command if this is a slash command."""
|
|
162
|
+
if self.is_slash_command:
|
|
163
|
+
return self.parameters.get("command")
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def dynamic_command(self) -> Optional[DynamicCommand]:
|
|
168
|
+
"""Get the dynamic command if this is a dynamic command."""
|
|
169
|
+
if self.is_dynamic_command:
|
|
170
|
+
return self.parameters.get("command")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def args(self) -> List[str]:
|
|
175
|
+
"""Get command arguments."""
|
|
176
|
+
return self.parameters.get("args", [])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def parse_command(user_input: str) -> Command:
|
|
180
|
+
"""
|
|
181
|
+
Parse user input into a Command object with validation and sanitization.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
user_input: Raw input from the user
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Parsed Command object
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: If input contains invalid characters or is malformed
|
|
191
|
+
"""
|
|
192
|
+
if user_input is None:
|
|
193
|
+
return Command.create_chat_message("")
|
|
194
|
+
|
|
195
|
+
# Sanitize input - remove control characters except newlines and tabs
|
|
196
|
+
sanitized_input = sanitize_input(user_input)
|
|
197
|
+
|
|
198
|
+
if not sanitized_input.strip():
|
|
199
|
+
return Command.create_chat_message("")
|
|
200
|
+
|
|
201
|
+
# Check for slash commands
|
|
202
|
+
if sanitized_input.startswith("/"):
|
|
203
|
+
return _parse_slash_command(sanitized_input)
|
|
204
|
+
|
|
205
|
+
# Check for system commands (exit, quit, etc.)
|
|
206
|
+
if sanitized_input.lower().strip() in ["exit", "quit", "q"]:
|
|
207
|
+
return Command.create_system_command("quit")
|
|
208
|
+
|
|
209
|
+
# Validate chat message length
|
|
210
|
+
if len(sanitized_input) > MAX_MESSAGE_LENGTH:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Message too long. Maximum length is {MAX_MESSAGE_LENGTH} characters."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Default to chat message
|
|
216
|
+
return Command.create_chat_message(sanitized_input)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _parse_slash_command(user_input: str) -> Command:
|
|
220
|
+
"""
|
|
221
|
+
Parse a slash command with argument validation.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
user_input: Input starting with '/'
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Parsed Command object
|
|
228
|
+
"""
|
|
229
|
+
parts = user_input.split()
|
|
230
|
+
command_str = parts[0]
|
|
231
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
232
|
+
|
|
233
|
+
# Handle /exit as an alias for /quit
|
|
234
|
+
if command_str.lower() == "/exit":
|
|
235
|
+
command_str = "/quit"
|
|
236
|
+
|
|
237
|
+
# Check for built-in slash commands first
|
|
238
|
+
slash_cmd = SlashCommand.from_string(command_str)
|
|
239
|
+
if slash_cmd:
|
|
240
|
+
# Validate command-specific arguments
|
|
241
|
+
validated_args = _validate_command_args(slash_cmd, args)
|
|
242
|
+
return Command.create_slash_command(slash_cmd, validated_args, user_input)
|
|
243
|
+
|
|
244
|
+
# Check for dynamic commands
|
|
245
|
+
registry = get_command_registry()
|
|
246
|
+
dynamic_cmd = registry.get_command(command_str)
|
|
247
|
+
if dynamic_cmd:
|
|
248
|
+
# No validation for dynamic commands yet - let handler manage it
|
|
249
|
+
return Command.create_dynamic_command(dynamic_cmd, args, user_input)
|
|
250
|
+
|
|
251
|
+
# Unknown slash command, treat as chat message
|
|
252
|
+
return Command.create_chat_message(user_input)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _validate_command_args(command: SlashCommand, args: List[str]) -> List[str]:
|
|
256
|
+
"""
|
|
257
|
+
Validate arguments for specific slash commands.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
command: The slash command
|
|
261
|
+
args: List of arguments
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Validated arguments list
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ValueError: If arguments are invalid
|
|
268
|
+
"""
|
|
269
|
+
if command == SlashCommand.SWITCH:
|
|
270
|
+
if len(args) < 1:
|
|
271
|
+
raise ValueError(
|
|
272
|
+
"Switch command requires at least one argument: provider name"
|
|
273
|
+
)
|
|
274
|
+
if len(args) > 2:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
"Switch command accepts at most two arguments: provider and model"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Validate provider name (alphanumeric and underscores only)
|
|
280
|
+
provider = args[0]
|
|
281
|
+
if not provider.replace("_", "").isalnum():
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"Provider name must contain only letters, numbers, and underscores"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Validate model name if provided
|
|
287
|
+
if len(args) == 2:
|
|
288
|
+
model = args[1]
|
|
289
|
+
# Allow model IDs with common formats: org/model:variant, model-name, model.version, etc.
|
|
290
|
+
# Only restrict dangerous characters that could cause issues
|
|
291
|
+
invalid_model_chars = [
|
|
292
|
+
"\0",
|
|
293
|
+
"\n",
|
|
294
|
+
"\r",
|
|
295
|
+
"\t",
|
|
296
|
+
"\\",
|
|
297
|
+
'"',
|
|
298
|
+
"'",
|
|
299
|
+
"`",
|
|
300
|
+
"|",
|
|
301
|
+
"&",
|
|
302
|
+
";",
|
|
303
|
+
"(",
|
|
304
|
+
")",
|
|
305
|
+
"{",
|
|
306
|
+
"}",
|
|
307
|
+
"[",
|
|
308
|
+
"]",
|
|
309
|
+
"<",
|
|
310
|
+
">",
|
|
311
|
+
"$",
|
|
312
|
+
]
|
|
313
|
+
if any(char in model for char in invalid_model_chars):
|
|
314
|
+
raise ValueError("Model name contains invalid characters")
|
|
315
|
+
|
|
316
|
+
elif command == SlashCommand.SAVE:
|
|
317
|
+
if len(args) > 1:
|
|
318
|
+
raise ValueError("Save command accepts at most one argument: filename")
|
|
319
|
+
|
|
320
|
+
if args and args[0]:
|
|
321
|
+
filename = args[0]
|
|
322
|
+
# Basic filename validation
|
|
323
|
+
if any(
|
|
324
|
+
char in filename
|
|
325
|
+
for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]
|
|
326
|
+
):
|
|
327
|
+
raise ValueError("Filename contains invalid characters")
|
|
328
|
+
|
|
329
|
+
elif command == SlashCommand.LOAD:
|
|
330
|
+
if len(args) != 1:
|
|
331
|
+
raise ValueError("Load command requires exactly one argument: filename")
|
|
332
|
+
|
|
333
|
+
filename = args[0]
|
|
334
|
+
if any(
|
|
335
|
+
char in filename for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]
|
|
336
|
+
):
|
|
337
|
+
raise ValueError("Filename contains invalid characters")
|
|
338
|
+
|
|
339
|
+
elif command == SlashCommand.MCP:
|
|
340
|
+
if len(args) > 2:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
"MCP command accepts at most two arguments: action and server name"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if args:
|
|
346
|
+
action = args[0].lower()
|
|
347
|
+
valid_actions = [
|
|
348
|
+
"status",
|
|
349
|
+
"reload",
|
|
350
|
+
"connect",
|
|
351
|
+
"disconnect",
|
|
352
|
+
"health",
|
|
353
|
+
]
|
|
354
|
+
if action not in valid_actions:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
f"Invalid MCP action. Valid actions: {', '.join(valid_actions)}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
elif command in [
|
|
360
|
+
SlashCommand.HELP,
|
|
361
|
+
SlashCommand.CLEAR,
|
|
362
|
+
SlashCommand.QUIT,
|
|
363
|
+
SlashCommand.STATUS,
|
|
364
|
+
]:
|
|
365
|
+
if args:
|
|
366
|
+
raise ValueError(f"{command.value} command does not accept arguments")
|
|
367
|
+
|
|
368
|
+
elif command in [SlashCommand.MODELS, SlashCommand.MODEL]:
|
|
369
|
+
# /models [filter_type] [filter_value]
|
|
370
|
+
if len(args) > 2:
|
|
371
|
+
raise ValueError(
|
|
372
|
+
"Models command accepts at most two arguments: filter type and filter value"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if args:
|
|
376
|
+
filter_type = args[0].lower()
|
|
377
|
+
valid_filters = [
|
|
378
|
+
"provider",
|
|
379
|
+
"capability",
|
|
380
|
+
"price",
|
|
381
|
+
"performance",
|
|
382
|
+
"free",
|
|
383
|
+
"latest",
|
|
384
|
+
]
|
|
385
|
+
if filter_type not in valid_filters:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
f"Invalid filter type. Valid filters: {', '.join(valid_filters)}"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Validate second argument based on filter type
|
|
391
|
+
if len(args) == 2:
|
|
392
|
+
if filter_type == "capability":
|
|
393
|
+
valid_capabilities = ["tools", "multimodal"]
|
|
394
|
+
if args[1].lower() not in valid_capabilities:
|
|
395
|
+
raise ValueError(
|
|
396
|
+
f"Invalid capability. Valid capabilities: {', '.join(valid_capabilities)}"
|
|
397
|
+
)
|
|
398
|
+
elif filter_type == "price":
|
|
399
|
+
try:
|
|
400
|
+
float(args[1])
|
|
401
|
+
except ValueError:
|
|
402
|
+
raise ValueError("Price filter value must be a number")
|
|
403
|
+
elif filter_type == "performance":
|
|
404
|
+
try:
|
|
405
|
+
float(args[1])
|
|
406
|
+
except ValueError:
|
|
407
|
+
raise ValueError("Performance filter value must be a number")
|
|
408
|
+
elif filter_type in ["free", "latest"]:
|
|
409
|
+
raise ValueError(f"{filter_type} filter does not accept a value")
|
|
410
|
+
elif filter_type in [
|
|
411
|
+
"provider",
|
|
412
|
+
"capability",
|
|
413
|
+
"price",
|
|
414
|
+
"performance",
|
|
415
|
+
]:
|
|
416
|
+
raise ValueError(f"{filter_type} filter requires a value")
|
|
417
|
+
|
|
418
|
+
elif command == SlashCommand.VALIDATE:
|
|
419
|
+
# /validate [provider] [--fix]
|
|
420
|
+
if len(args) > 2:
|
|
421
|
+
raise ValueError(
|
|
422
|
+
"Validate command accepts at most two arguments: provider and --fix flag"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if args:
|
|
426
|
+
# First arg should be provider name or --fix
|
|
427
|
+
if (
|
|
428
|
+
args[0] not in ["--fix", "--auto-fix"]
|
|
429
|
+
and not args[0].replace("_", "").replace("-", "").isalnum()
|
|
430
|
+
):
|
|
431
|
+
raise ValueError("Invalid provider name format")
|
|
432
|
+
|
|
433
|
+
elif command == SlashCommand.HEALTH:
|
|
434
|
+
# /health [provider] [--monitor] [--interval]
|
|
435
|
+
if len(args) > 3:
|
|
436
|
+
raise ValueError("Health command accepts at most three arguments")
|
|
437
|
+
|
|
438
|
+
if args:
|
|
439
|
+
# Validate provider name if not a flag
|
|
440
|
+
if (
|
|
441
|
+
not args[0].startswith("--")
|
|
442
|
+
and not args[0].replace("_", "").replace("-", "").isalnum()
|
|
443
|
+
):
|
|
444
|
+
raise ValueError("Invalid provider name format")
|
|
445
|
+
|
|
446
|
+
elif command == SlashCommand.REPAIR:
|
|
447
|
+
# /repair [provider] [--auto] [--backup]
|
|
448
|
+
if len(args) > 3:
|
|
449
|
+
raise ValueError("Repair command accepts at most three arguments")
|
|
450
|
+
|
|
451
|
+
if args:
|
|
452
|
+
# Validate provider name if not a flag
|
|
453
|
+
if (
|
|
454
|
+
not args[0].startswith("--")
|
|
455
|
+
and not args[0].replace("_", "").replace("-", "").isalnum()
|
|
456
|
+
):
|
|
457
|
+
raise ValueError("Invalid provider name format")
|
|
458
|
+
|
|
459
|
+
elif command == SlashCommand.DIAGNOSE:
|
|
460
|
+
# /diagnose [provider] [--detailed]
|
|
461
|
+
if len(args) > 2:
|
|
462
|
+
raise ValueError(
|
|
463
|
+
"Diagnose command accepts at most two arguments: provider and --detailed flag"
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if args:
|
|
467
|
+
# Validate provider name if not a flag
|
|
468
|
+
if (
|
|
469
|
+
not args[0].startswith("--")
|
|
470
|
+
and not args[0].replace("_", "").replace("-", "").isalnum()
|
|
471
|
+
):
|
|
472
|
+
raise ValueError("Invalid provider name format")
|
|
473
|
+
|
|
474
|
+
elif command == SlashCommand.HISTORY:
|
|
475
|
+
# /history [recent|search|clear|export] [args...]
|
|
476
|
+
if len(args) > 3:
|
|
477
|
+
raise ValueError("History command accepts at most three arguments")
|
|
478
|
+
|
|
479
|
+
if args:
|
|
480
|
+
action = args[0].lower()
|
|
481
|
+
valid_actions = ["recent", "search", "clear", "export", "stats"]
|
|
482
|
+
if action not in valid_actions:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
f"Invalid history action. Valid actions: {', '.join(valid_actions)}"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
elif command == SlashCommand.ADD_MODEL:
|
|
488
|
+
# /add-model <name> <provider> [description] [other_params...]
|
|
489
|
+
if len(args) < 2:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
"Add-model command requires at least two arguments: model name and provider"
|
|
492
|
+
)
|
|
493
|
+
if len(args) > 10:
|
|
494
|
+
raise ValueError("Add-model command accepts at most 10 arguments total")
|
|
495
|
+
|
|
496
|
+
# Validate model name and provider
|
|
497
|
+
model_name = args[0]
|
|
498
|
+
provider = args[1]
|
|
499
|
+
|
|
500
|
+
# Allow model IDs with common formats: org/model:variant, model-name, model.version, etc.
|
|
501
|
+
# Only restrict dangerous characters that could cause issues
|
|
502
|
+
invalid_model_chars = [
|
|
503
|
+
"\0",
|
|
504
|
+
"\n",
|
|
505
|
+
"\r",
|
|
506
|
+
"\t",
|
|
507
|
+
"\\",
|
|
508
|
+
'"',
|
|
509
|
+
"'",
|
|
510
|
+
"`",
|
|
511
|
+
"|",
|
|
512
|
+
"&",
|
|
513
|
+
";",
|
|
514
|
+
"(",
|
|
515
|
+
")",
|
|
516
|
+
"{",
|
|
517
|
+
"}",
|
|
518
|
+
"[",
|
|
519
|
+
"]",
|
|
520
|
+
"<",
|
|
521
|
+
">",
|
|
522
|
+
"$",
|
|
523
|
+
]
|
|
524
|
+
if any(char in model_name for char in invalid_model_chars):
|
|
525
|
+
raise ValueError("Model name contains invalid characters")
|
|
526
|
+
|
|
527
|
+
# Validate provider name (more restrictive since it's used internally)
|
|
528
|
+
if not provider.replace("_", "").replace("-", "").isalnum():
|
|
529
|
+
raise ValueError("Provider name contains invalid characters")
|
|
530
|
+
|
|
531
|
+
elif command == SlashCommand.REMOVE_MODEL:
|
|
532
|
+
# /remove-model <name> <provider>
|
|
533
|
+
if len(args) != 2:
|
|
534
|
+
raise ValueError(
|
|
535
|
+
"Remove-model command requires exactly two arguments: model name and provider"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Validate model name and provider
|
|
539
|
+
model_name = args[0]
|
|
540
|
+
provider = args[1]
|
|
541
|
+
|
|
542
|
+
# Allow model IDs with common formats: org/model:variant, model-name, model.version, etc.
|
|
543
|
+
# Only restrict dangerous characters that could cause issues
|
|
544
|
+
invalid_model_chars = [
|
|
545
|
+
"\0",
|
|
546
|
+
"\n",
|
|
547
|
+
"\r",
|
|
548
|
+
"\t",
|
|
549
|
+
"\\",
|
|
550
|
+
'"',
|
|
551
|
+
"'",
|
|
552
|
+
"`",
|
|
553
|
+
"|",
|
|
554
|
+
"&",
|
|
555
|
+
";",
|
|
556
|
+
"(",
|
|
557
|
+
")",
|
|
558
|
+
"{",
|
|
559
|
+
"}",
|
|
560
|
+
"[",
|
|
561
|
+
"]",
|
|
562
|
+
"<",
|
|
563
|
+
">",
|
|
564
|
+
"$",
|
|
565
|
+
]
|
|
566
|
+
if any(char in model_name for char in invalid_model_chars):
|
|
567
|
+
raise ValueError("Model name contains invalid characters")
|
|
568
|
+
|
|
569
|
+
# Validate provider name (more restrictive since it's used internally)
|
|
570
|
+
if not provider.replace("_", "").replace("-", "").isalnum():
|
|
571
|
+
raise ValueError("Provider name contains invalid characters")
|
|
572
|
+
|
|
573
|
+
elif command == SlashCommand.LIST_CUSTOM_MODELS:
|
|
574
|
+
# /list-custom-models
|
|
575
|
+
if args:
|
|
576
|
+
raise ValueError("List-custom-models command does not accept arguments")
|
|
577
|
+
|
|
578
|
+
elif command == SlashCommand.AGENTS:
|
|
579
|
+
# /agents [action] [agent_name] [options]
|
|
580
|
+
if len(args) > 3:
|
|
581
|
+
raise ValueError(
|
|
582
|
+
"Agents command accepts at most three arguments: action, agent_name, and options"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if args:
|
|
586
|
+
action = args[0].lower()
|
|
587
|
+
valid_actions = [
|
|
588
|
+
"list",
|
|
589
|
+
"enable",
|
|
590
|
+
"disable",
|
|
591
|
+
"status",
|
|
592
|
+
"config",
|
|
593
|
+
"switch",
|
|
594
|
+
"current",
|
|
595
|
+
"info",
|
|
596
|
+
]
|
|
597
|
+
if action not in valid_actions:
|
|
598
|
+
raise ValueError(
|
|
599
|
+
f"Invalid agents action. Valid actions: {', '.join(valid_actions)}"
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Some actions require an agent name
|
|
603
|
+
if (
|
|
604
|
+
action in ["enable", "disable", "config", "switch", "info"]
|
|
605
|
+
and len(args) < 2
|
|
606
|
+
):
|
|
607
|
+
raise ValueError(f"Action '{action}' requires an agent name")
|
|
608
|
+
|
|
609
|
+
elif command == SlashCommand.APPROVALS:
|
|
610
|
+
# /approvals [action] [signature]
|
|
611
|
+
if len(args) > 2:
|
|
612
|
+
raise ValueError(
|
|
613
|
+
"Approvals command accepts at most two arguments: action and signature"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if args:
|
|
617
|
+
action = args[0].lower()
|
|
618
|
+
valid_actions = ["list", "revoke", "clear", "stats", "cleanup"]
|
|
619
|
+
if action not in valid_actions:
|
|
620
|
+
raise ValueError(
|
|
621
|
+
f"Invalid approvals action. Valid actions: {', '.join(valid_actions)}"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Revoke action requires a signature
|
|
625
|
+
if action == "revoke" and len(args) < 2:
|
|
626
|
+
raise ValueError("Revoke action requires a signature to revoke")
|
|
627
|
+
|
|
628
|
+
elif command == SlashCommand.PERMISSIONS:
|
|
629
|
+
# /permissions [action] [args...]
|
|
630
|
+
if len(args) > 4:
|
|
631
|
+
raise ValueError("Permissions command accepts at most four arguments")
|
|
632
|
+
|
|
633
|
+
if args:
|
|
634
|
+
action = args[0].lower()
|
|
635
|
+
valid_actions = [
|
|
636
|
+
"view",
|
|
637
|
+
"set-level",
|
|
638
|
+
"add-rule",
|
|
639
|
+
"remove-rule",
|
|
640
|
+
"audit",
|
|
641
|
+
"learn",
|
|
642
|
+
"clear-learned",
|
|
643
|
+
"export",
|
|
644
|
+
"import",
|
|
645
|
+
]
|
|
646
|
+
if action not in valid_actions:
|
|
647
|
+
raise ValueError(
|
|
648
|
+
f"Invalid permissions action. Valid actions: {', '.join(valid_actions)}"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Actions that require additional arguments
|
|
652
|
+
if action == "set-level" and len(args) < 2:
|
|
653
|
+
raise ValueError(
|
|
654
|
+
"Set-level action requires a security level (auto_approve, ask_always, ask_but_remember)"
|
|
655
|
+
)
|
|
656
|
+
elif action in ["add-rule", "remove-rule"] and len(args) < 2:
|
|
657
|
+
raise ValueError(
|
|
658
|
+
f"{action.capitalize()} action requires at least one additional argument"
|
|
659
|
+
)
|
|
660
|
+
elif action in ["export", "import"] and len(args) < 2:
|
|
661
|
+
raise ValueError(f"{action.capitalize()} action requires a file path")
|
|
662
|
+
|
|
663
|
+
# Validate security levels for set-level
|
|
664
|
+
if action == "set-level" and len(args) >= 2:
|
|
665
|
+
level = args[1].lower()
|
|
666
|
+
valid_levels = [
|
|
667
|
+
"auto_approve",
|
|
668
|
+
"ask_always",
|
|
669
|
+
"ask_but_remember",
|
|
670
|
+
]
|
|
671
|
+
if level not in valid_levels:
|
|
672
|
+
raise ValueError(
|
|
673
|
+
f"Invalid security level. Valid levels: {', '.join(valid_levels)}"
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# Commands that don't accept arguments
|
|
677
|
+
elif command in [
|
|
678
|
+
SlashCommand.CLEAR,
|
|
679
|
+
SlashCommand.QUIT,
|
|
680
|
+
SlashCommand.STATUS,
|
|
681
|
+
SlashCommand.PROVIDERS,
|
|
682
|
+
SlashCommand.TOOLS,
|
|
683
|
+
SlashCommand.SETUP,
|
|
684
|
+
]:
|
|
685
|
+
if args:
|
|
686
|
+
raise ValueError(f"Command {command.value} does not accept arguments")
|
|
687
|
+
|
|
688
|
+
return args
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def sanitize_input(user_input: str) -> str:
|
|
692
|
+
"""
|
|
693
|
+
Sanitize user input by removing dangerous characters.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
user_input: Raw user input
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Sanitized input string
|
|
700
|
+
"""
|
|
701
|
+
if not user_input:
|
|
702
|
+
return ""
|
|
703
|
+
|
|
704
|
+
# Remove null bytes and other control characters except newlines and tabs
|
|
705
|
+
sanitized = "".join(
|
|
706
|
+
char for char in user_input if ord(char) >= 32 or char in ["\n", "\t"]
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
# Limit length to prevent memory issues
|
|
710
|
+
if len(sanitized) > MAX_INPUT_LENGTH:
|
|
711
|
+
sanitized = sanitized[:MAX_INPUT_LENGTH]
|
|
712
|
+
|
|
713
|
+
return sanitized
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def validate_command_syntax(command: Command) -> bool:
|
|
717
|
+
"""
|
|
718
|
+
Validate that a command has proper syntax and structure.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
command: Command to validate
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
True if command is valid
|
|
725
|
+
|
|
726
|
+
Raises:
|
|
727
|
+
ValueError: If command is invalid
|
|
728
|
+
"""
|
|
729
|
+
if not isinstance(command, Command):
|
|
730
|
+
raise ValueError("Invalid command object")
|
|
731
|
+
|
|
732
|
+
if not isinstance(command.type, CommandType):
|
|
733
|
+
raise ValueError("Invalid command type")
|
|
734
|
+
|
|
735
|
+
if command.content is None:
|
|
736
|
+
raise ValueError("Command content cannot be None")
|
|
737
|
+
|
|
738
|
+
if not isinstance(command.parameters, dict):
|
|
739
|
+
raise ValueError("Command parameters must be a dictionary")
|
|
740
|
+
|
|
741
|
+
# Validate slash command structure
|
|
742
|
+
if command.is_slash_command:
|
|
743
|
+
if not command.content.startswith("/"):
|
|
744
|
+
raise ValueError("Slash command must start with '/'")
|
|
745
|
+
|
|
746
|
+
if "command" not in command.parameters:
|
|
747
|
+
raise ValueError("Slash command missing command parameter")
|
|
748
|
+
|
|
749
|
+
if not isinstance(command.parameters["command"], SlashCommand):
|
|
750
|
+
raise ValueError("Invalid slash command type")
|
|
751
|
+
|
|
752
|
+
return True
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
# Constants for input validation
|
|
756
|
+
MAX_MESSAGE_LENGTH = 10000 # Maximum length for chat messages
|
|
757
|
+
MAX_INPUT_LENGTH = 50000 # Maximum length for any input
|