claude-mpm 3.1.2__py3-none-any.whl → 3.2.1__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/__init__.py +3 -3
 - claude_mpm/agents/INSTRUCTIONS.md +80 -2
 - claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
 - claude_mpm/agents/base_agent.json +1 -1
 - claude_mpm/agents/templates/pm.json +25 -0
 - claude_mpm/agents/templates/research.json +2 -1
 - claude_mpm/cli/__init__.py +6 -1
 - claude_mpm/cli/commands/__init__.py +3 -1
 - claude_mpm/cli/commands/memory.py +232 -0
 - claude_mpm/cli/commands/run.py +496 -8
 - claude_mpm/cli/parser.py +91 -1
 - claude_mpm/config/socketio_config.py +256 -0
 - claude_mpm/constants.py +9 -0
 - claude_mpm/core/__init__.py +2 -2
 - claude_mpm/core/claude_runner.py +919 -0
 - claude_mpm/core/config.py +21 -1
 - claude_mpm/core/hook_manager.py +196 -0
 - claude_mpm/core/pm_hook_interceptor.py +205 -0
 - claude_mpm/core/simple_runner.py +296 -16
 - claude_mpm/core/socketio_pool.py +582 -0
 - claude_mpm/core/websocket_handler.py +233 -0
 - claude_mpm/deployment_paths.py +261 -0
 - claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
 - claude_mpm/hooks/claude_hooks/hook_handler.py +669 -632
 - claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
 - claude_mpm/hooks/memory_integration_hook.py +312 -0
 - claude_mpm/orchestration/__init__.py +1 -1
 - claude_mpm/scripts/claude-mpm-socketio +32 -0
 - claude_mpm/scripts/claude_mpm_monitor.html +567 -0
 - claude_mpm/scripts/install_socketio_server.py +407 -0
 - claude_mpm/scripts/launch_monitor.py +132 -0
 - claude_mpm/scripts/manage_version.py +479 -0
 - claude_mpm/scripts/socketio_daemon.py +181 -0
 - claude_mpm/scripts/socketio_server_manager.py +428 -0
 - claude_mpm/services/__init__.py +5 -0
 - claude_mpm/services/agent_memory_manager.py +684 -0
 - claude_mpm/services/hook_service.py +362 -0
 - claude_mpm/services/socketio_client_manager.py +474 -0
 - claude_mpm/services/socketio_server.py +698 -0
 - claude_mpm/services/standalone_socketio_server.py +631 -0
 - claude_mpm/services/websocket_server.py +376 -0
 - claude_mpm/utils/dependency_manager.py +211 -0
 - claude_mpm/web/open_dashboard.py +34 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -1
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +50 -24
 - claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
 - claude_mpm/cli_old.py +0 -728
 - claude_mpm-3.1.2.dist-info/entry_points.txt +0 -4
 - /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/top_level.txt +0 -0
 
| 
         @@ -0,0 +1,376 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """WebSocket server for real-time monitoring of Claude MPM sessions."""
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            import asyncio
         
     | 
| 
      
 4 
     | 
    
         
            +
            import json
         
     | 
| 
      
 5 
     | 
    
         
            +
            import logging
         
     | 
| 
      
 6 
     | 
    
         
            +
            import os
         
     | 
| 
      
 7 
     | 
    
         
            +
            import subprocess
         
     | 
| 
      
 8 
     | 
    
         
            +
            import threading
         
     | 
| 
      
 9 
     | 
    
         
            +
            import time
         
     | 
| 
      
 10 
     | 
    
         
            +
            from datetime import datetime
         
     | 
| 
      
 11 
     | 
    
         
            +
            from typing import Set, Dict, Any, Optional, List
         
     | 
| 
      
 12 
     | 
    
         
            +
            from collections import deque
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            try:
         
     | 
| 
      
 15 
     | 
    
         
            +
                import websockets
         
     | 
| 
      
 16 
     | 
    
         
            +
                from websockets.server import WebSocketServerProtocol
         
     | 
| 
      
 17 
     | 
    
         
            +
                WEBSOCKETS_AVAILABLE = True
         
     | 
| 
      
 18 
     | 
    
         
            +
            except ImportError:
         
     | 
| 
      
 19 
     | 
    
         
            +
                WEBSOCKETS_AVAILABLE = False
         
     | 
| 
      
 20 
     | 
    
         
            +
                websockets = None
         
     | 
| 
      
 21 
     | 
    
         
            +
                WebSocketServerProtocol = None
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            from ..core.logger import get_logger
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            class WebSocketServer:
         
     | 
| 
      
 27 
     | 
    
         
            +
                """WebSocket server for broadcasting Claude MPM events."""
         
     | 
| 
      
 28 
     | 
    
         
            +
                
         
     | 
| 
      
 29 
     | 
    
         
            +
                def __init__(self, host: str = "localhost", port: int = 8765):
         
     | 
| 
      
 30 
     | 
    
         
            +
                    self.host = host
         
     | 
| 
      
 31 
     | 
    
         
            +
                    self.port = port
         
     | 
| 
      
 32 
     | 
    
         
            +
                    self.logger = get_logger("websocket_server")
         
     | 
| 
      
 33 
     | 
    
         
            +
                    self.clients: Set[WebSocketServerProtocol] = set() if WEBSOCKETS_AVAILABLE else set()
         
     | 
| 
      
 34 
     | 
    
         
            +
                    self.event_history: deque = deque(maxlen=1000)  # Keep last 1000 events
         
     | 
| 
      
 35 
     | 
    
         
            +
                    self.server = None
         
     | 
| 
      
 36 
     | 
    
         
            +
                    self.loop = None
         
     | 
| 
      
 37 
     | 
    
         
            +
                    self.thread = None
         
     | 
| 
      
 38 
     | 
    
         
            +
                    self.running = False
         
     | 
| 
      
 39 
     | 
    
         
            +
                    
         
     | 
| 
      
 40 
     | 
    
         
            +
                    # Session state
         
     | 
| 
      
 41 
     | 
    
         
            +
                    self.session_id = None
         
     | 
| 
      
 42 
     | 
    
         
            +
                    self.session_start = None
         
     | 
| 
      
 43 
     | 
    
         
            +
                    self.claude_status = "stopped"
         
     | 
| 
      
 44 
     | 
    
         
            +
                    self.claude_pid = None
         
     | 
| 
      
 45 
     | 
    
         
            +
                    
         
     | 
| 
      
 46 
     | 
    
         
            +
                    if not WEBSOCKETS_AVAILABLE:
         
     | 
| 
      
 47 
     | 
    
         
            +
                        self.logger.warning("WebSocket support not available. Install 'websockets' package to enable.")
         
     | 
| 
      
 48 
     | 
    
         
            +
                    
         
     | 
| 
      
 49 
     | 
    
         
            +
                def start(self):
         
     | 
| 
      
 50 
     | 
    
         
            +
                    """Start the WebSocket server in a background thread."""
         
     | 
| 
      
 51 
     | 
    
         
            +
                    if not WEBSOCKETS_AVAILABLE:
         
     | 
| 
      
 52 
     | 
    
         
            +
                        self.logger.debug("WebSocket server skipped - websockets package not installed")
         
     | 
| 
      
 53 
     | 
    
         
            +
                        return
         
     | 
| 
      
 54 
     | 
    
         
            +
                        
         
     | 
| 
      
 55 
     | 
    
         
            +
                    if self.running:
         
     | 
| 
      
 56 
     | 
    
         
            +
                        return
         
     | 
| 
      
 57 
     | 
    
         
            +
                        
         
     | 
| 
      
 58 
     | 
    
         
            +
                    self.running = True
         
     | 
| 
      
 59 
     | 
    
         
            +
                    self.thread = threading.Thread(target=self._run_server, daemon=True)
         
     | 
| 
      
 60 
     | 
    
         
            +
                    self.thread.start()
         
     | 
| 
      
 61 
     | 
    
         
            +
                    self.logger.info(f"WebSocket server starting on ws://{self.host}:{self.port}")
         
     | 
| 
      
 62 
     | 
    
         
            +
                    
         
     | 
| 
      
 63 
     | 
    
         
            +
                def stop(self):
         
     | 
| 
      
 64 
     | 
    
         
            +
                    """Stop the WebSocket server."""
         
     | 
| 
      
 65 
     | 
    
         
            +
                    self.running = False
         
     | 
| 
      
 66 
     | 
    
         
            +
                    if self.loop:
         
     | 
| 
      
 67 
     | 
    
         
            +
                        asyncio.run_coroutine_threadsafe(self._shutdown(), self.loop)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    if self.thread:
         
     | 
| 
      
 69 
     | 
    
         
            +
                        self.thread.join(timeout=5)
         
     | 
| 
      
 70 
     | 
    
         
            +
                    self.logger.info("WebSocket server stopped")
         
     | 
| 
      
 71 
     | 
    
         
            +
                    
         
     | 
| 
      
 72 
     | 
    
         
            +
                def _run_server(self):
         
     | 
| 
      
 73 
     | 
    
         
            +
                    """Run the server event loop."""
         
     | 
| 
      
 74 
     | 
    
         
            +
                    self.loop = asyncio.new_event_loop()
         
     | 
| 
      
 75 
     | 
    
         
            +
                    asyncio.set_event_loop(self.loop)
         
     | 
| 
      
 76 
     | 
    
         
            +
                    
         
     | 
| 
      
 77 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 78 
     | 
    
         
            +
                        self.loop.run_until_complete(self._serve())
         
     | 
| 
      
 79 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 80 
     | 
    
         
            +
                        self.logger.error(f"WebSocket server error: {e}")
         
     | 
| 
      
 81 
     | 
    
         
            +
                    finally:
         
     | 
| 
      
 82 
     | 
    
         
            +
                        self.loop.close()
         
     | 
| 
      
 83 
     | 
    
         
            +
                        
         
     | 
| 
      
 84 
     | 
    
         
            +
                async def _serve(self):
         
     | 
| 
      
 85 
     | 
    
         
            +
                    """Start the WebSocket server."""
         
     | 
| 
      
 86 
     | 
    
         
            +
                    async with websockets.serve(self._handle_client, self.host, self.port):
         
     | 
| 
      
 87 
     | 
    
         
            +
                        self.logger.info(f"WebSocket server listening on ws://{self.host}:{self.port}")
         
     | 
| 
      
 88 
     | 
    
         
            +
                        while self.running:
         
     | 
| 
      
 89 
     | 
    
         
            +
                            await asyncio.sleep(0.1)
         
     | 
| 
      
 90 
     | 
    
         
            +
                            
         
     | 
| 
      
 91 
     | 
    
         
            +
                async def _shutdown(self):
         
     | 
| 
      
 92 
     | 
    
         
            +
                    """Shutdown the server."""
         
     | 
| 
      
 93 
     | 
    
         
            +
                    # Close all client connections
         
     | 
| 
      
 94 
     | 
    
         
            +
                    if self.clients:
         
     | 
| 
      
 95 
     | 
    
         
            +
                        await asyncio.gather(
         
     | 
| 
      
 96 
     | 
    
         
            +
                            *[client.close() for client in self.clients],
         
     | 
| 
      
 97 
     | 
    
         
            +
                            return_exceptions=True
         
     | 
| 
      
 98 
     | 
    
         
            +
                        )
         
     | 
| 
      
 99 
     | 
    
         
            +
                        
         
     | 
| 
      
 100 
     | 
    
         
            +
                async def _handle_client(self, websocket: WebSocketServerProtocol, path: str):
         
     | 
| 
      
 101 
     | 
    
         
            +
                    """Handle a new client connection."""
         
     | 
| 
      
 102 
     | 
    
         
            +
                    self.clients.add(websocket)
         
     | 
| 
      
 103 
     | 
    
         
            +
                    client_addr = websocket.remote_address
         
     | 
| 
      
 104 
     | 
    
         
            +
                    self.logger.info(f"Client connected from {client_addr}")
         
     | 
| 
      
 105 
     | 
    
         
            +
                    
         
     | 
| 
      
 106 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 107 
     | 
    
         
            +
                        # Send current status
         
     | 
| 
      
 108 
     | 
    
         
            +
                        await self._send_current_status(websocket)
         
     | 
| 
      
 109 
     | 
    
         
            +
                        
         
     | 
| 
      
 110 
     | 
    
         
            +
                        # Handle client messages
         
     | 
| 
      
 111 
     | 
    
         
            +
                        async for message in websocket:
         
     | 
| 
      
 112 
     | 
    
         
            +
                            try:
         
     | 
| 
      
 113 
     | 
    
         
            +
                                data = json.loads(message)
         
     | 
| 
      
 114 
     | 
    
         
            +
                                await self._handle_command(websocket, data)
         
     | 
| 
      
 115 
     | 
    
         
            +
                            except json.JSONDecodeError:
         
     | 
| 
      
 116 
     | 
    
         
            +
                                await websocket.send(json.dumps({
         
     | 
| 
      
 117 
     | 
    
         
            +
                                    "type": "error",
         
     | 
| 
      
 118 
     | 
    
         
            +
                                    "data": {"message": "Invalid JSON"}
         
     | 
| 
      
 119 
     | 
    
         
            +
                                }))
         
     | 
| 
      
 120 
     | 
    
         
            +
                                
         
     | 
| 
      
 121 
     | 
    
         
            +
                    except websockets.exceptions.ConnectionClosed:
         
     | 
| 
      
 122 
     | 
    
         
            +
                        pass
         
     | 
| 
      
 123 
     | 
    
         
            +
                    finally:
         
     | 
| 
      
 124 
     | 
    
         
            +
                        self.clients.remove(websocket)
         
     | 
| 
      
 125 
     | 
    
         
            +
                        self.logger.info(f"Client disconnected from {client_addr}")
         
     | 
| 
      
 126 
     | 
    
         
            +
                        
         
     | 
| 
      
 127 
     | 
    
         
            +
                async def _send_current_status(self, websocket: WebSocketServerProtocol):
         
     | 
| 
      
 128 
     | 
    
         
            +
                    """Send current system status to a client."""
         
     | 
| 
      
 129 
     | 
    
         
            +
                    status = {
         
     | 
| 
      
 130 
     | 
    
         
            +
                        "type": "system.status",
         
     | 
| 
      
 131 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z",
         
     | 
| 
      
 132 
     | 
    
         
            +
                        "data": {
         
     | 
| 
      
 133 
     | 
    
         
            +
                            "session_id": self.session_id,
         
     | 
| 
      
 134 
     | 
    
         
            +
                            "session_start": self.session_start,
         
     | 
| 
      
 135 
     | 
    
         
            +
                            "claude_status": self.claude_status,
         
     | 
| 
      
 136 
     | 
    
         
            +
                            "claude_pid": self.claude_pid,
         
     | 
| 
      
 137 
     | 
    
         
            +
                            "connected_clients": len(self.clients),
         
     | 
| 
      
 138 
     | 
    
         
            +
                            "websocket_port": self.port,
         
     | 
| 
      
 139 
     | 
    
         
            +
                            "instance_info": {
         
     | 
| 
      
 140 
     | 
    
         
            +
                                "port": self.port,
         
     | 
| 
      
 141 
     | 
    
         
            +
                                "host": self.host,
         
     | 
| 
      
 142 
     | 
    
         
            +
                                "working_dir": os.getcwd() if self.session_id else None
         
     | 
| 
      
 143 
     | 
    
         
            +
                            }
         
     | 
| 
      
 144 
     | 
    
         
            +
                        }
         
     | 
| 
      
 145 
     | 
    
         
            +
                    }
         
     | 
| 
      
 146 
     | 
    
         
            +
                    await websocket.send(json.dumps(status))
         
     | 
| 
      
 147 
     | 
    
         
            +
                    
         
     | 
| 
      
 148 
     | 
    
         
            +
                async def _handle_command(self, websocket: WebSocketServerProtocol, data: Dict[str, Any]):
         
     | 
| 
      
 149 
     | 
    
         
            +
                    """Handle commands from clients."""
         
     | 
| 
      
 150 
     | 
    
         
            +
                    command = data.get("command")
         
     | 
| 
      
 151 
     | 
    
         
            +
                    
         
     | 
| 
      
 152 
     | 
    
         
            +
                    if command == "get_status":
         
     | 
| 
      
 153 
     | 
    
         
            +
                        await self._send_current_status(websocket)
         
     | 
| 
      
 154 
     | 
    
         
            +
                        
         
     | 
| 
      
 155 
     | 
    
         
            +
                    elif command == "get_history":
         
     | 
| 
      
 156 
     | 
    
         
            +
                        # Send recent events
         
     | 
| 
      
 157 
     | 
    
         
            +
                        params = data.get("params", {})
         
     | 
| 
      
 158 
     | 
    
         
            +
                        event_types = params.get("event_types", [])
         
     | 
| 
      
 159 
     | 
    
         
            +
                        limit = min(params.get("limit", 100), len(self.event_history))
         
     | 
| 
      
 160 
     | 
    
         
            +
                        
         
     | 
| 
      
 161 
     | 
    
         
            +
                        history = []
         
     | 
| 
      
 162 
     | 
    
         
            +
                        for event in reversed(self.event_history):
         
     | 
| 
      
 163 
     | 
    
         
            +
                            if not event_types or event["type"] in event_types:
         
     | 
| 
      
 164 
     | 
    
         
            +
                                history.append(event)
         
     | 
| 
      
 165 
     | 
    
         
            +
                                if len(history) >= limit:
         
     | 
| 
      
 166 
     | 
    
         
            +
                                    break
         
     | 
| 
      
 167 
     | 
    
         
            +
                                    
         
     | 
| 
      
 168 
     | 
    
         
            +
                        await websocket.send(json.dumps({
         
     | 
| 
      
 169 
     | 
    
         
            +
                            "type": "history",
         
     | 
| 
      
 170 
     | 
    
         
            +
                            "data": {"events": list(reversed(history))}
         
     | 
| 
      
 171 
     | 
    
         
            +
                        }))
         
     | 
| 
      
 172 
     | 
    
         
            +
                        
         
     | 
| 
      
 173 
     | 
    
         
            +
                    elif command == "subscribe":
         
     | 
| 
      
 174 
     | 
    
         
            +
                        # For now, all clients get all events
         
     | 
| 
      
 175 
     | 
    
         
            +
                        await websocket.send(json.dumps({
         
     | 
| 
      
 176 
     | 
    
         
            +
                            "type": "subscribed",
         
     | 
| 
      
 177 
     | 
    
         
            +
                            "data": {"channels": data.get("channels", ["*"])}
         
     | 
| 
      
 178 
     | 
    
         
            +
                        }))
         
     | 
| 
      
 179 
     | 
    
         
            +
                        
         
     | 
| 
      
 180 
     | 
    
         
            +
                def broadcast_event(self, event_type: str, data: Dict[str, Any]):
         
     | 
| 
      
 181 
     | 
    
         
            +
                    """Broadcast an event to all connected clients."""
         
     | 
| 
      
 182 
     | 
    
         
            +
                    if not WEBSOCKETS_AVAILABLE:
         
     | 
| 
      
 183 
     | 
    
         
            +
                        return
         
     | 
| 
      
 184 
     | 
    
         
            +
                        
         
     | 
| 
      
 185 
     | 
    
         
            +
                    event = {
         
     | 
| 
      
 186 
     | 
    
         
            +
                        "type": event_type,
         
     | 
| 
      
 187 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z",
         
     | 
| 
      
 188 
     | 
    
         
            +
                        "data": data
         
     | 
| 
      
 189 
     | 
    
         
            +
                    }
         
     | 
| 
      
 190 
     | 
    
         
            +
                    
         
     | 
| 
      
 191 
     | 
    
         
            +
                    # Store in history
         
     | 
| 
      
 192 
     | 
    
         
            +
                    self.event_history.append(event)
         
     | 
| 
      
 193 
     | 
    
         
            +
                    
         
     | 
| 
      
 194 
     | 
    
         
            +
                    # Broadcast to clients
         
     | 
| 
      
 195 
     | 
    
         
            +
                    if self.clients and self.loop:
         
     | 
| 
      
 196 
     | 
    
         
            +
                        asyncio.run_coroutine_threadsafe(
         
     | 
| 
      
 197 
     | 
    
         
            +
                            self._broadcast(json.dumps(event)),
         
     | 
| 
      
 198 
     | 
    
         
            +
                            self.loop
         
     | 
| 
      
 199 
     | 
    
         
            +
                        )
         
     | 
| 
      
 200 
     | 
    
         
            +
                        
         
     | 
| 
      
 201 
     | 
    
         
            +
                async def _broadcast(self, message: str):
         
     | 
| 
      
 202 
     | 
    
         
            +
                    """Send a message to all connected clients."""
         
     | 
| 
      
 203 
     | 
    
         
            +
                    if self.clients:
         
     | 
| 
      
 204 
     | 
    
         
            +
                        # Send to all clients concurrently
         
     | 
| 
      
 205 
     | 
    
         
            +
                        await asyncio.gather(
         
     | 
| 
      
 206 
     | 
    
         
            +
                            *[client.send(message) for client in self.clients],
         
     | 
| 
      
 207 
     | 
    
         
            +
                            return_exceptions=True
         
     | 
| 
      
 208 
     | 
    
         
            +
                        )
         
     | 
| 
      
 209 
     | 
    
         
            +
                        
         
     | 
| 
      
 210 
     | 
    
         
            +
                # Convenience methods for common events
         
     | 
| 
      
 211 
     | 
    
         
            +
                
         
     | 
| 
      
 212 
     | 
    
         
            +
                def _get_git_branch(self, working_dir: str) -> str:
         
     | 
| 
      
 213 
     | 
    
         
            +
                    """Get the current git branch for the working directory."""
         
     | 
| 
      
 214 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 215 
     | 
    
         
            +
                        result = subprocess.run(
         
     | 
| 
      
 216 
     | 
    
         
            +
                            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
         
     | 
| 
      
 217 
     | 
    
         
            +
                            cwd=working_dir,
         
     | 
| 
      
 218 
     | 
    
         
            +
                            capture_output=True,
         
     | 
| 
      
 219 
     | 
    
         
            +
                            text=True,
         
     | 
| 
      
 220 
     | 
    
         
            +
                            timeout=2
         
     | 
| 
      
 221 
     | 
    
         
            +
                        )
         
     | 
| 
      
 222 
     | 
    
         
            +
                        if result.returncode == 0:
         
     | 
| 
      
 223 
     | 
    
         
            +
                            return result.stdout.strip()
         
     | 
| 
      
 224 
     | 
    
         
            +
                    except Exception:
         
     | 
| 
      
 225 
     | 
    
         
            +
                        pass
         
     | 
| 
      
 226 
     | 
    
         
            +
                    return "not a git repo"
         
     | 
| 
      
 227 
     | 
    
         
            +
                
         
     | 
| 
      
 228 
     | 
    
         
            +
                def session_started(self, session_id: str, launch_method: str, working_dir: str):
         
     | 
| 
      
 229 
     | 
    
         
            +
                    """Notify that a session has started."""
         
     | 
| 
      
 230 
     | 
    
         
            +
                    self.session_id = session_id
         
     | 
| 
      
 231 
     | 
    
         
            +
                    self.session_start = datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 232 
     | 
    
         
            +
                    
         
     | 
| 
      
 233 
     | 
    
         
            +
                    # Get git branch if in a git repo
         
     | 
| 
      
 234 
     | 
    
         
            +
                    git_branch = self._get_git_branch(working_dir)
         
     | 
| 
      
 235 
     | 
    
         
            +
                    
         
     | 
| 
      
 236 
     | 
    
         
            +
                    self.broadcast_event("session.start", {
         
     | 
| 
      
 237 
     | 
    
         
            +
                        "session_id": session_id,
         
     | 
| 
      
 238 
     | 
    
         
            +
                        "start_time": self.session_start,
         
     | 
| 
      
 239 
     | 
    
         
            +
                        "launch_method": launch_method,
         
     | 
| 
      
 240 
     | 
    
         
            +
                        "working_directory": working_dir,
         
     | 
| 
      
 241 
     | 
    
         
            +
                        "git_branch": git_branch,
         
     | 
| 
      
 242 
     | 
    
         
            +
                        "websocket_port": self.port,
         
     | 
| 
      
 243 
     | 
    
         
            +
                        "instance_info": {
         
     | 
| 
      
 244 
     | 
    
         
            +
                            "port": self.port,
         
     | 
| 
      
 245 
     | 
    
         
            +
                            "host": self.host,
         
     | 
| 
      
 246 
     | 
    
         
            +
                            "working_dir": working_dir
         
     | 
| 
      
 247 
     | 
    
         
            +
                        }
         
     | 
| 
      
 248 
     | 
    
         
            +
                    })
         
     | 
| 
      
 249 
     | 
    
         
            +
                    
         
     | 
| 
      
 250 
     | 
    
         
            +
                def session_ended(self):
         
     | 
| 
      
 251 
     | 
    
         
            +
                    """Notify that a session has ended."""
         
     | 
| 
      
 252 
     | 
    
         
            +
                    if self.session_id:
         
     | 
| 
      
 253 
     | 
    
         
            +
                        duration = None
         
     | 
| 
      
 254 
     | 
    
         
            +
                        if self.session_start:
         
     | 
| 
      
 255 
     | 
    
         
            +
                            start = datetime.fromisoformat(self.session_start.replace("Z", "+00:00"))
         
     | 
| 
      
 256 
     | 
    
         
            +
                            duration = (datetime.utcnow() - start.replace(tzinfo=None)).total_seconds()
         
     | 
| 
      
 257 
     | 
    
         
            +
                            
         
     | 
| 
      
 258 
     | 
    
         
            +
                        self.broadcast_event("session.end", {
         
     | 
| 
      
 259 
     | 
    
         
            +
                            "session_id": self.session_id,
         
     | 
| 
      
 260 
     | 
    
         
            +
                            "end_time": datetime.utcnow().isoformat() + "Z",
         
     | 
| 
      
 261 
     | 
    
         
            +
                            "duration_seconds": duration
         
     | 
| 
      
 262 
     | 
    
         
            +
                        })
         
     | 
| 
      
 263 
     | 
    
         
            +
                        
         
     | 
| 
      
 264 
     | 
    
         
            +
                    self.session_id = None
         
     | 
| 
      
 265 
     | 
    
         
            +
                    self.session_start = None
         
     | 
| 
      
 266 
     | 
    
         
            +
                    
         
     | 
| 
      
 267 
     | 
    
         
            +
                def claude_status_changed(self, status: str, pid: Optional[int] = None, message: str = ""):
         
     | 
| 
      
 268 
     | 
    
         
            +
                    """Notify Claude status change."""
         
     | 
| 
      
 269 
     | 
    
         
            +
                    self.claude_status = status
         
     | 
| 
      
 270 
     | 
    
         
            +
                    self.claude_pid = pid
         
     | 
| 
      
 271 
     | 
    
         
            +
                    self.broadcast_event("claude.status", {
         
     | 
| 
      
 272 
     | 
    
         
            +
                        "status": status,
         
     | 
| 
      
 273 
     | 
    
         
            +
                        "pid": pid,
         
     | 
| 
      
 274 
     | 
    
         
            +
                        "message": message
         
     | 
| 
      
 275 
     | 
    
         
            +
                    })
         
     | 
| 
      
 276 
     | 
    
         
            +
                    
         
     | 
| 
      
 277 
     | 
    
         
            +
                def claude_output(self, content: str, stream: str = "stdout"):
         
     | 
| 
      
 278 
     | 
    
         
            +
                    """Broadcast Claude output."""
         
     | 
| 
      
 279 
     | 
    
         
            +
                    self.broadcast_event("claude.output", {
         
     | 
| 
      
 280 
     | 
    
         
            +
                        "content": content,
         
     | 
| 
      
 281 
     | 
    
         
            +
                        "stream": stream
         
     | 
| 
      
 282 
     | 
    
         
            +
                    })
         
     | 
| 
      
 283 
     | 
    
         
            +
                    
         
     | 
| 
      
 284 
     | 
    
         
            +
                def agent_delegated(self, agent: str, task: str, status: str = "started"):
         
     | 
| 
      
 285 
     | 
    
         
            +
                    """Notify agent delegation."""
         
     | 
| 
      
 286 
     | 
    
         
            +
                    self.broadcast_event("agent.delegation", {
         
     | 
| 
      
 287 
     | 
    
         
            +
                        "agent": agent,
         
     | 
| 
      
 288 
     | 
    
         
            +
                        "task": task,
         
     | 
| 
      
 289 
     | 
    
         
            +
                        "status": status,
         
     | 
| 
      
 290 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 291 
     | 
    
         
            +
                    })
         
     | 
| 
      
 292 
     | 
    
         
            +
                    
         
     | 
| 
      
 293 
     | 
    
         
            +
                def todo_updated(self, todos: List[Dict[str, Any]]):
         
     | 
| 
      
 294 
     | 
    
         
            +
                    """Notify todo list update."""
         
     | 
| 
      
 295 
     | 
    
         
            +
                    stats = {
         
     | 
| 
      
 296 
     | 
    
         
            +
                        "total": len(todos),
         
     | 
| 
      
 297 
     | 
    
         
            +
                        "completed": sum(1 for t in todos if t.get("status") == "completed"),
         
     | 
| 
      
 298 
     | 
    
         
            +
                        "in_progress": sum(1 for t in todos if t.get("status") == "in_progress"),
         
     | 
| 
      
 299 
     | 
    
         
            +
                        "pending": sum(1 for t in todos if t.get("status") == "pending")
         
     | 
| 
      
 300 
     | 
    
         
            +
                    }
         
     | 
| 
      
 301 
     | 
    
         
            +
                    
         
     | 
| 
      
 302 
     | 
    
         
            +
                    self.broadcast_event("todo.update", {
         
     | 
| 
      
 303 
     | 
    
         
            +
                        "todos": todos,
         
     | 
| 
      
 304 
     | 
    
         
            +
                        "stats": stats
         
     | 
| 
      
 305 
     | 
    
         
            +
                    })
         
     | 
| 
      
 306 
     | 
    
         
            +
                    
         
     | 
| 
      
 307 
     | 
    
         
            +
                def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
         
     | 
| 
      
 308 
     | 
    
         
            +
                    """Notify ticket creation."""
         
     | 
| 
      
 309 
     | 
    
         
            +
                    self.broadcast_event("ticket.created", {
         
     | 
| 
      
 310 
     | 
    
         
            +
                        "id": ticket_id,
         
     | 
| 
      
 311 
     | 
    
         
            +
                        "title": title,
         
     | 
| 
      
 312 
     | 
    
         
            +
                        "priority": priority,
         
     | 
| 
      
 313 
     | 
    
         
            +
                        "created_at": datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 314 
     | 
    
         
            +
                    })
         
     | 
| 
      
 315 
     | 
    
         
            +
                    
         
     | 
| 
      
 316 
     | 
    
         
            +
                def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
         
     | 
| 
      
 317 
     | 
    
         
            +
                    """Notify when agent memory is loaded from file."""
         
     | 
| 
      
 318 
     | 
    
         
            +
                    self.broadcast_event("memory:loaded", {
         
     | 
| 
      
 319 
     | 
    
         
            +
                        "agent_id": agent_id,
         
     | 
| 
      
 320 
     | 
    
         
            +
                        "memory_size": memory_size,
         
     | 
| 
      
 321 
     | 
    
         
            +
                        "sections_count": sections_count,
         
     | 
| 
      
 322 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 323 
     | 
    
         
            +
                    })
         
     | 
| 
      
 324 
     | 
    
         
            +
                    
         
     | 
| 
      
 325 
     | 
    
         
            +
                def memory_created(self, agent_id: str, template_type: str):
         
     | 
| 
      
 326 
     | 
    
         
            +
                    """Notify when new agent memory is created from template."""
         
     | 
| 
      
 327 
     | 
    
         
            +
                    self.broadcast_event("memory:created", {
         
     | 
| 
      
 328 
     | 
    
         
            +
                        "agent_id": agent_id,
         
     | 
| 
      
 329 
     | 
    
         
            +
                        "template_type": template_type,
         
     | 
| 
      
 330 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 331 
     | 
    
         
            +
                    })
         
     | 
| 
      
 332 
     | 
    
         
            +
                    
         
     | 
| 
      
 333 
     | 
    
         
            +
                def memory_updated(self, agent_id: str, learning_type: str, content: str, section: str):
         
     | 
| 
      
 334 
     | 
    
         
            +
                    """Notify when learning is added to agent memory."""
         
     | 
| 
      
 335 
     | 
    
         
            +
                    self.broadcast_event("memory:updated", {
         
     | 
| 
      
 336 
     | 
    
         
            +
                        "agent_id": agent_id,
         
     | 
| 
      
 337 
     | 
    
         
            +
                        "learning_type": learning_type,
         
     | 
| 
      
 338 
     | 
    
         
            +
                        "content": content,
         
     | 
| 
      
 339 
     | 
    
         
            +
                        "section": section,
         
     | 
| 
      
 340 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 341 
     | 
    
         
            +
                    })
         
     | 
| 
      
 342 
     | 
    
         
            +
                    
         
     | 
| 
      
 343 
     | 
    
         
            +
                def memory_injected(self, agent_id: str, context_size: int):
         
     | 
| 
      
 344 
     | 
    
         
            +
                    """Notify when agent memory is injected into context."""
         
     | 
| 
      
 345 
     | 
    
         
            +
                    self.broadcast_event("memory:injected", {
         
     | 
| 
      
 346 
     | 
    
         
            +
                        "agent_id": agent_id,
         
     | 
| 
      
 347 
     | 
    
         
            +
                        "context_size": context_size,
         
     | 
| 
      
 348 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat() + "Z"
         
     | 
| 
      
 349 
     | 
    
         
            +
                    })
         
     | 
| 
      
 350 
     | 
    
         
            +
             
     | 
| 
      
 351 
     | 
    
         
            +
             
     | 
| 
      
 352 
     | 
    
         
            +
            # Global instance for easy access
         
     | 
| 
      
 353 
     | 
    
         
            +
            _websocket_server: Optional[WebSocketServer] = None
         
     | 
| 
      
 354 
     | 
    
         
            +
             
     | 
| 
      
 355 
     | 
    
         
            +
             
     | 
| 
      
 356 
     | 
    
         
            +
            def get_websocket_server() -> WebSocketServer:
         
     | 
| 
      
 357 
     | 
    
         
            +
                """Get or create the global WebSocket server instance."""
         
     | 
| 
      
 358 
     | 
    
         
            +
                global _websocket_server
         
     | 
| 
      
 359 
     | 
    
         
            +
                if _websocket_server is None:
         
     | 
| 
      
 360 
     | 
    
         
            +
                    _websocket_server = WebSocketServer()
         
     | 
| 
      
 361 
     | 
    
         
            +
                return _websocket_server
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
             
     | 
| 
      
 364 
     | 
    
         
            +
            def start_websocket_server():
         
     | 
| 
      
 365 
     | 
    
         
            +
                """Start the global WebSocket server."""
         
     | 
| 
      
 366 
     | 
    
         
            +
                server = get_websocket_server()
         
     | 
| 
      
 367 
     | 
    
         
            +
                server.start()
         
     | 
| 
      
 368 
     | 
    
         
            +
                return server
         
     | 
| 
      
 369 
     | 
    
         
            +
             
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
            def stop_websocket_server():
         
     | 
| 
      
 372 
     | 
    
         
            +
                """Stop the global WebSocket server."""
         
     | 
| 
      
 373 
     | 
    
         
            +
                global _websocket_server
         
     | 
| 
      
 374 
     | 
    
         
            +
                if _websocket_server:
         
     | 
| 
      
 375 
     | 
    
         
            +
                    _websocket_server.stop()
         
     | 
| 
      
 376 
     | 
    
         
            +
                    _websocket_server = None
         
     | 
| 
         @@ -0,0 +1,211 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """
         
     | 
| 
      
 2 
     | 
    
         
            +
            Dependency management utilities for claude-mpm.
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            WHY: This module handles automatic installation of optional dependencies
         
     | 
| 
      
 5 
     | 
    
         
            +
            like Socket.IO monitoring tools. It ensures users can run --monitor without
         
     | 
| 
      
 6 
     | 
    
         
            +
            manual dependency setup.
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            DESIGN DECISION: We use subprocess to install packages in the same environment
         
     | 
| 
      
 9 
     | 
    
         
            +
            that's running claude-mpm, respecting virtual environments and user setups.
         
     | 
| 
      
 10 
     | 
    
         
            +
            """
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            import subprocess
         
     | 
| 
      
 13 
     | 
    
         
            +
            import sys
         
     | 
| 
      
 14 
     | 
    
         
            +
            import importlib
         
     | 
| 
      
 15 
     | 
    
         
            +
            from typing import List, Tuple, Optional
         
     | 
| 
      
 16 
     | 
    
         
            +
            from pathlib import Path
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            from ..core.logger import get_logger
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            def check_dependency(package_name: str, import_name: Optional[str] = None) -> bool:
         
     | 
| 
      
 22 
     | 
    
         
            +
                """
         
     | 
| 
      
 23 
     | 
    
         
            +
                Check if a Python package is installed and importable.
         
     | 
| 
      
 24 
     | 
    
         
            +
                
         
     | 
| 
      
 25 
     | 
    
         
            +
                WHY: We need to verify if optional dependencies are available before
         
     | 
| 
      
 26 
     | 
    
         
            +
                attempting to use them. This prevents ImportError crashes.
         
     | 
| 
      
 27 
     | 
    
         
            +
                
         
     | 
| 
      
 28 
     | 
    
         
            +
                DESIGN DECISION: We use importlib.util.find_spec() which is more reliable
         
     | 
| 
      
 29 
     | 
    
         
            +
                than try/except import because it doesn't execute module code.
         
     | 
| 
      
 30 
     | 
    
         
            +
                
         
     | 
| 
      
 31 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 32 
     | 
    
         
            +
                    package_name: Name of the package (e.g., 'python-socketio')
         
     | 
| 
      
 33 
     | 
    
         
            +
                    import_name: Name to import (e.g., 'socketio'). Defaults to package_name.
         
     | 
| 
      
 34 
     | 
    
         
            +
                    
         
     | 
| 
      
 35 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 36 
     | 
    
         
            +
                    bool: True if package is available, False otherwise
         
     | 
| 
      
 37 
     | 
    
         
            +
                """
         
     | 
| 
      
 38 
     | 
    
         
            +
                if import_name is None:
         
     | 
| 
      
 39 
     | 
    
         
            +
                    import_name = package_name.replace('-', '_')
         
     | 
| 
      
 40 
     | 
    
         
            +
                
         
     | 
| 
      
 41 
     | 
    
         
            +
                try:
         
     | 
| 
      
 42 
     | 
    
         
            +
                    import importlib.util
         
     | 
| 
      
 43 
     | 
    
         
            +
                    spec = importlib.util.find_spec(import_name)
         
     | 
| 
      
 44 
     | 
    
         
            +
                    return spec is not None
         
     | 
| 
      
 45 
     | 
    
         
            +
                except (ImportError, ModuleNotFoundError, ValueError):
         
     | 
| 
      
 46 
     | 
    
         
            +
                    return False
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            def install_packages(packages: List[str], logger=None) -> Tuple[bool, str]:
         
     | 
| 
      
 50 
     | 
    
         
            +
                """
         
     | 
| 
      
 51 
     | 
    
         
            +
                Install Python packages using pip in the current environment.
         
     | 
| 
      
 52 
     | 
    
         
            +
                
         
     | 
| 
      
 53 
     | 
    
         
            +
                WHY: Users should not need to manually install optional dependencies.
         
     | 
| 
      
 54 
     | 
    
         
            +
                This function handles automatic installation while respecting the current
         
     | 
| 
      
 55 
     | 
    
         
            +
                Python environment (including virtual environments).
         
     | 
| 
      
 56 
     | 
    
         
            +
                
         
     | 
| 
      
 57 
     | 
    
         
            +
                DESIGN DECISION: We use subprocess to call pip instead of importlib
         
     | 
| 
      
 58 
     | 
    
         
            +
                because pip installation needs to be done in the same environment
         
     | 
| 
      
 59 
     | 
    
         
            +
                that's running the application.
         
     | 
| 
      
 60 
     | 
    
         
            +
                
         
     | 
| 
      
 61 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 62 
     | 
    
         
            +
                    packages: List of package names to install
         
     | 
| 
      
 63 
     | 
    
         
            +
                    logger: Optional logger for output
         
     | 
| 
      
 64 
     | 
    
         
            +
                    
         
     | 
| 
      
 65 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 66 
     | 
    
         
            +
                    Tuple[bool, str]: (success, error_message_if_failed)
         
     | 
| 
      
 67 
     | 
    
         
            +
                """
         
     | 
| 
      
 68 
     | 
    
         
            +
                if logger is None:
         
     | 
| 
      
 69 
     | 
    
         
            +
                    logger = get_logger("dependency_manager")
         
     | 
| 
      
 70 
     | 
    
         
            +
                
         
     | 
| 
      
 71 
     | 
    
         
            +
                try:
         
     | 
| 
      
 72 
     | 
    
         
            +
                    # Use the same Python executable that's running this script
         
     | 
| 
      
 73 
     | 
    
         
            +
                    cmd = [sys.executable, "-m", "pip", "install"] + packages
         
     | 
| 
      
 74 
     | 
    
         
            +
                    
         
     | 
| 
      
 75 
     | 
    
         
            +
                    logger.info(f"Installing packages: {packages}")
         
     | 
| 
      
 76 
     | 
    
         
            +
                    logger.debug(f"Running command: {' '.join(cmd)}")
         
     | 
| 
      
 77 
     | 
    
         
            +
                    
         
     | 
| 
      
 78 
     | 
    
         
            +
                    # Run pip install with proper error handling
         
     | 
| 
      
 79 
     | 
    
         
            +
                    result = subprocess.run(
         
     | 
| 
      
 80 
     | 
    
         
            +
                        cmd,
         
     | 
| 
      
 81 
     | 
    
         
            +
                        capture_output=True,
         
     | 
| 
      
 82 
     | 
    
         
            +
                        text=True,
         
     | 
| 
      
 83 
     | 
    
         
            +
                        timeout=300  # 5 minute timeout for installation
         
     | 
| 
      
 84 
     | 
    
         
            +
                    )
         
     | 
| 
      
 85 
     | 
    
         
            +
                    
         
     | 
| 
      
 86 
     | 
    
         
            +
                    if result.returncode == 0:
         
     | 
| 
      
 87 
     | 
    
         
            +
                        logger.info(f"Successfully installed packages: {packages}")
         
     | 
| 
      
 88 
     | 
    
         
            +
                        return True, ""
         
     | 
| 
      
 89 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 90 
     | 
    
         
            +
                        error_msg = f"pip install failed with return code {result.returncode}"
         
     | 
| 
      
 91 
     | 
    
         
            +
                        if result.stderr:
         
     | 
| 
      
 92 
     | 
    
         
            +
                            error_msg += f": {result.stderr.strip()}"
         
     | 
| 
      
 93 
     | 
    
         
            +
                        logger.error(error_msg)
         
     | 
| 
      
 94 
     | 
    
         
            +
                        return False, error_msg
         
     | 
| 
      
 95 
     | 
    
         
            +
                        
         
     | 
| 
      
 96 
     | 
    
         
            +
                except subprocess.TimeoutExpired:
         
     | 
| 
      
 97 
     | 
    
         
            +
                    error_msg = "Package installation timed out after 5 minutes"
         
     | 
| 
      
 98 
     | 
    
         
            +
                    logger.error(error_msg)
         
     | 
| 
      
 99 
     | 
    
         
            +
                    return False, error_msg
         
     | 
| 
      
 100 
     | 
    
         
            +
                except Exception as e:
         
     | 
| 
      
 101 
     | 
    
         
            +
                    error_msg = f"Failed to install packages: {e}"
         
     | 
| 
      
 102 
     | 
    
         
            +
                    logger.error(error_msg)
         
     | 
| 
      
 103 
     | 
    
         
            +
                    return False, error_msg
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
            def ensure_socketio_dependencies(logger=None) -> Tuple[bool, str]:
         
     | 
| 
      
 107 
     | 
    
         
            +
                """
         
     | 
| 
      
 108 
     | 
    
         
            +
                Ensure Socket.IO dependencies are installed for monitoring features.
         
     | 
| 
      
 109 
     | 
    
         
            +
                
         
     | 
| 
      
 110 
     | 
    
         
            +
                WHY: The --monitor flag requires python-socketio and aiohttp, but we want
         
     | 
| 
      
 111 
     | 
    
         
            +
                the base package to work without these heavy dependencies. This function
         
     | 
| 
      
 112 
     | 
    
         
            +
                installs them on-demand.
         
     | 
| 
      
 113 
     | 
    
         
            +
                
         
     | 
| 
      
 114 
     | 
    
         
            +
                DESIGN DECISION: We check each dependency individually and only install
         
     | 
| 
      
 115 
     | 
    
         
            +
                what's missing, reducing installation time and avoiding unnecessary work.
         
     | 
| 
      
 116 
     | 
    
         
            +
                
         
     | 
| 
      
 117 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 118 
     | 
    
         
            +
                    logger: Optional logger for output
         
     | 
| 
      
 119 
     | 
    
         
            +
                    
         
     | 
| 
      
 120 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 121 
     | 
    
         
            +
                    Tuple[bool, str]: (success, error_message_if_failed)
         
     | 
| 
      
 122 
     | 
    
         
            +
                """
         
     | 
| 
      
 123 
     | 
    
         
            +
                if logger is None:
         
     | 
| 
      
 124 
     | 
    
         
            +
                    logger = get_logger("dependency_manager")
         
     | 
| 
      
 125 
     | 
    
         
            +
                
         
     | 
| 
      
 126 
     | 
    
         
            +
                # Define required packages for Socket.IO monitoring
         
     | 
| 
      
 127 
     | 
    
         
            +
                required_packages = [
         
     | 
| 
      
 128 
     | 
    
         
            +
                    ("python-socketio", "socketio"),
         
     | 
| 
      
 129 
     | 
    
         
            +
                    ("aiohttp", "aiohttp"),
         
     | 
| 
      
 130 
     | 
    
         
            +
                    ("python-engineio", "engineio")
         
     | 
| 
      
 131 
     | 
    
         
            +
                ]
         
     | 
| 
      
 132 
     | 
    
         
            +
                
         
     | 
| 
      
 133 
     | 
    
         
            +
                missing_packages = []
         
     | 
| 
      
 134 
     | 
    
         
            +
                
         
     | 
| 
      
 135 
     | 
    
         
            +
                # Check which packages are missing
         
     | 
| 
      
 136 
     | 
    
         
            +
                for package_name, import_name in required_packages:
         
     | 
| 
      
 137 
     | 
    
         
            +
                    if not check_dependency(package_name, import_name):
         
     | 
| 
      
 138 
     | 
    
         
            +
                        missing_packages.append(package_name)
         
     | 
| 
      
 139 
     | 
    
         
            +
                        logger.debug(f"Missing dependency: {package_name}")
         
     | 
| 
      
 140 
     | 
    
         
            +
                
         
     | 
| 
      
 141 
     | 
    
         
            +
                if not missing_packages:
         
     | 
| 
      
 142 
     | 
    
         
            +
                    logger.debug("All Socket.IO dependencies are already installed")
         
     | 
| 
      
 143 
     | 
    
         
            +
                    return True, ""
         
     | 
| 
      
 144 
     | 
    
         
            +
                
         
     | 
| 
      
 145 
     | 
    
         
            +
                # Install missing packages
         
     | 
| 
      
 146 
     | 
    
         
            +
                logger.info(f"Installing missing Socket.IO dependencies: {missing_packages}")
         
     | 
| 
      
 147 
     | 
    
         
            +
                success, error_msg = install_packages(missing_packages, logger)
         
     | 
| 
      
 148 
     | 
    
         
            +
                
         
     | 
| 
      
 149 
     | 
    
         
            +
                if success:
         
     | 
| 
      
 150 
     | 
    
         
            +
                    # Verify installation worked
         
     | 
| 
      
 151 
     | 
    
         
            +
                    for package_name, import_name in required_packages:
         
     | 
| 
      
 152 
     | 
    
         
            +
                        if not check_dependency(package_name, import_name):
         
     | 
| 
      
 153 
     | 
    
         
            +
                            error_msg = f"Package {package_name} was installed but is not importable"
         
     | 
| 
      
 154 
     | 
    
         
            +
                            logger.error(error_msg)
         
     | 
| 
      
 155 
     | 
    
         
            +
                            return False, error_msg
         
     | 
| 
      
 156 
     | 
    
         
            +
                    
         
     | 
| 
      
 157 
     | 
    
         
            +
                    logger.info("Socket.IO dependencies installed and verified successfully")
         
     | 
| 
      
 158 
     | 
    
         
            +
                    return True, ""
         
     | 
| 
      
 159 
     | 
    
         
            +
                else:
         
     | 
| 
      
 160 
     | 
    
         
            +
                    return False, error_msg
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
            def get_pip_freeze_output() -> List[str]:
         
     | 
| 
      
 164 
     | 
    
         
            +
                """
         
     | 
| 
      
 165 
     | 
    
         
            +
                Get the output of 'pip freeze' for debugging dependency issues.
         
     | 
| 
      
 166 
     | 
    
         
            +
                
         
     | 
| 
      
 167 
     | 
    
         
            +
                WHY: When dependency installation fails, we need to help users
         
     | 
| 
      
 168 
     | 
    
         
            +
                understand what packages are installed and what might be conflicting.
         
     | 
| 
      
 169 
     | 
    
         
            +
                
         
     | 
| 
      
 170 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 171 
     | 
    
         
            +
                    List[str]: List of installed packages from pip freeze
         
     | 
| 
      
 172 
     | 
    
         
            +
                """
         
     | 
| 
      
 173 
     | 
    
         
            +
                try:
         
     | 
| 
      
 174 
     | 
    
         
            +
                    result = subprocess.run(
         
     | 
| 
      
 175 
     | 
    
         
            +
                        [sys.executable, "-m", "pip", "freeze"],
         
     | 
| 
      
 176 
     | 
    
         
            +
                        capture_output=True,
         
     | 
| 
      
 177 
     | 
    
         
            +
                        text=True,
         
     | 
| 
      
 178 
     | 
    
         
            +
                        timeout=30
         
     | 
| 
      
 179 
     | 
    
         
            +
                    )
         
     | 
| 
      
 180 
     | 
    
         
            +
                    
         
     | 
| 
      
 181 
     | 
    
         
            +
                    if result.returncode == 0:
         
     | 
| 
      
 182 
     | 
    
         
            +
                        return result.stdout.strip().split('\n')
         
     | 
| 
      
 183 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 184 
     | 
    
         
            +
                        return [f"pip freeze failed: {result.stderr}"]
         
     | 
| 
      
 185 
     | 
    
         
            +
                        
         
     | 
| 
      
 186 
     | 
    
         
            +
                except Exception as e:
         
     | 
| 
      
 187 
     | 
    
         
            +
                    return [f"Failed to get pip freeze output: {e}"]
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
             
     | 
| 
      
 190 
     | 
    
         
            +
            def check_virtual_environment() -> Tuple[bool, str]:
         
     | 
| 
      
 191 
     | 
    
         
            +
                """
         
     | 
| 
      
 192 
     | 
    
         
            +
                Check if we're running in a virtual environment.
         
     | 
| 
      
 193 
     | 
    
         
            +
                
         
     | 
| 
      
 194 
     | 
    
         
            +
                WHY: Installation behavior might differ between virtual environments
         
     | 
| 
      
 195 
     | 
    
         
            +
                and system Python. This helps with debugging and user guidance.
         
     | 
| 
      
 196 
     | 
    
         
            +
                
         
     | 
| 
      
 197 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 198 
     | 
    
         
            +
                    Tuple[bool, str]: (is_virtual_env, environment_info)
         
     | 
| 
      
 199 
     | 
    
         
            +
                """
         
     | 
| 
      
 200 
     | 
    
         
            +
                # Check for virtual environment indicators
         
     | 
| 
      
 201 
     | 
    
         
            +
                in_venv = (
         
     | 
| 
      
 202 
     | 
    
         
            +
                    hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
         
     | 
| 
      
 203 
     | 
    
         
            +
                    or hasattr(sys, 'real_prefix')
         
     | 
| 
      
 204 
     | 
    
         
            +
                    or (hasattr(sys, 'prefix') and 'conda' in sys.prefix.lower())
         
     | 
| 
      
 205 
     | 
    
         
            +
                )
         
     | 
| 
      
 206 
     | 
    
         
            +
                
         
     | 
| 
      
 207 
     | 
    
         
            +
                if in_venv:
         
     | 
| 
      
 208 
     | 
    
         
            +
                    venv_path = getattr(sys, 'prefix', 'unknown')
         
     | 
| 
      
 209 
     | 
    
         
            +
                    return True, f"Virtual environment: {venv_path}"
         
     | 
| 
      
 210 
     | 
    
         
            +
                else:
         
     | 
| 
      
 211 
     | 
    
         
            +
                    return False, f"System Python: {sys.prefix}"
         
     | 
| 
         @@ -0,0 +1,34 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env python3
         
     | 
| 
      
 2 
     | 
    
         
            +
            """Open the dashboard statically in the browser."""
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            import os
         
     | 
| 
      
 5 
     | 
    
         
            +
            import webbrowser
         
     | 
| 
      
 6 
     | 
    
         
            +
            from pathlib import Path
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            def open_dashboard(port=8765, autoconnect=True):
         
     | 
| 
      
 9 
     | 
    
         
            +
                """Open the dashboard HTML file directly in the browser.
         
     | 
| 
      
 10 
     | 
    
         
            +
                
         
     | 
| 
      
 11 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 12 
     | 
    
         
            +
                    port: Socket.IO server port to connect to
         
     | 
| 
      
 13 
     | 
    
         
            +
                    autoconnect: Whether to auto-connect on load
         
     | 
| 
      
 14 
     | 
    
         
            +
                """
         
     | 
| 
      
 15 
     | 
    
         
            +
                # Get the static index.html path (main entry point)
         
     | 
| 
      
 16 
     | 
    
         
            +
                dashboard_path = Path(__file__).parent / "templates" / "index.html"
         
     | 
| 
      
 17 
     | 
    
         
            +
                
         
     | 
| 
      
 18 
     | 
    
         
            +
                if not dashboard_path.exists():
         
     | 
| 
      
 19 
     | 
    
         
            +
                    raise FileNotFoundError(f"Dashboard not found at {dashboard_path}")
         
     | 
| 
      
 20 
     | 
    
         
            +
                
         
     | 
| 
      
 21 
     | 
    
         
            +
                # Build URL with query parameters for Socket.IO connection
         
     | 
| 
      
 22 
     | 
    
         
            +
                dashboard_url = f"file://{dashboard_path.absolute()}?port={port}"
         
     | 
| 
      
 23 
     | 
    
         
            +
                if autoconnect:
         
     | 
| 
      
 24 
     | 
    
         
            +
                    dashboard_url += "&autoconnect=true"
         
     | 
| 
      
 25 
     | 
    
         
            +
                
         
     | 
| 
      
 26 
     | 
    
         
            +
                print(f"🌐 Opening static dashboard: {dashboard_url}")
         
     | 
| 
      
 27 
     | 
    
         
            +
                print(f"📡 Dashboard will connect to Socket.IO server at localhost:{port}")
         
     | 
| 
      
 28 
     | 
    
         
            +
                webbrowser.open(dashboard_url)
         
     | 
| 
      
 29 
     | 
    
         
            +
                
         
     | 
| 
      
 30 
     | 
    
         
            +
                return dashboard_url
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            if __name__ == "__main__":
         
     | 
| 
      
 33 
     | 
    
         
            +
                # Test opening the dashboard
         
     | 
| 
      
 34 
     | 
    
         
            +
                open_dashboard()
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.4
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: claude-mpm
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 3.1 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 3.2.1
         
     | 
| 
       4 
4 
     | 
    
         
             
            Summary: Claude Multi-agent Project Manager - Clean orchestration with ticket management
         
     | 
| 
       5 
5 
     | 
    
         
             
            Home-page: https://github.com/bobmatnyc/claude-mpm
         
     | 
| 
       6 
6 
     | 
    
         
             
            Author: Claude MPM Team
         
     | 
| 
         @@ -39,6 +39,10 @@ Requires-Dist: pytest-cov; extra == "dev" 
     | 
|
| 
       39 
39 
     | 
    
         
             
            Requires-Dist: black; extra == "dev"
         
     | 
| 
       40 
40 
     | 
    
         
             
            Requires-Dist: flake8; extra == "dev"
         
     | 
| 
       41 
41 
     | 
    
         
             
            Requires-Dist: mypy; extra == "dev"
         
     | 
| 
      
 42 
     | 
    
         
            +
            Provides-Extra: monitor
         
     | 
| 
      
 43 
     | 
    
         
            +
            Requires-Dist: python-socketio>=5.11.0; extra == "monitor"
         
     | 
| 
      
 44 
     | 
    
         
            +
            Requires-Dist: aiohttp>=3.9.0; extra == "monitor"
         
     | 
| 
      
 45 
     | 
    
         
            +
            Requires-Dist: python-engineio>=4.8.0; extra == "monitor"
         
     | 
| 
       42 
46 
     | 
    
         
             
            Dynamic: author-email
         
     | 
| 
       43 
47 
     | 
    
         
             
            Dynamic: home-page
         
     | 
| 
       44 
48 
     | 
    
         
             
            Dynamic: license-file
         
     | 
| 
         @@ -63,6 +67,9 @@ A framework for Claude that enables multi-agent workflows and extensible capabil 
     | 
|
| 
       63 
67 
     | 
    
         
             
            # Install globally via npm (recommended)
         
     | 
| 
       64 
68 
     | 
    
         
             
            npm install -g @bobmatnyc/claude-mpm
         
     | 
| 
       65 
69 
     | 
    
         | 
| 
      
 70 
     | 
    
         
            +
            # Or install via PyPI
         
     | 
| 
      
 71 
     | 
    
         
            +
            pip install claude-mpm
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
       66 
73 
     | 
    
         
             
            # Or use npx for one-time usage
         
     | 
| 
       67 
74 
     | 
    
         
             
            npx @bobmatnyc/claude-mpm
         
     | 
| 
       68 
75 
     | 
    
         
             
            ```
         
     | 
| 
         @@ -210,6 +217,14 @@ Claude MPM provides a modular framework for extending Claude's capabilities: 
     | 
|
| 
       210 
217 
     | 
    
         
             
            - Reusable business logic
         
     | 
| 
       211 
218 
     | 
    
         
             
            - Well-defined interfaces
         
     | 
| 
       212 
219 
     | 
    
         | 
| 
      
 220 
     | 
    
         
            +
            ### Real-Time Monitoring
         
     | 
| 
      
 221 
     | 
    
         
            +
            - **Live Dashboard**: Monitor Claude interactions with a real-time web dashboard
         
     | 
| 
      
 222 
     | 
    
         
            +
            - **Event Tracking**: View all events, agent activities, tool usage, and file operations
         
     | 
| 
      
 223 
     | 
    
         
            +
            - **Multi-Tab Interface**: Organized views for Events, Agents, Tools, and Files
         
     | 
| 
      
 224 
     | 
    
         
            +
            - **Zero Configuration**: Simple `--monitor` flag enables monitoring
         
     | 
| 
      
 225 
     | 
    
         
            +
            - **Development Focus**: Basic monitoring with enhanced features planned
         
     | 
| 
      
 226 
     | 
    
         
            +
            - **Full Documentation**: See [monitoring documentation](docs/user/monitoring/) for complete details
         
     | 
| 
      
 227 
     | 
    
         
            +
             
     | 
| 
       213 
228 
     | 
    
         
             
            ### Session Management
         
     | 
| 
       214 
229 
     | 
    
         
             
            - Comprehensive logging of all interactions
         
     | 
| 
       215 
230 
     | 
    
         
             
            - Debug mode for troubleshooting
         
     | 
| 
         @@ -297,6 +312,9 @@ These tree-sitter dependencies enable: 
     | 
|
| 
       297 
312 
     | 
    
         
             
            # Run interactive session
         
     | 
| 
       298 
313 
     | 
    
         
             
            claude-mpm
         
     | 
| 
       299 
314 
     | 
    
         | 
| 
      
 315 
     | 
    
         
            +
            # Run with real-time monitoring dashboard
         
     | 
| 
      
 316 
     | 
    
         
            +
            claude-mpm run --monitor
         
     | 
| 
      
 317 
     | 
    
         
            +
             
     | 
| 
       300 
318 
     | 
    
         
             
            # Run with debug logging
         
     | 
| 
       301 
319 
     | 
    
         
             
            claude-mpm --debug
         
     | 
| 
       302 
320 
     | 
    
         | 
| 
         @@ -316,6 +334,7 @@ Options: 
     | 
|
| 
       316 
334 
     | 
    
         | 
| 
       317 
335 
     | 
    
         
             
            Commands:
         
     | 
| 
       318 
336 
     | 
    
         
             
              run                  Run Claude session (default)
         
     | 
| 
      
 337 
     | 
    
         
            +
                --monitor          Launch with real-time monitoring dashboard
         
     | 
| 
       319 
338 
     | 
    
         
             
              info                 Show framework and configuration info
         
     | 
| 
       320 
339 
     | 
    
         
             
            ```
         
     | 
| 
       321 
340 
     | 
    
         |