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,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