claude-mpm 3.9.2__py3-none-any.whl → 3.9.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/config/paths.py +56 -6
- claude_mpm/hooks/claude_hooks/hook_handler.py +167 -105
- claude_mpm/utils/paths.py +112 -8
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/METADATA +1 -1
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/RECORD +10 -10
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.2.dist-info → claude_mpm-3.9.4.dist-info}/top_level.txt +0 -0
    
        claude_mpm/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            3.9. | 
| 1 | 
            +
            3.9.4
         | 
    
        claude_mpm/config/paths.py
    CHANGED
    
    | @@ -33,6 +33,7 @@ class ClaudeMPMPaths: | |
| 33 33 |  | 
| 34 34 | 
             
                _instance: Optional['ClaudeMPMPaths'] = None
         | 
| 35 35 | 
             
                _project_root: Optional[Path] = None
         | 
| 36 | 
            +
                _is_installed: bool = False
         | 
| 36 37 |  | 
| 37 38 | 
             
                def __new__(cls) -> 'ClaudeMPMPaths':
         | 
| 38 39 | 
             
                    """Singleton pattern to ensure single instance."""
         | 
| @@ -43,6 +44,7 @@ class ClaudeMPMPaths: | |
| 43 44 | 
             
                def __init__(self):
         | 
| 44 45 | 
             
                    """Initialize paths if not already done."""
         | 
| 45 46 | 
             
                    if self._project_root is None:
         | 
| 47 | 
            +
                        self._is_installed = False
         | 
| 46 48 | 
             
                        self._detect_project_root()
         | 
| 47 49 |  | 
| 48 50 | 
             
                def _detect_project_root(self) -> None:
         | 
| @@ -53,16 +55,29 @@ class ClaudeMPMPaths: | |
| 53 55 | 
             
                    1. Look for definitive project markers (pyproject.toml, setup.py)
         | 
| 54 56 | 
             
                    2. Look for combination of markers to ensure we're at the right level
         | 
| 55 57 | 
             
                    3. Walk up from current file location
         | 
| 58 | 
            +
                    4. Handle both development and installed environments
         | 
| 56 59 | 
             
                    """
         | 
| 57 60 | 
             
                    # Start from this file's location
         | 
| 58 61 | 
             
                    current = Path(__file__).resolve()
         | 
| 59 62 |  | 
| 60 | 
            -
                    #  | 
| 63 | 
            +
                    # Check if we're in an installed environment (site-packages)
         | 
| 64 | 
            +
                    # In pip/pipx installs, the package is directly in site-packages
         | 
| 65 | 
            +
                    if 'site-packages' in str(current) or 'dist-packages' in str(current):
         | 
| 66 | 
            +
                        # We're in an installed environment
         | 
| 67 | 
            +
                        # The claude_mpm package directory itself is the "root" for resources
         | 
| 68 | 
            +
                        import claude_mpm
         | 
| 69 | 
            +
                        self._project_root = Path(claude_mpm.__file__).parent
         | 
| 70 | 
            +
                        self._is_installed = True
         | 
| 71 | 
            +
                        logger.debug(f"Installed environment detected, using package dir: {self._project_root}")
         | 
| 72 | 
            +
                        return
         | 
| 73 | 
            +
                    
         | 
| 74 | 
            +
                    # We're in a development environment, look for project markers
         | 
| 61 75 | 
             
                    for parent in current.parents:
         | 
| 62 76 | 
             
                        # Check for definitive project root indicators
         | 
| 63 77 | 
             
                        # Prioritize pyproject.toml and setup.py as they're only at root
         | 
| 64 78 | 
             
                        if (parent / 'pyproject.toml').exists() or (parent / 'setup.py').exists():
         | 
| 65 79 | 
             
                            self._project_root = parent
         | 
| 80 | 
            +
                            self._is_installed = False
         | 
| 66 81 | 
             
                            logger.debug(f"Project root detected at: {parent} (found pyproject.toml or setup.py)")
         | 
| 67 82 | 
             
                            return
         | 
| 68 83 |  | 
| @@ -70,6 +85,7 @@ class ClaudeMPMPaths: | |
| 70 85 | 
             
                        # This combination is more likely to be the real project root
         | 
| 71 86 | 
             
                        if (parent / '.git').exists() and (parent / 'VERSION').exists():
         | 
| 72 87 | 
             
                            self._project_root = parent
         | 
| 88 | 
            +
                            self._is_installed = False
         | 
| 73 89 | 
             
                            logger.debug(f"Project root detected at: {parent} (found .git and VERSION)")
         | 
| 74 90 | 
             
                            return
         | 
| 75 91 |  | 
| @@ -77,12 +93,14 @@ class ClaudeMPMPaths: | |
| 77 93 | 
             
                    for parent in current.parents:
         | 
| 78 94 | 
             
                        if parent.name == 'claude-mpm':
         | 
| 79 95 | 
             
                            self._project_root = parent
         | 
| 96 | 
            +
                            self._is_installed = False
         | 
| 80 97 | 
             
                            logger.debug(f"Project root detected at: {parent} (by directory name)")
         | 
| 81 98 | 
             
                            return
         | 
| 82 99 |  | 
| 83 100 | 
             
                    # Last resort fallback: 3 levels up from this file
         | 
| 84 101 | 
             
                    # paths.py is in src/claude_mpm/config/
         | 
| 85 102 | 
             
                    self._project_root = current.parent.parent.parent
         | 
| 103 | 
            +
                    self._is_installed = False
         | 
| 86 104 | 
             
                    logger.warning(f"Project root fallback to: {self._project_root}")
         | 
| 87 105 |  | 
| 88 106 | 
             
                @property
         | 
| @@ -95,16 +113,26 @@ class ClaudeMPMPaths: | |
| 95 113 | 
             
                @property
         | 
| 96 114 | 
             
                def src_dir(self) -> Path:
         | 
| 97 115 | 
             
                    """Get the src directory."""
         | 
| 116 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 117 | 
            +
                        # In installed environment, there's no src directory
         | 
| 118 | 
            +
                        # Return the package directory itself
         | 
| 119 | 
            +
                        return self.project_root.parent
         | 
| 98 120 | 
             
                    return self.project_root / "src"
         | 
| 99 121 |  | 
| 100 122 | 
             
                @property
         | 
| 101 123 | 
             
                def claude_mpm_dir(self) -> Path:
         | 
| 102 124 | 
             
                    """Get the main claude_mpm package directory."""
         | 
| 125 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 126 | 
            +
                        # In installed environment, project_root IS the claude_mpm directory
         | 
| 127 | 
            +
                        return self.project_root
         | 
| 103 128 | 
             
                    return self.src_dir / "claude_mpm"
         | 
| 104 129 |  | 
| 105 130 | 
             
                @property
         | 
| 106 131 | 
             
                def agents_dir(self) -> Path:
         | 
| 107 132 | 
             
                    """Get the agents directory."""
         | 
| 133 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 134 | 
            +
                        # In installed environment, agents is directly under the package
         | 
| 135 | 
            +
                        return self.project_root / "agents"
         | 
| 108 136 | 
             
                    return self.claude_mpm_dir / "agents"
         | 
| 109 137 |  | 
| 110 138 | 
             
                @property
         | 
| @@ -140,36 +168,58 @@ class ClaudeMPMPaths: | |
| 140 168 | 
             
                @property
         | 
| 141 169 | 
             
                def scripts_dir(self) -> Path:
         | 
| 142 170 | 
             
                    """Get the scripts directory."""
         | 
| 171 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 172 | 
            +
                        # In installed environment, scripts might be in a different location or not exist
         | 
| 173 | 
            +
                        # Return a path that won't cause issues but indicates it's not available
         | 
| 174 | 
            +
                        return Path.home() / '.claude-mpm' / 'scripts'
         | 
| 143 175 | 
             
                    return self.project_root / "scripts"
         | 
| 144 176 |  | 
| 145 177 | 
             
                @property
         | 
| 146 178 | 
             
                def tests_dir(self) -> Path:
         | 
| 147 179 | 
             
                    """Get the tests directory."""
         | 
| 180 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 181 | 
            +
                        # Tests aren't distributed with installed packages
         | 
| 182 | 
            +
                        return Path.home() / '.claude-mpm' / 'tests'
         | 
| 148 183 | 
             
                    return self.project_root / "tests"
         | 
| 149 184 |  | 
| 150 185 | 
             
                @property
         | 
| 151 186 | 
             
                def docs_dir(self) -> Path:
         | 
| 152 187 | 
             
                    """Get the documentation directory."""
         | 
| 188 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 189 | 
            +
                        # Docs might be installed separately or not at all
         | 
| 190 | 
            +
                        return Path.home() / '.claude-mpm' / 'docs'
         | 
| 153 191 | 
             
                    return self.project_root / "docs"
         | 
| 154 192 |  | 
| 155 193 | 
             
                @property
         | 
| 156 194 | 
             
                def logs_dir(self) -> Path:
         | 
| 157 195 | 
             
                    """Get the logs directory (creates if doesn't exist)."""
         | 
| 158 | 
            -
                     | 
| 159 | 
            -
             | 
| 196 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 197 | 
            +
                        # Use user's home directory for logs in installed environment
         | 
| 198 | 
            +
                        logs = Path.home() / '.claude-mpm' / 'logs'
         | 
| 199 | 
            +
                    else:
         | 
| 200 | 
            +
                        logs = self.project_root / "logs"
         | 
| 201 | 
            +
                    logs.mkdir(parents=True, exist_ok=True)
         | 
| 160 202 | 
             
                    return logs
         | 
| 161 203 |  | 
| 162 204 | 
             
                @property
         | 
| 163 205 | 
             
                def temp_dir(self) -> Path:
         | 
| 164 206 | 
             
                    """Get the temporary files directory (creates if doesn't exist)."""
         | 
| 165 | 
            -
                     | 
| 166 | 
            -
             | 
| 207 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 208 | 
            +
                        # Use user's home directory for temp files in installed environment
         | 
| 209 | 
            +
                        temp = Path.home() / '.claude-mpm' / '.tmp'
         | 
| 210 | 
            +
                    else:
         | 
| 211 | 
            +
                        temp = self.project_root / ".tmp"
         | 
| 212 | 
            +
                    temp.mkdir(parents=True, exist_ok=True)
         | 
| 167 213 | 
             
                    return temp
         | 
| 168 214 |  | 
| 169 215 | 
             
                @property
         | 
| 170 216 | 
             
                def claude_mpm_dir_hidden(self) -> Path:
         | 
| 171 217 | 
             
                    """Get the hidden .claude-mpm directory (creates if doesn't exist)."""
         | 
| 172 | 
            -
                     | 
| 218 | 
            +
                    if hasattr(self, '_is_installed') and self._is_installed:
         | 
| 219 | 
            +
                        # Use current working directory in installed environment
         | 
| 220 | 
            +
                        hidden = Path.cwd() / ".claude-mpm"
         | 
| 221 | 
            +
                    else:
         | 
| 222 | 
            +
                        hidden = self.project_root / ".claude-mpm"
         | 
| 173 223 | 
             
                    hidden.mkdir(exist_ok=True)
         | 
| 174 224 | 
             
                    return hidden
         | 
| 175 225 |  | 
| @@ -21,6 +21,7 @@ import time | |
| 21 21 | 
             
            import asyncio
         | 
| 22 22 | 
             
            from pathlib import Path
         | 
| 23 23 | 
             
            from collections import deque
         | 
| 24 | 
            +
            import threading
         | 
| 24 25 |  | 
| 25 26 | 
             
            # Import constants for configuration
         | 
| 26 27 | 
             
            try:
         | 
| @@ -87,6 +88,102 @@ except ImportError: | |
| 87 88 | 
             
            # No fallback needed - we only use Socket.IO now
         | 
| 88 89 |  | 
| 89 90 |  | 
| 91 | 
            +
             | 
| 92 | 
            +
            class SocketIOConnectionPool:
         | 
| 93 | 
            +
                """Connection pool for Socket.IO clients to prevent connection leaks."""
         | 
| 94 | 
            +
                
         | 
| 95 | 
            +
                def __init__(self, max_connections=3):
         | 
| 96 | 
            +
                    self.max_connections = max_connections
         | 
| 97 | 
            +
                    self.connections = []
         | 
| 98 | 
            +
                    self.last_cleanup = time.time()
         | 
| 99 | 
            +
                    
         | 
| 100 | 
            +
                def get_connection(self, port):
         | 
| 101 | 
            +
                    """Get or create a connection to the specified port."""
         | 
| 102 | 
            +
                    if time.time() - self.last_cleanup > 60:
         | 
| 103 | 
            +
                        self._cleanup_dead_connections()
         | 
| 104 | 
            +
                        self.last_cleanup = time.time()
         | 
| 105 | 
            +
                    
         | 
| 106 | 
            +
                    for conn in self.connections:
         | 
| 107 | 
            +
                        if conn.get('port') == port and conn.get('client'):
         | 
| 108 | 
            +
                            client = conn['client']
         | 
| 109 | 
            +
                            if self._is_connection_alive(client):
         | 
| 110 | 
            +
                                return client
         | 
| 111 | 
            +
                            else:
         | 
| 112 | 
            +
                                self.connections.remove(conn)
         | 
| 113 | 
            +
                    
         | 
| 114 | 
            +
                    if len(self.connections) < self.max_connections:
         | 
| 115 | 
            +
                        client = self._create_connection(port)
         | 
| 116 | 
            +
                        if client:
         | 
| 117 | 
            +
                            self.connections.append({
         | 
| 118 | 
            +
                                'port': port,
         | 
| 119 | 
            +
                                'client': client,
         | 
| 120 | 
            +
                                'created': time.time()
         | 
| 121 | 
            +
                            })
         | 
| 122 | 
            +
                            return client
         | 
| 123 | 
            +
                    
         | 
| 124 | 
            +
                    if self.connections:
         | 
| 125 | 
            +
                        oldest = min(self.connections, key=lambda x: x['created'])
         | 
| 126 | 
            +
                        self._close_connection(oldest['client'])
         | 
| 127 | 
            +
                        oldest['client'] = self._create_connection(port)
         | 
| 128 | 
            +
                        oldest['port'] = port
         | 
| 129 | 
            +
                        oldest['created'] = time.time()
         | 
| 130 | 
            +
                        return oldest['client']
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    return None
         | 
| 133 | 
            +
                
         | 
| 134 | 
            +
                def _create_connection(self, port):
         | 
| 135 | 
            +
                    """Create a new Socket.IO connection."""
         | 
| 136 | 
            +
                    if not SOCKETIO_AVAILABLE:
         | 
| 137 | 
            +
                        return None
         | 
| 138 | 
            +
                    try:
         | 
| 139 | 
            +
                        client = socketio.Client(
         | 
| 140 | 
            +
                            reconnection=False,  # Disable auto-reconnect
         | 
| 141 | 
            +
                            logger=False,
         | 
| 142 | 
            +
                            engineio_logger=False
         | 
| 143 | 
            +
                        )
         | 
| 144 | 
            +
                        client.connect(f'http://localhost:{port}', 
         | 
| 145 | 
            +
                                      wait=True, 
         | 
| 146 | 
            +
                                      wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT)
         | 
| 147 | 
            +
                        if client.connected:
         | 
| 148 | 
            +
                            return client
         | 
| 149 | 
            +
                    except Exception:
         | 
| 150 | 
            +
                        pass
         | 
| 151 | 
            +
                    return None
         | 
| 152 | 
            +
                
         | 
| 153 | 
            +
                def _is_connection_alive(self, client):
         | 
| 154 | 
            +
                    """Check if a connection is still alive."""
         | 
| 155 | 
            +
                    try:
         | 
| 156 | 
            +
                        return client and client.connected
         | 
| 157 | 
            +
                    except:
         | 
| 158 | 
            +
                        return False
         | 
| 159 | 
            +
                
         | 
| 160 | 
            +
                def _close_connection(self, client):
         | 
| 161 | 
            +
                    """Safely close a connection."""
         | 
| 162 | 
            +
                    try:
         | 
| 163 | 
            +
                        if client:
         | 
| 164 | 
            +
                            client.disconnect()
         | 
| 165 | 
            +
                    except:
         | 
| 166 | 
            +
                        pass
         | 
| 167 | 
            +
                
         | 
| 168 | 
            +
                def _cleanup_dead_connections(self):
         | 
| 169 | 
            +
                    """Remove dead connections from the pool."""
         | 
| 170 | 
            +
                    self.connections = [
         | 
| 171 | 
            +
                        conn for conn in self.connections 
         | 
| 172 | 
            +
                        if self._is_connection_alive(conn.get('client'))
         | 
| 173 | 
            +
                    ]
         | 
| 174 | 
            +
                
         | 
| 175 | 
            +
                def close_all(self):
         | 
| 176 | 
            +
                    """Close all connections in the pool."""
         | 
| 177 | 
            +
                    for conn in self.connections:
         | 
| 178 | 
            +
                        self._close_connection(conn.get('client'))
         | 
| 179 | 
            +
                    self.connections.clear()
         | 
| 180 | 
            +
             | 
| 181 | 
            +
             | 
| 182 | 
            +
            # Global singleton handler instance
         | 
| 183 | 
            +
            _global_handler = None
         | 
| 184 | 
            +
            _handler_lock = threading.Lock()
         | 
| 185 | 
            +
             | 
| 186 | 
            +
             | 
| 90 187 | 
             
            class ClaudeHookHandler:
         | 
| 91 188 | 
             
                """Optimized hook handler with direct Socket.IO client.
         | 
| 92 189 |  | 
| @@ -99,8 +196,17 @@ class ClaudeHookHandler: | |
| 99 196 |  | 
| 100 197 | 
             
                def __init__(self):
         | 
| 101 198 | 
             
                    # Socket.IO client (persistent if possible)
         | 
| 102 | 
            -
                    self. | 
| 103 | 
            -
                     | 
| 199 | 
            +
                    self.connection_pool = SocketIOConnectionPool(max_connections=3)
         | 
| 200 | 
            +
                    # Track events for periodic cleanup
         | 
| 201 | 
            +
                    self.events_processed = 0
         | 
| 202 | 
            +
                    self.last_cleanup = time.time()
         | 
| 203 | 
            +
                    
         | 
| 204 | 
            +
                    # Maximum sizes for tracking
         | 
| 205 | 
            +
                    self.MAX_DELEGATION_TRACKING = 200
         | 
| 206 | 
            +
                    self.MAX_PROMPT_TRACKING = 100
         | 
| 207 | 
            +
                    self.MAX_CACHE_AGE_SECONDS = 300
         | 
| 208 | 
            +
                    self.CLEANUP_INTERVAL_EVENTS = 100
         | 
| 209 | 
            +
             | 
| 104 210 |  | 
| 105 211 | 
             
                    # Agent delegation tracking
         | 
| 106 212 | 
             
                    # Store recent Task delegations: session_id -> agent_type
         | 
| @@ -179,6 +285,36 @@ class ClaudeHookHandler: | |
| 179 285 | 
             
                            if key in self.delegation_requests:
         | 
| 180 286 | 
             
                                del self.delegation_requests[key]
         | 
| 181 287 |  | 
| 288 | 
            +
                
         | 
| 289 | 
            +
                def _cleanup_old_entries(self):
         | 
| 290 | 
            +
                    """Clean up old entries to prevent memory growth."""
         | 
| 291 | 
            +
                    cutoff_time = datetime.now().timestamp() - self.MAX_CACHE_AGE_SECONDS
         | 
| 292 | 
            +
                    
         | 
| 293 | 
            +
                    # Clean up delegation tracking dictionaries
         | 
| 294 | 
            +
                    for storage in [self.active_delegations, self.delegation_requests]:
         | 
| 295 | 
            +
                        if len(storage) > self.MAX_DELEGATION_TRACKING:
         | 
| 296 | 
            +
                            # Keep only the most recent entries
         | 
| 297 | 
            +
                            sorted_keys = sorted(storage.keys())
         | 
| 298 | 
            +
                            excess = len(storage) - self.MAX_DELEGATION_TRACKING
         | 
| 299 | 
            +
                            for key in sorted_keys[:excess]:
         | 
| 300 | 
            +
                                del storage[key]
         | 
| 301 | 
            +
                    
         | 
| 302 | 
            +
                    # Clean up pending prompts
         | 
| 303 | 
            +
                    if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
         | 
| 304 | 
            +
                        sorted_keys = sorted(self.pending_prompts.keys())
         | 
| 305 | 
            +
                        excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
         | 
| 306 | 
            +
                        for key in sorted_keys[:excess]:
         | 
| 307 | 
            +
                            del self.pending_prompts[key]
         | 
| 308 | 
            +
                    
         | 
| 309 | 
            +
                    # Clean up git branch cache
         | 
| 310 | 
            +
                    expired_keys = [
         | 
| 311 | 
            +
                        key for key, cache_time in self._git_branch_cache_time.items()
         | 
| 312 | 
            +
                        if datetime.now().timestamp() - cache_time > self.MAX_CACHE_AGE_SECONDS
         | 
| 313 | 
            +
                    ]
         | 
| 314 | 
            +
                    for key in expired_keys:
         | 
| 315 | 
            +
                        self._git_branch_cache.pop(key, None)
         | 
| 316 | 
            +
                        self._git_branch_cache_time.pop(key, None)
         | 
| 317 | 
            +
             | 
| 182 318 | 
             
                def _get_delegation_agent_type(self, session_id: str) -> str:
         | 
| 183 319 | 
             
                    """Get the agent type for a session's active delegation."""
         | 
| 184 320 | 
             
                    # First try exact session match
         | 
| @@ -455,97 +591,6 @@ class ClaudeHookHandler: | |
| 455 591 | 
             
                        self._git_branch_cache_time[cache_key] = current_time
         | 
| 456 592 | 
             
                        return 'Unknown'
         | 
| 457 593 |  | 
| 458 | 
            -
                def _get_socketio_client(self):
         | 
| 459 | 
            -
                    """Get or create Socket.IO client with improved reliability.
         | 
| 460 | 
            -
                    
         | 
| 461 | 
            -
                    WHY improved approach:
         | 
| 462 | 
            -
                    - Implements retry logic with exponential backoff
         | 
| 463 | 
            -
                    - Properly tests connection before returning
         | 
| 464 | 
            -
                    - Ensures connection persists across events
         | 
| 465 | 
            -
                    - Better error handling and recovery
         | 
| 466 | 
            -
                    """
         | 
| 467 | 
            -
                    if not SOCKETIO_AVAILABLE:
         | 
| 468 | 
            -
                        return None
         | 
| 469 | 
            -
                        
         | 
| 470 | 
            -
                    # Check if we have a connected client
         | 
| 471 | 
            -
                    if self.sio_client and self.sio_connected:
         | 
| 472 | 
            -
                        try:
         | 
| 473 | 
            -
                            # Test if still connected
         | 
| 474 | 
            -
                            if self.sio_client.connected:
         | 
| 475 | 
            -
                                return self.sio_client
         | 
| 476 | 
            -
                            else:
         | 
| 477 | 
            -
                                # Connection lost, clear it
         | 
| 478 | 
            -
                                if DEBUG:
         | 
| 479 | 
            -
                                    print("Hook handler: Socket.IO connection lost, reconnecting...", file=sys.stderr)
         | 
| 480 | 
            -
                                self.sio_connected = False
         | 
| 481 | 
            -
                        except:
         | 
| 482 | 
            -
                            self.sio_connected = False
         | 
| 483 | 
            -
                    
         | 
| 484 | 
            -
                    # Need to create or reconnect client
         | 
| 485 | 
            -
                    port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 486 | 
            -
                    max_retries = RetryConfig.MAX_RETRIES
         | 
| 487 | 
            -
                    retry_delay = RetryConfig.INITIAL_RETRY_DELAY
         | 
| 488 | 
            -
                    
         | 
| 489 | 
            -
                    for attempt in range(max_retries):
         | 
| 490 | 
            -
                        try:
         | 
| 491 | 
            -
                            # Clean up old client if exists
         | 
| 492 | 
            -
                            if self.sio_client and not self.sio_connected:
         | 
| 493 | 
            -
                                try:
         | 
| 494 | 
            -
                                    self.sio_client.disconnect()
         | 
| 495 | 
            -
                                except:
         | 
| 496 | 
            -
                                    pass
         | 
| 497 | 
            -
                                self.sio_client = None
         | 
| 498 | 
            -
                            
         | 
| 499 | 
            -
                            # Create new client
         | 
| 500 | 
            -
                            self.sio_client = socketio.Client(
         | 
| 501 | 
            -
                                reconnection=True,  # Enable auto-reconnection
         | 
| 502 | 
            -
                                reconnection_attempts=3,
         | 
| 503 | 
            -
                                reconnection_delay=NetworkConfig.RECONNECTION_DELAY,
         | 
| 504 | 
            -
                                reconnection_delay_max=2,
         | 
| 505 | 
            -
                                logger=False,
         | 
| 506 | 
            -
                                engineio_logger=False
         | 
| 507 | 
            -
                            )
         | 
| 508 | 
            -
                            
         | 
| 509 | 
            -
                            # Try to connect with proper wait
         | 
| 510 | 
            -
                            self.sio_client.connect(
         | 
| 511 | 
            -
                                f'http://localhost:{port}', 
         | 
| 512 | 
            -
                                wait=True, 
         | 
| 513 | 
            -
                                wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT
         | 
| 514 | 
            -
                            )
         | 
| 515 | 
            -
                            
         | 
| 516 | 
            -
                            # Verify connection
         | 
| 517 | 
            -
                            if self.sio_client.connected:
         | 
| 518 | 
            -
                                self.sio_connected = True
         | 
| 519 | 
            -
                                if DEBUG:
         | 
| 520 | 
            -
                                    print(f"Hook handler: Successfully connected to Socket.IO server on port {port} (attempt {attempt + 1})", file=sys.stderr)
         | 
| 521 | 
            -
                                return self.sio_client
         | 
| 522 | 
            -
                                
         | 
| 523 | 
            -
                        except Exception as e:
         | 
| 524 | 
            -
                            if DEBUG and attempt == max_retries - 1:
         | 
| 525 | 
            -
                                print(f"Hook handler: Failed to connect to Socket.IO after {max_retries} attempts: {e}", file=sys.stderr)
         | 
| 526 | 
            -
                            elif DEBUG:
         | 
| 527 | 
            -
                                print(f"Hook handler: Connection attempt {attempt + 1} failed, retrying...", file=sys.stderr)
         | 
| 528 | 
            -
                            
         | 
| 529 | 
            -
                            # Exponential backoff with async delay
         | 
| 530 | 
            -
                            if attempt < max_retries - 1:
         | 
| 531 | 
            -
                                # Use asyncio.sleep if in async context, otherwise fall back to time.sleep
         | 
| 532 | 
            -
                                try:
         | 
| 533 | 
            -
                                    loop = asyncio.get_event_loop()
         | 
| 534 | 
            -
                                    if loop.is_running():
         | 
| 535 | 
            -
                                        # We're in an async context, use async sleep
         | 
| 536 | 
            -
                                        asyncio.create_task(asyncio.sleep(retry_delay))
         | 
| 537 | 
            -
                                    else:
         | 
| 538 | 
            -
                                        # Sync context, use regular sleep
         | 
| 539 | 
            -
                                        time.sleep(retry_delay)
         | 
| 540 | 
            -
                                except:
         | 
| 541 | 
            -
                                    # Fallback to sync sleep if asyncio not available
         | 
| 542 | 
            -
                                    time.sleep(retry_delay)
         | 
| 543 | 
            -
                                retry_delay *= 2  # Double the delay for next attempt
         | 
| 544 | 
            -
                    
         | 
| 545 | 
            -
                    # All attempts failed
         | 
| 546 | 
            -
                    self.sio_client = None
         | 
| 547 | 
            -
                    self.sio_connected = False
         | 
| 548 | 
            -
                    return None
         | 
| 549 594 |  | 
| 550 595 | 
             
                def handle(self):
         | 
| 551 596 | 
             
                    """Process hook event with minimal overhead and zero blocking delays.
         | 
| @@ -565,6 +610,13 @@ class ClaudeHookHandler: | |
| 565 610 | 
             
                            self._continue_execution()
         | 
| 566 611 | 
             
                            return
         | 
| 567 612 |  | 
| 613 | 
            +
                        # Increment event counter and perform periodic cleanup
         | 
| 614 | 
            +
                        self.events_processed += 1
         | 
| 615 | 
            +
                        if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
         | 
| 616 | 
            +
                            self._cleanup_old_entries()
         | 
| 617 | 
            +
                            if DEBUG:
         | 
| 618 | 
            +
                                print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
         | 
| 619 | 
            +
                        
         | 
| 568 620 | 
             
                        # Route event to appropriate handler
         | 
| 569 621 | 
             
                        self._route_event(event)
         | 
| 570 622 |  | 
| @@ -647,7 +699,8 @@ class ClaudeHookHandler: | |
| 647 699 | 
             
                    # The daemon should be running when manager is active
         | 
| 648 700 |  | 
| 649 701 | 
             
                    # Get Socket.IO client
         | 
| 650 | 
            -
                     | 
| 702 | 
            +
                    port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 703 | 
            +
                    client = self.connection_pool.get_connection(port)
         | 
| 651 704 | 
             
                    if not client:
         | 
| 652 705 | 
             
                        if DEBUG:
         | 
| 653 706 | 
             
                            print(f"Hook handler: No Socket.IO client available for event: hook.{event}", file=sys.stderr)
         | 
| @@ -680,22 +733,18 @@ class ClaudeHookHandler: | |
| 680 733 | 
             
                                print(f"✅ Successfully emitted Socket.IO event: hook.{event}", file=sys.stderr)
         | 
| 681 734 | 
             
                            else:
         | 
| 682 735 | 
             
                                print(f"⚠️ Event emitted but connection status uncertain: hook.{event}", file=sys.stderr)
         | 
| 683 | 
            -
                                self.sio_connected = False  # Force reconnection next time
         | 
| 684 736 |  | 
| 685 737 | 
             
                    except Exception as e:
         | 
| 686 738 | 
             
                        if DEBUG:
         | 
| 687 739 | 
             
                            print(f"❌ Socket.IO emit failed for hook.{event}: {e}", file=sys.stderr)
         | 
| 688 | 
            -
                        # Mark as disconnected so next call will reconnect
         | 
| 689 | 
            -
                        self.sio_connected = False
         | 
| 690 740 |  | 
| 691 741 | 
             
                        # Try to reconnect immediately for critical events
         | 
| 692 742 | 
             
                        if event in ['subagent_stop', 'pre_tool']:
         | 
| 693 743 | 
             
                            if DEBUG:
         | 
| 694 744 | 
             
                                print(f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}", file=sys.stderr)
         | 
| 695 | 
            -
                            # Clear the client to force reconnection
         | 
| 696 | 
            -
                            self.sio_client = None
         | 
| 697 745 | 
             
                            # Try to get a new client and emit again
         | 
| 698 | 
            -
                             | 
| 746 | 
            +
                            retry_port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 747 | 
            +
                            retry_client = self.connection_pool.get_connection(retry_port)
         | 
| 699 748 | 
             
                            if retry_client:
         | 
| 700 749 | 
             
                                try:
         | 
| 701 750 | 
             
                                    retry_client.emit('claude_event', claude_event_data)
         | 
| @@ -1667,18 +1716,31 @@ class ClaudeHookHandler: | |
| 1667 1716 | 
             
                        # Don't fail the delegation result - memory is optional
         | 
| 1668 1717 |  | 
| 1669 1718 | 
             
                def __del__(self):
         | 
| 1670 | 
            -
                    """Cleanup Socket.IO  | 
| 1671 | 
            -
                    if self | 
| 1719 | 
            +
                    """Cleanup Socket.IO connections on handler destruction."""
         | 
| 1720 | 
            +
                    if hasattr(self, 'connection_pool') and self.connection_pool:
         | 
| 1672 1721 | 
             
                        try:
         | 
| 1673 | 
            -
                            self. | 
| 1722 | 
            +
                            self.connection_pool.close_all()
         | 
| 1674 1723 | 
             
                        except:
         | 
| 1675 1724 | 
             
                            pass
         | 
| 1676 1725 |  | 
| 1677 1726 |  | 
| 1678 1727 | 
             
            def main():
         | 
| 1679 | 
            -
                """Entry point with  | 
| 1728 | 
            +
                """Entry point with singleton pattern to prevent multiple instances."""
         | 
| 1729 | 
            +
                global _global_handler
         | 
| 1730 | 
            +
                
         | 
| 1680 1731 | 
             
                try:
         | 
| 1681 | 
            -
                     | 
| 1732 | 
            +
                    # Use singleton pattern to prevent creating multiple instances
         | 
| 1733 | 
            +
                    with _handler_lock:
         | 
| 1734 | 
            +
                        if _global_handler is None:
         | 
| 1735 | 
            +
                            _global_handler = ClaudeHookHandler()
         | 
| 1736 | 
            +
                            if DEBUG:
         | 
| 1737 | 
            +
                                print(f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
         | 
| 1738 | 
            +
                        else:
         | 
| 1739 | 
            +
                            if DEBUG:
         | 
| 1740 | 
            +
                                print(f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
         | 
| 1741 | 
            +
                        
         | 
| 1742 | 
            +
                        handler = _global_handler
         | 
| 1743 | 
            +
                    
         | 
| 1682 1744 | 
             
                    handler.handle()
         | 
| 1683 1745 | 
             
                except Exception as e:
         | 
| 1684 1746 | 
             
                    # Always output continue action to not block Claude
         | 
    
        claude_mpm/utils/paths.py
    CHANGED
    
    | @@ -5,6 +5,7 @@ to avoid duplication across the codebase. | |
| 5 5 | 
             
            """
         | 
| 6 6 |  | 
| 7 7 | 
             
            import os
         | 
| 8 | 
            +
            import sys
         | 
| 8 9 | 
             
            from pathlib import Path
         | 
| 9 10 | 
             
            from typing import Optional, Union, List
         | 
| 10 11 | 
             
            from functools import lru_cache
         | 
| @@ -45,11 +46,17 @@ class PathResolver: | |
| 45 46 | 
             
                    try:
         | 
| 46 47 | 
             
                        import claude_mpm
         | 
| 47 48 | 
             
                        module_path = Path(claude_mpm.__file__).parent
         | 
| 48 | 
            -
                         | 
| 49 | 
            +
                        
         | 
| 50 | 
            +
                        # Check if we're in a development environment (has src/ directory)
         | 
| 49 51 | 
             
                        if module_path.parent.name == 'src':
         | 
| 52 | 
            +
                            # Development structure: project_root/src/claude_mpm/
         | 
| 50 53 | 
             
                            return module_path.parent.parent
         | 
| 51 | 
            -
                         | 
| 54 | 
            +
                        
         | 
| 55 | 
            +
                        # Check if we're in an installed environment (site-packages)
         | 
| 56 | 
            +
                        # In this case, the module itself IS the framework root for resources
         | 
| 57 | 
            +
                        # No src/ directory exists in installed packages
         | 
| 52 58 | 
             
                        return module_path
         | 
| 59 | 
            +
                        
         | 
| 53 60 | 
             
                    except ImportError:
         | 
| 54 61 | 
             
                        pass
         | 
| 55 62 |  | 
| @@ -78,22 +85,50 @@ class PathResolver: | |
| 78 85 | 
             
                    Raises:
         | 
| 79 86 | 
             
                        FileNotFoundError: If agents directory doesn't exist
         | 
| 80 87 | 
             
                    """
         | 
| 88 | 
            +
                    # Try using importlib.resources first (Python 3.9+)
         | 
| 89 | 
            +
                    try:
         | 
| 90 | 
            +
                        if sys.version_info >= (3, 9):
         | 
| 91 | 
            +
                            from importlib.resources import files
         | 
| 92 | 
            +
                            agents_path = files('claude_mpm').joinpath('agents')
         | 
| 93 | 
            +
                            if agents_path.exists():
         | 
| 94 | 
            +
                                return Path(str(agents_path))
         | 
| 95 | 
            +
                        else:
         | 
| 96 | 
            +
                            # For Python 3.7-3.8, use the backport approach
         | 
| 97 | 
            +
                            from importlib import resources
         | 
| 98 | 
            +
                            with resources.path('claude_mpm', 'agents') as agents_path:
         | 
| 99 | 
            +
                                if agents_path.exists():
         | 
| 100 | 
            +
                                    return agents_path
         | 
| 101 | 
            +
                    except (ImportError, ModuleNotFoundError, TypeError):
         | 
| 102 | 
            +
                        # Fall back to manual detection if importlib.resources fails
         | 
| 103 | 
            +
                        pass
         | 
| 104 | 
            +
                    
         | 
| 105 | 
            +
                    # Fallback to manual detection
         | 
| 81 106 | 
             
                    framework_root = cls.get_framework_root()
         | 
| 82 107 |  | 
| 83 | 
            -
                    #  | 
| 84 | 
            -
                     | 
| 108 | 
            +
                    # First check if we're in development structure (framework_root is project root)
         | 
| 109 | 
            +
                    # This happens when framework_root has a src/ directory
         | 
| 110 | 
            +
                    if (framework_root / 'src').exists():
         | 
| 111 | 
            +
                        agents_dir = framework_root / 'src' / 'claude_mpm' / 'agents'
         | 
| 112 | 
            +
                        if agents_dir.exists():
         | 
| 113 | 
            +
                            return agents_dir
         | 
| 114 | 
            +
                    
         | 
| 115 | 
            +
                    # Otherwise we're in installed structure (framework_root is claude_mpm package)
         | 
| 116 | 
            +
                    # In pip/pipx installs, framework_root already points to claude_mpm package
         | 
| 117 | 
            +
                    agents_dir = framework_root / 'agents'
         | 
| 85 118 | 
             
                    if agents_dir.exists():
         | 
| 86 119 | 
             
                        return agents_dir
         | 
| 87 120 |  | 
| 88 | 
            -
                    #  | 
| 121 | 
            +
                    # Last fallback for edge cases
         | 
| 89 122 | 
             
                    agents_dir = framework_root / 'claude_mpm' / 'agents'
         | 
| 90 123 | 
             
                    if agents_dir.exists():
         | 
| 91 124 | 
             
                        return agents_dir
         | 
| 92 125 |  | 
| 93 126 | 
             
                    raise FileNotFoundError(
         | 
| 94 127 | 
             
                        f"Agents directory not found. Searched in:\n"
         | 
| 95 | 
            -
                        f"  -  | 
| 96 | 
            -
                        f"  - {framework_root / 'claude_mpm' / 'agents'}"
         | 
| 128 | 
            +
                        f"  - importlib.resources lookup\n"
         | 
| 129 | 
            +
                        f"  - {framework_root / 'src' / 'claude_mpm' / 'agents'} (development)\n"
         | 
| 130 | 
            +
                        f"  - {framework_root / 'agents'} (installed)\n"
         | 
| 131 | 
            +
                        f"  - {framework_root / 'claude_mpm' / 'agents'} (fallback)"
         | 
| 97 132 | 
             
                    )
         | 
| 98 133 |  | 
| 99 134 | 
             
                @classmethod
         | 
| @@ -260,6 +295,70 @@ class PathResolver: | |
| 260 295 | 
             
                    logger.debug(f"Found {len(matches)} files matching '{pattern}' in {root}")
         | 
| 261 296 | 
             
                    return matches
         | 
| 262 297 |  | 
| 298 | 
            +
                @classmethod
         | 
| 299 | 
            +
                def get_package_resource_path(cls, resource_path: str) -> Path:
         | 
| 300 | 
            +
                    """Get the path to a resource within the claude_mpm package.
         | 
| 301 | 
            +
                    
         | 
| 302 | 
            +
                    This method handles both development and installed environments correctly.
         | 
| 303 | 
            +
                    
         | 
| 304 | 
            +
                    Args:
         | 
| 305 | 
            +
                        resource_path: Relative path within the claude_mpm package (e.g., 'agents/templates')
         | 
| 306 | 
            +
                        
         | 
| 307 | 
            +
                    Returns:
         | 
| 308 | 
            +
                        Path: Full path to the resource
         | 
| 309 | 
            +
                        
         | 
| 310 | 
            +
                    Raises:
         | 
| 311 | 
            +
                        FileNotFoundError: If the resource doesn't exist
         | 
| 312 | 
            +
                    """
         | 
| 313 | 
            +
                    # Try using importlib.resources first (most reliable for installed packages)
         | 
| 314 | 
            +
                    try:
         | 
| 315 | 
            +
                        if sys.version_info >= (3, 9):
         | 
| 316 | 
            +
                            from importlib.resources import files
         | 
| 317 | 
            +
                            resource = files('claude_mpm').joinpath(resource_path)
         | 
| 318 | 
            +
                            if resource.exists():
         | 
| 319 | 
            +
                                return Path(str(resource))
         | 
| 320 | 
            +
                        else:
         | 
| 321 | 
            +
                            # For Python 3.7-3.8
         | 
| 322 | 
            +
                            from importlib import resources
         | 
| 323 | 
            +
                            parts = resource_path.split('/')
         | 
| 324 | 
            +
                            if len(parts) == 1:
         | 
| 325 | 
            +
                                with resources.path('claude_mpm', parts[0]) as p:
         | 
| 326 | 
            +
                                    if p.exists():
         | 
| 327 | 
            +
                                        return p
         | 
| 328 | 
            +
                            else:
         | 
| 329 | 
            +
                                # For nested paths, navigate step by step
         | 
| 330 | 
            +
                                package = 'claude_mpm'
         | 
| 331 | 
            +
                                for part in parts[:-1]:
         | 
| 332 | 
            +
                                    package = f"{package}.{part}"
         | 
| 333 | 
            +
                                with resources.path(package, parts[-1]) as p:
         | 
| 334 | 
            +
                                    if p.exists():
         | 
| 335 | 
            +
                                        return p
         | 
| 336 | 
            +
                    except (ImportError, ModuleNotFoundError, TypeError, AttributeError):
         | 
| 337 | 
            +
                        # Fall back to file system detection
         | 
| 338 | 
            +
                        pass
         | 
| 339 | 
            +
                    
         | 
| 340 | 
            +
                    # Fallback: Use file system detection
         | 
| 341 | 
            +
                    import claude_mpm
         | 
| 342 | 
            +
                    module_path = Path(claude_mpm.__file__).parent
         | 
| 343 | 
            +
                    resource = module_path / resource_path
         | 
| 344 | 
            +
                    
         | 
| 345 | 
            +
                    if resource.exists():
         | 
| 346 | 
            +
                        return resource
         | 
| 347 | 
            +
                    
         | 
| 348 | 
            +
                    # Try with framework root
         | 
| 349 | 
            +
                    framework_root = cls.get_framework_root()
         | 
| 350 | 
            +
                    if (framework_root / 'src').exists():
         | 
| 351 | 
            +
                        # Development environment
         | 
| 352 | 
            +
                        resource = framework_root / 'src' / 'claude_mpm' / resource_path
         | 
| 353 | 
            +
                    else:
         | 
| 354 | 
            +
                        # Installed environment
         | 
| 355 | 
            +
                        resource = framework_root / resource_path
         | 
| 356 | 
            +
                    
         | 
| 357 | 
            +
                    if resource.exists():
         | 
| 358 | 
            +
                        return resource
         | 
| 359 | 
            +
                    
         | 
| 360 | 
            +
                    raise FileNotFoundError(f"Resource not found: {resource_path}")
         | 
| 361 | 
            +
                
         | 
| 263 362 | 
             
                @classmethod
         | 
| 264 363 | 
             
                def clear_cache(cls):
         | 
| 265 364 | 
             
                    """Clear all cached path lookups.
         | 
| @@ -288,4 +387,9 @@ def get_project_root() -> Path: | |
| 288 387 |  | 
| 289 388 | 
             
            def find_file_upwards(filename: str, start_path: Optional[Path] = None) -> Optional[Path]:
         | 
| 290 389 | 
             
                """Search for a file by traversing up the directory tree."""
         | 
| 291 | 
            -
                return PathResolver.find_file_upwards(filename, start_path)
         | 
| 390 | 
            +
                return PathResolver.find_file_upwards(filename, start_path)
         | 
| 391 | 
            +
             | 
| 392 | 
            +
             | 
| 393 | 
            +
            def get_package_resource_path(resource_path: str) -> Path:
         | 
| 394 | 
            +
                """Get the path to a resource within the claude_mpm package."""
         | 
| 395 | 
            +
                return PathResolver.get_package_resource_path(resource_path)
         | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            claude_mpm/VERSION,sha256= | 
| 1 | 
            +
            claude_mpm/VERSION,sha256=qncwOqyY7d82tgxVa7TxJStIKb52qA2UhTpaLs0fB2Y,6
         | 
| 2 2 | 
             
            claude_mpm/__init__.py,sha256=ix_J0PHZBz37nVBDEYJmLpwnURlWuBKKQ8rK_00TFpk,964
         | 
| 3 3 | 
             
            claude_mpm/__main__.py,sha256=8IcM9tEbTqSN_er04eKTPX3AGo6qzRiTnPI7KfIf7rw,641
         | 
| 4 4 | 
             
            claude_mpm/constants.py,sha256=xdYTQfOdrnp_fp2A-P4gA68X-XMq29cCwF6Xdwg3oQE,5217
         | 
| @@ -69,7 +69,7 @@ claude_mpm/cli_module/commands.py,sha256=CBNfO-bXrZ0spjeW_7-swprEq5V4PQSc0qhl9SL | |
| 69 69 | 
             
            claude_mpm/cli_module/migration_example.py,sha256=C-_GbsW8dGzutnNeRLLld74ibDLyAOQx0stdpYZS0hs,6137
         | 
| 70 70 | 
             
            claude_mpm/config/__init__.py,sha256=KTJkOHVtC1t6goMkj2xaQWZKZfokEHNw0wxPE48bl1g,828
         | 
| 71 71 | 
             
            claude_mpm/config/agent_config.py,sha256=QS-25LSiNz4uOocjIM_FX_SGoRQHJOfkBZCKlz09K5k,13830
         | 
| 72 | 
            -
            claude_mpm/config/paths.py,sha256= | 
| 72 | 
            +
            claude_mpm/config/paths.py,sha256=MznUhL7kvpOhwqtNdOavEE1JvUauvfIN_8PUXl8MytU,12553
         | 
| 73 73 | 
             
            claude_mpm/config/socketio_config.py,sha256=rf5XKtdAKeypwZZPF8r6uSHnJHVXeMmX1YiYQmzUOZo,9727
         | 
| 74 74 | 
             
            claude_mpm/core/__init__.py,sha256=5Hdb4fLdMEqXdOeBJB0zFGJx8CxtaSRYIiD-foiz3yQ,874
         | 
| 75 75 | 
             
            claude_mpm/core/agent_name_normalizer.py,sha256=-X68oz3_74t9BRbHA54NEGyjy0xjTsGp_sCUHDtKp1s,9269
         | 
| @@ -135,7 +135,7 @@ claude_mpm/hooks/memory_integration_hook.py,sha256=z0I5R4rsmLx3mzYf7QLeMTYbRShag | |
| 135 135 | 
             
            claude_mpm/hooks/tool_call_interceptor.py,sha256=08_Odgm6Sg1zBJhGjwzVa03AReeBPZHTjndyjEO99cY,7629
         | 
| 136 136 | 
             
            claude_mpm/hooks/validation_hooks.py,sha256=7TU2N4SzCm2nwpsR0xiNKsHQNsWODnOVAmK9jHq1QqM,6582
         | 
| 137 137 | 
             
            claude_mpm/hooks/claude_hooks/__init__.py,sha256=bMUwt2RzDGAcEbtDMA7vWS1uJsauOY0OixIe4pHwgQ0,129
         | 
| 138 | 
            -
            claude_mpm/hooks/claude_hooks/hook_handler.py,sha256= | 
| 138 | 
            +
            claude_mpm/hooks/claude_hooks/hook_handler.py,sha256=hMJRlFIpYtl3SGDMseS1_kiXMrzm_w1mFG-oqU5hJ8Q,79765
         | 
| 139 139 | 
             
            claude_mpm/hooks/claude_hooks/hook_handler_fixed.py,sha256=hYE5Tc4RaaK0kLmFNLVXvDhiuTLA40Gt3hmhCQUO5UA,15626
         | 
| 140 140 | 
             
            claude_mpm/hooks/claude_hooks/hook_wrapper.sh,sha256=JBbedWNs1EHaUsAkmqfPv_tWxV_DcRP707hma74oHU0,2370
         | 
| 141 141 | 
             
            claude_mpm/models/__init__.py,sha256=vy2NLX2KT9QeH76SjCYh9dOYKPLRgxGrnwkQFAg08gc,465
         | 
| @@ -254,15 +254,15 @@ claude_mpm/utils/framework_detection.py,sha256=nzs1qRZK9K-zT0382z1FpGDvgzUNrUg8r | |
| 254 254 | 
             
            claude_mpm/utils/import_migration_example.py,sha256=W4a4XH3FY_VBB00BB8Lae2aRPM021PxLHzdUfEs0B5w,2463
         | 
| 255 255 | 
             
            claude_mpm/utils/imports.py,sha256=wX-SOXUHbemx01MHRGQpVwajzXH6qYdQkYNFCIbb2mw,6851
         | 
| 256 256 | 
             
            claude_mpm/utils/path_operations.py,sha256=6pLMnAWBVzHkgp6JyQHmHbGD-dWn-nX21yV4E_eT-kM,11614
         | 
| 257 | 
            -
            claude_mpm/utils/paths.py,sha256= | 
| 257 | 
            +
            claude_mpm/utils/paths.py,sha256=_4d2uV7QDLMeybSNch6B5H3s0cQ6Ii-idI8rFC-OCn4,14653
         | 
| 258 258 | 
             
            claude_mpm/utils/robust_installer.py,sha256=5-iW4Qpba4DBitx5Ie3uoUJgXBpbvuLUJ_uNGgOxwi4,19855
         | 
| 259 259 | 
             
            claude_mpm/utils/session_logging.py,sha256=9G0AzB7V0WkhLQlN0ocqbyDv0ifooEsJ5UPXIhA-wt0,3022
         | 
| 260 260 | 
             
            claude_mpm/validation/__init__.py,sha256=bJ19g9lnk7yIjtxzN8XPegp87HTFBzCrGQOpFgRTf3g,155
         | 
| 261 261 | 
             
            claude_mpm/validation/agent_validator.py,sha256=OEYhmy0K99pkoCCoVea2Q-d1JMiDyhEpzEJikuF8T-U,20910
         | 
| 262 262 | 
             
            claude_mpm/validation/frontmatter_validator.py,sha256=vSinu0XD9-31h0-ePYiYivBbxTZEanhymLinTCODr7k,7206
         | 
| 263 | 
            -
            claude_mpm-3.9. | 
| 264 | 
            -
            claude_mpm-3.9. | 
| 265 | 
            -
            claude_mpm-3.9. | 
| 266 | 
            -
            claude_mpm-3.9. | 
| 267 | 
            -
            claude_mpm-3.9. | 
| 268 | 
            -
            claude_mpm-3.9. | 
| 263 | 
            +
            claude_mpm-3.9.4.dist-info/licenses/LICENSE,sha256=cSdDfXjoTVhstrERrqme4zgxAu4GubU22zVEHsiXGxs,1071
         | 
| 264 | 
            +
            claude_mpm-3.9.4.dist-info/METADATA,sha256=_rGegraOba_N2dJxC3RuA0Y34dl5raiQVaxdnyNeq0g,8680
         | 
| 265 | 
            +
            claude_mpm-3.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
         | 
| 266 | 
            +
            claude_mpm-3.9.4.dist-info/entry_points.txt,sha256=3_d7wLrg9sRmQ1SfrFGWoTNL8Wrd6lQb2XVSYbTwRIg,324
         | 
| 267 | 
            +
            claude_mpm-3.9.4.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
         | 
| 268 | 
            +
            claude_mpm-3.9.4.dist-info/RECORD,,
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |