claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/engineer.json +33 -11
- claude_mpm/cli/commands/agents.py +556 -1009
- claude_mpm/cli/commands/memory.py +248 -927
- claude_mpm/cli/commands/run.py +139 -484
- claude_mpm/cli/startup_logging.py +76 -0
- claude_mpm/core/agent_registry.py +6 -10
- claude_mpm/core/framework_loader.py +114 -595
- claude_mpm/core/logging_config.py +2 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
- claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
- claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
- claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
- claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
- claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
- claude_mpm/services/agents/memory/memory_file_service.py +103 -0
- claude_mpm/services/agents/memory/memory_format_service.py +201 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
- claude_mpm/services/agents/registry/__init__.py +1 -1
- claude_mpm/services/cli/__init__.py +18 -0
- claude_mpm/services/cli/agent_cleanup_service.py +407 -0
- claude_mpm/services/cli/agent_dependency_service.py +395 -0
- claude_mpm/services/cli/agent_listing_service.py +463 -0
- claude_mpm/services/cli/agent_output_formatter.py +605 -0
- claude_mpm/services/cli/agent_validation_service.py +589 -0
- claude_mpm/services/cli/dashboard_launcher.py +424 -0
- claude_mpm/services/cli/memory_crud_service.py +617 -0
- claude_mpm/services/cli/memory_output_formatter.py +604 -0
- claude_mpm/services/cli/session_manager.py +513 -0
- claude_mpm/services/cli/socketio_manager.py +498 -0
- claude_mpm/services/cli/startup_checker.py +370 -0
- claude_mpm/services/core/cache_manager.py +311 -0
- claude_mpm/services/core/memory_manager.py +637 -0
- claude_mpm/services/core/path_resolver.py +498 -0
- claude_mpm/services/core/service_container.py +520 -0
- claude_mpm/services/core/service_interfaces.py +436 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
- claude_mpm/cli/commands/run_config_checker.py +0 -159
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
claude_mpm/cli/commands/run.py
CHANGED
|
@@ -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.
|
|
28
|
-
from ...
|
|
29
|
-
from ...services.
|
|
30
|
-
from ...
|
|
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.
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
423
|
-
|
|
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
|
-
#
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
529
|
+
new_session = session_manager.create_session("default")
|
|
520
530
|
context = create_simple_context()
|
|
521
|
-
self.logger.info(f"Created new session {
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
858
|
+
# Use SocketIOManager for server management
|
|
859
|
+
socketio_manager = SocketIOManager(logger)
|
|
860
|
+
|
|
861
|
+
# Check dependencies
|
|
863
862
|
print("🔧 Checking Socket.IO dependencies...")
|
|
864
|
-
|
|
863
|
+
deps_ok, error_msg = socketio_manager.ensure_dependencies()
|
|
865
864
|
|
|
866
|
-
if not
|
|
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
|
-
|
|
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
|
-
|
|
888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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.
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
938
|
+
new_session = session_manager.create_session("default")
|
|
936
939
|
context = create_simple_context()
|
|
937
|
-
logger.info(f"Created new session {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
"""Check configuration health at startup and warn about issues.
|
|
1018
|
+
# Use new StartupCheckerService
|
|
1019
|
+
from ...core.config import Config
|
|
1287
1020
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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)
|