claude-mpm 4.0.23__py3-none-any.whl → 4.0.28__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 (60) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
  4. claude_mpm/agents/BASE_PM.md +3 -0
  5. claude_mpm/agents/templates/code_analyzer.json +2 -2
  6. claude_mpm/cli/commands/agents.py +453 -113
  7. claude_mpm/cli/commands/aggregate.py +107 -15
  8. claude_mpm/cli/commands/cleanup.py +142 -10
  9. claude_mpm/cli/commands/config.py +358 -224
  10. claude_mpm/cli/commands/info.py +184 -75
  11. claude_mpm/cli/commands/mcp_command_router.py +5 -76
  12. claude_mpm/cli/commands/mcp_install_commands.py +68 -36
  13. claude_mpm/cli/commands/mcp_server_commands.py +30 -37
  14. claude_mpm/cli/commands/memory.py +331 -61
  15. claude_mpm/cli/commands/monitor.py +101 -7
  16. claude_mpm/cli/commands/run.py +368 -8
  17. claude_mpm/cli/commands/tickets.py +206 -24
  18. claude_mpm/cli/parsers/mcp_parser.py +3 -0
  19. claude_mpm/cli/shared/__init__.py +40 -0
  20. claude_mpm/cli/shared/argument_patterns.py +212 -0
  21. claude_mpm/cli/shared/command_base.py +234 -0
  22. claude_mpm/cli/shared/error_handling.py +238 -0
  23. claude_mpm/cli/shared/output_formatters.py +231 -0
  24. claude_mpm/config/agent_config.py +29 -8
  25. claude_mpm/core/container.py +6 -4
  26. claude_mpm/core/service_registry.py +4 -2
  27. claude_mpm/core/shared/__init__.py +17 -0
  28. claude_mpm/core/shared/config_loader.py +320 -0
  29. claude_mpm/core/shared/path_resolver.py +277 -0
  30. claude_mpm/core/shared/singleton_manager.py +208 -0
  31. claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
  32. claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
  33. claude_mpm/hooks/memory_integration_hook.py +11 -2
  34. claude_mpm/services/agents/deployment/agent_deployment.py +49 -23
  35. claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
  36. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
  37. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
  38. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
  39. claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
  40. claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
  41. claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
  42. claude_mpm/services/event_aggregator.py +4 -2
  43. claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
  44. claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
  45. claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
  46. claude_mpm/services/memory/builder.py +6 -1
  47. claude_mpm/services/response_tracker.py +3 -1
  48. claude_mpm/services/runner_configuration_service.py +15 -6
  49. claude_mpm/services/shared/__init__.py +20 -0
  50. claude_mpm/services/shared/async_service_base.py +219 -0
  51. claude_mpm/services/shared/config_service_base.py +292 -0
  52. claude_mpm/services/shared/lifecycle_service_base.py +317 -0
  53. claude_mpm/services/shared/manager_base.py +303 -0
  54. claude_mpm/services/shared/service_factory.py +308 -0
  55. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/METADATA +19 -13
  56. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/RECORD +60 -44
  57. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/WHEEL +0 -0
  58. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,234 @@
1
+ """
2
+ Base command class to reduce duplication in CLI command implementations.
3
+ """
4
+
5
+ import os
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ from ...core.config import Config
12
+ from ...core.logger import get_logger
13
+ from ...core.shared.config_loader import ConfigLoader
14
+
15
+
16
+ @dataclass
17
+ class CommandResult:
18
+ """Standard result structure for CLI commands."""
19
+
20
+ success: bool
21
+ exit_code: int = 0
22
+ message: Optional[str] = None
23
+ data: Optional[Dict[str, Any]] = None
24
+
25
+ @classmethod
26
+ def success_result(cls, message: str = None, data: Dict[str, Any] = None) -> "CommandResult":
27
+ """Create a success result."""
28
+ return cls(success=True, exit_code=0, message=message, data=data)
29
+
30
+ @classmethod
31
+ def error_result(cls, message: str, exit_code: int = 1, data: Dict[str, Any] = None) -> "CommandResult":
32
+ """Create an error result."""
33
+ return cls(success=False, exit_code=exit_code, message=message, data=data)
34
+
35
+
36
+ class BaseCommand(ABC):
37
+ """
38
+ Base class for CLI commands to reduce duplication.
39
+
40
+ Provides common functionality:
41
+ - Logger initialization
42
+ - Configuration loading
43
+ - Working directory handling
44
+ - Standard error handling patterns
45
+ """
46
+
47
+ def __init__(self, command_name: str):
48
+ """
49
+ Initialize base command.
50
+
51
+ Args:
52
+ command_name: Name of the command for logging
53
+ """
54
+ self.command_name = command_name
55
+ self.logger = get_logger(f"cli.{command_name}")
56
+ self._config: Optional[Config] = None
57
+ self._working_dir: Optional[Path] = None
58
+
59
+ @property
60
+ def config(self) -> Config:
61
+ """Get configuration instance (lazy loaded)."""
62
+ if self._config is None:
63
+ config_loader = ConfigLoader()
64
+ self._config = config_loader.load_main_config()
65
+ return self._config
66
+
67
+ @property
68
+ def working_dir(self) -> Path:
69
+ """Get working directory (respects CLAUDE_MPM_USER_PWD)."""
70
+ if self._working_dir is None:
71
+ # Use CLAUDE_MPM_USER_PWD if available (when called via shell script)
72
+ user_pwd = os.environ.get("CLAUDE_MPM_USER_PWD", os.getcwd())
73
+ self._working_dir = Path(user_pwd)
74
+ return self._working_dir
75
+
76
+ def setup_logging(self, args) -> None:
77
+ """Setup logging based on command arguments."""
78
+ import logging
79
+
80
+ # Set log level based on arguments
81
+ if hasattr(args, 'debug') and args.debug:
82
+ logging.getLogger().setLevel(logging.DEBUG)
83
+ elif hasattr(args, 'verbose') and args.verbose:
84
+ logging.getLogger().setLevel(logging.INFO)
85
+ elif hasattr(args, 'quiet') and args.quiet:
86
+ logging.getLogger().setLevel(logging.WARNING)
87
+
88
+ def load_config(self, args) -> None:
89
+ """Load configuration from arguments."""
90
+ config_loader = ConfigLoader()
91
+ if hasattr(args, 'config') and args.config:
92
+ # Use specific config file with ConfigLoader
93
+ from ...core.shared.config_loader import ConfigPattern
94
+ pattern = ConfigPattern(
95
+ filenames=[Path(args.config).name],
96
+ search_paths=[str(Path(args.config).parent)],
97
+ env_prefix="CLAUDE_MPM_"
98
+ )
99
+ self._config = config_loader.load_config(pattern, cache_key=f"cli_{args.config}")
100
+ else:
101
+ self._config = config_loader.load_main_config()
102
+
103
+ def validate_args(self, args) -> Optional[str]:
104
+ """
105
+ Validate command arguments.
106
+
107
+ Args:
108
+ args: Parsed command arguments
109
+
110
+ Returns:
111
+ Error message if validation fails, None if valid
112
+ """
113
+ # Base validation - subclasses can override
114
+ return None
115
+
116
+ def execute(self, args) -> CommandResult:
117
+ """
118
+ Execute the command with standard error handling.
119
+
120
+ Args:
121
+ args: Parsed command arguments
122
+
123
+ Returns:
124
+ CommandResult with execution status
125
+ """
126
+ try:
127
+ # Setup
128
+ self.setup_logging(args)
129
+ self.load_config(args)
130
+
131
+ # Validate arguments
132
+ validation_error = self.validate_args(args)
133
+ if validation_error:
134
+ return CommandResult.error_result(validation_error)
135
+
136
+ # Execute command-specific logic
137
+ return self.run(args)
138
+
139
+ except KeyboardInterrupt:
140
+ self.logger.info("Command interrupted by user")
141
+ return CommandResult.error_result("Operation cancelled by user", exit_code=130)
142
+
143
+ except Exception as e:
144
+ self.logger.error(f"Command failed: {e}", exc_info=True)
145
+ return CommandResult.error_result(f"Command failed: {e}")
146
+
147
+ @abstractmethod
148
+ def run(self, args) -> CommandResult:
149
+ """
150
+ Run the command-specific logic.
151
+
152
+ Args:
153
+ args: Parsed command arguments
154
+
155
+ Returns:
156
+ CommandResult with execution status
157
+ """
158
+ pass
159
+
160
+ def print_result(self, result: CommandResult, args) -> None:
161
+ """
162
+ Print command result based on output format.
163
+
164
+ Args:
165
+ result: Command result to print
166
+ args: Command arguments (for format options)
167
+ """
168
+ from .output_formatters import format_output
169
+
170
+ # Determine output format
171
+ output_format = getattr(args, 'format', 'text')
172
+
173
+ # Format and print result
174
+ formatted_output = format_output(result, output_format)
175
+
176
+ if hasattr(args, 'output') and args.output:
177
+ # Write to file
178
+ with open(args.output, 'w') as f:
179
+ f.write(formatted_output)
180
+ self.logger.info(f"Output written to {args.output}")
181
+ else:
182
+ # Print to stdout
183
+ print(formatted_output)
184
+
185
+
186
+ class ServiceCommand(BaseCommand):
187
+ """Base class for commands that work with services."""
188
+
189
+ def __init__(self, command_name: str, service_class: type):
190
+ """
191
+ Initialize service command.
192
+
193
+ Args:
194
+ command_name: Name of the command
195
+ service_class: Service class to instantiate
196
+ """
197
+ super().__init__(command_name)
198
+ self.service_class = service_class
199
+ self._service = None
200
+
201
+ @property
202
+ def service(self):
203
+ """Get service instance (lazy loaded)."""
204
+ if self._service is None:
205
+ self._service = self.service_class()
206
+ return self._service
207
+
208
+
209
+ class AgentCommand(BaseCommand):
210
+ """Base class for agent-related commands."""
211
+
212
+ def get_agent_dir(self, args) -> Path:
213
+ """Get agent directory from arguments or default."""
214
+ if hasattr(args, 'agent_dir') and args.agent_dir:
215
+ return args.agent_dir
216
+
217
+ # Default to working directory
218
+ return self.working_dir
219
+
220
+ def get_agent_pattern(self, args) -> Optional[str]:
221
+ """Get agent name pattern from arguments."""
222
+ return getattr(args, 'agent', None)
223
+
224
+
225
+ class MemoryCommand(BaseCommand):
226
+ """Base class for memory-related commands."""
227
+
228
+ def get_memory_dir(self, args) -> Path:
229
+ """Get memory directory from arguments or default."""
230
+ if hasattr(args, 'memory_dir') and args.memory_dir:
231
+ return args.memory_dir
232
+
233
+ # Default to .claude-mpm/memories in working directory
234
+ return self.working_dir / ".claude-mpm" / "memories"
@@ -0,0 +1,238 @@
1
+ """
2
+ Common error handling patterns for CLI commands.
3
+ """
4
+
5
+ import traceback
6
+ from functools import wraps
7
+ from typing import Any, Callable
8
+
9
+ from ...core.logger import get_logger
10
+
11
+
12
+ class CLIErrorHandler:
13
+ """Centralized error handling for CLI commands."""
14
+
15
+ def __init__(self, command_name: str):
16
+ """
17
+ Initialize error handler.
18
+
19
+ Args:
20
+ command_name: Name of the command for logging context
21
+ """
22
+ self.command_name = command_name
23
+ self.logger = get_logger(f"cli.{command_name}")
24
+
25
+ def handle_error(self, error: Exception, context: str = None) -> int:
26
+ """
27
+ Handle an error with appropriate logging and user feedback.
28
+
29
+ Args:
30
+ error: The exception that occurred
31
+ context: Additional context about when the error occurred
32
+
33
+ Returns:
34
+ Appropriate exit code
35
+ """
36
+ # Build error message
37
+ error_msg = str(error)
38
+ if context:
39
+ error_msg = f"{context}: {error_msg}"
40
+
41
+ # Determine error type and appropriate response
42
+ if isinstance(error, KeyboardInterrupt):
43
+ self.logger.info("Operation cancelled by user")
44
+ print("\nOperation cancelled by user.")
45
+ return 130 # Standard exit code for SIGINT
46
+
47
+ elif isinstance(error, FileNotFoundError):
48
+ self.logger.error(f"File not found: {error}")
49
+ print(f"Error: File not found - {error}")
50
+ return 2
51
+
52
+ elif isinstance(error, PermissionError):
53
+ self.logger.error(f"Permission denied: {error}")
54
+ print(f"Error: Permission denied - {error}")
55
+ return 13
56
+
57
+ elif isinstance(error, ValueError):
58
+ self.logger.error(f"Invalid value: {error}")
59
+ print(f"Error: Invalid value - {error}")
60
+ return 22
61
+
62
+ else:
63
+ # Generic error handling
64
+ self.logger.error(f"Command failed: {error}", exc_info=True)
65
+ print(f"Error: {error_msg}")
66
+
67
+ # Show traceback in debug mode
68
+ if self.logger.isEnabledFor(10): # DEBUG level
69
+ traceback.print_exc()
70
+
71
+ return 1
72
+
73
+ def handle_validation_error(self, message: str) -> int:
74
+ """
75
+ Handle validation errors.
76
+
77
+ Args:
78
+ message: Validation error message
79
+
80
+ Returns:
81
+ Exit code for validation errors
82
+ """
83
+ self.logger.error(f"Validation error: {message}")
84
+ print(f"Error: {message}")
85
+ return 22 # Invalid argument exit code
86
+
87
+ def handle_config_error(self, error: Exception) -> int:
88
+ """
89
+ Handle configuration-related errors.
90
+
91
+ Args:
92
+ error: Configuration error
93
+
94
+ Returns:
95
+ Exit code for configuration errors
96
+ """
97
+ self.logger.error(f"Configuration error: {error}")
98
+ print(f"Configuration Error: {error}")
99
+ print("Please check your configuration file and try again.")
100
+ return 78 # Configuration error exit code
101
+
102
+
103
+ def handle_cli_errors(command_name: str):
104
+ """
105
+ Decorator to add standard error handling to CLI command functions.
106
+
107
+ Args:
108
+ command_name: Name of the command for error context
109
+
110
+ Returns:
111
+ Decorated function with error handling
112
+ """
113
+ def decorator(func: Callable) -> Callable:
114
+ @wraps(func)
115
+ def wrapper(*args, **kwargs) -> int:
116
+ error_handler = CLIErrorHandler(command_name)
117
+
118
+ try:
119
+ result = func(*args, **kwargs)
120
+
121
+ # Handle different return types
122
+ if isinstance(result, int):
123
+ return result
124
+ elif hasattr(result, 'exit_code'):
125
+ return result.exit_code
126
+ else:
127
+ return 0 # Success
128
+
129
+ except Exception as e:
130
+ return error_handler.handle_error(e)
131
+
132
+ return wrapper
133
+ return decorator
134
+
135
+
136
+ def safe_execute(func: Callable, *args, error_handler: CLIErrorHandler = None, **kwargs) -> Any:
137
+ """
138
+ Safely execute a function with error handling.
139
+
140
+ Args:
141
+ func: Function to execute
142
+ *args: Function arguments
143
+ error_handler: Optional error handler
144
+ **kwargs: Function keyword arguments
145
+
146
+ Returns:
147
+ Function result or None if error occurred
148
+ """
149
+ try:
150
+ return func(*args, **kwargs)
151
+ except Exception as e:
152
+ if error_handler:
153
+ error_handler.handle_error(e)
154
+ else:
155
+ # Fallback error handling
156
+ logger = get_logger("cli.safe_execute")
157
+ logger.error(f"Error executing {func.__name__}: {e}", exc_info=True)
158
+ return None
159
+
160
+
161
+ def validate_file_exists(file_path: str, error_handler: CLIErrorHandler = None) -> bool:
162
+ """
163
+ Validate that a file exists.
164
+
165
+ Args:
166
+ file_path: Path to validate
167
+ error_handler: Optional error handler
168
+
169
+ Returns:
170
+ True if file exists, False otherwise
171
+ """
172
+ from pathlib import Path
173
+
174
+ path = Path(file_path)
175
+ if not path.exists():
176
+ message = f"File does not exist: {file_path}"
177
+ if error_handler:
178
+ error_handler.handle_validation_error(message)
179
+ return False
180
+
181
+ if not path.is_file():
182
+ message = f"Path is not a file: {file_path}"
183
+ if error_handler:
184
+ error_handler.handle_validation_error(message)
185
+ return False
186
+
187
+ return True
188
+
189
+
190
+ def validate_directory_exists(dir_path: str, error_handler: CLIErrorHandler = None) -> bool:
191
+ """
192
+ Validate that a directory exists.
193
+
194
+ Args:
195
+ dir_path: Directory path to validate
196
+ error_handler: Optional error handler
197
+
198
+ Returns:
199
+ True if directory exists, False otherwise
200
+ """
201
+ from pathlib import Path
202
+
203
+ path = Path(dir_path)
204
+ if not path.exists():
205
+ message = f"Directory does not exist: {dir_path}"
206
+ if error_handler:
207
+ error_handler.handle_validation_error(message)
208
+ return False
209
+
210
+ if not path.is_dir():
211
+ message = f"Path is not a directory: {dir_path}"
212
+ if error_handler:
213
+ error_handler.handle_validation_error(message)
214
+ return False
215
+
216
+ return True
217
+
218
+
219
+ def confirm_operation(message: str, force: bool = False) -> bool:
220
+ """
221
+ Ask user for confirmation unless force flag is set.
222
+
223
+ Args:
224
+ message: Confirmation message
225
+ force: If True, skip confirmation
226
+
227
+ Returns:
228
+ True if operation should proceed
229
+ """
230
+ if force:
231
+ return True
232
+
233
+ try:
234
+ response = input(f"{message} (y/N): ").strip().lower()
235
+ return response in ('y', 'yes')
236
+ except (EOFError, KeyboardInterrupt):
237
+ print("\nOperation cancelled.")
238
+ return False
@@ -0,0 +1,231 @@
1
+ """
2
+ Output formatting utilities for CLI commands.
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Dict, List, Union
7
+
8
+ import yaml
9
+
10
+
11
+ class OutputFormatter:
12
+ """Handles formatting output in different formats."""
13
+
14
+ @staticmethod
15
+ def format_json(data: Any, indent: int = 2) -> str:
16
+ """Format data as JSON."""
17
+ return json.dumps(data, indent=indent, default=str)
18
+
19
+ @staticmethod
20
+ def format_yaml(data: Any) -> str:
21
+ """Format data as YAML."""
22
+ return yaml.dump(data, default_flow_style=False, sort_keys=False)
23
+
24
+ @staticmethod
25
+ def format_table(data: Union[List[Dict], Dict], headers: List[str] = None) -> str:
26
+ """Format data as a simple table."""
27
+ if not data:
28
+ return "No data to display"
29
+
30
+ # Handle single dict
31
+ if isinstance(data, dict):
32
+ data = [data]
33
+
34
+ # Auto-detect headers if not provided
35
+ if headers is None and data:
36
+ headers = list(data[0].keys())
37
+
38
+ if not headers:
39
+ return "No data to display"
40
+
41
+ # Calculate column widths
42
+ col_widths = {}
43
+ for header in headers:
44
+ col_widths[header] = len(header)
45
+ for row in data:
46
+ value = str(row.get(header, ''))
47
+ col_widths[header] = max(col_widths[header], len(value))
48
+
49
+ # Build table
50
+ lines = []
51
+
52
+ # Header row
53
+ header_row = " | ".join(header.ljust(col_widths[header]) for header in headers)
54
+ lines.append(header_row)
55
+
56
+ # Separator row
57
+ separator = " | ".join("-" * col_widths[header] for header in headers)
58
+ lines.append(separator)
59
+
60
+ # Data rows
61
+ for row in data:
62
+ data_row = " | ".join(
63
+ str(row.get(header, '')).ljust(col_widths[header])
64
+ for header in headers
65
+ )
66
+ lines.append(data_row)
67
+
68
+ return "\n".join(lines)
69
+
70
+ @staticmethod
71
+ def format_text(data: Any) -> str:
72
+ """Format data as human-readable text."""
73
+ if isinstance(data, dict):
74
+ lines = []
75
+ for key, value in data.items():
76
+ if isinstance(value, (dict, list)):
77
+ lines.append(f"{key}:")
78
+ # Indent nested content
79
+ nested = OutputFormatter.format_text(value)
80
+ for line in nested.split('\n'):
81
+ if line.strip():
82
+ lines.append(f" {line}")
83
+ else:
84
+ lines.append(f"{key}: {value}")
85
+ return "\n".join(lines)
86
+
87
+ elif isinstance(data, list):
88
+ if not data:
89
+ return "No items"
90
+
91
+ lines = []
92
+ for i, item in enumerate(data):
93
+ if isinstance(item, dict):
94
+ lines.append(f"Item {i + 1}:")
95
+ nested = OutputFormatter.format_text(item)
96
+ for line in nested.split('\n'):
97
+ if line.strip():
98
+ lines.append(f" {line}")
99
+ else:
100
+ lines.append(f"- {item}")
101
+ return "\n".join(lines)
102
+
103
+ else:
104
+ return str(data)
105
+
106
+
107
+ def format_output(data: Any, format_type: str = "text", **kwargs) -> str:
108
+ """
109
+ Format data according to the specified format.
110
+
111
+ Args:
112
+ data: Data to format
113
+ format_type: Output format ('json', 'yaml', 'table', 'text')
114
+ **kwargs: Additional formatting options
115
+
116
+ Returns:
117
+ Formatted string
118
+ """
119
+ formatter = OutputFormatter()
120
+
121
+ if format_type == "json":
122
+ return formatter.format_json(data, **kwargs)
123
+ elif format_type == "yaml":
124
+ return formatter.format_yaml(data)
125
+ elif format_type == "table":
126
+ return formatter.format_table(data, **kwargs)
127
+ elif format_type == "text":
128
+ return formatter.format_text(data)
129
+ else:
130
+ # Fallback to text format
131
+ return formatter.format_text(data)
132
+
133
+
134
+ def format_success_message(message: str, data: Any = None, format_type: str = "text") -> str:
135
+ """
136
+ Format a success message with optional data.
137
+
138
+ Args:
139
+ message: Success message
140
+ data: Optional data to include
141
+ format_type: Output format
142
+
143
+ Returns:
144
+ Formatted success message
145
+ """
146
+ if format_type in ("json", "yaml"):
147
+ result = {"success": True, "message": message}
148
+ if data is not None:
149
+ result["data"] = data
150
+ return format_output(result, format_type)
151
+ else:
152
+ # Text format
153
+ lines = [f"✓ {message}"]
154
+ if data is not None:
155
+ lines.append("")
156
+ lines.append(format_output(data, format_type))
157
+ return "\n".join(lines)
158
+
159
+
160
+ def format_error_message(message: str, details: Any = None, format_type: str = "text") -> str:
161
+ """
162
+ Format an error message with optional details.
163
+
164
+ Args:
165
+ message: Error message
166
+ details: Optional error details
167
+ format_type: Output format
168
+
169
+ Returns:
170
+ Formatted error message
171
+ """
172
+ if format_type in ("json", "yaml"):
173
+ result = {"success": False, "error": message}
174
+ if details is not None:
175
+ result["details"] = details
176
+ return format_output(result, format_type)
177
+ else:
178
+ # Text format
179
+ lines = [f"✗ {message}"]
180
+ if details is not None:
181
+ lines.append("")
182
+ lines.append(format_output(details, format_type))
183
+ return "\n".join(lines)
184
+
185
+
186
+ def format_list_output(items: List[Any],
187
+ title: str = None,
188
+ format_type: str = "text",
189
+ headers: List[str] = None) -> str:
190
+ """
191
+ Format a list of items for output.
192
+
193
+ Args:
194
+ items: List of items to format
195
+ title: Optional title for the list
196
+ format_type: Output format
197
+ headers: Optional headers for table format
198
+
199
+ Returns:
200
+ Formatted list output
201
+ """
202
+ if not items:
203
+ empty_msg = f"No {title.lower() if title else 'items'} found"
204
+ if format_type in ("json", "yaml"):
205
+ return format_output({"items": [], "message": empty_msg}, format_type)
206
+ else:
207
+ return empty_msg
208
+
209
+ if format_type in ("json", "yaml"):
210
+ result = {"items": items}
211
+ if title:
212
+ result["title"] = title
213
+ return format_output(result, format_type)
214
+
215
+ elif format_type == "table":
216
+ output = ""
217
+ if title:
218
+ output += f"{title}\n{'=' * len(title)}\n\n"
219
+ output += format_output(items, "table", headers=headers)
220
+ return output
221
+
222
+ else:
223
+ # Text format
224
+ lines = []
225
+ if title:
226
+ lines.append(title)
227
+ lines.append("=" * len(title))
228
+ lines.append("")
229
+
230
+ lines.append(format_output(items, "text"))
231
+ return "\n".join(lines)