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,787 @@
1
+ """
2
+ Core engine for Omnimancer CLI.
3
+
4
+ This module provides the main engine class that coordinates between
5
+ providers, configuration, and chat management.
6
+
7
+ Version: 1.0.0
8
+ """
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from ..core.models import ChatResponse, EnhancedModelInfo, ModelInfo
14
+ from ..providers.base import BaseProvider
15
+ from ..ui.progress_indicator import OperationType, get_progress_indicator
16
+ from ..utils.errors import ConfigurationError
17
+ from .chat_manager import ChatManager
18
+ from .config_manager import ConfigManager
19
+ from .conversation_manager import ConversationManager
20
+ from .health_monitor import HealthMonitor
21
+ from .provider_initializer import ProviderInitializer
22
+ from .provider_registry import ProviderRegistry
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class CoreEngine:
28
+ """
29
+ Core engine that coordinates all Omnimancer functionality.
30
+
31
+ This class manages providers, configuration, chat sessions,
32
+ and provides the main interface for the CLI.
33
+ """
34
+
35
+ def __init__(self, config_manager: ConfigManager):
36
+ """
37
+ Initialize the core engine.
38
+
39
+ Args:
40
+ config_manager: Configuration manager instance
41
+ """
42
+ self.config_manager = config_manager
43
+ self.chat_manager = ChatManager()
44
+
45
+ # Get storage path from config or use default
46
+ config = config_manager.get_config()
47
+ storage_path = getattr(config, "storage_path", "~/.omnimancer")
48
+ self.conversation_manager = ConversationManager(storage_path)
49
+
50
+ self.health_monitor = HealthMonitor()
51
+ self.provider_initializer = ProviderInitializer()
52
+
53
+ # Initialize provider registry
54
+ self.provider_registry = ProviderRegistry()
55
+
56
+ # Initialize MCP manager
57
+ mcp_config = getattr(config, "mcp", None)
58
+ if mcp_config:
59
+ from ..mcp.manager import MCPManager
60
+
61
+ self.mcp_manager = MCPManager(mcp_config)
62
+ else:
63
+ self.mcp_manager = None
64
+
65
+ self.providers: Dict[str, BaseProvider] = {}
66
+ self.current_provider: Optional[BaseProvider] = None
67
+ self._initialized = False
68
+
69
+ # Initialize agent engine for autonomous operations
70
+ self.agent_engine = None
71
+
72
+ async def initialize_providers(self) -> None:
73
+ """Initialize all configured providers."""
74
+ try:
75
+ config = self.config_manager.get_config()
76
+
77
+ # Use the optimized provider initializer with config_manager for API key decryption
78
+ self.providers = await self.provider_initializer.initialize_providers(
79
+ config.providers, self.config_manager
80
+ )
81
+
82
+ # Register providers with the provider registry for catalog management
83
+ from ..providers.factory import ProviderFactory
84
+
85
+ factory = ProviderFactory()
86
+ available_providers = factory.get_available_providers()
87
+
88
+ # Register all available providers (not just configured ones) for catalog management
89
+ for provider_name in available_providers:
90
+ try:
91
+ # Just register the provider name, the registry will handle class loading
92
+ self.provider_registry.register_provider(provider_name, None)
93
+ except Exception as e:
94
+ logger.warning(f"Failed to register provider {provider_name}: {e}")
95
+
96
+ # Set default provider
97
+ if config.default_provider and config.default_provider in self.providers:
98
+ self.current_provider = self.providers[config.default_provider]
99
+ elif self.providers:
100
+ # Use first available provider as default
101
+ self.current_provider = next(iter(self.providers.values()))
102
+
103
+ # Initialize agent engine after providers are ready
104
+ self._initialize_agent_engine()
105
+
106
+ self._initialized = True
107
+ logger.info(f"Initialized {len(self.providers)} providers")
108
+
109
+ except Exception as e:
110
+ logger.error(f"Failed to initialize providers: {e}")
111
+ raise ConfigurationError(f"Provider initialization failed: {e}")
112
+
113
+ async def switch_model(
114
+ self, provider_name: str, model_name: Optional[str] = None
115
+ ) -> bool:
116
+ """
117
+ Switch to a different provider/model.
118
+
119
+ Args:
120
+ provider_name: Name of the provider to switch to
121
+ model_name: Optional model name to use
122
+
123
+ Returns:
124
+ True if switch was successful, False otherwise
125
+ """
126
+ try:
127
+ if provider_name not in self.providers:
128
+ raise ConfigurationError(f"Provider '{provider_name}' is not available")
129
+
130
+ provider = self.providers[provider_name]
131
+
132
+ # Switch model if specified
133
+ if model_name:
134
+ # Validate model is available for this provider
135
+ available_models = provider.get_available_models()
136
+ model_names = [m.name for m in available_models]
137
+
138
+ # Also check custom models for this provider
139
+ custom_models = self.config_manager.get_custom_models()
140
+ custom_model_names = [
141
+ m.name for m in custom_models if m.provider == provider_name
142
+ ]
143
+
144
+ # Combine both lists
145
+ all_model_names = model_names + custom_model_names
146
+
147
+ if model_name not in all_model_names:
148
+ raise ConfigurationError(
149
+ f"Model '{model_name}' not available for provider '{provider_name}'"
150
+ )
151
+
152
+ provider.model = model_name
153
+
154
+ # Switch to the provider
155
+ self.current_provider = provider
156
+
157
+ # Update chat manager with new model
158
+ self.chat_manager.set_current_model(provider.model)
159
+
160
+ logger.info(
161
+ f"Switched to provider: {provider_name}, model: {provider.model}"
162
+ )
163
+ return True
164
+
165
+ except Exception as e:
166
+ logger.error(f"Failed to switch model: {e}")
167
+ return False
168
+
169
+ async def send_message(self, message: str) -> ChatResponse:
170
+ """
171
+ Send a message using the current provider.
172
+
173
+ Args:
174
+ message: Message to send
175
+
176
+ Returns:
177
+ Chat response from the provider
178
+ """
179
+ if not self.current_provider:
180
+ return ChatResponse(
181
+ content="",
182
+ model_used="",
183
+ tokens_used=0,
184
+ error="No provider available. Please configure a provider first.",
185
+ )
186
+
187
+ try:
188
+ # Get progress indicator
189
+ progress = get_progress_indicator()
190
+
191
+ # Get current chat context
192
+ if progress and progress.enabled:
193
+ progress.start_operation(
194
+ "engine_context",
195
+ OperationType.ANALYZE,
196
+ "Getting chat context",
197
+ )
198
+ context = self.chat_manager.get_current_context()
199
+ if progress and progress.enabled:
200
+ progress.complete_operation("engine_context", "completed")
201
+
202
+ # Send message to provider
203
+ if progress and progress.enabled:
204
+ progress.start_operation(
205
+ "engine_provider",
206
+ OperationType.NETWORK,
207
+ f"Sending to {self.current_provider.get_provider_name()}",
208
+ )
209
+ response = await self.current_provider.send_message(message, context)
210
+ if progress and progress.enabled:
211
+ progress.complete_operation(
212
+ "engine_provider",
213
+ "completed" if response.is_success else "failed",
214
+ )
215
+
216
+ # Always add user message to chat history
217
+ self.chat_manager.add_user_message(message)
218
+
219
+ # Only add assistant message if response was successful
220
+ if response.is_success:
221
+ if progress and progress.enabled:
222
+ progress.start_operation(
223
+ "engine_history",
224
+ OperationType.WRITE,
225
+ "Updating chat history",
226
+ )
227
+ self.chat_manager.add_assistant_message(
228
+ response.content, self.current_provider.model
229
+ )
230
+ if progress and progress.enabled:
231
+ progress.complete_operation("engine_history", "completed")
232
+
233
+ return response
234
+
235
+ except Exception as e:
236
+ logger.error(f"Failed to send message: {e}")
237
+ return ChatResponse(
238
+ content="",
239
+ model_used="",
240
+ tokens_used=0,
241
+ error=f"Failed to send message: {str(e)}",
242
+ )
243
+
244
+ def get_available_models(self) -> List[ModelInfo]:
245
+ """Get all available models from all providers."""
246
+ all_models = []
247
+
248
+ for provider in self.providers.values():
249
+ try:
250
+ models = provider.get_available_models()
251
+ # Convert EnhancedModelInfo to ModelInfo if needed
252
+ for model in models:
253
+ if isinstance(model, EnhancedModelInfo):
254
+ all_models.append(model.to_model_info())
255
+ else:
256
+ all_models.append(model)
257
+ except Exception as e:
258
+ logger.warning(
259
+ f"Failed to get models from provider {provider.get_provider_name()}: {e}"
260
+ )
261
+
262
+ return all_models
263
+
264
+ def get_all_models(self) -> Dict[str, List[Dict[str, Any]]]:
265
+ """Get all models organized by provider (for CLI display)."""
266
+ result = {}
267
+
268
+ for provider_name, provider in self.providers.items():
269
+ try:
270
+ models = provider.get_available_models()
271
+ result[provider_name] = []
272
+
273
+ for model in models:
274
+ model_dict = {
275
+ "name": model.name,
276
+ "provider": model.provider,
277
+ "supports_tools": getattr(model, "supports_tools", False),
278
+ "supports_multimodal": getattr(
279
+ model, "supports_multimodal", False
280
+ ),
281
+ "available": getattr(model, "available", True),
282
+ }
283
+
284
+ # Add enhanced info if available
285
+ if isinstance(model, EnhancedModelInfo):
286
+ model_dict.update(
287
+ {
288
+ "swe_score": model.swe_score,
289
+ "cost_display": model.get_cost_display(),
290
+ "latest_version": model.latest_version,
291
+ }
292
+ )
293
+
294
+ result[provider_name].append(model_dict)
295
+
296
+ except Exception as e:
297
+ logger.warning(
298
+ f"Failed to get models from provider {provider_name}: {e}"
299
+ )
300
+ result[provider_name] = []
301
+
302
+ return result
303
+
304
+ def get_current_config(self) -> Dict[str, Any]:
305
+ """Get current configuration for display."""
306
+ try:
307
+ config = self.config_manager.get_config()
308
+ return {
309
+ "default_provider": config.default_provider,
310
+ "providers": {
311
+ name: {
312
+ "model": provider_config.model,
313
+ "api_key": (
314
+ f"{provider_config.api_key[:8]}***"
315
+ if provider_config.api_key
316
+ else "Not set"
317
+ ),
318
+ }
319
+ for name, provider_config in config.providers.items()
320
+ },
321
+ "current_provider": (
322
+ self.current_provider.get_provider_name()
323
+ if self.current_provider
324
+ else None
325
+ ),
326
+ "current_model": (
327
+ self.current_provider.model if self.current_provider else None
328
+ ),
329
+ }
330
+ except Exception as e:
331
+ logger.error(f"Failed to get current config: {e}")
332
+ return {"error": str(e)}
333
+
334
+ def get_current_model_info(self) -> Optional[Dict[str, Any]]:
335
+ """Get information about the current model."""
336
+ if not self.current_provider:
337
+ return None
338
+
339
+ try:
340
+ model_info = self.current_provider.get_model_info()
341
+ return {
342
+ "name": model_info.name,
343
+ "provider": model_info.provider,
344
+ "supports_tools": getattr(model_info, "supports_tools", False),
345
+ "supports_multimodal": getattr(
346
+ model_info, "supports_multimodal", False
347
+ ),
348
+ "available": getattr(model_info, "available", True),
349
+ }
350
+ except Exception as e:
351
+ logger.warning(f"Failed to get current model info: {e}")
352
+ return None
353
+
354
+ def get_conversation_summary(self) -> Dict[str, Any]:
355
+ """Get summary of current conversation."""
356
+ try:
357
+ context = self.chat_manager.get_current_context()
358
+ return {
359
+ "message_count": len(context.messages),
360
+ "current_model": (
361
+ self.current_provider.model if self.current_provider else None
362
+ ),
363
+ "session_id": context.session_id,
364
+ }
365
+ except Exception as e:
366
+ logger.error(f"Failed to get conversation summary: {e}")
367
+ return {"error": str(e)}
368
+
369
+ async def validate_current_provider(self) -> bool:
370
+ """Validate that the current provider is working."""
371
+ if not self.current_provider:
372
+ return False
373
+
374
+ try:
375
+ return await self.current_provider.validate_credentials()
376
+ except Exception as e:
377
+ logger.error(f"Provider validation failed: {e}")
378
+ return False
379
+
380
+ async def check_provider_health(
381
+ self, provider_name: Optional[str] = None, force: bool = False
382
+ ) -> Dict[str, Any]:
383
+ """
384
+ Check health status of providers using the optimized health monitor.
385
+
386
+ Args:
387
+ provider_name: Name of specific provider to check, or None for all
388
+ force: Force check even if cached result is available
389
+
390
+ Returns:
391
+ Dictionary with health status information
392
+ """
393
+ try:
394
+ config = self.config_manager.get_config()
395
+
396
+ if provider_name:
397
+ # Check specific provider
398
+ if provider_name not in config.providers:
399
+ return {
400
+ provider_name: {
401
+ "status": "error",
402
+ "message": f"Provider {provider_name} not configured",
403
+ "available": False,
404
+ "credentials_valid": False,
405
+ }
406
+ }
407
+
408
+ provider_config = config.providers[provider_name]
409
+ status = await self.health_monitor.check_provider_health(
410
+ provider_name, provider_config, force=force
411
+ )
412
+ return {provider_name: status}
413
+ else:
414
+ # Check all providers
415
+ return await self.health_monitor.check_all_providers_health(
416
+ config.providers, force=force
417
+ )
418
+
419
+ except Exception as e:
420
+ logger.error(f"Error checking provider health: {e}")
421
+ if provider_name:
422
+ return {
423
+ provider_name: {
424
+ "status": "error",
425
+ "message": f"Health check failed: {str(e)}",
426
+ "available": False,
427
+ "credentials_valid": False,
428
+ }
429
+ }
430
+ else:
431
+ return {"error": f"Health check failed: {str(e)}"}
432
+
433
+ def save_conversation(self, name: str) -> str:
434
+ """Save current conversation."""
435
+ try:
436
+ context = self.chat_manager.get_current_context()
437
+ return self.conversation_manager.save_conversation(context, name)
438
+ except Exception as e:
439
+ logger.error(f"Failed to save conversation: {e}")
440
+ raise
441
+
442
+ def list_conversations(self) -> List[Dict[str, Any]]:
443
+ """List saved conversations."""
444
+ try:
445
+ return self.conversation_manager.list_conversations()
446
+ except Exception as e:
447
+ logger.error(f"Failed to list conversations: {e}")
448
+ return []
449
+
450
+ def load_conversation(self, filename: str) -> bool:
451
+ """Load a saved conversation."""
452
+ try:
453
+ context = self.conversation_manager.load_conversation(filename)
454
+ self.chat_manager.current_context = context
455
+ return True
456
+ except Exception as e:
457
+ logger.error(f"Failed to load conversation: {e}")
458
+ return False
459
+
460
+ async def initialize_mcp(self) -> None:
461
+ """Initialize MCP (Model Context Protocol) servers."""
462
+ try:
463
+ if self.mcp_manager:
464
+ await self.mcp_manager.initialize_servers()
465
+ logger.info("MCP servers initialized successfully")
466
+ else:
467
+ logger.info("MCP manager not configured - skipping MCP initialization")
468
+ except Exception as e:
469
+ logger.error(f"Failed to initialize MCP: {e}")
470
+ raise
471
+
472
+ async def shutdown_mcp(self) -> None:
473
+ """Shutdown MCP servers gracefully."""
474
+ try:
475
+ if self.mcp_manager:
476
+ await self.mcp_manager.shutdown()
477
+ logger.info("MCP servers shutdown successfully")
478
+ else:
479
+ logger.info("MCP manager not configured - skipping MCP shutdown")
480
+ except Exception as e:
481
+ logger.error(f"Failed to shutdown MCP: {e}")
482
+ # Don't raise during shutdown
483
+
484
+ def _get_models_list(self) -> str:
485
+ """Get a basic models list as fallback."""
486
+ models = self.get_available_models()
487
+ if not models:
488
+ return "No models available."
489
+
490
+ lines = []
491
+ for model in models:
492
+ lines.append(f"- {model.name} ({model.provider})")
493
+
494
+ return "\n".join(lines)
495
+
496
+ async def _get_tools_list(self) -> str:
497
+ """Get formatted list of available MCP tools."""
498
+ if not self.mcp_manager or not self.mcp_manager.initialized:
499
+ return "MCP is not initialized. No tools available."
500
+
501
+ try:
502
+ # Get available tools from MCP manager
503
+ tools = await self.mcp_manager.get_available_tools()
504
+
505
+ if not tools:
506
+ return "No MCP tools available."
507
+
508
+ # Group tools by server
509
+ tools_by_server = {}
510
+ for tool in tools:
511
+ server_name = getattr(tool, "server_name", "Unknown")
512
+ if server_name not in tools_by_server:
513
+ tools_by_server[server_name] = []
514
+ tools_by_server[server_name].append(tool)
515
+
516
+ # Format output
517
+ lines = []
518
+ lines.append("Available MCP Tools:")
519
+ lines.append("=" * 50)
520
+
521
+ for server_name, server_tools in tools_by_server.items():
522
+ lines.append(f"\n📡 {server_name} ({len(server_tools)} tools)")
523
+ lines.append("-" * 30)
524
+
525
+ for tool in server_tools:
526
+ name = getattr(tool, "name", "Unknown")
527
+ description = getattr(tool, "description", "No description")
528
+ lines.append(f" 🔧 {name}")
529
+ if description and description != "No description":
530
+ lines.append(f" {description}")
531
+
532
+ lines.append(
533
+ f"\nTotal: {len(tools)} tools across {len(tools_by_server)} servers"
534
+ )
535
+ return "\n".join(lines)
536
+
537
+ except Exception as e:
538
+ logger.error(f"Error getting tools list: {e}")
539
+ return f"Error retrieving tools list: {str(e)}"
540
+
541
+ async def _handle_mcp_command(self, command_obj) -> str:
542
+ """Handle MCP management commands."""
543
+ if not self.mcp_manager:
544
+ return "MCP is not configured for this installation."
545
+
546
+ # Extract arguments from Command object or treat as string
547
+ if hasattr(command_obj, "args") and command_obj.args:
548
+ args = " ".join(command_obj.args)
549
+ elif isinstance(command_obj, str):
550
+ args = command_obj
551
+ else:
552
+ args = ""
553
+
554
+ args = args.strip()
555
+ if not args:
556
+ args = "status"
557
+
558
+ command_parts = args.split()
559
+ command = command_parts[0].lower()
560
+
561
+ try:
562
+ if command == "status":
563
+ return await self._mcp_status()
564
+ elif command == "reload":
565
+ return await self._mcp_reload()
566
+ elif command == "connect":
567
+ server_name = command_parts[1] if len(command_parts) > 1 else None
568
+ return await self._mcp_connect(server_name)
569
+ elif command == "disconnect":
570
+ server_name = command_parts[1] if len(command_parts) > 1 else None
571
+ return await self._mcp_disconnect(server_name)
572
+ elif command == "health":
573
+ return await self._mcp_health()
574
+ elif command == "servers":
575
+ return self._mcp_servers()
576
+ elif command == "tools":
577
+ server_name = command_parts[1] if len(command_parts) > 1 else None
578
+ return await self._mcp_tools(server_name)
579
+ else:
580
+ return self._mcp_help()
581
+
582
+ except Exception as e:
583
+ logger.error(f"Error handling MCP command '{command}': {e}")
584
+ return f"Error executing MCP command: {str(e)}"
585
+
586
+ async def _mcp_status(self) -> str:
587
+ """Get MCP system status."""
588
+ if not self.mcp_manager:
589
+ return "MCP is not configured."
590
+
591
+ status_info = []
592
+ status_info.append("MCP System Status")
593
+ status_info.append("=" * 40)
594
+
595
+ # Basic status
596
+ status_info.append(f"Enabled: {'Yes' if self.mcp_manager.is_enabled else 'No'}")
597
+ status_info.append(
598
+ f"Initialized: {'Yes' if self.mcp_manager.initialized else 'No'}"
599
+ )
600
+ status_info.append(
601
+ f"Connected Servers: {self.mcp_manager.connected_server_count}"
602
+ )
603
+ status_info.append(f"Total Tools: {self.mcp_manager.total_tool_count}")
604
+
605
+ # Degradation status
606
+ degradation = self.mcp_manager.get_degradation_status()
607
+ status_info.append(f"Degradation Level: {degradation['degradation_level']}")
608
+
609
+ if degradation["functionality_impact"]:
610
+ status_info.append("\nFunctionality Impact:")
611
+ for impact in degradation["functionality_impact"]:
612
+ status_info.append(f" • {impact}")
613
+
614
+ return "\n".join(status_info)
615
+
616
+ async def _mcp_reload(self) -> str:
617
+ """Reload MCP servers."""
618
+ if not self.mcp_manager:
619
+ return "MCP is not configured."
620
+
621
+ try:
622
+ await self.mcp_manager.reload_servers()
623
+ return "MCP servers reloaded successfully."
624
+ except Exception as e:
625
+ return f"Error reloading MCP servers: {str(e)}"
626
+
627
+ async def _mcp_connect(self, server_name: str = None) -> str:
628
+ """Connect to MCP server(s)."""
629
+ if not self.mcp_manager:
630
+ return "MCP is not configured."
631
+
632
+ try:
633
+ if server_name:
634
+ # For specific server, we'd need a method to connect individual servers
635
+ return f"Connecting to specific server '{server_name}' is not yet implemented. Use reload to reconnect all servers."
636
+ else:
637
+ await self.mcp_manager.initialize_servers()
638
+ return "Attempted to connect to all MCP servers."
639
+ except Exception as e:
640
+ return f"Error connecting to MCP servers: {str(e)}"
641
+
642
+ async def _mcp_disconnect(self, server_name: str = None) -> str:
643
+ """Disconnect from MCP server(s)."""
644
+ if not self.mcp_manager:
645
+ return "MCP is not configured."
646
+
647
+ try:
648
+ if server_name:
649
+ success = await self.mcp_manager.shutdown_servers(server_name)
650
+ if success:
651
+ return f"Disconnected from server '{server_name}'."
652
+ else:
653
+ return (
654
+ f"Server '{server_name}' was not connected or does not exist."
655
+ )
656
+ else:
657
+ await self.mcp_manager.shutdown()
658
+ return "Disconnected from all MCP servers."
659
+ except Exception as e:
660
+ return f"Error disconnecting from MCP servers: {str(e)}"
661
+
662
+ async def _mcp_health(self) -> str:
663
+ """Get MCP health status."""
664
+ if not self.mcp_manager:
665
+ return "MCP is not configured."
666
+
667
+ try:
668
+ # Get both server status and health check
669
+ server_status = self.mcp_manager.get_server_status()
670
+ health_status = await self.mcp_manager.health_check()
671
+
672
+ health_info = []
673
+ health_info.append("MCP Server Health")
674
+ health_info.append("=" * 40)
675
+
676
+ for server_name, status in server_status.items():
677
+ health_check = health_status.get(server_name, False)
678
+ health_icon = "🟢" if health_check else "🔴"
679
+
680
+ health_info.append(f"\n{health_icon} {server_name}")
681
+ health_info.append(
682
+ f" Enabled: {'Yes' if status['enabled'] else 'No'}"
683
+ )
684
+ health_info.append(
685
+ f" Connected: {'Yes' if status['connected'] else 'No'}"
686
+ )
687
+ health_info.append(f" Healthy: {'Yes' if health_check else 'No'}")
688
+ health_info.append(f" Tools: {status['tool_count']}")
689
+ health_info.append(f" Command: {status['command']}")
690
+
691
+ overall_health = health_status.get("overall_healthy", False)
692
+ health_info.append(
693
+ f"\nOverall Health: {'🟢 Healthy' if overall_health else '🔴 Issues detected'}"
694
+ )
695
+
696
+ return "\n".join(health_info)
697
+
698
+ except Exception as e:
699
+ return f"Error getting health status: {str(e)}"
700
+
701
+ def _mcp_servers(self) -> str:
702
+ """List MCP servers."""
703
+ if not self.mcp_manager:
704
+ return "MCP is not configured."
705
+
706
+ try:
707
+ server_status = self.mcp_manager.get_server_status()
708
+
709
+ servers_info = []
710
+ servers_info.append("MCP Servers")
711
+ servers_info.append("=" * 30)
712
+
713
+ for server_name, status in server_status.items():
714
+ icon = "🟢" if status["connected"] else "🔴"
715
+ servers_info.append(f"{icon} {server_name}")
716
+ servers_info.append(f" Tools: {status['tool_count']}")
717
+ if status["args"]:
718
+ servers_info.append(f" Args: {' '.join(status['args'])}")
719
+
720
+ return "\n".join(servers_info)
721
+
722
+ except Exception as e:
723
+ return f"Error listing servers: {str(e)}"
724
+
725
+ async def _mcp_tools(self, server_name: str = None) -> str:
726
+ """List tools from specific server or all servers."""
727
+ if not self.mcp_manager:
728
+ return "MCP is not configured."
729
+
730
+ try:
731
+ if server_name:
732
+ tools = self.mcp_manager.get_tools_by_server(server_name)
733
+ if not tools:
734
+ return f"No tools found for server '{server_name}' or server not connected."
735
+
736
+ tools_info = []
737
+ tools_info.append(f"Tools from {server_name}")
738
+ tools_info.append("=" * 40)
739
+
740
+ for tool in tools:
741
+ name = getattr(tool, "name", "Unknown")
742
+ description = getattr(tool, "description", "No description")
743
+ tools_info.append(f"🔧 {name}")
744
+ if description:
745
+ tools_info.append(f" {description}")
746
+
747
+ return "\n".join(tools_info)
748
+ else:
749
+ # Return summary of all tools
750
+ return await self._get_tools_list()
751
+
752
+ except Exception as e:
753
+ return f"Error listing tools: {str(e)}"
754
+
755
+ def _mcp_help(self) -> str:
756
+ """Show MCP command help."""
757
+ help_text = """
758
+ MCP Commands:
759
+ =============
760
+
761
+ /mcp status - Show MCP system status
762
+ /mcp health - Show server health status
763
+ /mcp servers - List all configured servers
764
+ /mcp tools - List all available tools
765
+ /mcp tools <server> - List tools from specific server
766
+ /mcp reload - Reload MCP configuration
767
+ /mcp connect [server] - Connect to server(s)
768
+ /mcp disconnect [server] - Disconnect from server(s)
769
+
770
+ Examples:
771
+ /mcp status
772
+ /mcp tools filesystem
773
+ /mcp health
774
+ """
775
+ return help_text.strip()
776
+
777
+ def _initialize_agent_engine(self) -> None:
778
+ """Initialize the agent engine for autonomous operations."""
779
+ try:
780
+ from .agent_engine import AgentEngine
781
+
782
+ self.agent_engine = AgentEngine(self.config_manager)
783
+ logger.info("Agent engine initialized successfully")
784
+ except Exception as e:
785
+ logger.warning(f"Failed to initialize agent engine: {e}")
786
+ # Don't fail completely if agent engine can't be initialized
787
+ self.agent_engine = None