claude-mpm 4.2.1__py3-none-any.whl → 4.2.3__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/agents/templates/agent-manager.json +1 -1
  3. claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
  4. claude_mpm/agents/templates/api_qa.json +1 -1
  5. claude_mpm/agents/templates/code_analyzer.json +1 -1
  6. claude_mpm/agents/templates/data_engineer.json +1 -1
  7. claude_mpm/agents/templates/documentation.json +1 -1
  8. claude_mpm/agents/templates/engineer.json +2 -2
  9. claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
  10. claude_mpm/agents/templates/imagemagick.json +1 -1
  11. claude_mpm/agents/templates/memory_manager.json +1 -1
  12. claude_mpm/agents/templates/ops.json +1 -1
  13. claude_mpm/agents/templates/project_organizer.json +1 -1
  14. claude_mpm/agents/templates/qa.json +2 -2
  15. claude_mpm/agents/templates/refactoring_engineer.json +1 -1
  16. claude_mpm/agents/templates/research.json +3 -3
  17. claude_mpm/agents/templates/security.json +1 -1
  18. claude_mpm/agents/templates/test-non-mpm.json +20 -0
  19. claude_mpm/agents/templates/ticketing.json +1 -1
  20. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  21. claude_mpm/agents/templates/version_control.json +1 -1
  22. claude_mpm/agents/templates/web_qa.json +3 -8
  23. claude_mpm/agents/templates/web_ui.json +1 -1
  24. claude_mpm/cli/commands/agents.py +3 -0
  25. claude_mpm/cli/commands/dashboard.py +3 -3
  26. claude_mpm/cli/commands/monitor.py +227 -64
  27. claude_mpm/core/config.py +25 -0
  28. claude_mpm/core/unified_agent_registry.py +2 -2
  29. claude_mpm/dashboard/static/css/code-tree.css +220 -1
  30. claude_mpm/dashboard/static/css/dashboard.css +286 -0
  31. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  32. claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
  33. claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
  34. claude_mpm/dashboard/static/js/socket-client.js +5 -2
  35. claude_mpm/dashboard/templates/code_simple.html +79 -0
  36. claude_mpm/dashboard/templates/index.html +42 -41
  37. claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
  38. claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
  39. claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
  40. claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
  41. claude_mpm/services/agents/deployment/agent_validator.py +11 -6
  42. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
  43. claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
  44. claude_mpm/services/cli/agent_listing_service.py +2 -2
  45. claude_mpm/services/dashboard/stable_server.py +389 -0
  46. claude_mpm/services/socketio/client_proxy.py +16 -0
  47. claude_mpm/services/socketio/dashboard_server.py +360 -0
  48. claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
  49. claude_mpm/services/socketio/monitor_client.py +366 -0
  50. claude_mpm/services/socketio/monitor_server.py +505 -0
  51. claude_mpm/tools/code_tree_analyzer.py +95 -17
  52. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
  53. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
  54. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
  55. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
  56. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
  57. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,366 @@
1
+ """
2
+ SocketIO Monitor Client for claude-mpm dashboard.
3
+
4
+ WHY: This module provides a SocketIO client that connects the dashboard to the
5
+ independent monitor server, allowing the dashboard to receive events even when
6
+ running as a separate service.
7
+
8
+ DESIGN DECISIONS:
9
+ - Asynchronous client using python-socketio AsyncClient
10
+ - Automatic reconnection with exponential backoff
11
+ - Event relay from monitor to dashboard UI
12
+ - Graceful degradation when monitor is not available
13
+ - Health monitoring and connection status reporting
14
+ """
15
+
16
+ import asyncio
17
+ import threading
18
+ import time
19
+ from datetime import datetime
20
+ from typing import Any, Callable, Dict
21
+
22
+ try:
23
+ import socketio
24
+
25
+ SOCKETIO_AVAILABLE = True
26
+ except ImportError:
27
+ SOCKETIO_AVAILABLE = False
28
+ socketio = None
29
+
30
+ from ...core.logging_config import get_logger
31
+
32
+
33
+ class MonitorClient:
34
+ """SocketIO client for connecting dashboard to monitor server.
35
+
36
+ WHY: This client allows the dashboard service to receive events from
37
+ the monitor server, enabling decoupled architecture where:
38
+ - Monitor server (port 8766) collects events from hooks
39
+ - Dashboard service (port 8765) provides UI and connects to monitor
40
+ - Dashboard can restart without affecting event collection
41
+ - Multiple dashboards can connect to the same monitor
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ monitor_host: str = "localhost",
47
+ monitor_port: int = 8766,
48
+ auto_reconnect: bool = True,
49
+ ):
50
+ self.monitor_host = monitor_host
51
+ self.monitor_port = monitor_port
52
+ self.monitor_url = f"http://{monitor_host}:{monitor_port}"
53
+ self.auto_reconnect = auto_reconnect
54
+
55
+ self.logger = get_logger(__name__ + ".MonitorClient")
56
+
57
+ # Client state
58
+ self.client = None
59
+ self.connected = False
60
+ self.connecting = False
61
+ self.should_stop = False
62
+ self.connection_thread = None
63
+ self.reconnect_task = None
64
+
65
+ # Event handlers - functions to call when events are received
66
+ self.event_handlers: Dict[str, Callable] = {}
67
+
68
+ # Connection statistics
69
+ self.stats = {
70
+ "connection_attempts": 0,
71
+ "successful_connections": 0,
72
+ "events_received": 0,
73
+ "last_connected": None,
74
+ "last_disconnected": None,
75
+ "auto_reconnect_enabled": auto_reconnect,
76
+ }
77
+
78
+ # Reconnection settings
79
+ self.reconnect_delay = 1.0 # Start with 1 second
80
+ self.max_reconnect_delay = 30.0 # Max 30 seconds between attempts
81
+ self.reconnect_backoff = 1.5 # Exponential backoff multiplier
82
+
83
+ def start(self) -> bool:
84
+ """Start the monitor client and connect to monitor server."""
85
+ if not SOCKETIO_AVAILABLE:
86
+ self.logger.error("SocketIO not available - monitor client cannot start")
87
+ return False
88
+
89
+ if self.connected or self.connecting:
90
+ self.logger.info("Monitor client already connected or connecting")
91
+ return True
92
+
93
+ self.logger.info(f"Starting monitor client connection to {self.monitor_url}")
94
+ self.should_stop = False
95
+
96
+ # Start connection in background thread
97
+ self.connection_thread = threading.Thread(
98
+ target=self._run_client, daemon=True, name="MonitorClient"
99
+ )
100
+ self.connection_thread.start()
101
+
102
+ # Wait a moment for initial connection attempt
103
+ time.sleep(1)
104
+ return self.connecting or self.connected
105
+
106
+ def stop(self):
107
+ """Stop the monitor client and disconnect."""
108
+ self.logger.info("Stopping monitor client")
109
+ self.should_stop = True
110
+
111
+ if self.reconnect_task:
112
+ try:
113
+ self.reconnect_task.cancel()
114
+ except:
115
+ pass
116
+
117
+ if self.client and self.connected:
118
+ try:
119
+ # Use the event loop from the client thread
120
+ if hasattr(self.client, "disconnect"):
121
+ asyncio.run(self.client.disconnect())
122
+ except:
123
+ pass
124
+
125
+ self.connected = False
126
+ self.connecting = False
127
+
128
+ # Wait for thread to finish
129
+ if self.connection_thread and self.connection_thread.is_alive():
130
+ self.connection_thread.join(timeout=5)
131
+
132
+ def add_event_handler(self, event_name: str, handler: Callable):
133
+ """Add an event handler for a specific event type."""
134
+ self.event_handlers[event_name] = handler
135
+ self.logger.debug(f"Added event handler for '{event_name}'")
136
+
137
+ def remove_event_handler(self, event_name: str):
138
+ """Remove an event handler."""
139
+ if event_name in self.event_handlers:
140
+ del self.event_handlers[event_name]
141
+ self.logger.debug(f"Removed event handler for '{event_name}'")
142
+
143
+ def is_connected(self) -> bool:
144
+ """Check if client is connected to monitor."""
145
+ return self.connected
146
+
147
+ def get_stats(self) -> Dict[str, Any]:
148
+ """Get client connection statistics."""
149
+ return {
150
+ **self.stats,
151
+ "connected": self.connected,
152
+ "monitor_url": self.monitor_url,
153
+ "uptime": (
154
+ (
155
+ datetime.now()
156
+ - datetime.fromisoformat(self.stats["last_connected"])
157
+ ).total_seconds()
158
+ if self.stats["last_connected"] and self.connected
159
+ else 0
160
+ ),
161
+ }
162
+
163
+ def _run_client(self):
164
+ """Run the client in its own event loop."""
165
+ try:
166
+ # Create new event loop for this thread
167
+ loop = asyncio.new_event_loop()
168
+ asyncio.set_event_loop(loop)
169
+
170
+ # Run the async client
171
+ loop.run_until_complete(self._run_client_async())
172
+
173
+ except Exception as e:
174
+ self.logger.error(f"Error in monitor client thread: {e}")
175
+ finally:
176
+ self.connected = False
177
+ self.connecting = False
178
+
179
+ async def _run_client_async(self):
180
+ """Run the async client with reconnection logic."""
181
+ while not self.should_stop:
182
+ try:
183
+ await self._connect_to_monitor()
184
+
185
+ if self.connected:
186
+ # Wait for disconnection or stop signal
187
+ while self.connected and not self.should_stop:
188
+ await asyncio.sleep(1)
189
+
190
+ # Handle reconnection
191
+ if not self.should_stop and self.auto_reconnect:
192
+ await self._handle_reconnection()
193
+ else:
194
+ break
195
+
196
+ except Exception as e:
197
+ self.logger.error(f"Error in monitor client loop: {e}")
198
+ if not self.auto_reconnect:
199
+ break
200
+ await asyncio.sleep(self.reconnect_delay)
201
+
202
+ async def _connect_to_monitor(self):
203
+ """Establish connection to monitor server."""
204
+ try:
205
+ self.connecting = True
206
+ self.stats["connection_attempts"] += 1
207
+
208
+ self.logger.info(f"Connecting to monitor server at {self.monitor_url}")
209
+
210
+ # Create new SocketIO client
211
+ self.client = socketio.AsyncClient(
212
+ logger=False,
213
+ engineio_logger=False,
214
+ reconnection=False, # We handle reconnection ourselves
215
+ )
216
+
217
+ # Register event handlers
218
+ self._register_client_events()
219
+
220
+ # Connect to monitor server
221
+ await self.client.connect(self.monitor_url)
222
+
223
+ # Connection successful
224
+ self.connected = True
225
+ self.connecting = False
226
+ self.stats["successful_connections"] += 1
227
+ self.stats["last_connected"] = datetime.now().isoformat()
228
+ self.reconnect_delay = 1.0 # Reset reconnect delay on successful connection
229
+
230
+ self.logger.info(f"Connected to monitor server at {self.monitor_url}")
231
+
232
+ # Request status from monitor
233
+ await self.client.emit("get_status")
234
+
235
+ except Exception as e:
236
+ self.logger.error(f"Failed to connect to monitor server: {e}")
237
+ self.connected = False
238
+ self.connecting = False
239
+
240
+ if self.client:
241
+ try:
242
+ await self.client.disconnect()
243
+ except:
244
+ pass
245
+ self.client = None
246
+
247
+ def _register_client_events(self):
248
+ """Register client-side event handlers."""
249
+
250
+ @self.client.event
251
+ async def connect():
252
+ """Handle successful connection."""
253
+ self.logger.info("Successfully connected to monitor server")
254
+
255
+ @self.client.event
256
+ async def disconnect():
257
+ """Handle disconnection."""
258
+ self.logger.info("Disconnected from monitor server")
259
+ self.connected = False
260
+ self.stats["last_disconnected"] = datetime.now().isoformat()
261
+
262
+ @self.client.event
263
+ async def connect_error(data):
264
+ """Handle connection error."""
265
+ self.logger.error(f"Connection error: {data}")
266
+ self.connected = False
267
+
268
+ @self.client.event
269
+ async def status_response(data):
270
+ """Handle status response from monitor."""
271
+ self.logger.debug(f"Monitor server status: {data}")
272
+
273
+ # Register handlers for all monitor events that we want to relay
274
+ monitor_events = [
275
+ "session_started",
276
+ "session_ended",
277
+ "claude_status",
278
+ "claude_output",
279
+ "agent_delegated",
280
+ "todos_updated",
281
+ "ticket_created",
282
+ "memory_loaded",
283
+ "memory_created",
284
+ "memory_updated",
285
+ "memory_injected",
286
+ "file_changed",
287
+ "git_status_changed",
288
+ "project_analyzed",
289
+ "connection_status",
290
+ "heartbeat",
291
+ ]
292
+
293
+ for event_name in monitor_events:
294
+ # Create a closure to capture the event name
295
+ def make_handler(event_name):
296
+ async def handler(data):
297
+ await self._handle_monitor_event(event_name, data)
298
+
299
+ return handler
300
+
301
+ self.client.on(event_name, make_handler(event_name))
302
+
303
+ async def _handle_monitor_event(self, event_name: str, data: Any):
304
+ """Handle event received from monitor server."""
305
+ self.stats["events_received"] += 1
306
+ self.logger.debug(f"Received event from monitor: {event_name}")
307
+
308
+ # Call registered event handler if available
309
+ if event_name in self.event_handlers:
310
+ try:
311
+ handler = self.event_handlers[event_name]
312
+ if asyncio.iscoroutinefunction(handler):
313
+ await handler(data)
314
+ else:
315
+ handler(data)
316
+ except Exception as e:
317
+ self.logger.error(f"Error in event handler for {event_name}: {e}")
318
+
319
+ async def _handle_reconnection(self):
320
+ """Handle reconnection with exponential backoff."""
321
+ if self.should_stop:
322
+ return
323
+
324
+ self.logger.info(
325
+ f"Attempting to reconnect in {self.reconnect_delay:.1f} seconds"
326
+ )
327
+
328
+ try:
329
+ await asyncio.sleep(self.reconnect_delay)
330
+
331
+ # Increase reconnect delay for next attempt (exponential backoff)
332
+ self.reconnect_delay = min(
333
+ self.reconnect_delay * self.reconnect_backoff, self.max_reconnect_delay
334
+ )
335
+
336
+ except asyncio.CancelledError:
337
+ # Sleep was cancelled, probably due to stop()
338
+ pass
339
+
340
+ def send_to_monitor(self, event_name: str, data: Any = None) -> bool:
341
+ """Send an event to the monitor server."""
342
+ if not self.connected or not self.client:
343
+ self.logger.warning(
344
+ f"Cannot send event '{event_name}' - not connected to monitor"
345
+ )
346
+ return False
347
+
348
+ try:
349
+ # Schedule the emission in the client's event loop
350
+ if hasattr(self.client, "emit"):
351
+ # Create a simple coroutine to emit the event
352
+ async def emit_event():
353
+ await self.client.emit(event_name, data)
354
+
355
+ # Run it in the client's event loop if available
356
+ if hasattr(self.client, "eio") and hasattr(self.client.eio, "loop"):
357
+ loop = self.client.eio.loop
358
+ if loop and not loop.is_closed():
359
+ asyncio.run_coroutine_threadsafe(emit_event(), loop)
360
+ return True
361
+
362
+ return False
363
+
364
+ except Exception as e:
365
+ self.logger.error(f"Error sending event to monitor: {e}")
366
+ return False