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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/agent-manager.json +1 -1
- claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
- claude_mpm/agents/templates/api_qa.json +1 -1
- claude_mpm/agents/templates/code_analyzer.json +1 -1
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
- claude_mpm/agents/templates/imagemagick.json +1 -1
- claude_mpm/agents/templates/memory_manager.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/project_organizer.json +1 -1
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +1 -1
- claude_mpm/agents/templates/research.json +3 -3
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/test-non-mpm.json +20 -0
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +3 -8
- claude_mpm/agents/templates/web_ui.json +1 -1
- claude_mpm/cli/commands/agents.py +3 -0
- claude_mpm/cli/commands/dashboard.py +3 -3
- claude_mpm/cli/commands/monitor.py +227 -64
- claude_mpm/core/config.py +25 -0
- claude_mpm/core/unified_agent_registry.py +2 -2
- claude_mpm/dashboard/static/css/code-tree.css +220 -1
- claude_mpm/dashboard/static/css/dashboard.css +286 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
- claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
- claude_mpm/dashboard/static/js/socket-client.js +5 -2
- claude_mpm/dashboard/templates/code_simple.html +79 -0
- claude_mpm/dashboard/templates/index.html +42 -41
- claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
- claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
- claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
- claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
- claude_mpm/services/agents/deployment/agent_validator.py +11 -6
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
- claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
- claude_mpm/services/cli/agent_listing_service.py +2 -2
- claude_mpm/services/dashboard/stable_server.py +389 -0
- claude_mpm/services/socketio/client_proxy.py +16 -0
- claude_mpm/services/socketio/dashboard_server.py +360 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
- claude_mpm/services/socketio/monitor_client.py +366 -0
- claude_mpm/services/socketio/monitor_server.py +505 -0
- claude_mpm/tools/code_tree_analyzer.py +95 -17
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
- {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
|