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.
Files changed (52) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/agents/INSTRUCTIONS.md +80 -2
  3. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  4. claude_mpm/agents/base_agent.json +1 -1
  5. claude_mpm/agents/templates/pm.json +25 -0
  6. claude_mpm/agents/templates/research.json +2 -1
  7. claude_mpm/cli/__init__.py +6 -1
  8. claude_mpm/cli/commands/__init__.py +3 -1
  9. claude_mpm/cli/commands/memory.py +232 -0
  10. claude_mpm/cli/commands/run.py +496 -8
  11. claude_mpm/cli/parser.py +91 -1
  12. claude_mpm/config/socketio_config.py +256 -0
  13. claude_mpm/constants.py +9 -0
  14. claude_mpm/core/__init__.py +2 -2
  15. claude_mpm/core/claude_runner.py +919 -0
  16. claude_mpm/core/config.py +21 -1
  17. claude_mpm/core/hook_manager.py +196 -0
  18. claude_mpm/core/pm_hook_interceptor.py +205 -0
  19. claude_mpm/core/simple_runner.py +296 -16
  20. claude_mpm/core/socketio_pool.py +582 -0
  21. claude_mpm/core/websocket_handler.py +233 -0
  22. claude_mpm/deployment_paths.py +261 -0
  23. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  24. claude_mpm/hooks/claude_hooks/hook_handler.py +669 -632
  25. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  26. claude_mpm/hooks/memory_integration_hook.py +312 -0
  27. claude_mpm/orchestration/__init__.py +1 -1
  28. claude_mpm/scripts/claude-mpm-socketio +32 -0
  29. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  30. claude_mpm/scripts/install_socketio_server.py +407 -0
  31. claude_mpm/scripts/launch_monitor.py +132 -0
  32. claude_mpm/scripts/manage_version.py +479 -0
  33. claude_mpm/scripts/socketio_daemon.py +181 -0
  34. claude_mpm/scripts/socketio_server_manager.py +428 -0
  35. claude_mpm/services/__init__.py +5 -0
  36. claude_mpm/services/agent_memory_manager.py +684 -0
  37. claude_mpm/services/hook_service.py +362 -0
  38. claude_mpm/services/socketio_client_manager.py +474 -0
  39. claude_mpm/services/socketio_server.py +698 -0
  40. claude_mpm/services/standalone_socketio_server.py +631 -0
  41. claude_mpm/services/websocket_server.py +376 -0
  42. claude_mpm/utils/dependency_manager.py +211 -0
  43. claude_mpm/web/open_dashboard.py +34 -0
  44. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -1
  45. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +50 -24
  46. claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
  47. claude_mpm/cli_old.py +0 -728
  48. claude_mpm-3.1.2.dist-info/entry_points.txt +0 -4
  49. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  50. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
  51. {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
  52. {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.2
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