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,772 @@
1
+ """
2
+ Configuration migration system for Omnimancer.
3
+
4
+ This module handles automatic migration from old configuration formats
5
+ to new ones, including backup and rollback functionality.
6
+ """
7
+
8
+ import json
9
+ import shutil
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Tuple
13
+
14
+ from ..utils.errors import ConfigurationError
15
+ from .models import (
16
+ Config,
17
+ ProviderConfig,
18
+ )
19
+
20
+
21
+ class ConfigMigration:
22
+ """Handles configuration migration between versions."""
23
+
24
+ def __init__(self, config_path):
25
+ """
26
+ Initialize the migration system.
27
+
28
+ Args:
29
+ config_path: Path to the configuration file (str or Path)
30
+ """
31
+ if isinstance(config_path, str):
32
+ self.config_path = Path(config_path)
33
+ elif hasattr(config_path, "__fspath__") or isinstance(config_path, Path):
34
+ self.config_path = Path(config_path)
35
+ else:
36
+ # Handle other path-like objects
37
+ self.config_path = Path(str(config_path))
38
+
39
+ self.backup_dir = self.config_path.parent / "backups"
40
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
41
+
42
+ def needs_migration(self) -> bool:
43
+ """
44
+ Check if the configuration needs migration.
45
+
46
+ Returns:
47
+ True if migration is needed
48
+ """
49
+ if not self.config_path.exists():
50
+ return False
51
+
52
+ try:
53
+ with open(self.config_path, "r") as f:
54
+ config_data = json.load(f)
55
+
56
+ # Check version
57
+ current_version = config_data.get("config_version", "1.0")
58
+ return current_version != "2.0"
59
+
60
+ except (json.JSONDecodeError, KeyError):
61
+ # If we can't read the config or it's malformed, assume migration needed
62
+ return True
63
+
64
+ def migrate_config(self) -> Tuple[bool, List[str]]:
65
+ """
66
+ Migrate configuration to the latest version.
67
+
68
+ Returns:
69
+ Tuple of (success, list of migration messages)
70
+ """
71
+ messages = []
72
+
73
+ if not self.config_path.exists():
74
+ messages.append("No configuration file found, nothing to migrate")
75
+ return True, messages
76
+
77
+ try:
78
+ # Create backup first
79
+ backup_path = self.create_backup()
80
+ messages.append(f"Created backup at: {backup_path}")
81
+
82
+ # Load old configuration
83
+ with open(self.config_path, "r") as f:
84
+ old_config = json.load(f)
85
+
86
+ # Determine migration path
87
+ old_version = old_config.get("config_version", "1.0")
88
+ messages.append(f"Migrating from version {old_version} to 2.0")
89
+
90
+ # Perform migration based on version
91
+ if old_version == "1.0" or "config_version" not in old_config:
92
+ new_config = self._migrate_from_v1(old_config)
93
+ messages.extend(self._get_v1_migration_messages())
94
+ else:
95
+ # Future migrations would go here
96
+ new_config = old_config
97
+ messages.append(f"No migration needed for version {old_version}")
98
+
99
+ # Validate migrated configuration
100
+ try:
101
+ config_obj = Config(**new_config)
102
+ validation_errors = config_obj.validate_configuration()
103
+ if validation_errors:
104
+ messages.append(
105
+ "Warning: Migrated configuration has validation errors:"
106
+ )
107
+ messages.extend([f" - {error}" for error in validation_errors])
108
+ except Exception as e:
109
+ messages.append(
110
+ f"Warning: Could not validate migrated configuration: {e}"
111
+ )
112
+
113
+ # Save migrated configuration
114
+ with open(self.config_path, "w") as f:
115
+ json.dump(new_config, f, indent=2)
116
+
117
+ messages.append("Configuration migration completed successfully")
118
+ return True, messages
119
+
120
+ except Exception as e:
121
+ messages.append(f"Migration failed: {e}")
122
+ # Try to restore backup
123
+ try:
124
+ self.restore_backup(backup_path)
125
+ messages.append("Restored backup due to migration failure")
126
+ except Exception as restore_error:
127
+ messages.append(f"Failed to restore backup: {restore_error}")
128
+
129
+ return False, messages
130
+
131
+ def create_backup(self) -> Path:
132
+ """
133
+ Create a backup of the current configuration.
134
+
135
+ Returns:
136
+ Path to the backup file
137
+
138
+ Raises:
139
+ ConfigurationError: If backup creation fails
140
+ """
141
+ if not self.config_path.exists():
142
+ raise ConfigurationError("No configuration file to backup")
143
+
144
+ # Create unique timestamp with microseconds to avoid collisions
145
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
146
+ backup_filename = f"config_backup_{timestamp}.json"
147
+ backup_path = self.backup_dir / backup_filename
148
+
149
+ # Ensure backup directory exists
150
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
151
+
152
+ try:
153
+ shutil.copy2(self.config_path, backup_path)
154
+ return backup_path
155
+ except Exception as e:
156
+ raise ConfigurationError(f"Failed to create backup: {e}")
157
+
158
+ def restore_backup(self, backup_path: Path) -> None:
159
+ """
160
+ Restore configuration from a backup.
161
+
162
+ Args:
163
+ backup_path: Path to the backup file
164
+
165
+ Raises:
166
+ ConfigurationError: If restore fails
167
+ """
168
+ if not backup_path.exists():
169
+ raise ConfigurationError(f"Backup file not found: {backup_path}")
170
+
171
+ try:
172
+ shutil.copy2(backup_path, self.config_path)
173
+ except Exception as e:
174
+ raise ConfigurationError(f"Failed to restore backup: {e}")
175
+
176
+ def list_backups(self) -> List[Dict[str, Any]]:
177
+ """
178
+ List available configuration backups.
179
+
180
+ Returns:
181
+ List of backup information dictionaries
182
+ """
183
+ backups = []
184
+
185
+ if not self.backup_dir.exists():
186
+ return backups
187
+
188
+ for backup_file in self.backup_dir.glob("config_backup_*.json"):
189
+ try:
190
+ stat = backup_file.stat()
191
+ backups.append(
192
+ {
193
+ "path": backup_file,
194
+ "filename": backup_file.name,
195
+ "created": datetime.fromtimestamp(stat.st_ctime),
196
+ "size": stat.st_size,
197
+ }
198
+ )
199
+ except Exception:
200
+ # Skip files we can't read
201
+ continue
202
+
203
+ # Sort by creation time, newest first
204
+ backups.sort(key=lambda x: x["created"], reverse=True)
205
+ return backups
206
+
207
+ def cleanup_old_backups(self, keep_count: int = 10) -> int:
208
+ """
209
+ Clean up old backup files, keeping only the most recent ones.
210
+
211
+ Args:
212
+ keep_count: Number of backups to keep
213
+
214
+ Returns:
215
+ Number of backups deleted
216
+ """
217
+ backups = self.list_backups()
218
+
219
+ if len(backups) <= keep_count:
220
+ return 0
221
+
222
+ deleted_count = 0
223
+ # Keep the first keep_count backups (newest), delete the rest (oldest)
224
+ for backup in backups[keep_count:]:
225
+ try:
226
+ backup["path"].unlink()
227
+ deleted_count += 1
228
+ except Exception:
229
+ # Skip files we can't delete
230
+ continue
231
+
232
+ return deleted_count
233
+
234
+ def _migrate_from_v1(self, old_config: Dict[str, Any]) -> Dict[str, Any]:
235
+ """
236
+ Migrate from version 1.0 configuration format.
237
+
238
+ Args:
239
+ old_config: Old configuration dictionary
240
+
241
+ Returns:
242
+ New configuration dictionary
243
+ """
244
+ new_config = {
245
+ "config_version": "2.0",
246
+ "created_at": datetime.now().isoformat(),
247
+ "updated_at": datetime.now().isoformat(),
248
+ "default_provider": old_config.get("default_provider", "claude"),
249
+ "storage_path": old_config.get(
250
+ "storage_path", str(Path.home() / ".omnimancer")
251
+ ),
252
+ "providers": {},
253
+ "chat_settings": {
254
+ "max_tokens": old_config.get("max_tokens"),
255
+ "temperature": old_config.get("temperature"),
256
+ "context_length": old_config.get("context_length", 4000),
257
+ "save_history": old_config.get("save_history", True),
258
+ },
259
+ "mcp": {
260
+ "enabled": True,
261
+ "servers": {},
262
+ "auto_approve_timeout": 30,
263
+ "max_concurrent_servers": 10,
264
+ "default_timeout": 30,
265
+ "retry_attempts": 3,
266
+ "retry_delay": 1.0,
267
+ "allow_dangerous_tools": False,
268
+ "sandbox_mode": True,
269
+ "max_tool_execution_time": 300,
270
+ "log_tool_calls": True,
271
+ "log_level": "INFO",
272
+ "metrics_enabled": False,
273
+ "auto_discover_tools": True,
274
+ "tool_cache_ttl": 3600,
275
+ "refresh_tools_on_startup": True,
276
+ },
277
+ "profiles": {},
278
+ "active_profile": None,
279
+ "config_sources": {},
280
+ "debug_mode": old_config.get("debug_mode", False),
281
+ "log_level": old_config.get("log_level", "INFO"),
282
+ "auto_update_check": True,
283
+ "telemetry_enabled": False,
284
+ "provider_fallback_enabled": True,
285
+ "provider_health_check_enabled": True,
286
+ "provider_timeout_default": 30.0,
287
+ "api_key_encryption_enabled": True,
288
+ "secure_storage_enabled": True,
289
+ "concurrent_requests_limit": 5,
290
+ "request_cache_enabled": True,
291
+ "request_cache_ttl": 300,
292
+ }
293
+
294
+ # Migrate provider configurations
295
+ old_providers = old_config.get("providers", {})
296
+ for provider_name, provider_config in old_providers.items():
297
+ new_provider_config = self._migrate_provider_config_v1(
298
+ provider_name, provider_config
299
+ )
300
+ new_config["providers"][provider_name] = new_provider_config
301
+
302
+ # Migrate MCP configuration if it exists
303
+ if "mcp" in old_config:
304
+ old_mcp = old_config["mcp"]
305
+ new_config["mcp"].update(
306
+ {
307
+ "enabled": old_mcp.get("enabled", True),
308
+ "servers": self._migrate_mcp_servers_v1(old_mcp.get("servers", {})),
309
+ "auto_approve_timeout": old_mcp.get("auto_approve_timeout", 30),
310
+ "max_concurrent_servers": old_mcp.get("max_concurrent_servers", 10),
311
+ }
312
+ )
313
+
314
+ return new_config
315
+
316
+ def _migrate_provider_config_v1(
317
+ self, provider_name: str, old_config: Dict[str, Any]
318
+ ) -> Dict[str, Any]:
319
+ """
320
+ Migrate a single provider configuration from v1 format.
321
+
322
+ Args:
323
+ provider_name: Name of the provider
324
+ old_config: Old provider configuration
325
+
326
+ Returns:
327
+ New provider configuration dictionary
328
+ """
329
+ # Get provider defaults
330
+ provider_defaults = self._get_provider_defaults(provider_name)
331
+
332
+ new_config = {
333
+ "api_key": old_config.get("api_key"),
334
+ "model": old_config.get("model", provider_defaults.get("model", "default")),
335
+ "max_tokens": old_config.get("max_tokens"),
336
+ "temperature": old_config.get("temperature"),
337
+ "base_url": old_config.get("base_url"),
338
+ "organization": old_config.get("organization"),
339
+ "project_id": old_config.get("project_id"),
340
+ "timeout": old_config.get("timeout"),
341
+ "top_p": old_config.get("top_p"),
342
+ "frequency_penalty": old_config.get("frequency_penalty"),
343
+ "presence_penalty": old_config.get("presence_penalty"),
344
+ "config_version": "2.0",
345
+ "enabled": old_config.get("enabled", True),
346
+ "priority": old_config.get(
347
+ "priority", provider_defaults.get("priority", 0)
348
+ ),
349
+ "max_retries": old_config.get("max_retries", 3),
350
+ "retry_delay": old_config.get("retry_delay", 1.0),
351
+ "exponential_backoff": old_config.get("exponential_backoff", True),
352
+ "health_check_enabled": old_config.get("health_check_enabled", True),
353
+ "health_check_interval": old_config.get("health_check_interval", 300),
354
+ "health_check_timeout": old_config.get("health_check_timeout", 10.0),
355
+ "auth_type": old_config.get("auth_type", "api_key"),
356
+ "custom_headers": old_config.get("custom_headers"),
357
+ "oauth_config": old_config.get("oauth_config"),
358
+ "extra_settings": old_config.get("extra_settings"),
359
+ }
360
+
361
+ # Add provider-specific defaults
362
+ new_config.update(provider_defaults)
363
+
364
+ # Remove None values
365
+ return {k: v for k, v in new_config.items() if v is not None}
366
+
367
+ def _migrate_mcp_servers_v1(self, old_servers: Dict[str, Any]) -> Dict[str, Any]:
368
+ """
369
+ Migrate MCP server configurations from v1 format.
370
+
371
+ Args:
372
+ old_servers: Old MCP server configurations
373
+
374
+ Returns:
375
+ New MCP server configurations dictionary
376
+ """
377
+ new_servers = {}
378
+
379
+ for server_name, server_config in old_servers.items():
380
+ new_servers[server_name] = {
381
+ "name": server_config.get("name", server_name),
382
+ "command": server_config.get("command", ""),
383
+ "args": server_config.get("args", []),
384
+ "env": server_config.get("env", {}),
385
+ "enabled": server_config.get("enabled", True),
386
+ "auto_approve": server_config.get("auto_approve", []),
387
+ "timeout": server_config.get("timeout", 30),
388
+ }
389
+
390
+ return new_servers
391
+
392
+ def _get_provider_defaults(self, provider_name: str) -> Dict[str, Any]:
393
+ """
394
+ Get default configuration values for a provider.
395
+
396
+ Args:
397
+ provider_name: Name of the provider
398
+
399
+ Returns:
400
+ Dictionary of default configuration values
401
+ """
402
+ defaults = {
403
+ "claude": {
404
+ "model": "claude-3-sonnet-20240229",
405
+ "provider_type": "claude",
406
+ "supports_tools": True,
407
+ "supports_multimodal": True,
408
+ "supports_streaming": True,
409
+ "supports_function_calling": True,
410
+ "supports_system_messages": True,
411
+ "priority": 10,
412
+ "auth_type": "api_key",
413
+ },
414
+ "openai": {
415
+ "model": "gpt-4",
416
+ "provider_type": "openai",
417
+ "supports_tools": True,
418
+ "supports_multimodal": True,
419
+ "supports_streaming": True,
420
+ "supports_function_calling": True,
421
+ "supports_system_messages": True,
422
+ "priority": 9,
423
+ "auth_type": "api_key",
424
+ },
425
+ "gemini": {
426
+ "model": "gemini-1.5-pro",
427
+ "provider_type": "gemini",
428
+ "supports_tools": True,
429
+ "supports_multimodal": True,
430
+ "supports_streaming": True,
431
+ "supports_function_calling": True,
432
+ "supports_system_messages": True,
433
+ "priority": 8,
434
+ "auth_type": "api_key",
435
+ "safety_settings": {
436
+ "HARM_CATEGORY_HARASSMENT": "BLOCK_MEDIUM_AND_ABOVE",
437
+ "HARM_CATEGORY_HATE_SPEECH": "BLOCK_MEDIUM_AND_ABOVE",
438
+ "HARM_CATEGORY_SEXUALLY_EXPLICIT": "BLOCK_MEDIUM_AND_ABOVE",
439
+ "HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_MEDIUM_AND_ABOVE",
440
+ },
441
+ },
442
+ "cohere": {
443
+ "model": "command-r",
444
+ "provider_type": "cohere",
445
+ "supports_tools": False,
446
+ "supports_multimodal": False,
447
+ "supports_streaming": True,
448
+ "supports_function_calling": False,
449
+ "supports_system_messages": True,
450
+ "priority": 7,
451
+ "auth_type": "api_key",
452
+ },
453
+ "ollama": {
454
+ "model": "llama2",
455
+ "provider_type": "ollama",
456
+ "supports_tools": False,
457
+ "supports_multimodal": False,
458
+ "supports_streaming": True,
459
+ "supports_function_calling": False,
460
+ "supports_system_messages": True,
461
+ "priority": 6,
462
+ "auth_type": "none",
463
+ "base_url": "http://localhost:11434",
464
+ },
465
+ }
466
+
467
+ return defaults.get(
468
+ provider_name,
469
+ {
470
+ "provider_type": provider_name,
471
+ "supports_tools": False,
472
+ "supports_multimodal": False,
473
+ "supports_streaming": True,
474
+ "supports_function_calling": False,
475
+ "supports_system_messages": True,
476
+ "priority": 5,
477
+ "auth_type": "api_key",
478
+ },
479
+ )
480
+
481
+ def _get_v1_migration_messages(self) -> List[str]:
482
+ """
483
+ Get informational messages about v1 migration.
484
+
485
+ Returns:
486
+ List of migration messages
487
+ """
488
+ return [
489
+ "Added new provider capability flags (supports_tools, supports_multimodal, etc.)",
490
+ "Added provider priority and health check settings",
491
+ "Added MCP (Model Context Protocol) configuration section",
492
+ "Added configuration profiles support",
493
+ "Added enhanced security and performance settings",
494
+ "Added configuration source tracking",
495
+ "Updated configuration schema to version 2.0",
496
+ ]
497
+
498
+
499
+ class ConfigValidator:
500
+ """Validates configuration and provides detailed error reporting."""
501
+
502
+ def __init__(self, config: Config):
503
+ """
504
+ Initialize the validator.
505
+
506
+ Args:
507
+ config: Configuration to validate
508
+ """
509
+ self.config = config
510
+
511
+ def validate_config(self, config: Config) -> List[str]:
512
+ """
513
+ Validate configuration and return list of errors.
514
+
515
+ Args:
516
+ config: Configuration to validate
517
+
518
+ Returns:
519
+ List of validation errors
520
+ """
521
+ self.config = config
522
+ errors = []
523
+
524
+ # General validation
525
+ general_errors = self._validate_general()
526
+ errors.extend(general_errors)
527
+
528
+ # Provider validation
529
+ for provider_name, provider_config in config.providers.items():
530
+ provider_errors = self._validate_provider(provider_name, provider_config)
531
+ errors.extend(provider_errors)
532
+
533
+ # MCP validation
534
+ mcp_errors = self._validate_mcp()
535
+ errors.extend(mcp_errors)
536
+
537
+ return errors
538
+
539
+ def validate_full_config(self) -> Dict[str, List[str]]:
540
+ """
541
+ Perform comprehensive configuration validation.
542
+
543
+ Returns:
544
+ Dictionary mapping validation categories to error lists
545
+ """
546
+ validation_results = {
547
+ "general": [],
548
+ "providers": {},
549
+ "mcp": [],
550
+ "profiles": {},
551
+ "security": [],
552
+ "performance": [],
553
+ }
554
+
555
+ # General validation
556
+ validation_results["general"] = self._validate_general()
557
+
558
+ # Provider validation
559
+ for provider_name, provider_config in self.config.providers.items():
560
+ provider_errors = self._validate_provider(provider_name, provider_config)
561
+ if provider_errors:
562
+ validation_results["providers"][provider_name] = provider_errors
563
+
564
+ # MCP validation
565
+ validation_results["mcp"] = self._validate_mcp()
566
+
567
+ # Profile validation
568
+ for profile_name, profile in self.config.profiles.items():
569
+ profile_errors = self._validate_profile(profile_name, profile)
570
+ if profile_errors:
571
+ validation_results["profiles"][profile_name] = profile_errors
572
+
573
+ # Security validation
574
+ validation_results["security"] = self._validate_security()
575
+
576
+ # Performance validation
577
+ validation_results["performance"] = self._validate_performance()
578
+
579
+ # Remove empty categories
580
+ return {k: v for k, v in validation_results.items() if v}
581
+
582
+ def _validate_general(self) -> List[str]:
583
+ """Validate general configuration settings."""
584
+ errors = []
585
+
586
+ if not self.config.default_provider:
587
+ errors.append("No default provider specified")
588
+ elif self.config.default_provider not in self.config.providers:
589
+ errors.append(
590
+ f"Default provider '{self.config.default_provider}' not configured"
591
+ )
592
+
593
+ if not self.config.providers:
594
+ errors.append("No providers configured")
595
+
596
+ if not self.config.storage_path:
597
+ errors.append("No storage path specified")
598
+ else:
599
+ try:
600
+ storage_path = Path(self.config.storage_path).expanduser()
601
+ if not storage_path.parent.exists():
602
+ errors.append(
603
+ f"Storage path parent directory does not exist: {storage_path.parent}"
604
+ )
605
+ except Exception as e:
606
+ errors.append(f"Invalid storage path: {e}")
607
+
608
+ return errors
609
+
610
+ def _validate_provider(
611
+ self, provider_name: str, config: ProviderConfig
612
+ ) -> List[str]:
613
+ """Validate a single provider configuration."""
614
+ errors = []
615
+
616
+ if not config.model:
617
+ errors.append("No model specified")
618
+
619
+ if config.auth_type != "none" and not config.api_key:
620
+ errors.append("API key required but not provided")
621
+
622
+ # Provider-specific validation
623
+ if config.provider_type == "claude" or provider_name == "claude":
624
+ errors.extend(self._validate_claude_provider(config))
625
+ elif config.provider_type == "openai" or provider_name == "openai":
626
+ errors.extend(self._validate_openai_provider(config))
627
+ elif config.provider_type == "gemini" or provider_name == "gemini":
628
+ errors.extend(self._validate_gemini_provider(config))
629
+ elif config.provider_type == "cohere" or provider_name == "cohere":
630
+ errors.extend(self._validate_cohere_provider(config))
631
+ elif config.provider_type == "ollama" or provider_name == "ollama":
632
+ errors.extend(self._validate_ollama_provider(config))
633
+
634
+ return errors
635
+
636
+ def _validate_claude_provider(self, config: ProviderConfig) -> List[str]:
637
+ """Validate Claude provider configuration."""
638
+ errors = []
639
+
640
+ valid_models = [
641
+ "claude-3-sonnet-20240229",
642
+ "claude-3-haiku-20240307",
643
+ "claude-3-opus-20240229",
644
+ "claude-3-5-sonnet-20241022",
645
+ ]
646
+ if config.model not in valid_models:
647
+ errors.append(
648
+ f"Unknown Claude model '{config.model}'. Valid models: {', '.join(valid_models)}"
649
+ )
650
+
651
+ return errors
652
+
653
+ def _validate_openai_provider(self, config: ProviderConfig) -> List[str]:
654
+ """Validate OpenAI provider configuration."""
655
+ errors = []
656
+
657
+ valid_models = [
658
+ "gpt-4",
659
+ "gpt-4-turbo",
660
+ "gpt-4o",
661
+ "gpt-4o-mini",
662
+ "gpt-3.5-turbo",
663
+ "gpt-3.5-turbo-16k",
664
+ ]
665
+ if config.model not in valid_models:
666
+ errors.append(
667
+ f"Unknown OpenAI model '{config.model}'. Valid models: {', '.join(valid_models)}"
668
+ )
669
+
670
+ return errors
671
+
672
+ def _validate_gemini_provider(self, config: ProviderConfig) -> List[str]:
673
+ """Validate Gemini provider configuration."""
674
+ errors = []
675
+
676
+ valid_models = ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro"]
677
+ if config.model not in valid_models:
678
+ errors.append(
679
+ f"Unknown Gemini model '{config.model}'. Valid models: {', '.join(valid_models)}"
680
+ )
681
+
682
+ return errors
683
+
684
+ def _validate_cohere_provider(self, config: ProviderConfig) -> List[str]:
685
+ """Validate Cohere provider configuration."""
686
+ errors = []
687
+
688
+ valid_models = [
689
+ "command-r",
690
+ "command-r-plus",
691
+ "command-light",
692
+ "command",
693
+ ]
694
+ if config.model not in valid_models:
695
+ errors.append(
696
+ f"Unknown Cohere model '{config.model}'. Valid models: {', '.join(valid_models)}"
697
+ )
698
+
699
+ return errors
700
+
701
+ def _validate_ollama_provider(self, config: ProviderConfig) -> List[str]:
702
+ """Validate Ollama provider configuration."""
703
+ errors = []
704
+
705
+ if config.base_url and not config.base_url.startswith(("http://", "https://")):
706
+ errors.append("Ollama base_url must start with 'http://' or 'https://'")
707
+
708
+ return errors
709
+
710
+ def _validate_mcp(self) -> List[str]:
711
+ """Validate MCP configuration."""
712
+ errors = []
713
+
714
+ for server_name, server_config in self.config.mcp.servers.items():
715
+ if not server_config.name:
716
+ errors.append(f"MCP server '{server_name}' has no name")
717
+ if not server_config.command:
718
+ errors.append(f"MCP server '{server_name}' has no command")
719
+
720
+ return errors
721
+
722
+ def _validate_profile(
723
+ self, profile_name: str, profile: "ConfigProfile"
724
+ ) -> List[str]:
725
+ """Validate a configuration profile."""
726
+ errors = []
727
+
728
+ if not profile.default_provider:
729
+ errors.append("No default provider specified")
730
+ elif profile.default_provider not in profile.providers:
731
+ errors.append(
732
+ f"Default provider '{profile.default_provider}' not configured"
733
+ )
734
+
735
+ if not profile.providers:
736
+ errors.append("No providers configured")
737
+
738
+ return errors
739
+
740
+ def _validate_security(self) -> List[str]:
741
+ """Validate security-related settings."""
742
+ errors = []
743
+
744
+ # Check for insecure configurations
745
+ if not self.config.api_key_encryption_enabled:
746
+ errors.append("API key encryption is disabled - this is a security risk")
747
+
748
+ if not self.config.secure_storage_enabled:
749
+ errors.append("Secure storage is disabled - this is a security risk")
750
+
751
+ # Check MCP security settings
752
+ if self.config.mcp.allow_dangerous_tools:
753
+ errors.append("MCP dangerous tools are allowed - this is a security risk")
754
+
755
+ if not self.config.mcp.sandbox_mode:
756
+ errors.append("MCP sandbox mode is disabled - this is a security risk")
757
+
758
+ return errors
759
+
760
+ def _validate_performance(self) -> List[str]:
761
+ """Validate performance-related settings."""
762
+ errors = []
763
+
764
+ if self.config.concurrent_requests_limit > 20:
765
+ errors.append(
766
+ "High concurrent request limit may cause rate limiting issues"
767
+ )
768
+
769
+ if self.config.request_cache_ttl > 3600:
770
+ errors.append("Long cache TTL may cause stale data issues")
771
+
772
+ return errors