claude-mpm 3.9.8__py3-none-any.whl → 3.9.9__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/base_agent.json +1 -1
- claude_mpm/cli/__init__.py +3 -1
- claude_mpm/cli/commands/__init__.py +3 -1
- claude_mpm/cli/commands/cleanup.py +21 -1
- claude_mpm/cli/commands/mcp.py +821 -0
- claude_mpm/cli/parser.py +148 -1
- claude_mpm/config/memory_guardian_config.py +325 -0
- claude_mpm/constants.py +13 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +76 -19
- claude_mpm/models/state_models.py +433 -0
- claude_mpm/services/communication/__init__.py +2 -2
- claude_mpm/services/communication/socketio.py +18 -16
- claude_mpm/services/infrastructure/__init__.py +4 -1
- claude_mpm/services/infrastructure/logging.py +3 -3
- claude_mpm/services/infrastructure/memory_guardian.py +770 -0
- claude_mpm/services/mcp_gateway/__init__.py +28 -12
- claude_mpm/services/mcp_gateway/main.py +326 -0
- claude_mpm/services/mcp_gateway/registry/__init__.py +6 -3
- claude_mpm/services/mcp_gateway/registry/service_registry.py +397 -0
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +477 -0
- claude_mpm/services/mcp_gateway/server/__init__.py +9 -3
- claude_mpm/services/mcp_gateway/server/mcp_server.py +430 -0
- claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +444 -0
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +373 -0
- claude_mpm/services/mcp_gateway/tools/__init__.py +16 -3
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +497 -0
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +729 -0
- claude_mpm/services/mcp_gateway/tools/hello_world.py +551 -0
- claude_mpm/utils/file_utils.py +293 -0
- claude_mpm/utils/platform_memory.py +524 -0
- claude_mpm/utils/subprocess_utils.py +305 -0
- {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/METADATA +3 -1
- {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/RECORD +39 -28
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -36
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_agent.md +0 -39
- claude_mpm/agents/templates/.claude-mpm/memories/qa_agent.md +0 -38
- claude_mpm/agents/templates/.claude-mpm/memories/research_agent.md +0 -39
- claude_mpm/agents/templates/.claude-mpm/memories/version_control_agent.md +0 -38
- /claude_mpm/agents/templates/{research_memory_efficient.json → backup/research_memory_efficient.json} +0 -0
- {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/top_level.txt +0 -0
    
        claude_mpm/cli/parser.py
    CHANGED
    
    | @@ -14,7 +14,7 @@ import argparse | |
| 14 14 | 
             
            from pathlib import Path
         | 
| 15 15 | 
             
            from typing import Optional, List
         | 
| 16 16 |  | 
| 17 | 
            -
            from ..constants import CLICommands, CLIPrefix, AgentCommands, MemoryCommands, MonitorCommands, LogLevel, ConfigCommands, AggregateCommands, TicketCommands
         | 
| 17 | 
            +
            from ..constants import CLICommands, CLIPrefix, AgentCommands, MemoryCommands, MonitorCommands, LogLevel, ConfigCommands, AggregateCommands, TicketCommands, MCPCommands
         | 
| 18 18 |  | 
| 19 19 |  | 
| 20 20 | 
             
            def add_common_arguments(parser: argparse.ArgumentParser, version: str = None) -> None:
         | 
| @@ -979,6 +979,153 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp | |
| 979 979 | 
             
                from .commands.cleanup import add_cleanup_parser
         | 
| 980 980 | 
             
                add_cleanup_parser(subparsers)
         | 
| 981 981 |  | 
| 982 | 
            +
                # MCP command with subcommands
         | 
| 983 | 
            +
                mcp_parser = subparsers.add_parser(
         | 
| 984 | 
            +
                    CLICommands.MCP.value,
         | 
| 985 | 
            +
                    help="Manage MCP Gateway server and tools"
         | 
| 986 | 
            +
                )
         | 
| 987 | 
            +
                add_common_arguments(mcp_parser)
         | 
| 988 | 
            +
                
         | 
| 989 | 
            +
                mcp_subparsers = mcp_parser.add_subparsers(
         | 
| 990 | 
            +
                    dest="mcp_command",
         | 
| 991 | 
            +
                    help="MCP commands",
         | 
| 992 | 
            +
                    metavar="SUBCOMMAND"
         | 
| 993 | 
            +
                )
         | 
| 994 | 
            +
                
         | 
| 995 | 
            +
                # Install MCP Gateway
         | 
| 996 | 
            +
                install_mcp_parser = mcp_subparsers.add_parser(
         | 
| 997 | 
            +
                    MCPCommands.INSTALL.value,
         | 
| 998 | 
            +
                    help="Install and configure MCP Gateway"
         | 
| 999 | 
            +
                )
         | 
| 1000 | 
            +
                install_mcp_parser.add_argument(
         | 
| 1001 | 
            +
                    "--force",
         | 
| 1002 | 
            +
                    action="store_true",
         | 
| 1003 | 
            +
                    help="Force overwrite existing configuration"
         | 
| 1004 | 
            +
                )
         | 
| 1005 | 
            +
                
         | 
| 1006 | 
            +
                # Start MCP server
         | 
| 1007 | 
            +
                start_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1008 | 
            +
                    MCPCommands.START.value,
         | 
| 1009 | 
            +
                    help="Start the MCP Gateway server"
         | 
| 1010 | 
            +
                )
         | 
| 1011 | 
            +
                start_mcp_parser.add_argument(
         | 
| 1012 | 
            +
                    "--mode",
         | 
| 1013 | 
            +
                    choices=["stdio", "standalone"],
         | 
| 1014 | 
            +
                    default="stdio",
         | 
| 1015 | 
            +
                    help="Server mode: stdio for Claude integration, standalone for testing (default: stdio)"
         | 
| 1016 | 
            +
                )
         | 
| 1017 | 
            +
                start_mcp_parser.add_argument(
         | 
| 1018 | 
            +
                    "--port",
         | 
| 1019 | 
            +
                    type=int,
         | 
| 1020 | 
            +
                    default=8766,
         | 
| 1021 | 
            +
                    help="Port for standalone mode (default: 8766)"
         | 
| 1022 | 
            +
                )
         | 
| 1023 | 
            +
                start_mcp_parser.add_argument(
         | 
| 1024 | 
            +
                    "--config-file",
         | 
| 1025 | 
            +
                    type=Path,
         | 
| 1026 | 
            +
                    help="Path to MCP configuration file"
         | 
| 1027 | 
            +
                )
         | 
| 1028 | 
            +
                
         | 
| 1029 | 
            +
                # Stop MCP server
         | 
| 1030 | 
            +
                stop_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1031 | 
            +
                    MCPCommands.STOP.value,
         | 
| 1032 | 
            +
                    help="Stop the MCP Gateway server"
         | 
| 1033 | 
            +
                )
         | 
| 1034 | 
            +
                
         | 
| 1035 | 
            +
                # MCP status
         | 
| 1036 | 
            +
                status_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1037 | 
            +
                    MCPCommands.STATUS.value,
         | 
| 1038 | 
            +
                    help="Check server and tool status"
         | 
| 1039 | 
            +
                )
         | 
| 1040 | 
            +
                status_mcp_parser.add_argument(
         | 
| 1041 | 
            +
                    "--verbose",
         | 
| 1042 | 
            +
                    action="store_true",
         | 
| 1043 | 
            +
                    help="Show detailed status information"
         | 
| 1044 | 
            +
                )
         | 
| 1045 | 
            +
                
         | 
| 1046 | 
            +
                # List/manage tools
         | 
| 1047 | 
            +
                tools_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1048 | 
            +
                    MCPCommands.TOOLS.value,
         | 
| 1049 | 
            +
                    help="List and manage registered tools"
         | 
| 1050 | 
            +
                )
         | 
| 1051 | 
            +
                tools_mcp_parser.add_argument(
         | 
| 1052 | 
            +
                    "tool_action",
         | 
| 1053 | 
            +
                    nargs="?",
         | 
| 1054 | 
            +
                    choices=["list", "enable", "disable"],
         | 
| 1055 | 
            +
                    default="list",
         | 
| 1056 | 
            +
                    help="Tool action (default: list)"
         | 
| 1057 | 
            +
                )
         | 
| 1058 | 
            +
                tools_mcp_parser.add_argument(
         | 
| 1059 | 
            +
                    "tool_name",
         | 
| 1060 | 
            +
                    nargs="?",
         | 
| 1061 | 
            +
                    help="Tool name for enable/disable actions"
         | 
| 1062 | 
            +
                )
         | 
| 1063 | 
            +
                tools_mcp_parser.add_argument(
         | 
| 1064 | 
            +
                    "--verbose",
         | 
| 1065 | 
            +
                    action="store_true",
         | 
| 1066 | 
            +
                    help="Show detailed tool information including schemas"
         | 
| 1067 | 
            +
                )
         | 
| 1068 | 
            +
                
         | 
| 1069 | 
            +
                # Register new tool
         | 
| 1070 | 
            +
                register_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1071 | 
            +
                    MCPCommands.REGISTER.value,
         | 
| 1072 | 
            +
                    help="Register a new MCP tool"
         | 
| 1073 | 
            +
                )
         | 
| 1074 | 
            +
                register_mcp_parser.add_argument(
         | 
| 1075 | 
            +
                    "name",
         | 
| 1076 | 
            +
                    help="Tool name"
         | 
| 1077 | 
            +
                )
         | 
| 1078 | 
            +
                register_mcp_parser.add_argument(
         | 
| 1079 | 
            +
                    "description",
         | 
| 1080 | 
            +
                    help="Tool description"
         | 
| 1081 | 
            +
                )
         | 
| 1082 | 
            +
                register_mcp_parser.add_argument(
         | 
| 1083 | 
            +
                    "--schema-file",
         | 
| 1084 | 
            +
                    type=Path,
         | 
| 1085 | 
            +
                    help="Path to JSON schema file for tool input"
         | 
| 1086 | 
            +
                )
         | 
| 1087 | 
            +
                register_mcp_parser.add_argument(
         | 
| 1088 | 
            +
                    "--adapter",
         | 
| 1089 | 
            +
                    help="Path to custom tool adapter module"
         | 
| 1090 | 
            +
                )
         | 
| 1091 | 
            +
                register_mcp_parser.add_argument(
         | 
| 1092 | 
            +
                    "--save",
         | 
| 1093 | 
            +
                    action="store_true",
         | 
| 1094 | 
            +
                    help="Save tool to configuration"
         | 
| 1095 | 
            +
                )
         | 
| 1096 | 
            +
                
         | 
| 1097 | 
            +
                # Test tool invocation
         | 
| 1098 | 
            +
                test_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1099 | 
            +
                    MCPCommands.TEST.value,
         | 
| 1100 | 
            +
                    help="Test MCP tool invocation"
         | 
| 1101 | 
            +
                )
         | 
| 1102 | 
            +
                test_mcp_parser.add_argument(
         | 
| 1103 | 
            +
                    "tool_name",
         | 
| 1104 | 
            +
                    help="Name of tool to test"
         | 
| 1105 | 
            +
                )
         | 
| 1106 | 
            +
                test_mcp_parser.add_argument(
         | 
| 1107 | 
            +
                    "--args",
         | 
| 1108 | 
            +
                    help="Tool arguments as JSON string"
         | 
| 1109 | 
            +
                )
         | 
| 1110 | 
            +
                test_mcp_parser.add_argument(
         | 
| 1111 | 
            +
                    "--args-file",
         | 
| 1112 | 
            +
                    type=Path,
         | 
| 1113 | 
            +
                    help="Path to JSON file containing tool arguments"
         | 
| 1114 | 
            +
                )
         | 
| 1115 | 
            +
                
         | 
| 1116 | 
            +
                # Manage configuration
         | 
| 1117 | 
            +
                config_mcp_parser = mcp_subparsers.add_parser(
         | 
| 1118 | 
            +
                    MCPCommands.CONFIG.value,
         | 
| 1119 | 
            +
                    help="View and manage MCP configuration"
         | 
| 1120 | 
            +
                )
         | 
| 1121 | 
            +
                config_mcp_parser.add_argument(
         | 
| 1122 | 
            +
                    "config_action",
         | 
| 1123 | 
            +
                    nargs="?",
         | 
| 1124 | 
            +
                    choices=["view", "edit", "reset"],
         | 
| 1125 | 
            +
                    default="view",
         | 
| 1126 | 
            +
                    help="Configuration action (default: view)"
         | 
| 1127 | 
            +
                )
         | 
| 1128 | 
            +
                
         | 
| 982 1129 | 
             
                return parser
         | 
| 983 1130 |  | 
| 984 1131 |  | 
| @@ -0,0 +1,325 @@ | |
| 1 | 
            +
            """Memory Guardian configuration for managing Claude Code memory usage.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This module provides configuration management for the MemoryGuardian service
         | 
| 4 | 
            +
            that monitors and manages Claude Code subprocess memory consumption.
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Design Principles:
         | 
| 7 | 
            +
            - Platform-agnostic configuration with OS-specific overrides
         | 
| 8 | 
            +
            - Environment-based configuration for different deployment scenarios
         | 
| 9 | 
            +
            - Flexible thresholds for memory monitoring
         | 
| 10 | 
            +
            - Support for both psutil and fallback monitoring methods
         | 
| 11 | 
            +
            """
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            import os
         | 
| 14 | 
            +
            import platform
         | 
| 15 | 
            +
            from dataclasses import dataclass, field
         | 
| 16 | 
            +
            from pathlib import Path
         | 
| 17 | 
            +
            from typing import Dict, Any, Optional, List
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 20 | 
            +
            @dataclass
         | 
| 21 | 
            +
            class MemoryThresholds:
         | 
| 22 | 
            +
                """Memory threshold configuration in MB."""
         | 
| 23 | 
            +
                
         | 
| 24 | 
            +
                # Memory thresholds in MB (defaults for 24GB system)
         | 
| 25 | 
            +
                warning: int = 12288  # 12GB - Start monitoring closely
         | 
| 26 | 
            +
                critical: int = 15360  # 15GB - Consider restart
         | 
| 27 | 
            +
                emergency: int = 18432  # 18GB - Force restart
         | 
| 28 | 
            +
                
         | 
| 29 | 
            +
                # Percentage-based thresholds (as fallback when system memory is detected)
         | 
| 30 | 
            +
                warning_percent: float = 50.0  # 50% of system memory
         | 
| 31 | 
            +
                critical_percent: float = 65.0  # 65% of system memory
         | 
| 32 | 
            +
                emergency_percent: float = 75.0  # 75% of system memory
         | 
| 33 | 
            +
                
         | 
| 34 | 
            +
                def adjust_for_system_memory(self, total_memory_mb: int) -> None:
         | 
| 35 | 
            +
                    """Adjust thresholds based on available system memory."""
         | 
| 36 | 
            +
                    if total_memory_mb > 0:
         | 
| 37 | 
            +
                        self.warning = int(total_memory_mb * (self.warning_percent / 100))
         | 
| 38 | 
            +
                        self.critical = int(total_memory_mb * (self.critical_percent / 100))
         | 
| 39 | 
            +
                        self.emergency = int(total_memory_mb * (self.emergency_percent / 100))
         | 
| 40 | 
            +
                
         | 
| 41 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 42 | 
            +
                    """Convert thresholds to dictionary."""
         | 
| 43 | 
            +
                    return {
         | 
| 44 | 
            +
                        'warning_mb': self.warning,
         | 
| 45 | 
            +
                        'critical_mb': self.critical,
         | 
| 46 | 
            +
                        'emergency_mb': self.emergency,
         | 
| 47 | 
            +
                        'warning_percent': self.warning_percent,
         | 
| 48 | 
            +
                        'critical_percent': self.critical_percent,
         | 
| 49 | 
            +
                        'emergency_percent': self.emergency_percent
         | 
| 50 | 
            +
                    }
         | 
| 51 | 
            +
             | 
| 52 | 
            +
             | 
| 53 | 
            +
            @dataclass
         | 
| 54 | 
            +
            class RestartPolicy:
         | 
| 55 | 
            +
                """Configuration for process restart behavior."""
         | 
| 56 | 
            +
                
         | 
| 57 | 
            +
                # Restart attempts
         | 
| 58 | 
            +
                max_attempts: int = 3  # Maximum restart attempts before giving up
         | 
| 59 | 
            +
                attempt_window: int = 3600  # Window in seconds to count attempts (1 hour)
         | 
| 60 | 
            +
                
         | 
| 61 | 
            +
                # Cooldown periods
         | 
| 62 | 
            +
                initial_cooldown: int = 30  # Initial cooldown after restart (seconds)
         | 
| 63 | 
            +
                max_cooldown: int = 300  # Maximum cooldown period (5 minutes)
         | 
| 64 | 
            +
                cooldown_multiplier: float = 2.0  # Multiply cooldown on each retry
         | 
| 65 | 
            +
                
         | 
| 66 | 
            +
                # Graceful shutdown
         | 
| 67 | 
            +
                graceful_timeout: int = 30  # Time to wait for graceful shutdown (seconds)
         | 
| 68 | 
            +
                force_kill_timeout: int = 10  # Time to wait before SIGKILL after SIGTERM
         | 
| 69 | 
            +
                
         | 
| 70 | 
            +
                def get_cooldown(self, attempt: int) -> int:
         | 
| 71 | 
            +
                    """Calculate cooldown period for given attempt number."""
         | 
| 72 | 
            +
                    cooldown = self.initial_cooldown * (self.cooldown_multiplier ** (attempt - 1))
         | 
| 73 | 
            +
                    return min(int(cooldown), self.max_cooldown)
         | 
| 74 | 
            +
                
         | 
| 75 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 76 | 
            +
                    """Convert restart policy to dictionary."""
         | 
| 77 | 
            +
                    return {
         | 
| 78 | 
            +
                        'max_attempts': self.max_attempts,
         | 
| 79 | 
            +
                        'attempt_window': self.attempt_window,
         | 
| 80 | 
            +
                        'initial_cooldown': self.initial_cooldown,
         | 
| 81 | 
            +
                        'max_cooldown': self.max_cooldown,
         | 
| 82 | 
            +
                        'cooldown_multiplier': self.cooldown_multiplier,
         | 
| 83 | 
            +
                        'graceful_timeout': self.graceful_timeout,
         | 
| 84 | 
            +
                        'force_kill_timeout': self.force_kill_timeout
         | 
| 85 | 
            +
                    }
         | 
| 86 | 
            +
             | 
| 87 | 
            +
             | 
| 88 | 
            +
            @dataclass
         | 
| 89 | 
            +
            class MonitoringConfig:
         | 
| 90 | 
            +
                """Configuration for memory monitoring behavior."""
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                # Check intervals (seconds)
         | 
| 93 | 
            +
                normal_interval: int = 30  # Normal check interval
         | 
| 94 | 
            +
                warning_interval: int = 15  # Check interval when in warning state
         | 
| 95 | 
            +
                critical_interval: int = 5  # Check interval when in critical state
         | 
| 96 | 
            +
                
         | 
| 97 | 
            +
                # Monitoring method preferences
         | 
| 98 | 
            +
                prefer_psutil: bool = True  # Prefer psutil if available
         | 
| 99 | 
            +
                fallback_methods: List[str] = field(default_factory=lambda: [
         | 
| 100 | 
            +
                    'platform_specific',  # Use OS-specific commands
         | 
| 101 | 
            +
                    'resource_module',    # Use resource module as last resort
         | 
| 102 | 
            +
                ])
         | 
| 103 | 
            +
                
         | 
| 104 | 
            +
                # Logging and reporting
         | 
| 105 | 
            +
                log_memory_stats: bool = True  # Log memory statistics
         | 
| 106 | 
            +
                log_interval: int = 300  # Log summary every 5 minutes
         | 
| 107 | 
            +
                detailed_logging: bool = False  # Enable detailed debug logging
         | 
| 108 | 
            +
                
         | 
| 109 | 
            +
                def get_check_interval(self, memory_state: str) -> int:
         | 
| 110 | 
            +
                    """Get check interval based on current memory state."""
         | 
| 111 | 
            +
                    if memory_state == 'critical':
         | 
| 112 | 
            +
                        return self.critical_interval
         | 
| 113 | 
            +
                    elif memory_state == 'warning':
         | 
| 114 | 
            +
                        return self.warning_interval
         | 
| 115 | 
            +
                    else:
         | 
| 116 | 
            +
                        return self.normal_interval
         | 
| 117 | 
            +
                
         | 
| 118 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 119 | 
            +
                    """Convert monitoring config to dictionary."""
         | 
| 120 | 
            +
                    return {
         | 
| 121 | 
            +
                        'normal_interval': self.normal_interval,
         | 
| 122 | 
            +
                        'warning_interval': self.warning_interval,
         | 
| 123 | 
            +
                        'critical_interval': self.critical_interval,
         | 
| 124 | 
            +
                        'prefer_psutil': self.prefer_psutil,
         | 
| 125 | 
            +
                        'fallback_methods': self.fallback_methods,
         | 
| 126 | 
            +
                        'log_memory_stats': self.log_memory_stats,
         | 
| 127 | 
            +
                        'log_interval': self.log_interval,
         | 
| 128 | 
            +
                        'detailed_logging': self.detailed_logging
         | 
| 129 | 
            +
                    }
         | 
| 130 | 
            +
             | 
| 131 | 
            +
             | 
| 132 | 
            +
            @dataclass
         | 
| 133 | 
            +
            class PlatformOverrides:
         | 
| 134 | 
            +
                """Platform-specific configuration overrides."""
         | 
| 135 | 
            +
                
         | 
| 136 | 
            +
                # macOS specific
         | 
| 137 | 
            +
                macos_use_activity_monitor: bool = False  # Use Activity Monitor data if available
         | 
| 138 | 
            +
                macos_memory_pressure_check: bool = True  # Check system memory pressure
         | 
| 139 | 
            +
                
         | 
| 140 | 
            +
                # Linux specific
         | 
| 141 | 
            +
                linux_use_proc: bool = True  # Use /proc filesystem
         | 
| 142 | 
            +
                linux_check_oom_score: bool = True  # Monitor OOM killer score
         | 
| 143 | 
            +
                
         | 
| 144 | 
            +
                # Windows specific
         | 
| 145 | 
            +
                windows_use_wmi: bool = True  # Use WMI for monitoring
         | 
| 146 | 
            +
                windows_use_performance_counter: bool = False  # Use performance counters
         | 
| 147 | 
            +
                
         | 
| 148 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 149 | 
            +
                    """Convert platform overrides to dictionary."""
         | 
| 150 | 
            +
                    return {
         | 
| 151 | 
            +
                        'macos_use_activity_monitor': self.macos_use_activity_monitor,
         | 
| 152 | 
            +
                        'macos_memory_pressure_check': self.macos_memory_pressure_check,
         | 
| 153 | 
            +
                        'linux_use_proc': self.linux_use_proc,
         | 
| 154 | 
            +
                        'linux_check_oom_score': self.linux_check_oom_score,
         | 
| 155 | 
            +
                        'windows_use_wmi': self.windows_use_wmi,
         | 
| 156 | 
            +
                        'windows_use_performance_counter': self.windows_use_performance_counter
         | 
| 157 | 
            +
                    }
         | 
| 158 | 
            +
             | 
| 159 | 
            +
             | 
| 160 | 
            +
            @dataclass
         | 
| 161 | 
            +
            class MemoryGuardianConfig:
         | 
| 162 | 
            +
                """Complete configuration for MemoryGuardian service."""
         | 
| 163 | 
            +
                
         | 
| 164 | 
            +
                # Core configurations
         | 
| 165 | 
            +
                thresholds: MemoryThresholds = field(default_factory=MemoryThresholds)
         | 
| 166 | 
            +
                restart_policy: RestartPolicy = field(default_factory=RestartPolicy)
         | 
| 167 | 
            +
                monitoring: MonitoringConfig = field(default_factory=MonitoringConfig)
         | 
| 168 | 
            +
                platform_overrides: PlatformOverrides = field(default_factory=PlatformOverrides)
         | 
| 169 | 
            +
                
         | 
| 170 | 
            +
                # Process configuration
         | 
| 171 | 
            +
                process_command: List[str] = field(default_factory=lambda: ['claude-code'])
         | 
| 172 | 
            +
                process_args: List[str] = field(default_factory=list)
         | 
| 173 | 
            +
                process_env: Dict[str, str] = field(default_factory=dict)
         | 
| 174 | 
            +
                working_directory: Optional[str] = None
         | 
| 175 | 
            +
                
         | 
| 176 | 
            +
                # Service configuration
         | 
| 177 | 
            +
                enabled: bool = True  # Enable memory guardian
         | 
| 178 | 
            +
                auto_start: bool = True  # Auto-start monitored process
         | 
| 179 | 
            +
                persist_state: bool = True  # Persist state across restarts
         | 
| 180 | 
            +
                state_file: Optional[str] = None  # State file path
         | 
| 181 | 
            +
                
         | 
| 182 | 
            +
                @classmethod
         | 
| 183 | 
            +
                def from_env(cls) -> 'MemoryGuardianConfig':
         | 
| 184 | 
            +
                    """Create configuration from environment variables."""
         | 
| 185 | 
            +
                    config = cls()
         | 
| 186 | 
            +
                    
         | 
| 187 | 
            +
                    # Memory thresholds
         | 
| 188 | 
            +
                    if warning := os.getenv('CLAUDE_MPM_MEMORY_WARNING_MB'):
         | 
| 189 | 
            +
                        config.thresholds.warning = int(warning)
         | 
| 190 | 
            +
                    if critical := os.getenv('CLAUDE_MPM_MEMORY_CRITICAL_MB'):
         | 
| 191 | 
            +
                        config.thresholds.critical = int(critical)
         | 
| 192 | 
            +
                    if emergency := os.getenv('CLAUDE_MPM_MEMORY_EMERGENCY_MB'):
         | 
| 193 | 
            +
                        config.thresholds.emergency = int(emergency)
         | 
| 194 | 
            +
                    
         | 
| 195 | 
            +
                    # Restart policy
         | 
| 196 | 
            +
                    if max_attempts := os.getenv('CLAUDE_MPM_RESTART_MAX_ATTEMPTS'):
         | 
| 197 | 
            +
                        config.restart_policy.max_attempts = int(max_attempts)
         | 
| 198 | 
            +
                    if cooldown := os.getenv('CLAUDE_MPM_RESTART_COOLDOWN'):
         | 
| 199 | 
            +
                        config.restart_policy.initial_cooldown = int(cooldown)
         | 
| 200 | 
            +
                    
         | 
| 201 | 
            +
                    # Monitoring intervals
         | 
| 202 | 
            +
                    if interval := os.getenv('CLAUDE_MPM_MONITOR_INTERVAL'):
         | 
| 203 | 
            +
                        config.monitoring.normal_interval = int(interval)
         | 
| 204 | 
            +
                    if log_interval := os.getenv('CLAUDE_MPM_LOG_INTERVAL'):
         | 
| 205 | 
            +
                        config.monitoring.log_interval = int(log_interval)
         | 
| 206 | 
            +
                    
         | 
| 207 | 
            +
                    # Service settings
         | 
| 208 | 
            +
                    config.enabled = os.getenv('CLAUDE_MPM_MEMORY_GUARDIAN_ENABLED', 'true').lower() == 'true'
         | 
| 209 | 
            +
                    config.auto_start = os.getenv('CLAUDE_MPM_AUTO_START', 'true').lower() == 'true'
         | 
| 210 | 
            +
                    
         | 
| 211 | 
            +
                    # Process command
         | 
| 212 | 
            +
                    if command := os.getenv('CLAUDE_MPM_PROCESS_COMMAND'):
         | 
| 213 | 
            +
                        config.process_command = command.split()
         | 
| 214 | 
            +
                    
         | 
| 215 | 
            +
                    return config
         | 
| 216 | 
            +
                
         | 
| 217 | 
            +
                @classmethod
         | 
| 218 | 
            +
                def for_development(cls) -> 'MemoryGuardianConfig':
         | 
| 219 | 
            +
                    """Configuration optimized for development."""
         | 
| 220 | 
            +
                    config = cls()
         | 
| 221 | 
            +
                    config.thresholds.warning = 8192  # 8GB for dev machines
         | 
| 222 | 
            +
                    config.thresholds.critical = 10240  # 10GB
         | 
| 223 | 
            +
                    config.thresholds.emergency = 12288  # 12GB
         | 
| 224 | 
            +
                    config.monitoring.normal_interval = 60  # Check less frequently
         | 
| 225 | 
            +
                    config.monitoring.detailed_logging = True
         | 
| 226 | 
            +
                    return config
         | 
| 227 | 
            +
                
         | 
| 228 | 
            +
                @classmethod
         | 
| 229 | 
            +
                def for_production(cls) -> 'MemoryGuardianConfig':
         | 
| 230 | 
            +
                    """Configuration optimized for production."""
         | 
| 231 | 
            +
                    config = cls()
         | 
| 232 | 
            +
                    config.monitoring.normal_interval = 30
         | 
| 233 | 
            +
                    config.monitoring.log_memory_stats = True
         | 
| 234 | 
            +
                    config.persist_state = True
         | 
| 235 | 
            +
                    config.restart_policy.max_attempts = 5
         | 
| 236 | 
            +
                    return config
         | 
| 237 | 
            +
                
         | 
| 238 | 
            +
                @classmethod
         | 
| 239 | 
            +
                def for_platform(cls, platform_name: Optional[str] = None) -> 'MemoryGuardianConfig':
         | 
| 240 | 
            +
                    """Get platform-specific configuration."""
         | 
| 241 | 
            +
                    if platform_name is None:
         | 
| 242 | 
            +
                        platform_name = platform.system().lower()
         | 
| 243 | 
            +
                    
         | 
| 244 | 
            +
                    config = cls()
         | 
| 245 | 
            +
                    
         | 
| 246 | 
            +
                    if platform_name == 'darwin':  # macOS
         | 
| 247 | 
            +
                        config.platform_overrides.macos_memory_pressure_check = True
         | 
| 248 | 
            +
                    elif platform_name == 'linux':
         | 
| 249 | 
            +
                        config.platform_overrides.linux_use_proc = True
         | 
| 250 | 
            +
                        config.platform_overrides.linux_check_oom_score = True
         | 
| 251 | 
            +
                    elif platform_name == 'windows':
         | 
| 252 | 
            +
                        config.platform_overrides.windows_use_wmi = True
         | 
| 253 | 
            +
                    
         | 
| 254 | 
            +
                    return config
         | 
| 255 | 
            +
                
         | 
| 256 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 257 | 
            +
                    """Convert configuration to dictionary."""
         | 
| 258 | 
            +
                    return {
         | 
| 259 | 
            +
                        'thresholds': self.thresholds.to_dict(),
         | 
| 260 | 
            +
                        'restart_policy': self.restart_policy.to_dict(),
         | 
| 261 | 
            +
                        'monitoring': self.monitoring.to_dict(),
         | 
| 262 | 
            +
                        'platform_overrides': self.platform_overrides.to_dict(),
         | 
| 263 | 
            +
                        'process_command': self.process_command,
         | 
| 264 | 
            +
                        'process_args': self.process_args,
         | 
| 265 | 
            +
                        'process_env': self.process_env,
         | 
| 266 | 
            +
                        'working_directory': self.working_directory,
         | 
| 267 | 
            +
                        'enabled': self.enabled,
         | 
| 268 | 
            +
                        'auto_start': self.auto_start,
         | 
| 269 | 
            +
                        'persist_state': self.persist_state,
         | 
| 270 | 
            +
                        'state_file': self.state_file
         | 
| 271 | 
            +
                    }
         | 
| 272 | 
            +
                
         | 
| 273 | 
            +
                def validate(self) -> List[str]:
         | 
| 274 | 
            +
                    """Validate configuration and return list of issues."""
         | 
| 275 | 
            +
                    issues = []
         | 
| 276 | 
            +
                    
         | 
| 277 | 
            +
                    # Validate thresholds are in correct order
         | 
| 278 | 
            +
                    if self.thresholds.warning >= self.thresholds.critical:
         | 
| 279 | 
            +
                        issues.append("Warning threshold must be less than critical threshold")
         | 
| 280 | 
            +
                    if self.thresholds.critical >= self.thresholds.emergency:
         | 
| 281 | 
            +
                        issues.append("Critical threshold must be less than emergency threshold")
         | 
| 282 | 
            +
                    
         | 
| 283 | 
            +
                    # Validate intervals
         | 
| 284 | 
            +
                    if self.monitoring.normal_interval <= 0:
         | 
| 285 | 
            +
                        issues.append("Normal monitoring interval must be positive")
         | 
| 286 | 
            +
                    if self.monitoring.warning_interval <= 0:
         | 
| 287 | 
            +
                        issues.append("Warning monitoring interval must be positive")
         | 
| 288 | 
            +
                    if self.monitoring.critical_interval <= 0:
         | 
| 289 | 
            +
                        issues.append("Critical monitoring interval must be positive")
         | 
| 290 | 
            +
                    
         | 
| 291 | 
            +
                    # Validate restart policy
         | 
| 292 | 
            +
                    if self.restart_policy.max_attempts < 0:
         | 
| 293 | 
            +
                        issues.append("Max restart attempts cannot be negative")
         | 
| 294 | 
            +
                    if self.restart_policy.initial_cooldown <= 0:
         | 
| 295 | 
            +
                        issues.append("Initial cooldown must be positive")
         | 
| 296 | 
            +
                    
         | 
| 297 | 
            +
                    # Validate process command
         | 
| 298 | 
            +
                    if not self.process_command:
         | 
| 299 | 
            +
                        issues.append("Process command cannot be empty")
         | 
| 300 | 
            +
                    
         | 
| 301 | 
            +
                    return issues
         | 
| 302 | 
            +
             | 
| 303 | 
            +
             | 
| 304 | 
            +
            def get_default_config() -> MemoryGuardianConfig:
         | 
| 305 | 
            +
                """Get default configuration adjusted for current platform."""
         | 
| 306 | 
            +
                config = MemoryGuardianConfig.for_platform()
         | 
| 307 | 
            +
                
         | 
| 308 | 
            +
                # Try to adjust for available system memory
         | 
| 309 | 
            +
                try:
         | 
| 310 | 
            +
                    import psutil
         | 
| 311 | 
            +
                    total_memory_mb = psutil.virtual_memory().total // (1024 * 1024)
         | 
| 312 | 
            +
                    config.thresholds.adjust_for_system_memory(total_memory_mb)
         | 
| 313 | 
            +
                except ImportError:
         | 
| 314 | 
            +
                    # psutil not available, use defaults
         | 
| 315 | 
            +
                    pass
         | 
| 316 | 
            +
                except Exception:
         | 
| 317 | 
            +
                    # Any other error, use defaults
         | 
| 318 | 
            +
                    pass
         | 
| 319 | 
            +
                
         | 
| 320 | 
            +
                # Override with environment variables
         | 
| 321 | 
            +
                env_config = MemoryGuardianConfig.from_env()
         | 
| 322 | 
            +
                if env_config.enabled != config.enabled:
         | 
| 323 | 
            +
                    config = env_config
         | 
| 324 | 
            +
                
         | 
| 325 | 
            +
                return config
         | 
    
        claude_mpm/constants.py
    CHANGED
    
    | @@ -31,6 +31,7 @@ class CLICommands(str, Enum): | |
| 31 31 | 
             
                CONFIG = "config"
         | 
| 32 32 | 
             
                AGGREGATE = "aggregate"
         | 
| 33 33 | 
             
                CLEANUP = "cleanup-memory"
         | 
| 34 | 
            +
                MCP = "mcp"
         | 
| 34 35 |  | 
| 35 36 | 
             
                def with_prefix(self, prefix: CLIPrefix = CLIPrefix.MPM) -> str:
         | 
| 36 37 | 
             
                    """Get command with prefix."""
         | 
| @@ -100,6 +101,18 @@ class AggregateCommands(str, Enum): | |
| 100 101 | 
             
                EXPORT = "export"
         | 
| 101 102 |  | 
| 102 103 |  | 
| 104 | 
            +
            class MCPCommands(str, Enum):
         | 
| 105 | 
            +
                """MCP Gateway subcommand constants."""
         | 
| 106 | 
            +
                INSTALL = "install"
         | 
| 107 | 
            +
                START = "start"
         | 
| 108 | 
            +
                STOP = "stop"
         | 
| 109 | 
            +
                STATUS = "status"
         | 
| 110 | 
            +
                TOOLS = "tools"
         | 
| 111 | 
            +
                REGISTER = "register"
         | 
| 112 | 
            +
                TEST = "test"
         | 
| 113 | 
            +
                CONFIG = "config"
         | 
| 114 | 
            +
             | 
| 115 | 
            +
             | 
| 103 116 | 
             
            class TicketCommands(str, Enum):
         | 
| 104 117 | 
             
                """Ticket subcommand constants."""
         | 
| 105 118 | 
             
                CREATE = "create"
         | 
| @@ -16,6 +16,9 @@ import json | |
| 16 16 | 
             
            import sys
         | 
| 17 17 | 
             
            import os
         | 
| 18 18 | 
             
            import subprocess
         | 
| 19 | 
            +
            import signal
         | 
| 20 | 
            +
            import select
         | 
| 21 | 
            +
            import atexit
         | 
| 19 22 | 
             
            from datetime import datetime
         | 
| 20 23 | 
             
            import time
         | 
| 21 24 | 
             
            import asyncio
         | 
| @@ -593,56 +596,93 @@ class ClaudeHookHandler: | |
| 593 596 |  | 
| 594 597 |  | 
| 595 598 | 
             
                def handle(self):
         | 
| 596 | 
            -
                    """Process hook event with minimal overhead and  | 
| 597 | 
            -
             | 
| 599 | 
            +
                    """Process hook event with minimal overhead and timeout protection.
         | 
| 600 | 
            +
             | 
| 598 601 | 
             
                    WHY this approach:
         | 
| 599 602 | 
             
                    - Fast path processing for minimal latency (no blocking waits)
         | 
| 600 603 | 
             
                    - Non-blocking Socket.IO connection and event emission
         | 
| 601 | 
            -
                    -  | 
| 604 | 
            +
                    - Timeout protection prevents indefinite hangs
         | 
| 602 605 | 
             
                    - Connection timeout prevents indefinite hangs
         | 
| 603 606 | 
             
                    - Graceful degradation if Socket.IO unavailable
         | 
| 604 607 | 
             
                    - Always continues regardless of event status
         | 
| 608 | 
            +
                    - Process exits after handling to prevent accumulation
         | 
| 605 609 | 
             
                    """
         | 
| 610 | 
            +
                    def timeout_handler(signum, frame):
         | 
| 611 | 
            +
                        """Handle timeout by forcing exit."""
         | 
| 612 | 
            +
                        if DEBUG:
         | 
| 613 | 
            +
                            print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
         | 
| 614 | 
            +
                        self._continue_execution()
         | 
| 615 | 
            +
                        sys.exit(0)
         | 
| 616 | 
            +
             | 
| 606 617 | 
             
                    try:
         | 
| 618 | 
            +
                        # Set a 10-second timeout for the entire operation
         | 
| 619 | 
            +
                        signal.signal(signal.SIGALRM, timeout_handler)
         | 
| 620 | 
            +
                        signal.alarm(10)
         | 
| 621 | 
            +
             | 
| 607 622 | 
             
                        # Read and parse event
         | 
| 608 623 | 
             
                        event = self._read_hook_event()
         | 
| 609 624 | 
             
                        if not event:
         | 
| 610 625 | 
             
                            self._continue_execution()
         | 
| 611 626 | 
             
                            return
         | 
| 612 | 
            -
             | 
| 627 | 
            +
             | 
| 613 628 | 
             
                        # Increment event counter and perform periodic cleanup
         | 
| 614 629 | 
             
                        self.events_processed += 1
         | 
| 615 630 | 
             
                        if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
         | 
| 616 631 | 
             
                            self._cleanup_old_entries()
         | 
| 617 632 | 
             
                            if DEBUG:
         | 
| 618 633 | 
             
                                print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
         | 
| 619 | 
            -
             | 
| 634 | 
            +
             | 
| 620 635 | 
             
                        # Route event to appropriate handler
         | 
| 621 636 | 
             
                        self._route_event(event)
         | 
| 622 | 
            -
             | 
| 637 | 
            +
             | 
| 623 638 | 
             
                        # Always continue execution
         | 
| 624 639 | 
             
                        self._continue_execution()
         | 
| 625 | 
            -
             | 
| 640 | 
            +
             | 
| 626 641 | 
             
                    except:
         | 
| 627 642 | 
             
                        # Fail fast and silent
         | 
| 628 643 | 
             
                        self._continue_execution()
         | 
| 644 | 
            +
                    finally:
         | 
| 645 | 
            +
                        # Cancel the alarm
         | 
| 646 | 
            +
                        signal.alarm(0)
         | 
| 629 647 |  | 
| 630 648 | 
             
                def _read_hook_event(self) -> dict:
         | 
| 631 649 | 
             
                    """
         | 
| 632 | 
            -
                    Read and parse hook event from stdin.
         | 
| 633 | 
            -
             | 
| 634 | 
            -
                    WHY: Centralized event reading with error handling
         | 
| 635 | 
            -
                    ensures consistent parsing and validation | 
| 636 | 
            -
                    
         | 
| 650 | 
            +
                    Read and parse hook event from stdin with timeout.
         | 
| 651 | 
            +
             | 
| 652 | 
            +
                    WHY: Centralized event reading with error handling and timeout
         | 
| 653 | 
            +
                    ensures consistent parsing and validation while preventing
         | 
| 654 | 
            +
                    processes from hanging indefinitely on stdin.read().
         | 
| 655 | 
            +
             | 
| 637 656 | 
             
                    Returns:
         | 
| 638 | 
            -
                        Parsed event dictionary or None if invalid
         | 
| 657 | 
            +
                        Parsed event dictionary or None if invalid/timeout
         | 
| 639 658 | 
             
                    """
         | 
| 640 659 | 
             
                    try:
         | 
| 660 | 
            +
                        # Check if data is available on stdin with 1 second timeout
         | 
| 661 | 
            +
                        if sys.stdin.isatty():
         | 
| 662 | 
            +
                            # Interactive terminal - no data expected
         | 
| 663 | 
            +
                            return None
         | 
| 664 | 
            +
             | 
| 665 | 
            +
                        ready, _, _ = select.select([sys.stdin], [], [], 1.0)
         | 
| 666 | 
            +
                        if not ready:
         | 
| 667 | 
            +
                            # No data available within timeout
         | 
| 668 | 
            +
                            if DEBUG:
         | 
| 669 | 
            +
                                print("No hook event data received within timeout", file=sys.stderr)
         | 
| 670 | 
            +
                            return None
         | 
| 671 | 
            +
             | 
| 672 | 
            +
                        # Data is available, read it
         | 
| 641 673 | 
             
                        event_data = sys.stdin.read()
         | 
| 674 | 
            +
                        if not event_data.strip():
         | 
| 675 | 
            +
                            # Empty or whitespace-only data
         | 
| 676 | 
            +
                            return None
         | 
| 677 | 
            +
             | 
| 642 678 | 
             
                        return json.loads(event_data)
         | 
| 643 | 
            -
                    except (json.JSONDecodeError, ValueError):
         | 
| 679 | 
            +
                    except (json.JSONDecodeError, ValueError) as e:
         | 
| 680 | 
            +
                        if DEBUG:
         | 
| 681 | 
            +
                            print(f"Failed to parse hook event: {e}", file=sys.stderr)
         | 
| 682 | 
            +
                        return None
         | 
| 683 | 
            +
                    except Exception as e:
         | 
| 644 684 | 
             
                        if DEBUG:
         | 
| 645 | 
            -
                            print(" | 
| 685 | 
            +
                            print(f"Error reading hook event: {e}", file=sys.stderr)
         | 
| 646 686 | 
             
                        return None
         | 
| 647 687 |  | 
| 648 688 | 
             
                def _route_event(self, event: dict) -> None:
         | 
| @@ -1725,9 +1765,22 @@ class ClaudeHookHandler: | |
| 1725 1765 |  | 
| 1726 1766 |  | 
| 1727 1767 | 
             
            def main():
         | 
| 1728 | 
            -
                """Entry point with singleton pattern  | 
| 1768 | 
            +
                """Entry point with singleton pattern and proper cleanup."""
         | 
| 1729 1769 | 
             
                global _global_handler
         | 
| 1730 | 
            -
             | 
| 1770 | 
            +
             | 
| 1771 | 
            +
                def cleanup_handler(signum=None, frame=None):
         | 
| 1772 | 
            +
                    """Cleanup handler for signals and exit."""
         | 
| 1773 | 
            +
                    if DEBUG:
         | 
| 1774 | 
            +
                        print(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})", file=sys.stderr)
         | 
| 1775 | 
            +
                    # Always output continue action to not block Claude
         | 
| 1776 | 
            +
                    print(json.dumps({"action": "continue"}))
         | 
| 1777 | 
            +
                    sys.exit(0)
         | 
| 1778 | 
            +
             | 
| 1779 | 
            +
                # Register cleanup handlers
         | 
| 1780 | 
            +
                signal.signal(signal.SIGTERM, cleanup_handler)
         | 
| 1781 | 
            +
                signal.signal(signal.SIGINT, cleanup_handler)
         | 
| 1782 | 
            +
                atexit.register(cleanup_handler)
         | 
| 1783 | 
            +
             | 
| 1731 1784 | 
             
                try:
         | 
| 1732 1785 | 
             
                    # Use singleton pattern to prevent creating multiple instances
         | 
| 1733 1786 | 
             
                    with _handler_lock:
         | 
| @@ -1738,10 +1791,14 @@ def main(): | |
| 1738 1791 | 
             
                        else:
         | 
| 1739 1792 | 
             
                            if DEBUG:
         | 
| 1740 1793 | 
             
                                print(f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
         | 
| 1741 | 
            -
             | 
| 1794 | 
            +
             | 
| 1742 1795 | 
             
                        handler = _global_handler
         | 
| 1743 | 
            -
             | 
| 1796 | 
            +
             | 
| 1744 1797 | 
             
                    handler.handle()
         | 
| 1798 | 
            +
             | 
| 1799 | 
            +
                    # Ensure we exit after handling
         | 
| 1800 | 
            +
                    cleanup_handler()
         | 
| 1801 | 
            +
             | 
| 1745 1802 | 
             
                except Exception as e:
         | 
| 1746 1803 | 
             
                    # Always output continue action to not block Claude
         | 
| 1747 1804 | 
             
                    print(json.dumps({"action": "continue"}))
         |