claude-mpm 4.0.23__py3-none-any.whl → 4.0.25__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 +43 -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.25.dist-info}/METADATA +19 -13
  56. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/RECORD +60 -44
  57. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/WHEEL +0 -0
  58. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,12 @@ Run command implementation for claude-mpm.
5
5
 
6
6
  WHY: This module handles the main 'run' command which starts Claude sessions.
7
7
  It's the most commonly used command and handles both interactive and non-interactive modes.
8
+
9
+ DESIGN DECISIONS:
10
+ - Use BaseCommand for consistent CLI patterns
11
+ - Leverage shared utilities for argument parsing and output formatting
12
+ - Maintain backward compatibility with existing functionality
13
+ - Support multiple output formats (json, yaml, table, text)
8
14
  """
9
15
 
10
16
  import logging
@@ -14,13 +20,16 @@ import sys
14
20
  import time
15
21
  import webbrowser
16
22
  from datetime import datetime
23
+ from typing import Dict, Any, Optional
17
24
 
18
25
  from ...constants import LogLevel
19
26
  from ...core.config import Config
20
27
  from ...core.logger import get_logger
28
+ from ...core.shared.config_loader import ConfigLoader
21
29
  from ...core.unified_paths import get_package_root, get_scripts_dir
22
30
  from ...services.port_manager import PortManager
23
31
  from ...utils.dependency_manager import ensure_socketio_dependencies
32
+ from ..shared import BaseCommand, CommandResult
24
33
  from ..utils import get_user_input, list_agent_versions_at_startup
25
34
 
26
35
 
@@ -175,12 +184,363 @@ You are resuming session {session_id[:8]}... which was:
175
184
  return base_context + session_info
176
185
 
177
186
 
187
+ class RunCommand(BaseCommand):
188
+ """Run command using shared utilities."""
189
+
190
+ def __init__(self):
191
+ super().__init__("run")
192
+
193
+ def validate_args(self, args) -> Optional[str]:
194
+ """Validate command arguments."""
195
+ # Run command has minimal validation requirements
196
+ # Most validation is handled by the ClaudeRunner and related services
197
+ return None
198
+
199
+ def run(self, args) -> CommandResult:
200
+ """Execute the run command."""
201
+ try:
202
+ # Execute the main run logic
203
+ success = self._execute_run_session(args)
204
+
205
+ if success:
206
+ return CommandResult.success_result("Claude session completed successfully")
207
+ else:
208
+ return CommandResult.error_result("Claude session failed", exit_code=1)
209
+
210
+ except KeyboardInterrupt:
211
+ self.logger.info("Session interrupted by user")
212
+ return CommandResult.error_result("Session cancelled by user", exit_code=130)
213
+ except Exception as e:
214
+ self.logger.error(f"Error running Claude session: {e}", exc_info=True)
215
+ return CommandResult.error_result(f"Error running Claude session: {e}")
216
+
217
+ def _execute_run_session(self, args) -> bool:
218
+ """Execute the main run session logic."""
219
+ # For now, delegate to the legacy function to maintain compatibility
220
+ # TODO: Gradually migrate logic into the individual helper methods
221
+ try:
222
+ run_session_legacy(args)
223
+ return True
224
+ except Exception as e:
225
+ self.logger.error(f"Run session failed: {e}")
226
+ return False
227
+
228
+ def _execute_run_session_new(self, args) -> bool:
229
+ """Execute the main run session logic using new pattern."""
230
+ try:
231
+ # Log session start
232
+ if args.logging != LogLevel.OFF.value:
233
+ self.logger.info("Starting Claude MPM session")
234
+
235
+ # Perform startup checks
236
+ self._check_configuration_health()
237
+ self._check_claude_json_memory(args)
238
+
239
+ # Handle session management
240
+ session_manager, resume_session_id, resume_context = self._setup_session_management(args)
241
+
242
+ # Handle dependency checking
243
+ self._handle_dependency_checking(args)
244
+
245
+ # Setup monitoring if requested
246
+ monitor_mode, websocket_port = self._setup_monitoring(args)
247
+
248
+ # Configure Claude runner
249
+ runner = self._setup_claude_runner(args, monitor_mode, websocket_port)
250
+
251
+ # Create context and run session
252
+ context = self._create_session_context(args, session_manager, resume_session_id, resume_context)
253
+
254
+ # Execute the session
255
+ return self._execute_session(args, runner, context)
256
+
257
+ except Exception as e:
258
+ self.logger.error(f"Run session failed: {e}")
259
+ return False
260
+
261
+ def _check_configuration_health(self):
262
+ """Check configuration health at startup."""
263
+ from .run_config_checker import RunConfigChecker
264
+ checker = RunConfigChecker(self.logger)
265
+ checker.check_configuration_health()
266
+
267
+ def _check_claude_json_memory(self, args):
268
+ """Check .claude.json file size and warn about memory issues."""
269
+ from .run_config_checker import RunConfigChecker
270
+ checker = RunConfigChecker(self.logger)
271
+ checker.check_claude_json_memory(args)
272
+
273
+ def _setup_session_management(self, args):
274
+ """Setup session management and handle resumption."""
275
+ try:
276
+ from ...core.session_manager import SessionManager
277
+ except ImportError:
278
+ from claude_mpm.core.session_manager import SessionManager
279
+
280
+ session_manager = SessionManager()
281
+ resume_session_id = None
282
+ resume_context = None
283
+
284
+ if hasattr(args, "mpm_resume") and args.mpm_resume:
285
+ if args.mpm_resume == "last":
286
+ # Resume the last interactive session
287
+ resume_session_id = session_manager.get_last_interactive_session()
288
+ if resume_session_id:
289
+ session_data = session_manager.get_session_by_id(resume_session_id)
290
+ if session_data:
291
+ resume_context = session_data.get("context", "default")
292
+ self.logger.info(f"Resuming session {resume_session_id} (context: {resume_context})")
293
+ print(f"🔄 Resuming session {resume_session_id[:8]}... (created: {session_data.get('created_at', 'unknown')})")
294
+ else:
295
+ self.logger.warning(f"Session {resume_session_id} not found")
296
+ else:
297
+ self.logger.info("No recent interactive sessions found")
298
+ print("ℹ️ No recent interactive sessions found to resume")
299
+ else:
300
+ # Resume specific session by ID
301
+ resume_session_id = args.mpm_resume
302
+ session_data = session_manager.get_session_by_id(resume_session_id)
303
+ if session_data:
304
+ resume_context = session_data.get("context", "default")
305
+ self.logger.info(f"Resuming session {resume_session_id} (context: {resume_context})")
306
+ print(f"🔄 Resuming session {resume_session_id[:8]}... (context: {resume_context})")
307
+ else:
308
+ self.logger.error(f"Session {resume_session_id} not found")
309
+ print(f"❌ Session {resume_session_id} not found")
310
+ print("💡 Use 'claude-mpm sessions' to list available sessions")
311
+ raise RuntimeError(f"Session {resume_session_id} not found")
312
+
313
+ return session_manager, resume_session_id, resume_context
314
+
315
+ def _handle_dependency_checking(self, args):
316
+ """Handle smart dependency checking."""
317
+ # Smart dependency checking - only when needed
318
+ if getattr(args, "check_dependencies", True): # Default to checking
319
+ try:
320
+ from ...utils.agent_dependency_loader import AgentDependencyLoader
321
+ from ...utils.dependency_cache import SmartDependencyChecker
322
+ from ...utils.environment_context import should_prompt_for_dependencies
323
+
324
+ # Initialize smart checker
325
+ smart_checker = SmartDependencyChecker()
326
+ loader = AgentDependencyLoader(auto_install=False)
327
+
328
+ # Check if agents have changed
329
+ has_changed, deployment_hash = loader.has_agents_changed()
330
+
331
+ # Determine if we should check dependencies
332
+ should_check, check_reason = smart_checker.should_check_dependencies(
333
+ force_check=getattr(args, "force_check_dependencies", False),
334
+ deployment_hash=deployment_hash,
335
+ )
336
+
337
+ if should_check:
338
+ self.logger.info(f"Checking dependencies: {check_reason}")
339
+
340
+ # Check if we should prompt for dependencies
341
+ should_prompt = should_prompt_for_dependencies()
342
+
343
+ if should_prompt:
344
+ # Check dependencies and prompt for installation if needed
345
+ missing_deps = loader.check_dependencies()
346
+ if missing_deps:
347
+ self.logger.info(f"Found {len(missing_deps)} missing dependencies")
348
+
349
+ # Prompt user for installation
350
+ print(f"\n📦 Found {len(missing_deps)} missing dependencies:")
351
+ for dep in missing_deps[:5]: # Show first 5
352
+ print(f" • {dep}")
353
+ if len(missing_deps) > 5:
354
+ print(f" ... and {len(missing_deps) - 5} more")
355
+
356
+ response = input("\nInstall missing dependencies? (y/N): ").strip().lower()
357
+ if response in ['y', 'yes']:
358
+ loader.auto_install = True
359
+ loader.install_dependencies(missing_deps)
360
+ print("✅ Dependencies installed successfully")
361
+ else:
362
+ print("⚠️ Continuing without installing dependencies")
363
+ else:
364
+ # Just check without prompting
365
+ missing_deps = loader.check_dependencies()
366
+ if missing_deps:
367
+ self.logger.warning(f"Found {len(missing_deps)} missing dependencies")
368
+ print(f"⚠️ Found {len(missing_deps)} missing dependencies. Use --force-check-dependencies to install.")
369
+
370
+ # Update cache
371
+ smart_checker.update_cache(deployment_hash)
372
+ else:
373
+ self.logger.debug(f"Skipping dependency check: {check_reason}")
374
+
375
+ except ImportError as e:
376
+ self.logger.warning(f"Dependency checking not available: {e}")
377
+ except Exception as e:
378
+ self.logger.warning(f"Dependency check failed: {e}")
379
+
380
+ def _setup_monitoring(self, args):
381
+ """Setup monitoring configuration."""
382
+ monitor_mode = getattr(args, "monitor", False)
383
+ websocket_port = 8765 # Default port
384
+
385
+ if monitor_mode:
386
+ # Ensure Socket.IO dependencies are available
387
+ if not ensure_socketio_dependencies():
388
+ self.logger.warning("Socket.IO dependencies not available, disabling monitor mode")
389
+ monitor_mode = False
390
+ else:
391
+ # Get available port
392
+ port_manager = PortManager()
393
+ websocket_port = port_manager.get_available_port(8765)
394
+
395
+ # Start Socket.IO server if not running
396
+ if not self._is_socketio_server_running(websocket_port):
397
+ if not _start_socketio_server(websocket_port, self.logger):
398
+ self.logger.warning("Failed to start Socket.IO server, disabling monitor mode")
399
+ monitor_mode = False
400
+ else:
401
+ # Give server time to start
402
+ time.sleep(2)
403
+
404
+ if monitor_mode:
405
+ # Open browser to monitoring interface
406
+ monitor_url = f"http://localhost:{websocket_port}"
407
+ self.logger.info(f"Opening monitor interface: {monitor_url}")
408
+ try:
409
+ webbrowser.open(monitor_url)
410
+ args._browser_opened_by_cli = True
411
+ except Exception as e:
412
+ self.logger.warning(f"Could not open browser: {e}")
413
+ print(f"💡 Monitor interface available at: {monitor_url}")
414
+
415
+ return monitor_mode, websocket_port
416
+
417
+ def _setup_claude_runner(self, args, monitor_mode: bool, websocket_port: int):
418
+ """Setup and configure the Claude runner."""
419
+ try:
420
+ from ...core.claude_runner import ClaudeRunner
421
+ except ImportError:
422
+ from claude_mpm.core.claude_runner import ClaudeRunner
423
+
424
+ # Configure tickets
425
+ enable_tickets = not getattr(args, "no_tickets", False)
426
+
427
+ # Configure launch method
428
+ launch_method = "exec" # Default
429
+ if getattr(args, "subprocess", False):
430
+ launch_method = "subprocess"
431
+
432
+ # Configure WebSocket
433
+ enable_websocket = monitor_mode
434
+
435
+ # Build Claude arguments
436
+ claude_args = []
437
+ if hasattr(args, "claude_args") and args.claude_args:
438
+ claude_args.extend(args.claude_args)
439
+
440
+ # Create runner
441
+ runner = ClaudeRunner(
442
+ enable_tickets=enable_tickets,
443
+ log_level=args.logging,
444
+ claude_args=claude_args,
445
+ launch_method=launch_method,
446
+ enable_websocket=enable_websocket,
447
+ websocket_port=websocket_port,
448
+ )
449
+
450
+ # Set browser opening flag for monitor mode
451
+ if monitor_mode:
452
+ runner._should_open_monitor_browser = True
453
+ runner._browser_opened_by_cli = getattr(args, "_browser_opened_by_cli", False)
454
+
455
+ return runner
456
+
457
+ def _create_session_context(self, args, session_manager, resume_session_id, resume_context):
458
+ """Create session context."""
459
+ try:
460
+ from ...core.claude_runner import create_simple_context
461
+ except ImportError:
462
+ from claude_mpm.core.claude_runner import create_simple_context
463
+
464
+ if resume_session_id and resume_context:
465
+ # For resumed sessions, create enhanced context with session information
466
+ context = create_session_context(resume_session_id, session_manager)
467
+ # Update session usage
468
+ session_manager.active_sessions[resume_session_id]["last_used"] = datetime.now().isoformat()
469
+ session_manager.active_sessions[resume_session_id]["use_count"] += 1
470
+ session_manager._save_sessions()
471
+ else:
472
+ # Create a new session for tracking
473
+ new_session_id = session_manager.create_session("default")
474
+ context = create_simple_context()
475
+ self.logger.info(f"Created new session {new_session_id}")
476
+
477
+ return context
478
+
479
+ def _execute_session(self, args, runner, context) -> bool:
480
+ """Execute the Claude session."""
481
+ try:
482
+ # Run session based on mode
483
+ non_interactive = getattr(args, "non_interactive", False)
484
+ input_arg = getattr(args, "input", None)
485
+
486
+ if non_interactive or input_arg:
487
+ # Non-interactive mode
488
+ user_input = get_user_input(input_arg, self.logger)
489
+ success = runner.run_oneshot(user_input, context)
490
+ if not success:
491
+ self.logger.error("Session failed")
492
+ return False
493
+ else:
494
+ # Interactive mode
495
+ if getattr(args, "intercept_commands", False):
496
+ wrapper_path = get_scripts_dir() / "interactive_wrapper.py"
497
+ if wrapper_path.exists():
498
+ print("Starting interactive session with command interception...")
499
+ subprocess.run([sys.executable, str(wrapper_path)])
500
+ else:
501
+ self.logger.warning("Interactive wrapper not found, falling back to normal mode")
502
+ runner.run_interactive(context)
503
+ else:
504
+ runner.run_interactive(context)
505
+
506
+ return True
507
+
508
+ except Exception as e:
509
+ self.logger.error(f"Session execution failed: {e}")
510
+ return False
511
+
512
+ def _is_socketio_server_running(self, port: int) -> bool:
513
+ """Check if Socket.IO server is running on the specified port."""
514
+ try:
515
+ import socket
516
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
517
+ s.settimeout(1)
518
+ result = s.connect_ex(('localhost', port))
519
+ return result == 0
520
+ except Exception:
521
+ return False
522
+
523
+
178
524
  def run_session(args):
179
525
  """
180
- Run a simplified Claude session.
526
+ Main entry point for run command.
527
+
528
+ This function maintains backward compatibility while using the new BaseCommand pattern.
529
+ """
530
+ command = RunCommand()
531
+ result = command.execute(args)
181
532
 
182
- WHY: This is the primary command that users interact with. It sets up the
183
- environment, optionally deploys agents, and launches Claude with the MPM framework.
533
+ # For run command, we don't typically need structured output
534
+ # but we should respect the exit code
535
+ return result.exit_code
536
+
537
+
538
+ def run_session_legacy(args):
539
+ """
540
+ Legacy run session implementation.
541
+
542
+ WHY: This contains the original run_session logic, preserved during migration
543
+ to BaseCommand pattern. Will be gradually refactored into the RunCommand class.
184
544
 
185
545
  DESIGN DECISION: We use ClaudeRunner to handle the complexity of
186
546
  subprocess management and hook integration, keeping this function focused
@@ -779,9 +1139,8 @@ def _check_claude_json_memory(args, logger):
779
1139
 
780
1140
  # Get thresholds from configuration
781
1141
  try:
782
- from ...core.config import Config
783
-
784
- config = Config()
1142
+ config_loader = ConfigLoader()
1143
+ config = config_loader.load_main_config()
785
1144
  memory_config = config.get("memory_management", {})
786
1145
  warning_threshold = (
787
1146
  memory_config.get("claude_json_warning_threshold_kb", 500) * 1024
@@ -874,8 +1233,9 @@ def _check_configuration_health(logger):
874
1233
  logger: Logger instance for output
875
1234
  """
876
1235
  try:
877
- # Load configuration
878
- config = Config()
1236
+ # Load configuration using ConfigLoader
1237
+ config_loader = ConfigLoader()
1238
+ config = config_loader.load_main_config()
879
1239
 
880
1240
  # Validate configuration
881
1241
  is_valid, errors, warnings = config.validate_configuration()