claude-mpm 4.1.2__py3-none-any.whl → 4.1.4__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 (87) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +16 -19
  3. claude_mpm/agents/MEMORY.md +21 -49
  4. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +156 -0
  5. claude_mpm/agents/templates/api_qa.json +36 -116
  6. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +42 -9
  7. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +29 -6
  8. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +34 -6
  9. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +41 -9
  10. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +30 -8
  11. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +2 -2
  12. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +29 -6
  13. claude_mpm/agents/templates/backup/research_memory_efficient.json +2 -2
  14. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +41 -9
  15. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +23 -7
  16. claude_mpm/agents/templates/code_analyzer.json +18 -36
  17. claude_mpm/agents/templates/data_engineer.json +43 -14
  18. claude_mpm/agents/templates/documentation.json +55 -74
  19. claude_mpm/agents/templates/engineer.json +57 -40
  20. claude_mpm/agents/templates/imagemagick.json +7 -2
  21. claude_mpm/agents/templates/memory_manager.json +1 -1
  22. claude_mpm/agents/templates/ops.json +36 -4
  23. claude_mpm/agents/templates/project_organizer.json +23 -71
  24. claude_mpm/agents/templates/qa.json +34 -2
  25. claude_mpm/agents/templates/refactoring_engineer.json +9 -5
  26. claude_mpm/agents/templates/research.json +36 -4
  27. claude_mpm/agents/templates/security.json +29 -2
  28. claude_mpm/agents/templates/ticketing.json +3 -3
  29. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  30. claude_mpm/agents/templates/version_control.json +28 -2
  31. claude_mpm/agents/templates/web_qa.json +38 -151
  32. claude_mpm/agents/templates/web_ui.json +2 -2
  33. claude_mpm/cli/commands/agent_manager.py +221 -1
  34. claude_mpm/cli/commands/agents.py +556 -1009
  35. claude_mpm/cli/commands/memory.py +248 -927
  36. claude_mpm/cli/commands/run.py +139 -484
  37. claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
  38. claude_mpm/cli/startup_logging.py +76 -0
  39. claude_mpm/core/agent_registry.py +6 -10
  40. claude_mpm/core/framework_loader.py +205 -595
  41. claude_mpm/core/log_manager.py +49 -1
  42. claude_mpm/core/logging_config.py +2 -4
  43. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  44. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  45. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  47. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  48. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  49. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  50. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  51. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  52. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  53. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  54. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  55. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  56. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  57. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  58. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  59. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  60. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  61. claude_mpm/services/agents/registry/__init__.py +1 -1
  62. claude_mpm/services/cli/__init__.py +18 -0
  63. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  64. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  65. claude_mpm/services/cli/agent_listing_service.py +463 -0
  66. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  67. claude_mpm/services/cli/agent_validation_service.py +589 -0
  68. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  69. claude_mpm/services/cli/memory_crud_service.py +617 -0
  70. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  71. claude_mpm/services/cli/session_manager.py +513 -0
  72. claude_mpm/services/cli/socketio_manager.py +498 -0
  73. claude_mpm/services/cli/startup_checker.py +370 -0
  74. claude_mpm/services/core/cache_manager.py +311 -0
  75. claude_mpm/services/core/memory_manager.py +637 -0
  76. claude_mpm/services/core/path_resolver.py +498 -0
  77. claude_mpm/services/core/service_container.py +520 -0
  78. claude_mpm/services/core/service_interfaces.py +436 -0
  79. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  80. claude_mpm/services/memory/router.py +116 -10
  81. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
  82. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +86 -55
  83. claude_mpm/cli/commands/run_config_checker.py +0 -159
  84. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
  85. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
  86. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
  87. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/top_level.txt +0 -0
@@ -13,21 +13,18 @@ DESIGN DECISIONS:
13
13
  - Support multiple output formats (json, yaml, table, text)
14
14
  """
15
15
 
16
- import logging
17
- import os
18
16
  import subprocess
19
17
  import sys
20
- import time
21
- import webbrowser
22
18
  from datetime import datetime
23
19
  from typing import Optional
24
20
 
25
21
  from ...constants import LogLevel
26
22
  from ...core.logger import get_logger
27
- from ...core.shared.config_loader import ConfigLoader
28
- from ...core.unified_paths import get_package_root, get_scripts_dir
29
- from ...services.port_manager import PortManager
30
- from ...utils.dependency_manager import ensure_socketio_dependencies
23
+ from ...core.unified_paths import get_scripts_dir
24
+ from ...services.cli.dashboard_launcher import DashboardLauncher
25
+ from ...services.cli.session_manager import SessionManager
26
+ from ...services.cli.socketio_manager import SocketIOManager
27
+ from ...services.cli.startup_checker import StartupCheckerService
31
28
  from ..shared import BaseCommand, CommandResult
32
29
  from ..startup_logging import (
33
30
  cleanup_old_startup_logs,
@@ -35,7 +32,6 @@ from ..startup_logging import (
35
32
  setup_startup_logging,
36
33
  )
37
34
  from ..utils import get_user_input, list_agent_versions_at_startup
38
- from .run_config_checker import RunConfigChecker
39
35
 
40
36
 
41
37
  def filter_claude_mpm_args(claude_args):
@@ -159,7 +155,7 @@ def create_session_context(session_id, session_manager):
159
155
 
160
156
  base_context = create_simple_context()
161
157
 
162
- session_data = session_manager.get_session_by_id(session_id)
158
+ session_data = session_manager.get_session_info(session_id)
163
159
  if not session_data:
164
160
  return base_context
165
161
 
@@ -207,6 +203,11 @@ class RunCommand(BaseCommand):
207
203
  # Execute the main run logic
208
204
  success = self._execute_run_session(args)
209
205
 
206
+ # Log memory stats at session completion
207
+ from ..startup_logging import log_memory_stats
208
+
209
+ log_memory_stats(self.logger, "Session End Memory")
210
+
210
211
  if success:
211
212
  return CommandResult.success_result(
212
213
  "Claude session completed successfully"
@@ -278,21 +279,29 @@ class RunCommand(BaseCommand):
278
279
 
279
280
  def _check_configuration_health(self):
280
281
  """Check configuration health at startup."""
281
- checker = RunConfigChecker(self.logger)
282
- checker.check_configuration_health()
282
+ # Use new StartupCheckerService
283
+ from ...core.config import Config
284
+
285
+ config_service = Config()
286
+ checker = StartupCheckerService(config_service)
287
+ warnings = checker.check_configuration()
288
+ checker.display_warnings(warnings)
283
289
 
284
290
  def _check_claude_json_memory(self, args):
285
291
  """Check .claude.json file size and warn about memory issues."""
286
- checker = RunConfigChecker(self.logger)
287
- checker.check_claude_json_memory(args)
292
+ # Use new StartupCheckerService
293
+ from ...core.config import Config
294
+
295
+ config_service = Config()
296
+ checker = StartupCheckerService(config_service)
297
+ resume_enabled = getattr(args, "mpm_resume", False)
298
+ warning = checker.check_memory(resume_enabled)
299
+ if warning:
300
+ checker.display_warnings([warning])
288
301
 
289
302
  def _setup_session_management(self, args):
290
303
  """Setup session management and handle resumption."""
291
- try:
292
- from ...core.session_manager import SessionManager
293
- except ImportError:
294
- from claude_mpm.core.session_manager import SessionManager
295
-
304
+ # Use the new SessionManager service from the CLI services layer
296
305
  session_manager = SessionManager()
297
306
  resume_session_id = None
298
307
  resume_context = None
@@ -302,7 +311,7 @@ class RunCommand(BaseCommand):
302
311
  # Resume the last interactive session
303
312
  resume_session_id = session_manager.get_last_interactive_session()
304
313
  if resume_session_id:
305
- session_data = session_manager.get_session_by_id(resume_session_id)
314
+ session_data = session_manager.get_session_info(resume_session_id)
306
315
  if session_data:
307
316
  resume_context = session_data.get("context", "default")
308
317
  self.logger.info(
@@ -319,7 +328,7 @@ class RunCommand(BaseCommand):
319
328
  else:
320
329
  # Resume specific session by ID
321
330
  resume_session_id = args.mpm_resume
322
- session_data = session_manager.get_session_by_id(resume_session_id)
331
+ session_data = session_manager.get_session_info(resume_session_id)
323
332
  if session_data:
324
333
  resume_context = session_data.get("context", "default")
325
334
  self.logger.info(
@@ -414,42 +423,43 @@ class RunCommand(BaseCommand):
414
423
  self.logger.warning(f"Dependency check failed: {e}")
415
424
 
416
425
  def _setup_monitoring(self, args):
417
- """Setup monitoring configuration."""
426
+ """Setup monitoring configuration using SocketIOManager."""
418
427
  monitor_mode = getattr(args, "monitor", False)
419
428
  websocket_port = 8765 # Default port
420
429
 
421
430
  if monitor_mode:
422
- # Ensure Socket.IO dependencies are available
423
- if not ensure_socketio_dependencies():
431
+ # Use SocketIOManager for server management
432
+ socketio_manager = SocketIOManager(self.logger)
433
+
434
+ # Check dependencies
435
+ deps_ok, error_msg = socketio_manager.ensure_dependencies()
436
+ if not deps_ok:
424
437
  self.logger.warning(
425
- "Socket.IO dependencies not available, disabling monitor mode"
438
+ f"Socket.IO dependencies not available: {error_msg}, disabling monitor mode"
426
439
  )
427
440
  monitor_mode = False
428
441
  else:
429
- # Get available port
430
- port_manager = PortManager()
431
- websocket_port = port_manager.get_available_port(8765)
432
-
433
- # Start Socket.IO server if not running
434
- if not self._is_socketio_server_running(websocket_port):
435
- if not self._start_socketio_server(websocket_port, self.logger):
436
- self.logger.warning(
437
- "Failed to start Socket.IO server, disabling monitor mode"
438
- )
439
- monitor_mode = False
440
- else:
441
- # Give server time to start
442
- time.sleep(2)
443
-
444
- if monitor_mode:
445
- # Open browser to monitoring interface
446
- monitor_url = f"http://localhost:{websocket_port}"
447
- self.logger.info(f"Opening monitor interface: {monitor_url}")
448
- try:
449
- webbrowser.open(monitor_url)
450
- args._browser_opened_by_cli = True
451
- except Exception as e:
452
- self.logger.warning(f"Could not open browser: {e}")
442
+ # Find available port and start server
443
+ websocket_port = socketio_manager.find_available_port(8765)
444
+ success, server_info = socketio_manager.start_server(
445
+ port=websocket_port
446
+ )
447
+
448
+ if not success:
449
+ self.logger.warning(
450
+ "Failed to start Socket.IO server, disabling monitor mode"
451
+ )
452
+ monitor_mode = False
453
+ else:
454
+ # Use DashboardLauncher for browser opening only
455
+ dashboard_launcher = DashboardLauncher(self.logger)
456
+ monitor_url = dashboard_launcher.get_dashboard_url(websocket_port)
457
+
458
+ # Try to open browser
459
+ browser_opened = dashboard_launcher._open_browser(monitor_url)
460
+ args._browser_opened_by_cli = browser_opened
461
+
462
+ if not browser_opened:
453
463
  print(f"💡 Monitor interface available at: {monitor_url}")
454
464
 
455
465
  return monitor_mode, websocket_port
@@ -509,16 +519,16 @@ class RunCommand(BaseCommand):
509
519
  # For resumed sessions, create enhanced context with session information
510
520
  context = create_session_context(resume_session_id, session_manager)
511
521
  # Update session usage
512
- session_manager.active_sessions[resume_session_id][
513
- "last_used"
514
- ] = datetime.now().isoformat()
515
- session_manager.active_sessions[resume_session_id]["use_count"] += 1
516
- session_manager._save_sessions()
522
+ session = session_manager.load_session(resume_session_id)
523
+ if session:
524
+ session.last_used = datetime.now().isoformat()
525
+ session.use_count += 1
526
+ session_manager.save_session(session)
517
527
  else:
518
528
  # Create a new session for tracking
519
- new_session_id = session_manager.create_session("default")
529
+ new_session = session_manager.create_session("default")
520
530
  context = create_simple_context()
521
- self.logger.info(f"Created new session {new_session_id}")
531
+ self.logger.info(f"Created new session {new_session.id}")
522
532
 
523
533
  return context
524
534
 
@@ -556,18 +566,6 @@ class RunCommand(BaseCommand):
556
566
  self.logger.error(f"Session execution failed: {e}")
557
567
  return False
558
568
 
559
- def _is_socketio_server_running(self, port: int) -> bool:
560
- """Check if Socket.IO server is running on the specified port."""
561
- try:
562
- import socket
563
-
564
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
565
- s.settimeout(1)
566
- result = s.connect_ex(("localhost", port))
567
- return result == 0
568
- except Exception:
569
- return False
570
-
571
569
 
572
570
  def run_session(args):
573
571
  """
@@ -627,12 +625,10 @@ def run_session_legacy(args):
627
625
 
628
626
  try:
629
627
  from ...core.claude_runner import ClaudeRunner, create_simple_context
630
- from ...core.session_manager import SessionManager
631
628
  except ImportError:
632
629
  from claude_mpm.core.claude_runner import ClaudeRunner, create_simple_context
633
- from claude_mpm.core.session_manager import SessionManager
634
630
 
635
- # Handle session resumption
631
+ # Handle session resumption using the new SessionManager service
636
632
  session_manager = SessionManager()
637
633
  resume_session_id = None
638
634
  resume_context = None
@@ -642,7 +638,7 @@ def run_session_legacy(args):
642
638
  # Resume the last interactive session
643
639
  resume_session_id = session_manager.get_last_interactive_session()
644
640
  if resume_session_id:
645
- session_data = session_manager.get_session_by_id(resume_session_id)
641
+ session_data = session_manager.get_session_info(resume_session_id)
646
642
  if session_data:
647
643
  resume_context = session_data.get("context", "default")
648
644
  logger.info(
@@ -659,7 +655,7 @@ def run_session_legacy(args):
659
655
  else:
660
656
  # Resume specific session by ID
661
657
  resume_session_id = args.mpm_resume
662
- session_data = session_manager.get_session_by_id(resume_session_id)
658
+ session_data = session_manager.get_session_info(resume_session_id)
663
659
  if session_data:
664
660
  resume_context = session_data.get("context", "default")
665
661
  logger.info(
@@ -859,11 +855,14 @@ def run_session_legacy(args):
859
855
 
860
856
  # Display Socket.IO server info if enabled
861
857
  if enable_websocket:
862
- # Auto-install Socket.IO dependencies if needed
858
+ # Use SocketIOManager for server management
859
+ socketio_manager = SocketIOManager(logger)
860
+
861
+ # Check dependencies
863
862
  print("🔧 Checking Socket.IO dependencies...")
864
- dependencies_ok, error_msg = ensure_socketio_dependencies(logger)
863
+ deps_ok, error_msg = socketio_manager.ensure_dependencies()
865
864
 
866
- if not dependencies_ok:
865
+ if not deps_ok:
867
866
  print(f"❌ Failed to install Socket.IO dependencies: {error_msg}")
868
867
  print(
869
868
  " Please install manually: pip install python-socketio aiohttp python-engineio"
@@ -873,32 +872,36 @@ def run_session_legacy(args):
873
872
  else:
874
873
  print("✓ Socket.IO dependencies ready")
875
874
 
876
- try:
877
- import socketio
878
-
879
- print(f"✓ Socket.IO server enabled at http://localhost:{websocket_port}")
880
- if launch_method == "exec":
881
- print(
882
- " Note: Socket.IO monitoring using exec mode with Claude Code hooks"
883
- )
884
-
885
- # Launch Socket.IO dashboard if in monitor mode
875
+ # Find available port and start server if in monitor mode
886
876
  if monitor_mode:
887
- success, browser_opened = launch_socketio_monitor(
888
- websocket_port, logger
877
+ websocket_port = socketio_manager.find_available_port(websocket_port)
878
+ success, server_info = socketio_manager.start_server(
879
+ port=websocket_port
889
880
  )
890
- if not success:
881
+
882
+ if success:
883
+ print(f"✓ Socket.IO server enabled at {server_info.url}")
884
+ if launch_method == "exec":
885
+ print(
886
+ " Note: Socket.IO monitoring using exec mode with Claude Code hooks"
887
+ )
888
+
889
+ # Use DashboardLauncher for browser opening
890
+ dashboard_launcher = DashboardLauncher(logger)
891
+ monitor_url = dashboard_launcher.get_dashboard_url(websocket_port)
892
+ browser_opened = dashboard_launcher._open_browser(monitor_url)
893
+ args._browser_opened_by_cli = browser_opened
894
+
895
+ if not browser_opened:
896
+ print(f"💡 Monitor interface available at: {monitor_url}")
897
+ else:
891
898
  print("⚠️ Failed to launch Socket.IO monitor")
892
899
  print(
893
900
  f" You can manually run: python scripts/launch_socketio_dashboard.py --port {websocket_port}"
894
901
  )
895
- # Store whether browser was opened by CLI for coordination with ClaudeRunner
896
- args._browser_opened_by_cli = browser_opened
897
- except ImportError as e:
898
- print(f"⚠️ Socket.IO still not available after installation attempt: {e}")
899
- print(" This might be a virtual environment issue.")
900
- print(" Try: pip install python-socketio aiohttp python-engineio")
901
- print(" Or: pip install claude-mpm[monitor]")
902
+ args._browser_opened_by_cli = False
903
+ else:
904
+ print(f"✓ Socket.IO ready (port: {websocket_port})")
902
905
 
903
906
  runner = ClaudeRunner(
904
907
  enable_tickets=enable_tickets,
@@ -925,16 +928,16 @@ def run_session_legacy(args):
925
928
  # For resumed sessions, create enhanced context with session information
926
929
  context = create_session_context(resume_session_id, session_manager)
927
930
  # Update session usage
928
- session_manager.active_sessions[resume_session_id][
929
- "last_used"
930
- ] = datetime.now().isoformat()
931
- session_manager.active_sessions[resume_session_id]["use_count"] += 1
932
- session_manager._save_sessions()
931
+ session = session_manager.load_session(resume_session_id)
932
+ if session:
933
+ session.last_used = datetime.now().isoformat()
934
+ session.use_count += 1
935
+ session_manager.save_session(session)
933
936
  else:
934
937
  # Create a new session for tracking
935
- new_session_id = session_manager.create_session("default")
938
+ new_session = session_manager.create_session("default")
936
939
  context = create_simple_context()
937
- logger.info(f"Created new session {new_session_id}")
940
+ logger.info(f"Created new session {new_session.id}")
938
941
 
939
942
  # For monitor mode, we handled everything in launch_socketio_monitor
940
943
  # No need for ClaudeRunner browser delegation
@@ -963,407 +966,59 @@ def run_session_legacy(args):
963
966
  runner.run_interactive(context)
964
967
 
965
968
 
969
+ # Legacy helper functions - now delegating to SocketIOManager
966
970
  def launch_socketio_monitor(port, logger):
967
- """Launch the Socket.IO monitoring dashboard."""
968
- from .socketio_monitor import SocketIOMonitor
969
-
970
- monitor = SocketIOMonitor(logger)
971
- return monitor.launch_monitor(port)
971
+ """Launch the Socket.IO monitoring dashboard (legacy compatibility)."""
972
+ socketio_manager = SocketIOManager(logger)
973
+ success, server_info = socketio_manager.start_server(port=port)
972
974
 
975
+ if success:
976
+ # Open browser using DashboardLauncher
977
+ launcher = DashboardLauncher(logger)
978
+ browser_opened = launcher._open_browser(server_info.url)
979
+ return success, browser_opened
973
980
 
974
- # Socket.IO monitoring functions moved to socketio_monitor.py
981
+ return False, False
975
982
 
976
983
 
977
984
  def _check_socketio_server_running(port, logger):
978
- """Check if a Socket.IO server is running on the specified port."""
979
- from .socketio_monitor import SocketIOMonitor
980
-
981
- monitor = SocketIOMonitor(logger)
982
- return monitor.check_server_running(port)
985
+ """Check if a Socket.IO server is running on the specified port (legacy compatibility)."""
986
+ socketio_manager = SocketIOManager(logger)
987
+ return socketio_manager.is_server_running(port)
983
988
 
984
989
 
985
990
  def _start_standalone_socketio_server(port, logger):
986
- """Start a standalone Socket.IO server using the Python daemon."""
987
- from .socketio_monitor import SocketIOMonitor
988
-
989
- monitor = SocketIOMonitor(logger)
990
- return monitor.start_standalone_server(port)
991
- """
992
- Start a standalone Socket.IO server using the Python daemon.
993
-
994
- WHY: For monitor mode, we want a persistent server that runs independently
995
- of the Claude session. This allows users to monitor multiple sessions and
996
- keeps the dashboard available even when Claude isn't running.
997
-
998
- DESIGN DECISION: We use a pure Python daemon script to manage the server
999
- process. This avoids Node.js dependencies (like PM2) and provides proper
1000
- process management with PID tracking.
1001
-
1002
- Args:
1003
- port: Port number for the server
1004
- logger: Logger instance for output
1005
-
1006
- Returns:
1007
- bool: True if server started successfully, False otherwise
1008
- """
1009
- try:
1010
- import subprocess
1011
-
1012
- # Get path to daemon script in package
1013
- daemon_script = get_package_root() / "scripts" / "socketio_daemon.py"
1014
-
1015
- if not daemon_script.exists():
1016
- logger.error(f"Socket.IO daemon script not found: {daemon_script}")
1017
- return False
1018
-
1019
- logger.info(f"Starting Socket.IO server daemon on port {port}")
1020
-
1021
- # Start the daemon
1022
- result = subprocess.run(
1023
- [sys.executable, str(daemon_script), "start"],
1024
- capture_output=True,
1025
- text=True,
1026
- check=False,
1027
- )
1028
-
1029
- if result.returncode != 0:
1030
- logger.error(f"Failed to start Socket.IO daemon: {result.stderr}")
1031
- return False
1032
-
1033
- # Wait for server using event-based polling instead of fixed delays
1034
- # WHY: Replace fixed sleep delays with active polling for faster startup detection
1035
- max_wait_time = 15 # Maximum 15 seconds
1036
- poll_interval = 0.1 # Start with 100ms polling
1037
-
1038
- logger.info(f"Waiting up to {max_wait_time} seconds for server to be ready...")
1039
-
1040
- # Give daemon minimal time to fork
1041
- time.sleep(0.2) # Reduced from 0.5s
1042
-
1043
- start_time = time.time()
1044
- attempt = 0
1045
-
1046
- while time.time() - start_time < max_wait_time:
1047
- attempt += 1
1048
- elapsed = time.time() - start_time
1049
-
1050
- logger.debug(
1051
- f"Checking server readiness (attempt {attempt}, elapsed {elapsed:.1f}s)"
1052
- )
1053
-
1054
- # Adaptive polling - start fast, slow down over time
1055
- if elapsed < 2:
1056
- poll_interval = 0.1 # 100ms for first 2 seconds
1057
- elif elapsed < 5:
1058
- poll_interval = 0.25 # 250ms for next 3 seconds
1059
- else:
1060
- poll_interval = 0.5 # 500ms after 5 seconds
1061
-
1062
- time.sleep(poll_interval)
1063
-
1064
- # Check if the daemon server is accepting connections
1065
- if _check_socketio_server_running(port, logger):
1066
- logger.info(
1067
- f"✅ Standalone Socket.IO server started successfully on port {port}"
1068
- )
1069
- logger.info(
1070
- f"🕐 Server ready after {attempt} attempts ({elapsed:.1f}s)"
1071
- )
1072
- return True
1073
- logger.debug(f"Server not yet accepting connections on attempt {attempt}")
1074
-
1075
- # Timeout reached
1076
- time.time() - start_time
1077
- logger.error(
1078
- f"❌ Socket.IO server health check failed after {max_wait_time}s timeout ({attempt} attempts)"
1079
- )
1080
- logger.warning(
1081
- "⏱️ Server may still be starting - try waiting a few more seconds"
1082
- )
1083
- logger.warning(
1084
- "💡 The daemon process might be running but not yet accepting HTTP connections"
1085
- )
1086
- logger.error("🔧 Troubleshooting steps:")
1087
- logger.error(" - Wait a few more seconds and try again")
1088
- logger.error(f" - Check for port conflicts: lsof -i :{port}")
1089
- logger.error(" - Try a different port with --websocket-port")
1090
- logger.error(" - Verify dependencies: pip install python-socketio aiohttp")
1091
- return False
1092
-
1093
- except Exception as e:
1094
- logger.error(f"❌ Failed to start standalone Socket.IO server: {e}")
1095
- import traceback
1096
-
1097
- logger.error(f"📋 Stack trace: {traceback.format_exc()}")
1098
- logger.error(
1099
- "💡 This may be a dependency issue - try: pip install python-socketio aiohttp"
1100
- )
1101
- return False
991
+ """Start a standalone Socket.IO server (legacy compatibility)."""
992
+ socketio_manager = SocketIOManager(logger)
993
+ success, _ = socketio_manager.start_server(port=port)
994
+ return success
1102
995
 
1103
996
 
1104
997
  def open_in_browser_tab(url, logger):
1105
998
  """Open URL in browser, attempting to reuse existing tabs when possible."""
1106
- from .socketio_monitor import SocketIOMonitor
1107
-
1108
- monitor = SocketIOMonitor(logger)
1109
- return monitor.open_in_browser_tab(url)
1110
- """
1111
- Open URL in browser, attempting to reuse existing tabs when possible.
1112
-
1113
- WHY: Users prefer reusing browser tabs instead of opening new ones constantly.
1114
- This function attempts platform-specific solutions for tab reuse.
1115
-
1116
- DESIGN DECISION: We try different methods based on platform capabilities,
1117
- falling back to standard webbrowser.open() if needed.
1118
-
1119
- Args:
1120
- url: URL to open
1121
- logger: Logger instance for output
1122
- """
1123
- try:
1124
- # Platform-specific optimizations for tab reuse
1125
- import platform
1126
-
1127
- system = platform.system().lower()
1128
-
1129
- if system == "darwin": # macOS
1130
- # Just use the standard webbrowser module on macOS
1131
- # The AppleScript approach is too unreliable
1132
- webbrowser.open(url, new=0, autoraise=True) # new=0 tries to reuse window
1133
- logger.info("Opened browser on macOS")
1134
-
1135
- elif system == "linux":
1136
- # On Linux, try to use existing browser session
1137
- try:
1138
- # This is a best-effort approach for common browsers
1139
- webbrowser.get().open(
1140
- url, new=0
1141
- ) # new=0 tries to reuse existing window
1142
- logger.info("Attempted Linux browser tab reuse")
1143
- except Exception:
1144
- webbrowser.open(url, autoraise=True)
1145
-
1146
- elif system == "windows":
1147
- # On Windows, try to use existing browser
1148
- try:
1149
- webbrowser.get().open(
1150
- url, new=0
1151
- ) # new=0 tries to reuse existing window
1152
- logger.info("Attempted Windows browser tab reuse")
1153
- except Exception:
1154
- webbrowser.open(url, autoraise=True)
1155
- else:
1156
- # Unknown platform, use standard opening
1157
- webbrowser.open(url, autoraise=True)
1158
-
1159
- except Exception as e:
1160
- logger.warning(f"Browser opening failed: {e}")
1161
- # Final fallback
1162
- webbrowser.open(url)
999
+ launcher = DashboardLauncher(logger)
1000
+ return launcher._open_browser(url)
1163
1001
 
1164
1002
 
1165
1003
  def _check_claude_json_memory(args, logger):
1166
1004
  """Check .claude.json file size and warn about memory issues."""
1167
- checker = RunConfigChecker(logger)
1168
- checker.check_claude_json_memory(args)
1169
- """Check .claude.json file size and warn about memory issues.
1170
-
1171
- WHY: Large .claude.json files (>500KB) cause significant memory issues when
1172
- using --resume. Claude Code loads the entire conversation history into
1173
- memory, leading to 2GB+ memory consumption.
1174
-
1175
- DESIGN DECISIONS:
1176
- - Warn at 500KB (conservative threshold)
1177
- - Suggest cleanup command for remediation
1178
- - Allow bypass with --force flag
1179
- - Only check when using --resume
1180
-
1181
- Args:
1182
- args: Parsed command line arguments
1183
- logger: Logger instance for output
1184
- """
1185
- # Only check if using --mpm-resume
1186
- if not hasattr(args, "mpm_resume") or not args.mpm_resume:
1187
- return
1188
-
1189
- claude_json_path = Path.home() / ".claude.json"
1190
-
1191
- # Check if file exists
1192
- if not claude_json_path.exists():
1193
- logger.debug("No .claude.json file found")
1194
- return
1195
-
1196
- # Check file size
1197
- file_size = claude_json_path.stat().st_size
1005
+ # Use new StartupCheckerService
1006
+ from ...core.config import Config
1198
1007
 
1199
- # Format size for display
1200
- def format_size(size_bytes):
1201
- for unit in ["B", "KB", "MB", "GB"]:
1202
- if size_bytes < 1024.0:
1203
- return f"{size_bytes:.1f}{unit}"
1204
- size_bytes /= 1024.0
1205
- return f"{size_bytes:.1f}TB"
1206
-
1207
- # Get thresholds from configuration
1208
- try:
1209
- config_loader = ConfigLoader()
1210
- config = config_loader.load_main_config()
1211
- memory_config = config.get("memory_management", {})
1212
- warning_threshold = (
1213
- memory_config.get("claude_json_warning_threshold_kb", 500) * 1024
1214
- )
1215
- critical_threshold = (
1216
- memory_config.get("claude_json_critical_threshold_kb", 1024) * 1024
1217
- )
1218
- except Exception as e:
1219
- logger.debug(f"Could not load memory configuration: {e}")
1220
- # Fall back to defaults
1221
- warning_threshold = 500 * 1024 # 500KB
1222
- critical_threshold = 1024 * 1024 # 1MB
1223
-
1224
- if file_size > critical_threshold:
1225
- print(
1226
- f"\n⚠️ CRITICAL: Large .claude.json file detected ({format_size(file_size)})"
1227
- )
1228
- print(" This WILL cause memory issues when using --resume")
1229
- print(" Claude Code may consume 2GB+ of memory\n")
1230
-
1231
- if not getattr(args, "force", False):
1232
- print(" Recommended actions:")
1233
- print(" 1. Run 'claude-mpm cleanup-memory' to archive old conversations")
1234
- print(" 2. Use --force to bypass this warning (not recommended)")
1235
- sys.stdout.flush() # Ensure prompt is displayed before input
1236
-
1237
- # Check if we're in a TTY environment for proper input handling
1238
- if not sys.stdin.isatty():
1239
- # In non-TTY environment (like pipes), use readline
1240
- print(
1241
- "\n Would you like to continue anyway? [y/N]: ",
1242
- end="",
1243
- flush=True,
1244
- )
1245
- try:
1246
- response = sys.stdin.readline().strip().lower()
1247
- # Handle various line endings and control characters
1248
- response = response.replace("\r", "").replace("\n", "").strip()
1249
- except (EOFError, KeyboardInterrupt):
1250
- response = "n"
1251
- else:
1252
- # In TTY environment, use normal input()
1253
- print(
1254
- "\n Would you like to continue anyway? [y/N]: ",
1255
- end="",
1256
- flush=True,
1257
- )
1258
- try:
1259
- response = input().strip().lower()
1260
- except (EOFError, KeyboardInterrupt):
1261
- response = "n"
1262
-
1263
- if response != "y":
1264
- print(
1265
- "\n✅ Session cancelled. Run 'claude-mpm cleanup-memory' to fix this issue."
1266
- )
1267
- sys.exit(0)
1268
-
1269
- elif file_size > warning_threshold:
1270
- print(
1271
- f"\n⚠️ Warning: .claude.json file is getting large ({format_size(file_size)})"
1272
- )
1273
- print(" This may cause memory issues when using --resume")
1274
- print(
1275
- " 💡 Consider running 'claude-mpm cleanup-memory' to archive old conversations\n"
1276
- )
1277
- # Just warn, don't block execution
1278
-
1279
- logger.info(f".claude.json size: {format_size(file_size)}")
1008
+ config_service = Config()
1009
+ checker = StartupCheckerService(config_service)
1010
+ resume_enabled = getattr(args, "mpm_resume", False)
1011
+ warning = checker.check_memory(resume_enabled)
1012
+ if warning:
1013
+ checker.display_warnings([warning])
1280
1014
 
1281
1015
 
1282
1016
  def _check_configuration_health(logger):
1283
1017
  """Check configuration health at startup and warn about issues."""
1284
- checker = RunConfigChecker(logger)
1285
- checker.check_configuration_health()
1286
- """Check configuration health at startup and warn about issues.
1018
+ # Use new StartupCheckerService
1019
+ from ...core.config import Config
1287
1020
 
1288
- WHY: Configuration errors can cause silent failures, especially for response
1289
- logging. This function proactively checks configuration at startup and warns
1290
- users about any issues, providing actionable guidance.
1291
-
1292
- DESIGN DECISIONS:
1293
- - Non-blocking: Issues are logged as warnings, not errors
1294
- - Actionable: Provides specific commands to fix issues
1295
- - Focused: Only checks critical configuration that affects runtime
1296
-
1297
- Args:
1298
- logger: Logger instance for output
1299
- """
1300
- try:
1301
- # Load configuration using ConfigLoader
1302
- config_loader = ConfigLoader()
1303
- config = config_loader.load_main_config()
1304
-
1305
- # Validate configuration
1306
- is_valid, errors, warnings = config.validate_configuration()
1307
-
1308
- # Get configuration status for additional context
1309
- status = config.get_configuration_status()
1310
-
1311
- # Report critical errors that will affect functionality
1312
- if errors:
1313
- logger.warning("⚠️ Configuration issues detected:")
1314
- for error in errors[:3]: # Show first 3 errors
1315
- logger.warning(f" • {error}")
1316
- if len(errors) > 3:
1317
- logger.warning(f" • ... and {len(errors) - 3} more")
1318
- logger.info(
1319
- "💡 Run 'claude-mpm config validate' to see all issues and fixes"
1320
- )
1321
-
1322
- # Check response logging specifically since it's commonly misconfigured
1323
- response_logging_enabled = config.get("response_logging.enabled", False)
1324
- if not response_logging_enabled:
1325
- logger.debug(
1326
- "Response logging is disabled (response_logging.enabled=false)"
1327
- )
1328
- else:
1329
- # Check if session directory is writable
1330
- session_dir = Path(
1331
- config.get(
1332
- "response_logging.session_directory", ".claude-mpm/responses"
1333
- )
1334
- )
1335
- if not session_dir.is_absolute():
1336
- session_dir = Path.cwd() / session_dir
1337
-
1338
- if not session_dir.exists():
1339
- try:
1340
- session_dir.mkdir(parents=True, exist_ok=True)
1341
- logger.debug(f"Created response logging directory: {session_dir}")
1342
- except Exception as e:
1343
- logger.warning(
1344
- f"Cannot create response logging directory {session_dir}: {e}"
1345
- )
1346
- logger.info("💡 Fix with: mkdir -p " + str(session_dir))
1347
- elif not os.access(session_dir, os.W_OK):
1348
- logger.warning(
1349
- f"Response logging directory is not writable: {session_dir}"
1350
- )
1351
- logger.info("💡 Fix with: chmod 755 " + str(session_dir))
1352
-
1353
- # Report non-critical warnings (only in debug mode)
1354
- if warnings and logger.isEnabledFor(logging.DEBUG):
1355
- logger.debug("Configuration warnings:")
1356
- for warning in warnings:
1357
- logger.debug(f" • {warning}")
1358
-
1359
- # Log loaded configuration source for debugging
1360
- if status.get("loaded_from") and status["loaded_from"] != "defaults":
1361
- logger.debug(f"Configuration loaded from: {status['loaded_from']}")
1362
-
1363
- except Exception as e:
1364
- # Don't let configuration check errors prevent startup
1365
- logger.debug(f"Configuration check failed (non-critical): {e}")
1366
- # Only show user-facing message if it's likely to affect them
1367
- if "yaml" in str(e).lower():
1368
- logger.warning("⚠️ Configuration file may have YAML syntax errors")
1369
- logger.info("💡 Validate with: claude-mpm config validate")
1021
+ config_service = Config()
1022
+ checker = StartupCheckerService(config_service)
1023
+ warnings = checker.check_configuration()
1024
+ checker.display_warnings(warnings)