claude-mpm 3.7.8__py3-none-any.whl → 3.9.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 (100) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +0 -106
  3. claude_mpm/agents/INSTRUCTIONS.md +0 -96
  4. claude_mpm/agents/MEMORY.md +94 -0
  5. claude_mpm/agents/WORKFLOW.md +86 -0
  6. claude_mpm/agents/templates/code_analyzer.json +2 -2
  7. claude_mpm/agents/templates/data_engineer.json +1 -1
  8. claude_mpm/agents/templates/documentation.json +1 -1
  9. claude_mpm/agents/templates/engineer.json +1 -1
  10. claude_mpm/agents/templates/ops.json +1 -1
  11. claude_mpm/agents/templates/qa.json +1 -1
  12. claude_mpm/agents/templates/research.json +1 -1
  13. claude_mpm/agents/templates/security.json +1 -1
  14. claude_mpm/agents/templates/ticketing.json +3 -8
  15. claude_mpm/agents/templates/version_control.json +1 -1
  16. claude_mpm/agents/templates/web_qa.json +2 -2
  17. claude_mpm/agents/templates/web_ui.json +2 -2
  18. claude_mpm/cli/__init__.py +2 -2
  19. claude_mpm/cli/commands/__init__.py +2 -1
  20. claude_mpm/cli/commands/agents.py +8 -3
  21. claude_mpm/cli/commands/tickets.py +596 -19
  22. claude_mpm/cli/parser.py +217 -5
  23. claude_mpm/config/__init__.py +30 -39
  24. claude_mpm/config/socketio_config.py +8 -5
  25. claude_mpm/constants.py +13 -0
  26. claude_mpm/core/__init__.py +8 -18
  27. claude_mpm/core/cache.py +596 -0
  28. claude_mpm/core/claude_runner.py +166 -622
  29. claude_mpm/core/config.py +7 -3
  30. claude_mpm/core/constants.py +339 -0
  31. claude_mpm/core/container.py +548 -38
  32. claude_mpm/core/exceptions.py +392 -0
  33. claude_mpm/core/framework_loader.py +249 -93
  34. claude_mpm/core/interactive_session.py +479 -0
  35. claude_mpm/core/interfaces.py +424 -0
  36. claude_mpm/core/lazy.py +467 -0
  37. claude_mpm/core/logging_config.py +444 -0
  38. claude_mpm/core/oneshot_session.py +465 -0
  39. claude_mpm/core/optimized_agent_loader.py +485 -0
  40. claude_mpm/core/optimized_startup.py +490 -0
  41. claude_mpm/core/service_registry.py +52 -26
  42. claude_mpm/core/socketio_pool.py +162 -5
  43. claude_mpm/core/types.py +292 -0
  44. claude_mpm/core/typing_utils.py +477 -0
  45. claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
  46. claude_mpm/init.py +2 -1
  47. claude_mpm/services/__init__.py +78 -14
  48. claude_mpm/services/agent/__init__.py +24 -0
  49. claude_mpm/services/agent/deployment.py +2548 -0
  50. claude_mpm/services/agent/management.py +598 -0
  51. claude_mpm/services/agent/registry.py +813 -0
  52. claude_mpm/services/agents/deployment/agent_deployment.py +728 -308
  53. claude_mpm/services/agents/memory/agent_memory_manager.py +160 -4
  54. claude_mpm/services/async_session_logger.py +8 -3
  55. claude_mpm/services/communication/__init__.py +21 -0
  56. claude_mpm/services/communication/socketio.py +1933 -0
  57. claude_mpm/services/communication/websocket.py +479 -0
  58. claude_mpm/services/core/__init__.py +123 -0
  59. claude_mpm/services/core/base.py +247 -0
  60. claude_mpm/services/core/interfaces.py +951 -0
  61. claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
  62. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
  63. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
  64. claude_mpm/services/framework_claude_md_generator.py +3 -2
  65. claude_mpm/services/health_monitor.py +4 -3
  66. claude_mpm/services/hook_service.py +64 -4
  67. claude_mpm/services/infrastructure/__init__.py +21 -0
  68. claude_mpm/services/infrastructure/logging.py +202 -0
  69. claude_mpm/services/infrastructure/monitoring.py +893 -0
  70. claude_mpm/services/memory/indexed_memory.py +648 -0
  71. claude_mpm/services/project/__init__.py +21 -0
  72. claude_mpm/services/project/analyzer.py +864 -0
  73. claude_mpm/services/project/registry.py +608 -0
  74. claude_mpm/services/project_analyzer.py +95 -2
  75. claude_mpm/services/recovery_manager.py +15 -9
  76. claude_mpm/services/response_tracker.py +3 -5
  77. claude_mpm/services/socketio/__init__.py +25 -0
  78. claude_mpm/services/socketio/handlers/__init__.py +25 -0
  79. claude_mpm/services/socketio/handlers/base.py +121 -0
  80. claude_mpm/services/socketio/handlers/connection.py +198 -0
  81. claude_mpm/services/socketio/handlers/file.py +213 -0
  82. claude_mpm/services/socketio/handlers/git.py +723 -0
  83. claude_mpm/services/socketio/handlers/memory.py +27 -0
  84. claude_mpm/services/socketio/handlers/project.py +25 -0
  85. claude_mpm/services/socketio/handlers/registry.py +145 -0
  86. claude_mpm/services/socketio_client_manager.py +12 -7
  87. claude_mpm/services/socketio_server.py +156 -30
  88. claude_mpm/services/ticket_manager.py +172 -9
  89. claude_mpm/services/ticket_manager_di.py +1 -1
  90. claude_mpm/services/version_control/semantic_versioning.py +80 -7
  91. claude_mpm/services/version_control/version_parser.py +528 -0
  92. claude_mpm/utils/error_handler.py +1 -1
  93. claude_mpm/validation/agent_validator.py +27 -14
  94. claude_mpm/validation/frontmatter_validator.py +231 -0
  95. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/METADATA +38 -128
  96. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/RECORD +100 -59
  97. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/WHEEL +0 -0
  98. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/entry_points.txt +0 -0
  99. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/licenses/LICENSE +0 -0
  100. {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,479 @@
1
+ """Interactive session handler for Claude runner.
2
+
3
+ This module provides the InteractiveSession class that manages Claude's interactive mode
4
+ with proper separation of concerns and reduced complexity.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any, Tuple
12
+ import uuid
13
+ from datetime import datetime
14
+ import json
15
+
16
+ from claude_mpm.core.logger import get_logger
17
+
18
+
19
+ class InteractiveSession:
20
+ """
21
+ Handles interactive Claude sessions with proper separation of concerns.
22
+
23
+ WHY: The original run_interactive() method had complexity of 39 and 262 lines.
24
+ This class breaks down that functionality into smaller, focused methods with
25
+ complexity <10 and lines <80 each, improving maintainability and testability.
26
+
27
+ DESIGN DECISION: Uses composition over inheritance - takes ClaudeRunner as
28
+ dependency rather than inheriting from it. This maintains loose coupling
29
+ and makes testing easier while preserving all original functionality.
30
+ """
31
+
32
+ def __init__(self, runner):
33
+ """Initialize interactive session handler.
34
+
35
+ Args:
36
+ runner: ClaudeRunner instance with all necessary services
37
+ """
38
+ self.runner = runner
39
+ self.logger = get_logger("interactive_session")
40
+ self.session_id = None
41
+ self.original_cwd = os.getcwd()
42
+
43
+ # Initialize response tracking for interactive sessions
44
+ # WHY: Interactive sessions need response logging just like oneshot sessions.
45
+ # The hook system captures events, but we need the ResponseTracker to be
46
+ # initialized to actually store them.
47
+ self.response_tracker = None
48
+
49
+ # Check if response logging is enabled in configuration
50
+ try:
51
+ response_config = self.runner.config.get('response_logging', {})
52
+ response_logging_enabled = response_config.get('enabled', False)
53
+ except (AttributeError, TypeError):
54
+ # Handle mock or missing config gracefully
55
+ response_logging_enabled = False
56
+
57
+ if response_logging_enabled:
58
+ try:
59
+ from claude_mpm.services.response_tracker import ResponseTracker
60
+ self.response_tracker = ResponseTracker(self.runner.config)
61
+ self.logger.info("Response tracking initialized for interactive session")
62
+ except Exception as e:
63
+ self.logger.warning(f"Failed to initialize response tracker: {e}")
64
+ # Continue without response tracking - not fatal
65
+
66
+ def initialize_interactive_session(self) -> Tuple[bool, Optional[str]]:
67
+ """Initialize the interactive session environment.
68
+
69
+ Sets up WebSocket connections, generates session IDs, and prepares
70
+ the session for launch.
71
+
72
+ Returns:
73
+ Tuple of (success, error_message)
74
+ """
75
+ try:
76
+ # Generate session ID
77
+ self.session_id = str(uuid.uuid4())
78
+
79
+ # Initialize WebSocket if enabled
80
+ if self.runner.enable_websocket:
81
+ success, error = self._initialize_websocket()
82
+ if not success:
83
+ self.logger.warning(f"WebSocket initialization failed: {error}")
84
+ # Continue without WebSocket - not a fatal error
85
+
86
+ # Display welcome message
87
+ self._display_welcome_message()
88
+
89
+ # Log session start
90
+ if self.runner.project_logger:
91
+ self.runner.project_logger.log_system(
92
+ "Starting interactive session",
93
+ level="INFO",
94
+ component="session"
95
+ )
96
+
97
+ # Initialize response tracking session if enabled
98
+ # WHY: The ResponseTracker needs to know about the session to properly
99
+ # correlate prompts and responses during the interactive session.
100
+ if self.response_tracker and self.response_tracker.enabled:
101
+ try:
102
+ # Set the session ID in the tracker for correlation
103
+ if hasattr(self.response_tracker, 'session_logger') and self.response_tracker.session_logger:
104
+ self.response_tracker.session_logger.set_session_id(self.session_id)
105
+ self.logger.debug(f"Response tracker session ID set to: {self.session_id}")
106
+ except Exception as e:
107
+ self.logger.debug(f"Could not set session ID in response tracker: {e}")
108
+
109
+ return True, None
110
+
111
+ except Exception as e:
112
+ error_msg = f"Failed to initialize session: {e}"
113
+ self.logger.error(error_msg)
114
+ return False, error_msg
115
+
116
+ def setup_interactive_environment(self) -> Tuple[bool, Dict[str, Any]]:
117
+ """Set up the interactive environment including agents and commands.
118
+
119
+ Deploys system and project agents, prepares the command line,
120
+ and sets up the execution environment.
121
+
122
+ Returns:
123
+ Tuple of (success, environment_dict)
124
+ """
125
+ try:
126
+ # Deploy system agents
127
+ if not self.runner.setup_agents():
128
+ print("Continuing without native agents...")
129
+
130
+ # Deploy project-specific agents
131
+ self.runner.deploy_project_agents_to_claude()
132
+
133
+ # Build command
134
+ cmd = self._build_claude_command()
135
+
136
+ # Prepare environment
137
+ env = self._prepare_environment()
138
+
139
+ # Change to user directory if needed
140
+ self._change_to_user_directory(env)
141
+
142
+ return True, {
143
+ 'command': cmd,
144
+ 'environment': env,
145
+ 'session_id': self.session_id
146
+ }
147
+
148
+ except Exception as e:
149
+ error_msg = f"Failed to setup environment: {e}"
150
+ self.logger.error(error_msg)
151
+ return False, {}
152
+
153
+ def handle_interactive_input(self, environment: Dict[str, Any]) -> bool:
154
+ """Handle the interactive input/output loop.
155
+
156
+ Launches Claude and manages the interactive session using either
157
+ exec or subprocess method based on configuration.
158
+
159
+ Args:
160
+ environment: Dictionary with command, env vars, and session info
161
+
162
+ Returns:
163
+ bool: True if successful, False otherwise
164
+ """
165
+ try:
166
+ cmd = environment['command']
167
+ env = environment['environment']
168
+
169
+ print("Launching Claude...")
170
+
171
+ # Log launch attempt
172
+ self._log_launch_attempt(cmd)
173
+
174
+ # Notify WebSocket if connected
175
+ if self.runner.websocket_server:
176
+ self.runner.websocket_server.claude_status_changed(
177
+ status="starting",
178
+ message="Launching Claude interactive session"
179
+ )
180
+
181
+ # Launch using selected method
182
+ if self.runner.launch_method == "subprocess":
183
+ return self._launch_subprocess_mode(cmd, env)
184
+ else:
185
+ return self._launch_exec_mode(cmd, env)
186
+
187
+ except FileNotFoundError as e:
188
+ self._handle_launch_error("FileNotFoundError", e)
189
+ return False
190
+ except PermissionError as e:
191
+ self._handle_launch_error("PermissionError", e)
192
+ return False
193
+ except OSError as e:
194
+ self._handle_launch_error("OSError", e)
195
+ return self._attempt_fallback_launch(environment)
196
+ except KeyboardInterrupt:
197
+ self._handle_keyboard_interrupt()
198
+ return True # Clean exit
199
+ except Exception as e:
200
+ self._handle_launch_error("Exception", e)
201
+ return self._attempt_fallback_launch(environment)
202
+
203
+ def process_interactive_command(self, prompt: str) -> Optional[bool]:
204
+ """Process special interactive commands like /agents.
205
+
206
+ Args:
207
+ prompt: User input command
208
+
209
+ Returns:
210
+ Optional[bool]: True if handled, False if error, None if not a special command
211
+ """
212
+ # Check for special commands
213
+ if prompt.strip() == "/agents":
214
+ return self._show_available_agents()
215
+
216
+ # Not a special command
217
+ return None
218
+
219
+ def cleanup_interactive_session(self) -> None:
220
+ """Clean up resources after interactive session ends.
221
+
222
+ Restores original directory, closes connections, and logs session end.
223
+ """
224
+ try:
225
+ # Restore original directory
226
+ if self.original_cwd and os.path.exists(self.original_cwd):
227
+ try:
228
+ os.chdir(self.original_cwd)
229
+ except OSError:
230
+ pass
231
+
232
+ # Close WebSocket if connected
233
+ if self.runner.websocket_server:
234
+ self.runner.websocket_server.session_ended()
235
+ self.runner.websocket_server = None
236
+
237
+ # Log session end
238
+ if self.runner.project_logger:
239
+ self.runner.project_logger.log_system(
240
+ "Interactive session ended",
241
+ level="INFO",
242
+ component="session"
243
+ )
244
+
245
+ # Log session event
246
+ if self.runner.session_log_file:
247
+ self.runner._log_session_event({
248
+ "event": "session_end",
249
+ "session_id": self.session_id
250
+ })
251
+
252
+ # Clean up response tracker if initialized
253
+ # WHY: Ensure proper cleanup of response tracking resources and
254
+ # finalize any pending response logs.
255
+ if self.response_tracker:
256
+ try:
257
+ # Clear the session ID to stop tracking this session
258
+ if hasattr(self.response_tracker, 'session_logger') and self.response_tracker.session_logger:
259
+ self.response_tracker.session_logger.set_session_id(None)
260
+ self.logger.debug("Response tracker session cleared")
261
+ except Exception as e:
262
+ self.logger.debug(f"Error clearing response tracker session: {e}")
263
+
264
+ except Exception as e:
265
+ self.logger.debug(f"Error during cleanup: {e}")
266
+
267
+ # Private helper methods (each <80 lines, complexity <10)
268
+
269
+ def _initialize_websocket(self) -> Tuple[bool, Optional[str]]:
270
+ """Initialize WebSocket connection for monitoring."""
271
+ try:
272
+ from claude_mpm.services.socketio_server import SocketIOClientProxy
273
+ self.runner.websocket_server = SocketIOClientProxy(port=self.runner.websocket_port)
274
+ self.runner.websocket_server.start()
275
+ self.logger.info("Connected to Socket.IO monitoring server")
276
+
277
+ # Notify session start
278
+ self.runner.websocket_server.session_started(
279
+ session_id=self.session_id,
280
+ launch_method=self.runner.launch_method,
281
+ working_dir=os.getcwd()
282
+ )
283
+ return True, None
284
+
285
+ except ImportError as e:
286
+ return False, f"Socket.IO module not available: {e}"
287
+ except ConnectionError as e:
288
+ return False, f"Cannot connect to Socket.IO server: {e}"
289
+ except Exception as e:
290
+ return False, f"Unexpected error with Socket.IO: {e}"
291
+
292
+ def _display_welcome_message(self) -> None:
293
+ """Display the interactive session welcome message."""
294
+ version_str = self.runner._get_version()
295
+
296
+ print("\033[32m╭───────────────────────────────────────────────────╮\033[0m")
297
+ print("\033[32m│\033[0m ✻ Claude MPM - Interactive Session \033[32m│\033[0m")
298
+ print(f"\033[32m│\033[0m Version {version_str:<40}\033[32m│\033[0m")
299
+ print("\033[32m│ │\033[0m")
300
+ print("\033[32m│\033[0m Type '/agents' to see available agents \033[32m│\033[0m")
301
+ print("\033[32m╰───────────────────────────────────────────────────╯\033[0m")
302
+ print("") # Add blank line after box
303
+
304
+ def _build_claude_command(self) -> list:
305
+ """Build the Claude command with all necessary arguments."""
306
+ cmd = [
307
+ "claude",
308
+ "--model", "opus",
309
+ "--dangerously-skip-permissions"
310
+ ]
311
+
312
+ # Add custom arguments
313
+ if self.runner.claude_args:
314
+ cmd.extend(self.runner.claude_args)
315
+
316
+ # Add system instructions
317
+ from claude_mpm.core.claude_runner import create_simple_context
318
+ system_prompt = self.runner._create_system_prompt()
319
+ if system_prompt and system_prompt != create_simple_context():
320
+ cmd.extend(["--append-system-prompt", system_prompt])
321
+
322
+ return cmd
323
+
324
+ def _prepare_environment(self) -> dict:
325
+ """Prepare clean environment variables for Claude."""
326
+ clean_env = os.environ.copy()
327
+
328
+ # Remove Claude-specific variables that might interfere
329
+ claude_vars_to_remove = [
330
+ 'CLAUDE_CODE_ENTRYPOINT', 'CLAUDECODE', 'CLAUDE_CONFIG_DIR',
331
+ 'CLAUDE_MAX_PARALLEL_SUBAGENTS', 'CLAUDE_TIMEOUT'
332
+ ]
333
+ for var in claude_vars_to_remove:
334
+ clean_env.pop(var, None)
335
+
336
+ return clean_env
337
+
338
+ def _change_to_user_directory(self, env: dict) -> None:
339
+ """Change to user's working directory if specified."""
340
+ if 'CLAUDE_MPM_USER_PWD' in env:
341
+ user_pwd = env['CLAUDE_MPM_USER_PWD']
342
+ env['CLAUDE_WORKSPACE'] = user_pwd
343
+
344
+ try:
345
+ os.chdir(user_pwd)
346
+ self.logger.info(f"Changed working directory to: {user_pwd}")
347
+ except (PermissionError, FileNotFoundError, OSError) as e:
348
+ self.logger.warning(f"Could not change to directory {user_pwd}: {e}")
349
+
350
+ def _log_launch_attempt(self, cmd: list) -> None:
351
+ """Log the Claude launch attempt."""
352
+ if self.runner.project_logger:
353
+ self.runner.project_logger.log_system(
354
+ f"Launching Claude interactive mode with {self.runner.launch_method}",
355
+ level="INFO",
356
+ component="session"
357
+ )
358
+ self.runner._log_session_event({
359
+ "event": "launching_claude_interactive",
360
+ "command": " ".join(cmd),
361
+ "method": self.runner.launch_method
362
+ })
363
+
364
+ def _launch_exec_mode(self, cmd: list, env: dict) -> bool:
365
+ """Launch Claude using exec mode (replaces current process)."""
366
+ # Notify WebSocket before exec
367
+ if self.runner.websocket_server:
368
+ self.runner.websocket_server.claude_status_changed(
369
+ status="running",
370
+ message="Claude process started (exec mode)"
371
+ )
372
+
373
+ # This will not return if successful
374
+ os.execvpe(cmd[0], cmd, env)
375
+ return False # Only reached on failure
376
+
377
+ def _launch_subprocess_mode(self, cmd: list, env: dict) -> bool:
378
+ """Launch Claude as subprocess with PTY."""
379
+ # Delegate to runner's existing method
380
+ self.runner._launch_subprocess_interactive(cmd, env)
381
+ return True
382
+
383
+ def _handle_launch_error(self, error_type: str, error: Exception) -> None:
384
+ """Handle errors during Claude launch."""
385
+ error_messages = {
386
+ "FileNotFoundError": "Claude CLI not found. Please ensure 'claude' is installed and in your PATH",
387
+ "PermissionError": "Permission denied executing Claude CLI",
388
+ "OSError": "OS error launching Claude",
389
+ "Exception": "Unexpected error launching Claude"
390
+ }
391
+
392
+ error_msg = f"{error_messages.get(error_type, 'Error')}: {error}"
393
+ print(f"❌ {error_msg}")
394
+
395
+ if self.runner.project_logger:
396
+ self.runner.project_logger.log_system(error_msg, level="ERROR", component="session")
397
+ self.runner._log_session_event({
398
+ "event": "interactive_launch_failed",
399
+ "error": str(error),
400
+ "exception_type": error_type
401
+ })
402
+
403
+ # Notify WebSocket of error
404
+ if self.runner.websocket_server:
405
+ self.runner.websocket_server.claude_status_changed(
406
+ status="error",
407
+ message=f"Failed to launch Claude: {error}"
408
+ )
409
+
410
+ def _handle_keyboard_interrupt(self) -> None:
411
+ """Handle keyboard interrupt during session."""
412
+ print("\n⚠️ Session interrupted by user")
413
+
414
+ if self.runner.project_logger:
415
+ self.runner.project_logger.log_system(
416
+ "Session interrupted by user",
417
+ level="INFO",
418
+ component="session"
419
+ )
420
+ self.runner._log_session_event({
421
+ "event": "session_interrupted",
422
+ "reason": "user_interrupt"
423
+ })
424
+
425
+ def _attempt_fallback_launch(self, environment: Dict[str, Any]) -> bool:
426
+ """Attempt fallback launch using subprocess."""
427
+ print("\n🔄 Attempting fallback launch method...")
428
+
429
+ try:
430
+ cmd = environment['command']
431
+ env = environment['environment']
432
+
433
+ result = subprocess.run(cmd, stdin=None, stdout=None, stderr=None, env=env)
434
+
435
+ if result.returncode == 0:
436
+ if self.runner.project_logger:
437
+ self.runner.project_logger.log_system(
438
+ "Interactive session completed (subprocess fallback)",
439
+ level="INFO",
440
+ component="session"
441
+ )
442
+ return True
443
+ else:
444
+ print(f"⚠️ Claude exited with code {result.returncode}")
445
+ return False
446
+
447
+ except FileNotFoundError:
448
+ print("❌ Fallback failed: Claude CLI not found in PATH")
449
+ print("\n💡 To fix this issue:")
450
+ print(" 1. Install Claude CLI: npm install -g @anthropic-ai/claude-ai")
451
+ print(" 2. Or specify the full path to the claude binary")
452
+ return False
453
+ except KeyboardInterrupt:
454
+ print("\n⚠️ Fallback interrupted by user")
455
+ return True
456
+ except Exception as e:
457
+ print(f"❌ Fallback failed with unexpected error: {e}")
458
+ return False
459
+
460
+ def _show_available_agents(self) -> bool:
461
+ """Show available agents in the system."""
462
+ try:
463
+ from claude_mpm.cli import _get_agent_versions_display
464
+ agent_versions = _get_agent_versions_display()
465
+
466
+ if agent_versions:
467
+ print(agent_versions)
468
+ else:
469
+ print("No deployed agents found")
470
+ print("\nTo deploy agents, run: claude-mpm --mpm:agents deploy")
471
+
472
+ return True
473
+
474
+ except ImportError:
475
+ print("Error: CLI module not available")
476
+ return False
477
+ except Exception as e:
478
+ print(f"Error getting agent versions: {e}")
479
+ return False