claude-mpm 4.1.7__py3-none-any.whl → 4.1.10__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/INSTRUCTIONS.md +26 -1
- claude_mpm/agents/OUTPUT_STYLE.md +73 -0
- claude_mpm/agents/agents_metadata.py +57 -0
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
- claude_mpm/agents/templates/agent-manager.json +263 -17
- claude_mpm/agents/templates/agent-manager.md +248 -10
- claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
- claude_mpm/agents/templates/code_analyzer.json +18 -8
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/cli/__init__.py +4 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +547 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +223 -25
- claude_mpm/cli/commands/configure_tui.py +65 -61
- claude_mpm/cli/commands/debug.py +1387 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/base_parser.py +29 -0
- claude_mpm/cli/parsers/configure_parser.py +23 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/config/socketio_config.py +21 -21
- claude_mpm/constants.py +3 -1
- claude_mpm/core/framework_loader.py +148 -6
- claude_mpm/core/log_manager.py +16 -13
- claude_mpm/core/logger.py +1 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
- claude_mpm/dashboard/analysis_runner.py +428 -0
- claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/activity.css +549 -0
- claude_mpm/dashboard/static/css/code-tree.css +846 -0
- claude_mpm/dashboard/static/css/dashboard.css +245 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +1139 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1357 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +11 -0
- claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
- claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
- claude_mpm/dashboard/static/js/dashboard.js +39 -0
- claude_mpm/dashboard/static/js/socket-client.js +414 -20
- claude_mpm/dashboard/templates/index.html +184 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
- claude_mpm/hooks/claude_hooks/installer.py +728 -0
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- claude_mpm/services/agents/deployment/agent_config_provider.py +127 -27
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
- claude_mpm/services/agents/memory/memory_format_service.py +1 -5
- claude_mpm/services/cli/agent_cleanup_service.py +1 -2
- claude_mpm/services/cli/agent_dependency_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +3 -4
- claude_mpm/services/cli/dashboard_launcher.py +2 -3
- claude_mpm/services/cli/startup_checker.py +0 -10
- claude_mpm/services/core/cache_manager.py +1 -2
- claude_mpm/services/core/path_resolver.py +1 -4
- claude_mpm/services/core/service_container.py +2 -2
- claude_mpm/services/diagnostics/checks/instructions_check.py +2 -5
- claude_mpm/services/event_bus/direct_relay.py +98 -20
- claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
- claude_mpm/services/infrastructure/monitoring.py +11 -11
- claude_mpm/services/project/architecture_analyzer.py +1 -1
- claude_mpm/services/project/dependency_analyzer.py +4 -4
- claude_mpm/services/project/language_analyzer.py +3 -3
- claude_mpm/services/project/metrics_collector.py +3 -6
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +170 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +95 -65
- claude_mpm/services/socketio/server/core.py +125 -17
- claude_mpm/services/socketio/server/main.py +44 -5
- claude_mpm/services/visualization/__init__.py +19 -0
- claude_mpm/services/visualization/mermaid_generator.py +938 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer.py +778 -0
- claude_mpm/tools/code_tree_builder.py +632 -0
- claude_mpm/tools/code_tree_events.py +318 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/RECORD +108 -77
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.7.dist-info → claude_mpm-4.1.10.dist-info}/top_level.txt +0 -0
|
@@ -10,6 +10,7 @@ of client states, proper event delivery, and automatic recovery mechanisms.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
+
import contextlib
|
|
13
14
|
import time
|
|
14
15
|
from collections import deque
|
|
15
16
|
from dataclasses import dataclass, field
|
|
@@ -70,11 +71,11 @@ class ClientConnection:
|
|
|
70
71
|
metrics: ConnectionMetrics = field(default_factory=ConnectionMetrics)
|
|
71
72
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
72
73
|
|
|
73
|
-
def is_healthy(self, timeout: float =
|
|
74
|
+
def is_healthy(self, timeout: float = 90.0) -> bool:
|
|
74
75
|
"""Check if connection is healthy based on activity.
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
Args:
|
|
77
|
-
timeout: Seconds before considering connection unhealthy (default
|
|
78
|
+
timeout: Seconds before considering connection unhealthy (default 90s)
|
|
78
79
|
"""
|
|
79
80
|
if self.state != ConnectionState.CONNECTED:
|
|
80
81
|
return False
|
|
@@ -91,9 +92,9 @@ class ClientConnection:
|
|
|
91
92
|
self.connected_at,
|
|
92
93
|
)
|
|
93
94
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
return (now - last_activity) <
|
|
95
|
+
# More aggressive timeout for stale detection (no grace period)
|
|
96
|
+
# This helps identify truly stale connections faster
|
|
97
|
+
return (now - last_activity) < timeout
|
|
97
98
|
|
|
98
99
|
def calculate_quality(self) -> float:
|
|
99
100
|
"""Calculate connection quality score (0-1)."""
|
|
@@ -148,7 +149,7 @@ class ConnectionManager:
|
|
|
148
149
|
- Automatic event replay on reconnection
|
|
149
150
|
"""
|
|
150
151
|
|
|
151
|
-
def __init__(self, max_buffer_size: int = None, event_ttl: int = None):
|
|
152
|
+
def __init__(self, max_buffer_size: Optional[int] = None, event_ttl: Optional[int] = None):
|
|
152
153
|
"""
|
|
153
154
|
Initialize connection manager with centralized configuration.
|
|
154
155
|
|
|
@@ -157,17 +158,19 @@ class ConnectionManager:
|
|
|
157
158
|
event_ttl: Time-to-live for buffered events in seconds (uses config if None)
|
|
158
159
|
"""
|
|
159
160
|
from ....config.socketio_config import CONNECTION_CONFIG
|
|
160
|
-
|
|
161
|
+
|
|
161
162
|
self.logger = get_logger(__name__)
|
|
162
163
|
self.connections: Dict[str, ClientConnection] = {}
|
|
163
164
|
self.client_mapping: Dict[str, str] = {} # client_id -> current sid
|
|
164
|
-
|
|
165
|
+
|
|
165
166
|
# Use centralized configuration with optional overrides
|
|
166
|
-
self.max_buffer_size = max_buffer_size or CONNECTION_CONFIG[
|
|
167
|
-
self.event_ttl = event_ttl or CONNECTION_CONFIG[
|
|
167
|
+
self.max_buffer_size = max_buffer_size or CONNECTION_CONFIG["max_events_buffer"]
|
|
168
|
+
self.event_ttl = event_ttl or CONNECTION_CONFIG["event_ttl"]
|
|
168
169
|
self.global_sequence = 0
|
|
169
|
-
self.health_check_interval = CONNECTION_CONFIG[
|
|
170
|
-
|
|
170
|
+
self.health_check_interval = CONNECTION_CONFIG[
|
|
171
|
+
"health_check_interval"
|
|
172
|
+
] # 30 seconds
|
|
173
|
+
self.stale_timeout = CONNECTION_CONFIG["stale_timeout"] # 180 seconds (was 90)
|
|
171
174
|
self.health_task = None
|
|
172
175
|
self._lock = asyncio.Lock()
|
|
173
176
|
|
|
@@ -175,7 +178,7 @@ class ConnectionManager:
|
|
|
175
178
|
self, sid: str, client_id: Optional[str] = None
|
|
176
179
|
) -> ClientConnection:
|
|
177
180
|
"""
|
|
178
|
-
Register a new connection or reconnection.
|
|
181
|
+
Register a new connection or reconnection with retry logic.
|
|
179
182
|
|
|
180
183
|
Args:
|
|
181
184
|
sid: Socket ID
|
|
@@ -184,54 +187,84 @@ class ConnectionManager:
|
|
|
184
187
|
Returns:
|
|
185
188
|
ClientConnection object
|
|
186
189
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# Check if this is a reconnection
|
|
191
|
-
if client_id and client_id in self.client_mapping:
|
|
192
|
-
old_sid = self.client_mapping[client_id]
|
|
193
|
-
if old_sid in self.connections:
|
|
194
|
-
old_conn = self.connections[old_sid]
|
|
190
|
+
max_retries = 3
|
|
191
|
+
retry_delay = 0.1 # Start with 100ms
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
193
|
+
for attempt in range(max_retries):
|
|
194
|
+
try:
|
|
195
|
+
async with self._lock:
|
|
196
|
+
now = time.time()
|
|
197
|
+
|
|
198
|
+
# Check if this is a reconnection
|
|
199
|
+
if client_id and client_id in self.client_mapping:
|
|
200
|
+
old_sid = self.client_mapping[client_id]
|
|
201
|
+
if old_sid in self.connections:
|
|
202
|
+
old_conn = self.connections[old_sid]
|
|
203
|
+
|
|
204
|
+
# Create new connection with history
|
|
205
|
+
conn = ClientConnection(
|
|
206
|
+
sid=sid,
|
|
207
|
+
client_id=client_id,
|
|
208
|
+
state=ConnectionState.CONNECTED,
|
|
209
|
+
connected_at=now,
|
|
210
|
+
event_buffer=old_conn.event_buffer,
|
|
211
|
+
event_sequence=old_conn.event_sequence,
|
|
212
|
+
last_acked_sequence=old_conn.last_acked_sequence,
|
|
213
|
+
metrics=old_conn.metrics,
|
|
214
|
+
)
|
|
207
215
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
# Update metrics
|
|
217
|
+
conn.metrics.reconnect_count += 1
|
|
218
|
+
conn.metrics.connect_count += 1
|
|
219
|
+
if old_conn.disconnected_at:
|
|
220
|
+
conn.metrics.total_downtime += (
|
|
221
|
+
now - old_conn.disconnected_at
|
|
222
|
+
)
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
# Clean up old connection
|
|
225
|
+
del self.connections[old_sid]
|
|
216
226
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
self.logger.info(
|
|
228
|
+
f"Client {client_id} reconnected (new sid: {sid}, "
|
|
229
|
+
f"buffered events: {len(conn.event_buffer)})"
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
# No old connection found, create new
|
|
233
|
+
client_id = client_id or str(uuid4())
|
|
234
|
+
conn = self._create_new_connection(sid, client_id, now)
|
|
235
|
+
else:
|
|
236
|
+
# New client
|
|
237
|
+
client_id = client_id or str(uuid4())
|
|
238
|
+
conn = self._create_new_connection(sid, client_id, now)
|
|
229
239
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
240
|
+
# Register connection with validation
|
|
241
|
+
if conn and conn.state == ConnectionState.CONNECTED:
|
|
242
|
+
self.connections[sid] = conn
|
|
243
|
+
self.client_mapping[client_id] = sid
|
|
244
|
+
return conn
|
|
245
|
+
raise ValueError(f"Invalid connection state for {sid}")
|
|
233
246
|
|
|
234
|
-
|
|
247
|
+
except Exception as e:
|
|
248
|
+
self.logger.warning(
|
|
249
|
+
f"Failed to register connection {sid} (attempt {attempt + 1}/{max_retries}): {e}"
|
|
250
|
+
)
|
|
251
|
+
if attempt < max_retries - 1:
|
|
252
|
+
await asyncio.sleep(retry_delay)
|
|
253
|
+
retry_delay *= 2 # Exponential backoff
|
|
254
|
+
else:
|
|
255
|
+
# Final attempt failed, create minimal connection
|
|
256
|
+
self.logger.error(
|
|
257
|
+
f"All attempts failed for {sid}, creating minimal connection"
|
|
258
|
+
)
|
|
259
|
+
conn = ClientConnection(
|
|
260
|
+
sid=sid,
|
|
261
|
+
client_id=client_id or str(uuid4()),
|
|
262
|
+
state=ConnectionState.CONNECTED,
|
|
263
|
+
connected_at=time.time(),
|
|
264
|
+
)
|
|
265
|
+
self.connections[sid] = conn
|
|
266
|
+
return conn
|
|
267
|
+
return None
|
|
235
268
|
|
|
236
269
|
def _create_new_connection(
|
|
237
270
|
self, sid: str, client_id: str, now: float
|
|
@@ -403,10 +436,8 @@ class ConnectionManager:
|
|
|
403
436
|
"""Stop the health monitoring task."""
|
|
404
437
|
if self.health_task:
|
|
405
438
|
self.health_task.cancel()
|
|
406
|
-
|
|
439
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
407
440
|
await self.health_task
|
|
408
|
-
except asyncio.CancelledError:
|
|
409
|
-
pass
|
|
410
441
|
self.health_task = None
|
|
411
442
|
self.logger.info("Stopped connection health monitoring")
|
|
412
443
|
|
|
@@ -460,7 +491,7 @@ class ConnectionManager:
|
|
|
460
491
|
conn.connected_at,
|
|
461
492
|
)
|
|
462
493
|
time_since_activity = now - last_activity
|
|
463
|
-
|
|
494
|
+
|
|
464
495
|
# Only mark as stale if significantly over timeout (2x)
|
|
465
496
|
if time_since_activity > (self.stale_timeout * 2):
|
|
466
497
|
conn.state = ConnectionState.STALE
|
|
@@ -476,15 +507,14 @@ class ConnectionManager:
|
|
|
476
507
|
f"Connection {conn.client_id} borderline "
|
|
477
508
|
f"(last activity: {time_since_activity:.1f}s ago)"
|
|
478
509
|
)
|
|
479
|
-
|
|
510
|
+
|
|
480
511
|
elif conn.state == ConnectionState.DISCONNECTED:
|
|
481
512
|
report["disconnected"] += 1
|
|
482
513
|
|
|
483
514
|
# Clean up old disconnected connections (be conservative)
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
):
|
|
515
|
+
if conn.disconnected_at and (now - conn.disconnected_at) > (
|
|
516
|
+
self.event_ttl * 2
|
|
517
|
+
): # Double the TTL
|
|
488
518
|
to_clean.append(sid)
|
|
489
519
|
|
|
490
520
|
# Clean up old connections
|
|
@@ -160,22 +160,38 @@ class SocketIOServerCore:
|
|
|
160
160
|
try:
|
|
161
161
|
# Import centralized configuration for consistency
|
|
162
162
|
from ....config.socketio_config import CONNECTION_CONFIG
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
# Create Socket.IO server with centralized configuration
|
|
165
165
|
# CRITICAL: These values MUST match client settings to prevent disconnections
|
|
166
166
|
self.sio = socketio.AsyncServer(
|
|
167
167
|
cors_allowed_origins="*",
|
|
168
168
|
logger=False, # Disable Socket.IO's own logging
|
|
169
169
|
engineio_logger=False,
|
|
170
|
-
ping_interval=CONNECTION_CONFIG[
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
ping_interval=CONNECTION_CONFIG[
|
|
171
|
+
"ping_interval"
|
|
172
|
+
], # 45 seconds from config
|
|
173
|
+
ping_timeout=CONNECTION_CONFIG[
|
|
174
|
+
"ping_timeout"
|
|
175
|
+
], # 20 seconds from config
|
|
176
|
+
max_http_buffer_size=CONNECTION_CONFIG[
|
|
177
|
+
"max_http_buffer_size"
|
|
178
|
+
], # 100MB from config
|
|
173
179
|
)
|
|
174
180
|
|
|
175
181
|
# Create aiohttp application
|
|
176
182
|
self.app = web.Application()
|
|
177
183
|
self.sio.attach(self.app)
|
|
178
184
|
|
|
185
|
+
# CRITICAL: Register event handlers BEFORE starting the server
|
|
186
|
+
# This ensures handlers are ready when clients connect
|
|
187
|
+
if self.main_server and hasattr(self.main_server, "_register_events_async"):
|
|
188
|
+
self.logger.info(
|
|
189
|
+
"Registering Socket.IO event handlers before server start"
|
|
190
|
+
)
|
|
191
|
+
await self.main_server._register_events_async()
|
|
192
|
+
else:
|
|
193
|
+
self.logger.warning("Main server not available for event registration")
|
|
194
|
+
|
|
179
195
|
# Setup HTTP API endpoints for receiving events from hook handlers
|
|
180
196
|
self._setup_http_api()
|
|
181
197
|
|
|
@@ -202,11 +218,14 @@ class SocketIOServerCore:
|
|
|
202
218
|
|
|
203
219
|
# Conditionally start heartbeat task based on configuration
|
|
204
220
|
from ....config.socketio_config import CONNECTION_CONFIG
|
|
205
|
-
|
|
221
|
+
|
|
222
|
+
if CONNECTION_CONFIG.get("enable_extra_heartbeat", False):
|
|
206
223
|
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
207
224
|
self.logger.info("Started system heartbeat task")
|
|
208
225
|
else:
|
|
209
|
-
self.logger.info(
|
|
226
|
+
self.logger.info(
|
|
227
|
+
"System heartbeat disabled (using Socket.IO ping/pong instead)"
|
|
228
|
+
)
|
|
210
229
|
|
|
211
230
|
# Keep the server running
|
|
212
231
|
while self.running:
|
|
@@ -254,24 +273,113 @@ class SocketIOServerCore:
|
|
|
254
273
|
# Parse JSON payload
|
|
255
274
|
event_data = await request.json()
|
|
256
275
|
|
|
257
|
-
# Log receipt
|
|
258
|
-
event_type =
|
|
259
|
-
|
|
276
|
+
# Log receipt with more detail
|
|
277
|
+
event_type = (
|
|
278
|
+
event_data.get("subtype")
|
|
279
|
+
or event_data.get("hook_event_name")
|
|
280
|
+
or "unknown"
|
|
281
|
+
)
|
|
282
|
+
self.logger.info(f"📨 Received HTTP event: {event_type}")
|
|
283
|
+
self.logger.debug(f"Event data keys: {list(event_data.keys())}")
|
|
284
|
+
self.logger.debug(f"Connected clients: {len(self.connected_clients)}")
|
|
285
|
+
|
|
286
|
+
# Transform hook event format to claude_event format if needed
|
|
287
|
+
if "hook_event_name" in event_data and "event" not in event_data:
|
|
288
|
+
# This is a raw hook event, transform it
|
|
289
|
+
from claude_mpm.services.socketio.event_normalizer import (
|
|
290
|
+
EventNormalizer,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
normalizer = EventNormalizer()
|
|
294
|
+
|
|
295
|
+
# Create the format expected by normalizer
|
|
296
|
+
raw_event = {
|
|
297
|
+
"type": "hook",
|
|
298
|
+
"subtype": event_data.get("hook_event_name", "unknown")
|
|
299
|
+
.lower()
|
|
300
|
+
.replace("submit", "")
|
|
301
|
+
.replace("use", "_use"),
|
|
302
|
+
"timestamp": event_data.get("timestamp"),
|
|
303
|
+
"data": event_data.get("hook_input_data", {}),
|
|
304
|
+
"source": "claude_hooks",
|
|
305
|
+
"session_id": event_data.get("session_id"),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Map hook event names to dashboard subtypes
|
|
309
|
+
subtype_map = {
|
|
310
|
+
"UserPromptSubmit": "user_prompt",
|
|
311
|
+
"PreToolUse": "pre_tool",
|
|
312
|
+
"PostToolUse": "post_tool",
|
|
313
|
+
"Stop": "stop",
|
|
314
|
+
"SubagentStop": "subagent_stop",
|
|
315
|
+
"AssistantResponse": "assistant_response",
|
|
316
|
+
}
|
|
317
|
+
raw_event["subtype"] = subtype_map.get(
|
|
318
|
+
event_data.get("hook_event_name"), "unknown"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
normalized = normalizer.normalize(raw_event, source="hook")
|
|
322
|
+
event_data = normalized.to_dict()
|
|
323
|
+
self.logger.debug(
|
|
324
|
+
f"Normalized event: type={event_data.get('type')}, subtype={event_data.get('subtype')}"
|
|
325
|
+
)
|
|
260
326
|
|
|
261
327
|
# Broadcast to all connected dashboard clients via SocketIO
|
|
262
328
|
if self.sio:
|
|
263
|
-
#
|
|
264
|
-
|
|
329
|
+
# CRITICAL: Use the main server's broadcaster for proper event handling
|
|
330
|
+
# The broadcaster handles retries, connection management, and buffering
|
|
331
|
+
if (
|
|
332
|
+
self.main_server
|
|
333
|
+
and hasattr(self.main_server, "broadcaster")
|
|
334
|
+
and self.main_server.broadcaster
|
|
335
|
+
):
|
|
336
|
+
# The broadcaster expects raw event data and will normalize it
|
|
337
|
+
# Since we already normalized it, we need to pass it in a way that won't double-normalize
|
|
338
|
+
# We'll emit directly through the broadcaster's sio with proper handling
|
|
339
|
+
|
|
340
|
+
# Add to event buffer and history
|
|
341
|
+
with self.buffer_lock:
|
|
342
|
+
self.event_buffer.append(event_data)
|
|
343
|
+
self.stats["events_buffered"] = len(self.event_buffer)
|
|
344
|
+
|
|
345
|
+
# Add to main server's event history
|
|
346
|
+
if hasattr(self.main_server, "event_history"):
|
|
347
|
+
self.main_server.event_history.append(event_data)
|
|
348
|
+
|
|
349
|
+
# Use the broadcaster's sio to emit (it's the same as self.sio)
|
|
350
|
+
# This ensures the event goes through the proper channels
|
|
351
|
+
await self.sio.emit("claude_event", event_data)
|
|
352
|
+
|
|
353
|
+
# Update broadcaster stats
|
|
354
|
+
if hasattr(self.main_server.broadcaster, "stats"):
|
|
355
|
+
self.main_server.broadcaster.stats["events_sent"] = (
|
|
356
|
+
self.main_server.broadcaster.stats.get("events_sent", 0)
|
|
357
|
+
+ 1
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
self.logger.info(
|
|
361
|
+
f"✅ Event broadcasted: {event_data.get('subtype', 'unknown')} to {len(self.connected_clients)} clients"
|
|
362
|
+
)
|
|
363
|
+
self.logger.debug(
|
|
364
|
+
f"Connected client IDs: {list(self.connected_clients) if self.connected_clients else 'None'}"
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
# Fallback: Direct emit if broadcaster not available (shouldn't happen)
|
|
368
|
+
self.logger.warning(
|
|
369
|
+
"Broadcaster not available, using direct emit"
|
|
370
|
+
)
|
|
371
|
+
await self.sio.emit("claude_event", event_data)
|
|
265
372
|
|
|
266
|
-
|
|
267
|
-
|
|
373
|
+
# Update stats manually if using fallback
|
|
374
|
+
self.stats["events_sent"] = self.stats.get("events_sent", 0) + 1
|
|
268
375
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
376
|
+
# Add to event buffer for late-joining clients
|
|
377
|
+
with self.buffer_lock:
|
|
378
|
+
self.event_buffer.append(event_data)
|
|
379
|
+
self.stats["events_buffered"] = len(self.event_buffer)
|
|
273
380
|
|
|
274
381
|
# Return 204 No Content for success
|
|
382
|
+
self.logger.debug(f"✅ HTTP event processed successfully: {event_type}")
|
|
275
383
|
return web.Response(status=204)
|
|
276
384
|
|
|
277
385
|
except Exception as e:
|
|
@@ -111,7 +111,8 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
111
111
|
flush=True,
|
|
112
112
|
)
|
|
113
113
|
|
|
114
|
-
# Start the core server
|
|
114
|
+
# CRITICAL: Start the core server first to create sio instance
|
|
115
|
+
# Event handlers will be registered inside _start_server before accepting connections
|
|
115
116
|
self.core.start_sync()
|
|
116
117
|
|
|
117
118
|
# Initialize connection manager for robust connection tracking
|
|
@@ -163,8 +164,9 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
163
164
|
self.connection_manager.start_health_monitoring(), self.core.loop
|
|
164
165
|
)
|
|
165
166
|
|
|
166
|
-
# Register events
|
|
167
|
-
self.
|
|
167
|
+
# Register events if not already done in async context
|
|
168
|
+
if self.core.sio and not self.event_registry:
|
|
169
|
+
self._register_events()
|
|
168
170
|
|
|
169
171
|
# Setup EventBus integration
|
|
170
172
|
# WHY: This connects the EventBus to the Socket.IO server, allowing
|
|
@@ -263,10 +265,16 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
263
265
|
except Exception as e:
|
|
264
266
|
self.logger.error(f"Error during EventBus teardown: {e}")
|
|
265
267
|
|
|
266
|
-
# Stop
|
|
268
|
+
# Stop code analysis handler
|
|
267
269
|
if self.event_registry:
|
|
268
|
-
from ..handlers import ConnectionEventHandler
|
|
270
|
+
from ..handlers import CodeAnalysisEventHandler, ConnectionEventHandler
|
|
269
271
|
|
|
272
|
+
# Stop analysis runner
|
|
273
|
+
analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
|
|
274
|
+
if analysis_handler and hasattr(analysis_handler, "cleanup"):
|
|
275
|
+
analysis_handler.cleanup()
|
|
276
|
+
|
|
277
|
+
# Stop health monitoring in connection handler
|
|
270
278
|
conn_handler = self.event_registry.get_handler(ConnectionEventHandler)
|
|
271
279
|
if conn_handler and hasattr(conn_handler, "stop_health_monitoring"):
|
|
272
280
|
conn_handler.stop_health_monitoring()
|
|
@@ -281,6 +289,12 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
281
289
|
handlers in a modular way. Each handler focuses on a specific domain,
|
|
282
290
|
reducing complexity and improving maintainability.
|
|
283
291
|
"""
|
|
292
|
+
if not self.core.sio:
|
|
293
|
+
self.logger.error(
|
|
294
|
+
"Cannot register events - Socket.IO server not initialized"
|
|
295
|
+
)
|
|
296
|
+
return
|
|
297
|
+
|
|
284
298
|
# Initialize the event handler registry
|
|
285
299
|
self.event_registry = EventHandlerRegistry(self)
|
|
286
300
|
self.event_registry.initialize()
|
|
@@ -294,6 +308,31 @@ class SocketIOServer(SocketIOServiceInterface):
|
|
|
294
308
|
|
|
295
309
|
self.logger.info("All Socket.IO events registered via handler system")
|
|
296
310
|
|
|
311
|
+
async def _register_events_async(self):
|
|
312
|
+
"""Async version of event registration for calling from async context.
|
|
313
|
+
|
|
314
|
+
WHY: This allows us to register events from within the async _start_server
|
|
315
|
+
method before the server starts accepting connections.
|
|
316
|
+
"""
|
|
317
|
+
if not self.core.sio:
|
|
318
|
+
self.logger.error(
|
|
319
|
+
"Cannot register events - Socket.IO server not initialized"
|
|
320
|
+
)
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
# Initialize the event handler registry
|
|
324
|
+
self.event_registry = EventHandlerRegistry(self)
|
|
325
|
+
self.event_registry.initialize()
|
|
326
|
+
|
|
327
|
+
# Register all events from all handlers
|
|
328
|
+
self.event_registry.register_all_events()
|
|
329
|
+
|
|
330
|
+
# Keep handler instances for HTTP endpoint compatibility
|
|
331
|
+
self.file_handler = self.event_registry.get_handler(FileEventHandler)
|
|
332
|
+
self.git_handler = self.event_registry.get_handler(GitEventHandler)
|
|
333
|
+
|
|
334
|
+
self.logger.info("All Socket.IO events registered via handler system (async)")
|
|
335
|
+
|
|
297
336
|
# Delegate broadcasting methods to the broadcaster
|
|
298
337
|
def broadcast_event(self, event_type: str, data: Dict[str, Any]):
|
|
299
338
|
"""Broadcast an event to all connected clients."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visualization Services for Claude MPM
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
This module provides visualization services for code analysis,
|
|
6
|
+
including Mermaid diagram generation for various code structures.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .mermaid_generator import (
|
|
10
|
+
DiagramConfig,
|
|
11
|
+
DiagramType,
|
|
12
|
+
MermaidGeneratorService,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DiagramConfig",
|
|
17
|
+
"DiagramType",
|
|
18
|
+
"MermaidGeneratorService",
|
|
19
|
+
]
|