claude-mpm 4.2.7__py3-none-any.whl → 4.2.11__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 (57) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/dashboard.py +62 -120
  3. claude_mpm/cli/commands/monitor.py +71 -212
  4. claude_mpm/cli/commands/run.py +33 -33
  5. claude_mpm/cli/parser.py +79 -2
  6. claude_mpm/cli/parsers/__init__.py +29 -0
  7. claude_mpm/dashboard/static/css/code-tree.css +16 -4
  8. claude_mpm/dashboard/static/css/dashboard.css +15 -1
  9. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
  13. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  14. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  15. claude_mpm/dashboard/static/js/components/code-tree.js +775 -142
  16. claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
  17. claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
  18. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
  19. claude_mpm/dashboard/static/js/dashboard.js +108 -91
  20. claude_mpm/dashboard/static/js/socket-client.js +9 -7
  21. claude_mpm/dashboard/templates/index.html +5 -2
  22. claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
  23. claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
  24. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
  25. claude_mpm/services/agents/deployment/agent_format_converter.py +3 -3
  26. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -5
  27. claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
  28. claude_mpm/services/monitor/__init__.py +20 -0
  29. claude_mpm/services/monitor/daemon.py +256 -0
  30. claude_mpm/services/monitor/event_emitter.py +279 -0
  31. claude_mpm/services/monitor/handlers/__init__.py +20 -0
  32. claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
  33. claude_mpm/services/monitor/handlers/dashboard.py +298 -0
  34. claude_mpm/services/monitor/handlers/hooks.py +491 -0
  35. claude_mpm/services/monitor/management/__init__.py +18 -0
  36. claude_mpm/services/monitor/management/health.py +124 -0
  37. claude_mpm/services/monitor/management/lifecycle.py +298 -0
  38. claude_mpm/services/monitor/server.py +442 -0
  39. claude_mpm/services/socketio/client_proxy.py +20 -12
  40. claude_mpm/services/socketio/dashboard_server.py +4 -4
  41. claude_mpm/services/socketio/monitor_client.py +4 -6
  42. claude_mpm/tools/code_tree_analyzer.py +33 -17
  43. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/METADATA +1 -1
  44. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/RECORD +48 -43
  45. claude_mpm/cli/commands/socketio_monitor.py +0 -233
  46. claude_mpm/scripts/socketio_daemon.py +0 -571
  47. claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
  48. claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
  49. claude_mpm/scripts/socketio_server_manager.py +0 -349
  50. claude_mpm/services/cli/dashboard_launcher.py +0 -423
  51. claude_mpm/services/cli/socketio_manager.py +0 -595
  52. claude_mpm/services/dashboard/stable_server.py +0 -962
  53. claude_mpm/services/socketio/monitor_server.py +0 -505
  54. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/WHEEL +0 -0
  55. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/entry_points.txt +0 -0
  56. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/licenses/LICENSE +0 -0
  57. {claude_mpm-4.2.7.dist-info → claude_mpm-4.2.11.dist-info}/top_level.txt +0 -0
@@ -1,505 +0,0 @@
1
- """
2
- Lightweight MonitorServer for claude-mpm.
3
-
4
- WHY: This module provides a minimal, independent monitoring service that:
5
- - Runs as a stable background service on port 8765
6
- - Only handles event collection and relay (no UI components)
7
- - Has minimal dependencies and resource usage
8
- - Can run as always-on background service
9
- - Includes event buffering capabilities
10
- - Acts as a bridge between hooks and dashboard(s)
11
-
12
- DESIGN DECISIONS:
13
- - Minimal Socket.IO server with only essential features
14
- - Event buffering for reliable delivery to dashboard clients
15
- - Independent lifecycle from dashboard service
16
- - Configurable port with sensible defaults
17
- - Health monitoring and status endpoints
18
- """
19
-
20
- import asyncio
21
- import threading
22
- import time
23
- from collections import deque
24
- from datetime import datetime
25
- from typing import Any, Dict, List, Optional, Set
26
-
27
- try:
28
- import socketio
29
- from aiohttp import web
30
-
31
- SOCKETIO_AVAILABLE = True
32
- except ImportError:
33
- SOCKETIO_AVAILABLE = False
34
- socketio = None
35
- web = None
36
-
37
- from ...core.config import Config
38
- from ...core.constants import SystemLimits
39
- from ...core.logging_config import get_logger
40
- from ..core.interfaces.communication import SocketIOServiceInterface
41
-
42
-
43
- class MonitorServer(SocketIOServiceInterface):
44
- """Lightweight Socket.IO server for monitoring and event relay.
45
-
46
- WHY: This server acts as a stable, lightweight background service that:
47
- - Collects events from hooks and other system components
48
- - Buffers events for reliable delivery
49
- - Relays events to connected dashboard clients
50
- - Maintains minimal resource footprint
51
- - Can run independently of dashboard services
52
-
53
- This separation allows the monitor to be a stable always-on service
54
- while dashboards can come and go without affecting event collection.
55
- """
56
-
57
- def __init__(self, host: str = None, port: int = None):
58
- # Load configuration
59
- config = Config()
60
- monitor_config = config.get("monitor_server", {})
61
-
62
- self.host = host or monitor_config.get("host", "localhost")
63
- self.port = port or monitor_config.get("port", 8765)
64
- self.logger = get_logger(__name__ + ".MonitorServer")
65
-
66
- # Configuration-based settings
67
- self.event_buffer_size = monitor_config.get(
68
- "event_buffer_size", SystemLimits.MAX_EVENTS_BUFFER * 2
69
- )
70
- self.client_timeout = monitor_config.get("client_timeout", 60)
71
- self.health_monitoring_enabled = monitor_config.get(
72
- "enable_health_monitoring", True
73
- )
74
-
75
- # Server state
76
- self.running = False
77
- self.sio = None
78
- self.app = None
79
- self.runner = None
80
- self.site = None
81
- self.thread = None
82
- self.loop = None
83
-
84
- # Client management
85
- self.connected_clients: Set[str] = set()
86
- self.client_info: Dict[str, Dict[str, Any]] = {}
87
-
88
- # Event buffering - configurable buffer size for monitor server
89
- self.event_buffer = deque(maxlen=self.event_buffer_size)
90
- self.buffer_lock = threading.Lock()
91
-
92
- # Statistics
93
- self.stats = {
94
- "events_received": 0,
95
- "events_relayed": 0,
96
- "events_buffered": 0,
97
- "connections_total": 0,
98
- "start_time": None,
99
- "clients_connected": 0,
100
- }
101
-
102
- # Session tracking for compatibility
103
- self.session_id = None
104
- self.claude_status = "unknown"
105
- self.claude_pid = None
106
- self.active_sessions: Dict[str, Dict[str, Any]] = {}
107
-
108
- def start_sync(self):
109
- """Start the monitor server in a background thread."""
110
- if not SOCKETIO_AVAILABLE:
111
- self.logger.error("Socket.IO not available - monitor server cannot start")
112
- return False
113
-
114
- if self.running:
115
- self.logger.info("Monitor server already running")
116
- return True
117
-
118
- self.logger.info(
119
- f"Starting lightweight monitor server on {self.host}:{self.port}"
120
- )
121
-
122
- # Start server in background thread
123
- self.thread = threading.Thread(target=self._run_server, daemon=True)
124
- self.thread.start()
125
-
126
- # Wait for server to start
127
- max_wait = 10.0
128
- wait_interval = 0.1
129
- waited = 0.0
130
-
131
- while not self.running and waited < max_wait:
132
- time.sleep(wait_interval)
133
- waited += wait_interval
134
-
135
- if self.running:
136
- self.stats["start_time"] = datetime.now().isoformat()
137
- self.logger.info(
138
- f"Monitor server started successfully on {self.host}:{self.port}"
139
- )
140
- return True
141
- self.logger.error(f"Monitor server failed to start within {max_wait}s")
142
- return False
143
-
144
- def stop_sync(self):
145
- """Stop the monitor server."""
146
- if not self.running:
147
- return
148
-
149
- self.logger.info("Stopping monitor server...")
150
- self.running = False
151
-
152
- # Stop the server
153
- if self.loop and self.runner:
154
- try:
155
- # Schedule cleanup in the event loop
156
- asyncio.run_coroutine_threadsafe(self._stop_server(), self.loop)
157
-
158
- # Wait for thread to finish
159
- if self.thread and self.thread.is_alive():
160
- self.thread.join(timeout=5)
161
-
162
- except Exception as e:
163
- self.logger.error(f"Error stopping monitor server: {e}")
164
-
165
- self.logger.info("Monitor server stopped")
166
-
167
- def _run_server(self):
168
- """Run the server in its own event loop."""
169
- try:
170
- # Create new event loop for this thread
171
- self.loop = asyncio.new_event_loop()
172
- asyncio.set_event_loop(self.loop)
173
-
174
- # Start the server
175
- self.loop.run_until_complete(self._start_server())
176
-
177
- except Exception as e:
178
- self.logger.error(f"Error in monitor server thread: {e}")
179
- self.running = False
180
-
181
- async def _start_server(self):
182
- """Start the Socket.IO server with minimal configuration."""
183
- try:
184
- # Create Socket.IO server with minimal configuration
185
- self.sio = socketio.AsyncServer(
186
- cors_allowed_origins="*",
187
- logger=False, # Minimize logging overhead
188
- engineio_logger=False,
189
- )
190
-
191
- # Create minimal aiohttp application
192
- self.app = web.Application()
193
- self.sio.attach(self.app)
194
-
195
- # Register minimal event handlers
196
- self._register_events()
197
-
198
- # Add health check endpoint
199
- self.app.router.add_get("/health", self._health_check)
200
- self.app.router.add_get("/status", self._status_check)
201
-
202
- # Start the HTTP server
203
- self.runner = web.AppRunner(self.app)
204
- await self.runner.setup()
205
-
206
- self.site = web.TCPSite(
207
- self.runner, self.host, self.port, shutdown_timeout=1.0 # Fast shutdown
208
- )
209
-
210
- await self.site.start()
211
- self.running = True
212
-
213
- self.logger.info(f"Monitor server running on {self.host}:{self.port}")
214
-
215
- # Keep the server running
216
- while self.running:
217
- await asyncio.sleep(1)
218
-
219
- except Exception as e:
220
- self.logger.error(f"Failed to start monitor server: {e}")
221
- self.running = False
222
-
223
- async def _stop_server(self):
224
- """Stop the server components."""
225
- try:
226
- if self.site:
227
- await self.site.stop()
228
- if self.runner:
229
- await self.runner.cleanup()
230
- except Exception as e:
231
- self.logger.error(f"Error stopping server components: {e}")
232
-
233
- def _register_events(self):
234
- """Register minimal Socket.IO events for monitoring."""
235
-
236
- @self.sio.event
237
- async def connect(sid, environ):
238
- """Handle client connection."""
239
- self.connected_clients.add(sid)
240
- self.client_info[sid] = {
241
- "connected_at": datetime.now().isoformat(),
242
- "client_type": "dashboard", # Assume dashboard clients
243
- }
244
- self.stats["connections_total"] += 1
245
- self.stats["clients_connected"] = len(self.connected_clients)
246
-
247
- self.logger.info(f"Dashboard client connected: {sid}")
248
-
249
- # Send buffered events to new client
250
- await self._send_buffered_events(sid)
251
-
252
- @self.sio.event
253
- async def disconnect(sid):
254
- """Handle client disconnection."""
255
- self.connected_clients.discard(sid)
256
- self.client_info.pop(sid, None)
257
- self.stats["clients_connected"] = len(self.connected_clients)
258
-
259
- self.logger.info(f"Dashboard client disconnected: {sid}")
260
-
261
- @self.sio.event
262
- async def get_status(sid):
263
- """Handle status request from client."""
264
- status_data = {
265
- "server_type": "monitor",
266
- "running": self.running,
267
- "port": self.port,
268
- "connected_clients": len(self.connected_clients),
269
- "stats": self.stats,
270
- "active_sessions": list(self.active_sessions.values()),
271
- }
272
- await self.sio.emit("status_response", status_data, room=sid)
273
-
274
- async def _send_buffered_events(self, client_id: str):
275
- """Send buffered events to a newly connected client."""
276
- with self.buffer_lock:
277
- if self.event_buffer:
278
- self.logger.info(
279
- f"Sending {len(self.event_buffer)} buffered events to {client_id}"
280
- )
281
- for event in list(self.event_buffer):
282
- try:
283
- await self.sio.emit(
284
- event["type"], event["data"], room=client_id
285
- )
286
- except Exception as e:
287
- self.logger.error(
288
- f"Error sending buffered event to {client_id}: {e}"
289
- )
290
-
291
- async def _health_check(self, request):
292
- """Health check endpoint."""
293
- return web.json_response(
294
- {
295
- "status": "healthy" if self.running else "unhealthy",
296
- "service": "monitor-server",
297
- "port": self.port,
298
- "clients": len(self.connected_clients),
299
- }
300
- )
301
-
302
- async def _status_check(self, request):
303
- """Detailed status endpoint."""
304
- return web.json_response(
305
- {
306
- "running": self.running,
307
- "port": self.port,
308
- "host": self.host,
309
- "clients_connected": len(self.connected_clients),
310
- "stats": self.stats,
311
- "active_sessions": list(self.active_sessions.values()),
312
- "buffer_size": len(self.event_buffer),
313
- }
314
- )
315
-
316
- # SocketIOServiceInterface implementation
317
- def broadcast_event(self, event_type: str, data: Dict[str, Any]):
318
- """Broadcast an event to all connected dashboard clients."""
319
- self.stats["events_received"] += 1
320
-
321
- # Buffer the event
322
- event_data = {
323
- "type": event_type,
324
- "data": data,
325
- "timestamp": datetime.now().isoformat(),
326
- }
327
-
328
- with self.buffer_lock:
329
- self.event_buffer.append(event_data)
330
- self.stats["events_buffered"] += 1
331
-
332
- # Relay to connected clients if server is running
333
- if self.loop and self.sio and self.connected_clients:
334
- asyncio.run_coroutine_threadsafe(
335
- self._relay_event(event_type, data), self.loop
336
- )
337
-
338
- async def _relay_event(self, event_type: str, data: Dict[str, Any]):
339
- """Relay event to all connected dashboard clients."""
340
- if not self.connected_clients:
341
- return
342
-
343
- try:
344
- await self.sio.emit(event_type, data)
345
- self.stats["events_relayed"] += 1
346
- except Exception as e:
347
- self.logger.error(f"Error relaying event {event_type}: {e}")
348
-
349
- def send_to_client(
350
- self, client_id: str, event_type: str, data: Dict[str, Any]
351
- ) -> bool:
352
- """Send an event to a specific dashboard client."""
353
- if not self.loop or client_id not in self.connected_clients:
354
- return False
355
-
356
- try:
357
- asyncio.run_coroutine_threadsafe(
358
- self.sio.emit(event_type, data, room=client_id), self.loop
359
- )
360
- return True
361
- except Exception as e:
362
- self.logger.error(f"Error sending to client {client_id}: {e}")
363
- return False
364
-
365
- def get_connection_count(self) -> int:
366
- """Get number of connected dashboard clients."""
367
- return len(self.connected_clients)
368
-
369
- def is_running(self) -> bool:
370
- """Check if monitor server is running."""
371
- return self.running
372
-
373
- def get_stats(self) -> Dict[str, Any]:
374
- """Get monitor server statistics."""
375
- return {
376
- **self.stats,
377
- "clients_connected": len(self.connected_clients),
378
- "buffer_size": len(self.event_buffer),
379
- "uptime": (
380
- (
381
- datetime.now() - datetime.fromisoformat(self.stats["start_time"])
382
- ).total_seconds()
383
- if self.stats["start_time"]
384
- else 0
385
- ),
386
- }
387
-
388
- # Session tracking methods for compatibility with existing hooks
389
- def session_started(self, session_id: str, launch_method: str, working_dir: str):
390
- """Track session start."""
391
- self.session_id = session_id
392
- self.active_sessions[session_id] = {
393
- "session_id": session_id,
394
- "start_time": datetime.now().isoformat(),
395
- "agent": "pm",
396
- "status": "active",
397
- "launch_method": launch_method,
398
- "working_dir": working_dir,
399
- }
400
-
401
- self.broadcast_event(
402
- "session_started",
403
- {
404
- "session_id": session_id,
405
- "launch_method": launch_method,
406
- "working_dir": working_dir,
407
- },
408
- )
409
-
410
- def session_ended(self):
411
- """Track session end."""
412
- if self.session_id and self.session_id in self.active_sessions:
413
- session_data = self.active_sessions.pop(self.session_id)
414
- self.broadcast_event("session_ended", {"session_id": self.session_id})
415
-
416
- def claude_status_changed(
417
- self, status: str, pid: Optional[int] = None, message: str = ""
418
- ):
419
- """Track Claude status changes."""
420
- self.claude_status = status
421
- self.claude_pid = pid
422
- self.broadcast_event(
423
- "claude_status", {"status": status, "pid": pid, "message": message}
424
- )
425
-
426
- def claude_output(self, content: str, stream: str = "stdout"):
427
- """Relay Claude output."""
428
- self.broadcast_event("claude_output", {"content": content, "stream": stream})
429
-
430
- def agent_delegated(self, agent: str, task: str, status: str = "started"):
431
- """Track agent delegation."""
432
- if self.session_id and self.session_id in self.active_sessions:
433
- self.active_sessions[self.session_id]["agent"] = agent
434
- self.active_sessions[self.session_id]["status"] = status
435
-
436
- self.broadcast_event(
437
- "agent_delegated", {"agent": agent, "task": task, "status": status}
438
- )
439
-
440
- def todo_updated(self, todos: List[Dict[str, Any]]):
441
- """Relay todo updates."""
442
- self.broadcast_event("todos_updated", {"todos": todos})
443
-
444
- def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
445
- """Relay ticket creation."""
446
- self.broadcast_event(
447
- "ticket_created",
448
- {"ticket_id": ticket_id, "title": title, "priority": priority},
449
- )
450
-
451
- def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
452
- """Relay memory loaded event."""
453
- self.broadcast_event(
454
- "memory_loaded",
455
- {
456
- "agent_id": agent_id,
457
- "memory_size": memory_size,
458
- "sections_count": sections_count,
459
- },
460
- )
461
-
462
- def memory_created(self, agent_id: str, template_type: str):
463
- """Relay memory created event."""
464
- self.broadcast_event(
465
- "memory_created", {"agent_id": agent_id, "template_type": template_type}
466
- )
467
-
468
- def memory_updated(
469
- self, agent_id: str, learning_type: str, content: str, section: str
470
- ):
471
- """Relay memory update event."""
472
- self.broadcast_event(
473
- "memory_updated",
474
- {
475
- "agent_id": agent_id,
476
- "learning_type": learning_type,
477
- "content": content,
478
- "section": section,
479
- },
480
- )
481
-
482
- def memory_injected(self, agent_id: str, context_size: int):
483
- """Relay memory injection event."""
484
- self.broadcast_event(
485
- "memory_injected", {"agent_id": agent_id, "context_size": context_size}
486
- )
487
-
488
- def get_active_sessions(self) -> List[Dict[str, Any]]:
489
- """Get list of active sessions."""
490
- # Clean up old sessions (older than 1 hour)
491
- cutoff_time = datetime.now().timestamp() - 3600
492
- sessions_to_remove = []
493
-
494
- for session_id, session_data in self.active_sessions.items():
495
- try:
496
- start_time = datetime.fromisoformat(session_data["start_time"])
497
- if start_time.timestamp() < cutoff_time:
498
- sessions_to_remove.append(session_id)
499
- except:
500
- pass
501
-
502
- for session_id in sessions_to_remove:
503
- del self.active_sessions[session_id]
504
-
505
- return list(self.active_sessions.values())