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,9 +1,18 @@
1
1
  """Connection management service for Claude hook handler.
2
2
 
3
+ This service implements the SINGLE-PATH EVENT EMISSION ARCHITECTURE
4
+ to eliminate duplicate events and improve performance.
5
+
6
+ ARCHITECTURE: Hook → ConnectionManager → Direct Socket.IO → Dashboard
7
+ ↓ (fallback only)
8
+ HTTP POST → Monitor → Dashboard
9
+
10
+ CRITICAL: This service must maintain the single emission path principle.
11
+ See docs/developer/EVENT_EMISSION_ARCHITECTURE.md for full documentation.
12
+
3
13
  This service manages:
4
14
  - SocketIO connection pool initialization
5
- - EventBus initialization
6
- - Event emission through both channels
15
+ - Direct event emission with HTTP fallback
7
16
  - Connection cleanup
8
17
  """
9
18
 
@@ -61,14 +70,8 @@ except ImportError:
61
70
  )
62
71
 
63
72
 
64
- # Import EventBus for decoupled event distribution
65
- try:
66
- from claude_mpm.services.event_bus import EventBus
67
-
68
- EVENTBUS_AVAILABLE = True
69
- except ImportError:
70
- EVENTBUS_AVAILABLE = False
71
- EventBus = None
73
+ # EventBus removed - using direct Socket.IO calls with HTTP fallback
74
+ # This eliminates duplicate events and improves performance
72
75
 
73
76
 
74
77
  class ConnectionManagerService:
@@ -84,9 +87,7 @@ class ConnectionManagerService:
84
87
  self.connection_pool = None
85
88
  self._initialize_socketio_pool()
86
89
 
87
- # Initialize EventBus for in-process event distribution (optional)
88
- self.event_bus = None
89
- self._initialize_eventbus()
90
+ # EventBus removed - using direct Socket.IO with HTTP fallback only
90
91
 
91
92
  def _initialize_socketio_pool(self):
92
93
  """Initialize the SocketIO connection pool."""
@@ -102,25 +103,17 @@ class ConnectionManagerService:
102
103
  )
103
104
  self.connection_pool = None
104
105
 
105
- def _initialize_eventbus(self):
106
- """Initialize the EventBus for in-process distribution."""
107
- if EVENTBUS_AVAILABLE:
108
- try:
109
- self.event_bus = EventBus.get_instance()
110
- if DEBUG:
111
- print("✅ EventBus initialized for hook handler", file=sys.stderr)
112
- except Exception as e:
113
- if DEBUG:
114
- print(f"⚠️ Failed to initialize EventBus: {e}", file=sys.stderr)
115
- self.event_bus = None
116
-
117
106
  def emit_event(self, namespace: str, event: str, data: dict):
118
- """Emit event through both connection pool and EventBus.
107
+ """Emit event through direct Socket.IO connection with HTTP fallback.
108
+
109
+ 🚨 CRITICAL: This method implements the SINGLE-PATH EMISSION ARCHITECTURE.
110
+ DO NOT add additional emission paths (EventBus, etc.) as this creates duplicates.
111
+ See docs/developer/EVENT_EMISSION_ARCHITECTURE.md for details.
119
112
 
120
- WHY dual approach:
121
- - Connection pool: Direct Socket.IO connection for inter-process communication
122
- - EventBus: For in-process subscribers (if any)
123
- - Ensures events reach the dashboard regardless of process boundaries
113
+ High-performance single-path approach:
114
+ - Primary: Direct Socket.IO connection for ultra-low latency
115
+ - Fallback: HTTP POST for reliability when direct connection fails
116
+ - Eliminates duplicate events from multiple emission paths
124
117
  """
125
118
  # Create event data for normalization
126
119
  raw_event = {
@@ -152,49 +145,51 @@ class ConnectionManagerService:
152
145
  file=sys.stderr,
153
146
  )
154
147
 
155
- # First, try to emit through direct Socket.IO connection pool
156
- # This is the primary path for inter-process communication
148
+ # Emit through direct Socket.IO connection pool (primary path)
149
+ # This provides ultra-low latency direct async communication
157
150
  if self.connection_pool:
158
151
  try:
159
152
  # Emit to Socket.IO server directly
160
153
  self.connection_pool.emit("claude_event", claude_event_data)
161
154
  if DEBUG:
162
155
  print(f"✅ Emitted via connection pool: {event}", file=sys.stderr)
156
+ return # Success - no need for fallback
163
157
  except Exception as e:
164
158
  if DEBUG:
165
159
  print(f"⚠️ Failed to emit via connection pool: {e}", file=sys.stderr)
166
160
 
167
- # Also publish to EventBus for any in-process subscribers
168
- if self.event_bus and EVENTBUS_AVAILABLE:
169
- try:
170
- # Publish to EventBus with topic format: hook.{event}
171
- topic = f"hook.{event}"
172
- self.event_bus.publish(topic, claude_event_data)
161
+ # HTTP fallback for cross-process communication (when direct calls fail)
162
+ # This replaces EventBus for reliability without the complexity
163
+ self._try_http_fallback(claude_event_data)
173
164
 
174
- # Enhanced verification logging
175
- if DEBUG:
176
- print(f"✅ Published to EventBus: {topic}", file=sys.stderr)
177
- # Get EventBus stats to verify publication
178
- if hasattr(self.event_bus, "get_stats"):
179
- stats = self.event_bus.get_stats()
180
- print(
181
- f"📊 EventBus stats after publish: {stats}", file=sys.stderr
182
- )
183
- # Log the number of data keys being published
184
- if isinstance(claude_event_data, dict):
185
- print(
186
- f"📦 Published data keys: {list(claude_event_data.keys())}",
187
- file=sys.stderr,
188
- )
189
- except Exception as e:
165
+ def _try_http_fallback(self, claude_event_data: dict):
166
+ """HTTP fallback when direct Socket.IO connection fails."""
167
+ try:
168
+ import requests
169
+
170
+ # Send to monitor server HTTP API
171
+ response = requests.post(
172
+ "http://localhost:8765/api/events",
173
+ json=claude_event_data,
174
+ timeout=2.0,
175
+ headers={"Content-Type": "application/json"},
176
+ )
177
+
178
+ if response.status_code in [200, 204]:
190
179
  if DEBUG:
191
- print(f"⚠️ Failed to publish to EventBus: {e}", file=sys.stderr)
192
- import traceback
180
+ print(" HTTP fallback successful", file=sys.stderr)
181
+ elif DEBUG:
182
+ print(
183
+ f"⚠️ HTTP fallback failed: {response.status_code}",
184
+ file=sys.stderr,
185
+ )
193
186
 
194
- traceback.print_exc(file=sys.stderr)
187
+ except Exception as e:
188
+ if DEBUG:
189
+ print(f"⚠️ HTTP fallback error: {e}", file=sys.stderr)
195
190
 
196
- # Warn if neither method is available
197
- if not self.connection_pool and not self.event_bus and DEBUG:
191
+ # Warn if no emission method is available
192
+ if not self.connection_pool and DEBUG:
198
193
  print(f"⚠️ No event emission method available for: {event}", file=sys.stderr)
199
194
 
200
195
  def cleanup(self):
@@ -2,14 +2,14 @@
2
2
 
3
3
  This service manages:
4
4
  - HTTP POST event emission for ephemeral hook processes
5
- - EventBus initialization (optional)
6
- - Event emission through both channels
5
+ - Direct event emission without EventBus complexity
7
6
 
8
7
  DESIGN DECISION: Use stateless HTTP POST instead of persistent SocketIO
9
8
  connections because hook handlers are ephemeral processes (< 1 second lifetime).
10
9
  This eliminates disconnection issues and matches the process lifecycle.
11
10
  """
12
11
 
12
+ import asyncio
13
13
  import os
14
14
  import sys
15
15
  from datetime import datetime
@@ -26,6 +26,15 @@ except ImportError:
26
26
  REQUESTS_AVAILABLE = False
27
27
  requests = None
28
28
 
29
+ # Import high-performance event emitter
30
+ try:
31
+ from claude_mpm.services.monitor.event_emitter import get_event_emitter
32
+
33
+ EVENT_EMITTER_AVAILABLE = True
34
+ except ImportError:
35
+ EVENT_EMITTER_AVAILABLE = False
36
+ get_event_emitter = None
37
+
29
38
  # Import EventNormalizer for consistent event formatting
30
39
  try:
31
40
  from claude_mpm.services.socketio.event_normalizer import EventNormalizer
@@ -51,14 +60,8 @@ except ImportError:
51
60
  )
52
61
 
53
62
 
54
- # Import EventBus for decoupled event distribution
55
- try:
56
- from claude_mpm.services.event_bus import EventBus
57
-
58
- EVENTBUS_AVAILABLE = True
59
- except ImportError:
60
- EVENTBUS_AVAILABLE = False
61
- EventBus = None
63
+ # EventBus removed - using direct HTTP POST only
64
+ # This eliminates duplicate events and simplifies the architecture
62
65
 
63
66
 
64
67
  class ConnectionManagerService:
@@ -74,9 +77,7 @@ class ConnectionManagerService:
74
77
  self.server_port = int(os.environ.get("CLAUDE_MPM_SERVER_PORT", "8765"))
75
78
  self.http_endpoint = f"http://{self.server_host}:{self.server_port}/api/events"
76
79
 
77
- # Initialize EventBus for in-process event distribution (optional)
78
- self.event_bus = None
79
- self._initialize_eventbus()
80
+ # EventBus removed - using direct HTTP POST only
80
81
 
81
82
  # For backward compatibility with tests
82
83
  self.connection_pool = None # No longer used
@@ -87,26 +88,14 @@ class ConnectionManagerService:
87
88
  file=sys.stderr,
88
89
  )
89
90
 
90
- def _initialize_eventbus(self):
91
- """Initialize the EventBus for in-process distribution."""
92
- if EVENTBUS_AVAILABLE:
93
- try:
94
- self.event_bus = EventBus.get_instance()
95
- if DEBUG:
96
- print("✅ EventBus initialized for hook handler", file=sys.stderr)
97
- except Exception as e:
98
- if DEBUG:
99
- print(f"⚠️ Failed to initialize EventBus: {e}", file=sys.stderr)
100
- self.event_bus = None
101
-
102
91
  def emit_event(self, namespace: str, event: str, data: dict):
103
- """Emit event using HTTP POST and optionally EventBus.
92
+ """Emit event using high-performance async emitter with HTTP fallback.
104
93
 
105
- WHY HTTP POST approach:
106
- - Stateless: Perfect for ephemeral hook processes
107
- - Fire-and-forget: No connection management needed
108
- - Fast: Minimal overhead, no handshake
109
- - Reliable: Server handles buffering and retries
94
+ WHY Hybrid approach:
95
+ - Direct async calls for ultra-low latency in-process events
96
+ - HTTP POST fallback for cross-process communication
97
+ - Connection pooling for memory protection
98
+ - Automatic routing based on availability
110
99
  """
111
100
  # Create event data for normalization
112
101
  raw_event = {
@@ -138,52 +127,103 @@ class ConnectionManagerService:
138
127
  file=sys.stderr,
139
128
  )
140
129
 
141
- # Primary method: HTTP POST to server
142
- # This is fire-and-forget with a short timeout
143
- if REQUESTS_AVAILABLE:
130
+ # Try high-performance async emitter first (direct calls)
131
+ success = self._try_async_emit(namespace, event, claude_event_data)
132
+ if success:
133
+ return
134
+
135
+ # Fallback to HTTP POST for cross-process communication
136
+ self._try_http_emit(namespace, event, claude_event_data)
137
+
138
+ def _try_async_emit(self, namespace: str, event: str, data: dict) -> bool:
139
+ """Try to emit event using high-performance async emitter."""
140
+ if not EVENT_EMITTER_AVAILABLE:
141
+ return False
142
+
143
+ try:
144
+ # Run async emission in the current event loop or create one
145
+ loop = None
144
146
  try:
145
- # Send HTTP POST with short timeout (fire-and-forget pattern)
146
- response = requests.post(
147
- self.http_endpoint,
148
- json=claude_event_data,
149
- timeout=0.5, # 500ms timeout - don't wait long
150
- headers={"Content-Type": "application/json"},
151
- )
152
- if DEBUG and response.status_code == 204:
153
- print(f"✅ Emitted via HTTP POST: {event}", file=sys.stderr)
154
- elif DEBUG and response.status_code != 204:
155
- print(
156
- f"⚠️ HTTP POST returned status {response.status_code} for: {event}",
157
- file=sys.stderr,
158
- )
159
- except requests.exceptions.Timeout:
160
- # Timeout is expected for fire-and-forget pattern
161
- if DEBUG:
162
- print(f"✅ HTTP POST sent (timeout OK): {event}", file=sys.stderr)
163
- except requests.exceptions.ConnectionError:
164
- # Server might not be running - this is OK
147
+ loop = asyncio.get_running_loop()
148
+ except RuntimeError:
149
+ # No running loop, create a new one
150
+ pass
151
+
152
+ if loop:
153
+ # We're in an async context, create a task
154
+ task = loop.create_task(self._async_emit(namespace, event, data))
155
+ # Don't wait for completion to maintain low latency
165
156
  if DEBUG:
166
- print(f"⚠️ Server not available for: {event}", file=sys.stderr)
167
- except Exception as e:
168
- if DEBUG:
169
- print(f"⚠️ Failed to emit via HTTP POST: {e}", file=sys.stderr)
170
- elif DEBUG:
171
- print(
172
- "⚠️ requests module not available - cannot emit via HTTP",
173
- file=sys.stderr,
157
+ print(f" Async emit scheduled: {event}", file=sys.stderr)
158
+ return True
159
+ # No event loop, run synchronously
160
+ success = asyncio.run(self._async_emit(namespace, event, data))
161
+ if DEBUG and success:
162
+ print(f"✅ Async emit successful: {event}", file=sys.stderr)
163
+ return success
164
+
165
+ except Exception as e:
166
+ if DEBUG:
167
+ print(f"⚠️ Async emit failed: {e}", file=sys.stderr)
168
+ return False
169
+
170
+ async def _async_emit(self, namespace: str, event: str, data: dict) -> bool:
171
+ """Async helper for event emission."""
172
+ try:
173
+ emitter = await get_event_emitter()
174
+ return await emitter.emit_event(namespace, "claude_event", data)
175
+ except Exception as e:
176
+ if DEBUG:
177
+ print(f"⚠️ Async emitter error: {e}", file=sys.stderr)
178
+ return False
179
+
180
+ def _try_http_emit(self, namespace: str, event: str, data: dict):
181
+ """Try to emit event using HTTP POST fallback."""
182
+ if not REQUESTS_AVAILABLE:
183
+ if DEBUG:
184
+ print(
185
+ "⚠️ requests module not available - cannot emit via HTTP",
186
+ file=sys.stderr,
187
+ )
188
+ return
189
+
190
+ try:
191
+ # Create payload for HTTP API
192
+ payload = {
193
+ "namespace": namespace,
194
+ "event": "claude_event", # Standard event name for dashboard
195
+ "data": data,
196
+ }
197
+
198
+ # Send HTTP POST with reasonable timeout
199
+ response = requests.post(
200
+ self.http_endpoint,
201
+ json=payload,
202
+ timeout=2.0, # 2 second timeout
203
+ headers={"Content-Type": "application/json"},
174
204
  )
175
205
 
176
- # Also publish to EventBus for any in-process subscribers
177
- if self.event_bus and EVENTBUS_AVAILABLE:
178
- try:
179
- # Publish to EventBus with topic format: hook.{event}
180
- topic = f"hook.{event}"
181
- self.event_bus.publish(topic, claude_event_data)
182
- if DEBUG:
183
- print(f"✅ Published to EventBus: {topic}", file=sys.stderr)
184
- except Exception as e:
206
+ if response.status_code in [200, 204]:
185
207
  if DEBUG:
186
- print(f"⚠️ Failed to publish to EventBus: {e}", file=sys.stderr)
208
+ print(f" HTTP POST successful: {event}", file=sys.stderr)
209
+ elif DEBUG:
210
+ print(
211
+ f"⚠️ HTTP POST failed with status {response.status_code}: {event}",
212
+ file=sys.stderr,
213
+ )
214
+
215
+ except requests.exceptions.Timeout:
216
+ if DEBUG:
217
+ print(f"⚠️ HTTP POST timeout for: {event}", file=sys.stderr)
218
+ except requests.exceptions.ConnectionError:
219
+ if DEBUG:
220
+ print(
221
+ f"⚠️ HTTP POST connection failed for: {event} (server not running?)",
222
+ file=sys.stderr,
223
+ )
224
+ except Exception as e:
225
+ if DEBUG:
226
+ print(f"⚠️ HTTP POST error for {event}: {e}", file=sys.stderr)
187
227
 
188
228
  def cleanup(self):
189
229
  """Cleanup connections on service destruction."""
@@ -129,13 +129,13 @@ class AgentFormatConverter:
129
129
  if isinstance(tools_line, str):
130
130
  if tools_line.startswith("[") and tools_line.endswith("]"):
131
131
  # Already in list format
132
- tools_list = tools_line
132
+ pass
133
133
  else:
134
134
  # Convert comma-separated to list
135
135
  tools = [tool.strip() for tool in tools_line.split(",")]
136
- tools_list = str(tools).replace("'", '"')
136
+ str(tools).replace("'", '"')
137
137
  else:
138
- tools_list = '["Read", "Write", "Edit"]'
138
+ pass
139
139
 
140
140
  # Extract additional fields
141
141
  model = self.extract_yaml_field(yaml_content, "model") or "sonnet"
@@ -265,7 +265,7 @@ class AgentTemplateBuilder:
265
265
  has_core_tools = len(agent_tools.intersection(core_tools)) >= 5
266
266
 
267
267
  # Include tools field only if agent is clearly restricted (missing core tools or very few tools)
268
- include_tools_field = not has_core_tools or len(agent_tools) < 6
268
+ not has_core_tools or len(agent_tools) < 6
269
269
 
270
270
  # Build YAML frontmatter using Claude Code's compatible format
271
271
  # ONLY include fields that Claude Code recognizes
@@ -587,7 +587,7 @@ tools:
587
587
 
588
588
  # Combine enhanced description with examples
589
589
  if examples:
590
- description_parts = [enhanced_description, ""] + examples
590
+ description_parts = [enhanced_description, "", *examples]
591
591
  else:
592
592
  description_parts = [enhanced_description]
593
593
 
@@ -611,9 +611,7 @@ tools:
611
611
 
612
612
  # Remove redundant spaces around punctuation
613
613
  single_line = re.sub(r"\s+([,.!?;:])", r"\1", single_line)
614
- single_line = re.sub(r"([,.!?;:])\s+", r"\1 ", single_line)
615
-
616
- return single_line
614
+ return re.sub(r"([,.!?;:])\s+", r"\1 ", single_line)
617
615
 
618
616
  def _create_enhanced_description(
619
617
  self,