claude-mpm 4.0.32__py3-none-any.whl → 4.0.34__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 (67) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/documentation.json +51 -34
  3. claude_mpm/agents/templates/research.json +0 -11
  4. claude_mpm/cli/__init__.py +63 -26
  5. claude_mpm/cli/commands/agent_manager.py +10 -8
  6. claude_mpm/core/framework_loader.py +173 -84
  7. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  8. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  9. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  10. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/session-manager.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/agent-hierarchy.js +774 -0
  16. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  17. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  18. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  19. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  20. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  21. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  22. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  23. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  24. claude_mpm/dashboard/templates/index.html +1 -0
  25. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  26. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  28. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  29. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  30. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  32. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  33. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  34. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  35. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  36. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  37. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  38. claude_mpm/services/agents/memory/content_manager.py +98 -105
  39. claude_mpm/services/event_bus/__init__.py +18 -0
  40. claude_mpm/services/event_bus/event_bus.py +334 -0
  41. claude_mpm/services/event_bus/relay.py +301 -0
  42. claude_mpm/services/events/__init__.py +44 -0
  43. claude_mpm/services/events/consumers/__init__.py +18 -0
  44. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  45. claude_mpm/services/events/consumers/logging.py +183 -0
  46. claude_mpm/services/events/consumers/metrics.py +242 -0
  47. claude_mpm/services/events/consumers/socketio.py +376 -0
  48. claude_mpm/services/events/core.py +470 -0
  49. claude_mpm/services/events/interfaces.py +230 -0
  50. claude_mpm/services/events/producers/__init__.py +14 -0
  51. claude_mpm/services/events/producers/hook.py +269 -0
  52. claude_mpm/services/events/producers/system.py +327 -0
  53. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  54. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  55. claude_mpm/services/monitor_build_service.py +345 -0
  56. claude_mpm/services/socketio/event_normalizer.py +667 -0
  57. claude_mpm/services/socketio/handlers/connection.py +78 -20
  58. claude_mpm/services/socketio/handlers/hook.py +14 -5
  59. claude_mpm/services/socketio/migration_utils.py +329 -0
  60. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  61. claude_mpm/services/socketio/server/core.py +4 -3
  62. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  63. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +67 -46
  64. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  65. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  66. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  67. {claude_mpm-4.0.32.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,334 @@
1
+ """Event Bus implementation using pyee.
2
+
3
+ WHY pyee over alternatives:
4
+ - AsyncIOEventEmitter supports both sync and async handlers
5
+ - Battle-tested library with minimal dependencies
6
+ - Simple EventEmitter pattern familiar to developers
7
+ - Thread-safe for multi-threaded environments
8
+ - Efficient event dispatch with minimal overhead
9
+ """
10
+
11
+ import asyncio
12
+ import logging
13
+ import threading
14
+ from datetime import datetime
15
+ from typing import Any, Callable, Dict, List, Optional, Set
16
+ from pyee import AsyncIOEventEmitter
17
+
18
+ # Configure logger
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class EventBus:
23
+ """Singleton Event Bus for decoupled event handling.
24
+
25
+ WHY singleton pattern:
26
+ - Ensures single point of event coordination
27
+ - Prevents duplicate event processing
28
+ - Simplifies configuration and management
29
+ - Thread-safe initialization with proper locking
30
+ """
31
+
32
+ _instance: Optional["EventBus"] = None
33
+ _lock = threading.Lock()
34
+
35
+ def __new__(cls) -> "EventBus":
36
+ """Ensure singleton instance creation."""
37
+ if cls._instance is None:
38
+ with cls._lock:
39
+ if cls._instance is None:
40
+ cls._instance = super().__new__(cls)
41
+ return cls._instance
42
+
43
+ def __init__(self):
44
+ """Initialize the event bus once."""
45
+ # Only initialize once
46
+ if hasattr(self, "_initialized"):
47
+ return
48
+
49
+ self._initialized = True
50
+ self._emitter = AsyncIOEventEmitter()
51
+ self._enabled = True
52
+ self._event_filters: Set[str] = set()
53
+ self._stats = {
54
+ "events_published": 0,
55
+ "events_filtered": 0,
56
+ "events_failed": 0,
57
+ "last_event_time": None
58
+ }
59
+ self._debug = False
60
+
61
+ # Event history for debugging (limited size)
62
+ self._event_history: List[Dict[str, Any]] = []
63
+ self._max_history_size = 100
64
+
65
+ logger.info("EventBus initialized")
66
+
67
+ @classmethod
68
+ def get_instance(cls) -> "EventBus":
69
+ """Get the singleton EventBus instance.
70
+
71
+ Returns:
72
+ EventBus: The singleton instance
73
+ """
74
+ return cls()
75
+
76
+ def enable(self) -> None:
77
+ """Enable event bus processing."""
78
+ self._enabled = True
79
+ logger.info("EventBus enabled")
80
+
81
+ def disable(self) -> None:
82
+ """Disable event bus processing (for testing or maintenance)."""
83
+ self._enabled = False
84
+ logger.info("EventBus disabled")
85
+
86
+ def set_debug(self, debug: bool) -> None:
87
+ """Enable or disable debug logging.
88
+
89
+ Args:
90
+ debug: Whether to enable debug logging
91
+ """
92
+ self._debug = debug
93
+ if debug:
94
+ logger.setLevel(logging.DEBUG)
95
+ else:
96
+ logger.setLevel(logging.INFO)
97
+
98
+ def add_filter(self, pattern: str) -> None:
99
+ """Add an event filter pattern.
100
+
101
+ Only events matching filter patterns will be processed.
102
+ Use wildcards: 'hook.*' matches all hook events.
103
+
104
+ Args:
105
+ pattern: Event name pattern to allow
106
+ """
107
+ self._event_filters.add(pattern)
108
+ logger.debug(f"Added event filter: {pattern}")
109
+
110
+ def remove_filter(self, pattern: str) -> None:
111
+ """Remove an event filter pattern.
112
+
113
+ Args:
114
+ pattern: Event name pattern to remove
115
+ """
116
+ self._event_filters.discard(pattern)
117
+ logger.debug(f"Removed event filter: {pattern}")
118
+
119
+ def clear_filters(self) -> None:
120
+ """Clear all event filters (allow all events)."""
121
+ self._event_filters.clear()
122
+ logger.debug("Cleared all event filters")
123
+
124
+ def _should_process_event(self, event_type: str) -> bool:
125
+ """Check if an event should be processed based on filters.
126
+
127
+ Args:
128
+ event_type: The event type to check
129
+
130
+ Returns:
131
+ bool: True if event should be processed
132
+ """
133
+ # If no filters, process all events
134
+ if not self._event_filters:
135
+ return True
136
+
137
+ # Check if event matches any filter
138
+ for filter_pattern in self._event_filters:
139
+ if filter_pattern.endswith("*"):
140
+ # Wildcard pattern
141
+ prefix = filter_pattern[:-1]
142
+ if event_type.startswith(prefix):
143
+ return True
144
+ elif event_type == filter_pattern:
145
+ # Exact match
146
+ return True
147
+
148
+ return False
149
+
150
+ def publish(self, event_type: str, data: Any) -> bool:
151
+ """Publish an event synchronously (for use from sync contexts like hooks).
152
+
153
+ This method is thread-safe and can be called from any thread.
154
+ Events are dispatched asynchronously to handlers.
155
+
156
+ Args:
157
+ event_type: The event type (e.g., 'hook.pre_tool')
158
+ data: The event data
159
+
160
+ Returns:
161
+ bool: True if event was published, False if filtered or disabled
162
+ """
163
+ if not self._enabled:
164
+ if self._debug:
165
+ logger.debug(f"EventBus disabled, dropping event: {event_type}")
166
+ return False
167
+
168
+ # Check filters
169
+ if not self._should_process_event(event_type):
170
+ self._stats["events_filtered"] += 1
171
+ if self._debug:
172
+ logger.debug(f"Event filtered out: {event_type}")
173
+ return False
174
+
175
+ try:
176
+ # Record event in history
177
+ self._record_event(event_type, data)
178
+
179
+ # Emit event (pyee handles thread safety)
180
+ self._emitter.emit(event_type, data)
181
+
182
+ # Update stats
183
+ self._stats["events_published"] += 1
184
+ self._stats["last_event_time"] = datetime.now().isoformat()
185
+
186
+ if self._debug:
187
+ logger.debug(f"Published event: {event_type}")
188
+
189
+ return True
190
+
191
+ except Exception as e:
192
+ self._stats["events_failed"] += 1
193
+ logger.error(f"Failed to publish event {event_type}: {e}")
194
+ return False
195
+
196
+ async def publish_async(self, event_type: str, data: Any) -> bool:
197
+ """Publish an event from an async context.
198
+
199
+ Args:
200
+ event_type: The event type
201
+ data: The event data
202
+
203
+ Returns:
204
+ bool: True if event was published
205
+ """
206
+ # Just delegate to sync publish (pyee handles both)
207
+ return self.publish(event_type, data)
208
+
209
+ def on(self, event_type: str, handler: Callable) -> None:
210
+ """Register an event handler.
211
+
212
+ The handler can be sync or async. For async handlers,
213
+ they will be scheduled on the event loop.
214
+
215
+ Args:
216
+ event_type: The event type to listen for (supports wildcards)
217
+ handler: The handler function
218
+ """
219
+ if event_type.endswith("*"):
220
+ # Register for wildcard pattern
221
+ prefix = event_type[:-1]
222
+
223
+ # Create a wrapper that checks event names
224
+ def wildcard_wrapper(actual_event_type: str):
225
+ def wrapper(data):
226
+ if actual_event_type.startswith(prefix):
227
+ if asyncio.iscoroutinefunction(handler):
228
+ return handler(actual_event_type, data)
229
+ else:
230
+ handler(actual_event_type, data)
231
+ return wrapper
232
+
233
+ # Register for all possible events (we'll filter in the wrapper)
234
+ # For now, register common prefixes
235
+ for common_event in ["hook", "socketio", "system", "agent"]:
236
+ if common_event.startswith(prefix) or prefix.startswith(common_event):
237
+ self._emitter.on(f"{common_event}.*", wildcard_wrapper(f"{common_event}.*"))
238
+ else:
239
+ # Regular event registration
240
+ self._emitter.on(event_type, handler)
241
+
242
+ logger.debug(f"Registered handler for: {event_type}")
243
+
244
+ def once(self, event_type: str, handler: Callable) -> None:
245
+ """Register a one-time event handler.
246
+
247
+ Args:
248
+ event_type: The event type to listen for
249
+ handler: The handler function
250
+ """
251
+ self._emitter.once(event_type, handler)
252
+ logger.debug(f"Registered one-time handler for: {event_type}")
253
+
254
+ def remove_listener(self, event_type: str, handler: Callable) -> None:
255
+ """Remove an event handler.
256
+
257
+ Args:
258
+ event_type: The event type
259
+ handler: The handler to remove
260
+ """
261
+ self._emitter.remove_listener(event_type, handler)
262
+ logger.debug(f"Removed handler for: {event_type}")
263
+
264
+ def remove_all_listeners(self, event_type: Optional[str] = None) -> None:
265
+ """Remove all listeners for an event type, or all listeners.
266
+
267
+ Args:
268
+ event_type: Optional event type. If None, removes all listeners.
269
+ """
270
+ if event_type:
271
+ self._emitter.remove_all_listeners(event_type)
272
+ logger.debug(f"Removed all handlers for: {event_type}")
273
+ else:
274
+ self._emitter.remove_all_listeners()
275
+ logger.debug("Removed all event handlers")
276
+
277
+ def _record_event(self, event_type: str, data: Any) -> None:
278
+ """Record event in history for debugging.
279
+
280
+ Args:
281
+ event_type: The event type
282
+ data: The event data
283
+ """
284
+ event_record = {
285
+ "timestamp": datetime.now().isoformat(),
286
+ "type": event_type,
287
+ "data": data
288
+ }
289
+
290
+ self._event_history.append(event_record)
291
+
292
+ # Trim history if too large
293
+ if len(self._event_history) > self._max_history_size:
294
+ self._event_history = self._event_history[-self._max_history_size:]
295
+
296
+ def get_stats(self) -> Dict[str, Any]:
297
+ """Get event bus statistics.
298
+
299
+ Returns:
300
+ dict: Statistics about event processing
301
+ """
302
+ return {
303
+ **self._stats,
304
+ "enabled": self._enabled,
305
+ "filters_active": len(self._event_filters) > 0,
306
+ "filter_count": len(self._event_filters),
307
+ "history_size": len(self._event_history)
308
+ }
309
+
310
+ def get_recent_events(self, limit: int = 10) -> List[Dict[str, Any]]:
311
+ """Get recent events from history.
312
+
313
+ Args:
314
+ limit: Maximum number of events to return
315
+
316
+ Returns:
317
+ list: Recent events
318
+ """
319
+ return self._event_history[-limit:]
320
+
321
+ def clear_history(self) -> None:
322
+ """Clear the event history."""
323
+ self._event_history.clear()
324
+ logger.debug("Cleared event history")
325
+
326
+ def reset_stats(self) -> None:
327
+ """Reset event statistics."""
328
+ self._stats = {
329
+ "events_published": 0,
330
+ "events_filtered": 0,
331
+ "events_failed": 0,
332
+ "last_event_time": None
333
+ }
334
+ logger.debug("Reset event statistics")
@@ -0,0 +1,301 @@
1
+ """Socket.IO Relay - Consumes events from EventBus and relays to Socket.IO.
2
+
3
+ WHY separate relay component:
4
+ - Single point of Socket.IO connection management
5
+ - Isolates Socket.IO failures from event producers
6
+ - Enables graceful degradation when Socket.IO unavailable
7
+ - Simplifies testing by mocking just the relay
8
+ - Supports batching and retry logic in one place
9
+ """
10
+
11
+ import asyncio
12
+ import logging
13
+ import os
14
+ import time
15
+ from typing import Any, Dict, Optional
16
+ from datetime import datetime
17
+
18
+ # Socket.IO imports
19
+ try:
20
+ import socketio
21
+ SOCKETIO_AVAILABLE = True
22
+ except ImportError:
23
+ SOCKETIO_AVAILABLE = False
24
+ socketio = None
25
+
26
+ from .event_bus import EventBus
27
+
28
+ # Configure logger
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class SocketIORelay:
33
+ """Relay events from EventBus to Socket.IO clients.
34
+
35
+ WHY relay pattern:
36
+ - Decouples event production from Socket.IO emission
37
+ - Handles connection failures without affecting producers
38
+ - Provides single point for Socket.IO configuration
39
+ - Enables event batching and optimization
40
+ - Simplifies debugging with centralized logging
41
+ """
42
+
43
+ def __init__(self, port: Optional[int] = None):
44
+ """Initialize the Socket.IO relay.
45
+
46
+ Args:
47
+ port: Socket.IO server port (defaults to env var or 8765)
48
+ """
49
+ self.port = port or int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
50
+ self.event_bus = EventBus.get_instance()
51
+ self.client: Optional[Any] = None
52
+ self.connected = False
53
+ self.enabled = True
54
+ self.debug = os.environ.get("CLAUDE_MPM_RELAY_DEBUG", "false").lower() == "true"
55
+
56
+ # Connection retry settings
57
+ self.max_retries = 3
58
+ self.retry_delay = 0.5
59
+ self.last_connection_attempt = 0
60
+ self.connection_cooldown = 5.0 # Seconds between connection attempts
61
+
62
+ # Statistics
63
+ self.stats = {
64
+ "events_relayed": 0,
65
+ "events_failed": 0,
66
+ "connection_failures": 0,
67
+ "last_relay_time": None
68
+ }
69
+
70
+ if not SOCKETIO_AVAILABLE:
71
+ logger.warning("Socket.IO not available, relay will be disabled")
72
+ self.enabled = False
73
+
74
+ def enable(self) -> None:
75
+ """Enable the relay."""
76
+ if not SOCKETIO_AVAILABLE:
77
+ logger.warning("Cannot enable relay: Socket.IO not available")
78
+ return
79
+ self.enabled = True
80
+ logger.info("SocketIO relay enabled")
81
+
82
+ def disable(self) -> None:
83
+ """Disable the relay."""
84
+ self.enabled = False
85
+ if self.client and self.connected:
86
+ try:
87
+ self.client.disconnect()
88
+ except:
89
+ pass
90
+ logger.info("SocketIO relay disabled")
91
+
92
+ def _create_client(self) -> bool:
93
+ """Create and connect Socket.IO client.
94
+
95
+ Returns:
96
+ bool: True if connection successful
97
+ """
98
+ if not SOCKETIO_AVAILABLE or not self.enabled:
99
+ return False
100
+
101
+ # Check connection cooldown
102
+ current_time = time.time()
103
+ if current_time - self.last_connection_attempt < self.connection_cooldown:
104
+ return False
105
+
106
+ self.last_connection_attempt = current_time
107
+
108
+ try:
109
+ # Create new client
110
+ self.client = socketio.Client(
111
+ reconnection=True,
112
+ reconnection_attempts=3,
113
+ reconnection_delay=1,
114
+ logger=False,
115
+ engineio_logger=False
116
+ )
117
+
118
+ # Connect to server
119
+ self.client.connect(
120
+ f"http://localhost:{self.port}",
121
+ wait=True,
122
+ wait_timeout=2.0,
123
+ transports=['websocket', 'polling']
124
+ )
125
+
126
+ self.connected = True
127
+ logger.info(f"SocketIO relay connected to port {self.port}")
128
+ return True
129
+
130
+ except Exception as e:
131
+ self.stats["connection_failures"] += 1
132
+ if self.debug:
133
+ logger.debug(f"Failed to connect to Socket.IO server: {e}")
134
+ self.connected = False
135
+ self.client = None
136
+ return False
137
+
138
+ def _ensure_connection(self) -> bool:
139
+ """Ensure Socket.IO client is connected.
140
+
141
+ Returns:
142
+ bool: True if connected or reconnected
143
+ """
144
+ if not self.enabled:
145
+ return False
146
+
147
+ # Check existing connection
148
+ if self.client and self.connected:
149
+ try:
150
+ # Verify connection is still alive
151
+ if self.client.connected:
152
+ return True
153
+ except:
154
+ pass
155
+
156
+ # Need to create or reconnect
157
+ return self._create_client()
158
+
159
+ async def relay_event(self, event_type: str, data: Any) -> bool:
160
+ """Relay an event to Socket.IO.
161
+
162
+ Args:
163
+ event_type: The event type
164
+ data: The event data
165
+
166
+ Returns:
167
+ bool: True if successfully relayed
168
+ """
169
+ if not self.enabled:
170
+ return False
171
+
172
+ # Ensure we have a connection
173
+ if not self._ensure_connection():
174
+ self.stats["events_failed"] += 1
175
+ return False
176
+
177
+ try:
178
+ # Emit to Socket.IO
179
+ self.client.emit("claude_event", {
180
+ "event": "claude_event",
181
+ "type": event_type.split(".")[0] if "." in event_type else event_type,
182
+ "subtype": event_type.split(".", 1)[1] if "." in event_type else "generic",
183
+ "timestamp": data.get("timestamp", datetime.now().isoformat()),
184
+ "data": data,
185
+ "source": "event_bus"
186
+ })
187
+
188
+ # Update statistics
189
+ self.stats["events_relayed"] += 1
190
+ self.stats["last_relay_time"] = datetime.now().isoformat()
191
+
192
+ if self.debug:
193
+ logger.debug(f"Relayed event to Socket.IO: {event_type}")
194
+
195
+ return True
196
+
197
+ except Exception as e:
198
+ self.stats["events_failed"] += 1
199
+ if self.debug:
200
+ logger.debug(f"Failed to relay event {event_type}: {e}")
201
+ # Mark connection as failed for retry
202
+ self.connected = False
203
+ return False
204
+
205
+ def start(self) -> None:
206
+ """Start the relay by subscribing to EventBus events.
207
+
208
+ This sets up listeners for all hook events and relays them
209
+ to Socket.IO clients.
210
+ """
211
+ if not self.enabled:
212
+ logger.warning("Cannot start relay: disabled or Socket.IO not available")
213
+ return
214
+
215
+ # Define async handler for events
216
+ async def handle_hook_event(event_type: str, data: Any):
217
+ """Handle events from the event bus."""
218
+ # Only relay hook events by default
219
+ if event_type.startswith("hook."):
220
+ await self.relay_event(event_type, data)
221
+
222
+ # Subscribe to all hook events
223
+ self.event_bus.on("hook.*", handle_hook_event)
224
+
225
+ # Also subscribe to specific high-priority events
226
+ for event in ["hook.pre_tool", "hook.post_tool", "hook.subagent_stop",
227
+ "hook.user_prompt", "hook.assistant_response"]:
228
+ self.event_bus.on(event, lambda data, evt=event:
229
+ asyncio.create_task(self.relay_event(evt, data)))
230
+
231
+ logger.info("SocketIO relay started and subscribed to events")
232
+
233
+ def stop(self) -> None:
234
+ """Stop the relay and clean up resources."""
235
+ # Disconnect Socket.IO client
236
+ if self.client and self.connected:
237
+ try:
238
+ self.client.disconnect()
239
+ except:
240
+ pass
241
+
242
+ # Could remove event bus listeners here if needed
243
+ # For now, let them be cleaned up naturally
244
+
245
+ self.connected = False
246
+ self.client = None
247
+ logger.info("SocketIO relay stopped")
248
+
249
+ def get_stats(self) -> Dict[str, Any]:
250
+ """Get relay statistics.
251
+
252
+ Returns:
253
+ dict: Statistics about relay operation
254
+ """
255
+ return {
256
+ **self.stats,
257
+ "enabled": self.enabled,
258
+ "connected": self.connected,
259
+ "port": self.port
260
+ }
261
+
262
+
263
+ # Global relay instance
264
+ _relay_instance: Optional[SocketIORelay] = None
265
+
266
+
267
+ def get_relay(port: Optional[int] = None) -> SocketIORelay:
268
+ """Get or create the global SocketIO relay instance.
269
+
270
+ Args:
271
+ port: Optional port number
272
+
273
+ Returns:
274
+ SocketIORelay: The relay instance
275
+ """
276
+ global _relay_instance
277
+ if _relay_instance is None:
278
+ _relay_instance = SocketIORelay(port)
279
+ return _relay_instance
280
+
281
+
282
+ def start_relay(port: Optional[int] = None) -> SocketIORelay:
283
+ """Start the global SocketIO relay.
284
+
285
+ Args:
286
+ port: Optional port number
287
+
288
+ Returns:
289
+ SocketIORelay: The started relay instance
290
+ """
291
+ relay = get_relay(port)
292
+ relay.start()
293
+ return relay
294
+
295
+
296
+ def stop_relay() -> None:
297
+ """Stop the global SocketIO relay."""
298
+ global _relay_instance
299
+ if _relay_instance:
300
+ _relay_instance.stop()
301
+ _relay_instance = None
@@ -0,0 +1,44 @@
1
+ """
2
+ Event Bus System for Claude MPM
3
+ ===============================
4
+
5
+ A decoupled event system that separates event producers from consumers,
6
+ providing reliable, testable, and maintainable event handling.
7
+
8
+ Key Components:
9
+ - EventBus: Core pub/sub system
10
+ - Event: Standard event format
11
+ - IEventProducer: Interface for event producers
12
+ - IEventConsumer: Interface for event consumers
13
+ - Various consumer implementations
14
+ """
15
+
16
+ from .core import Event, EventBus, EventMetadata, EventPriority
17
+ from .interfaces import IEventConsumer, IEventProducer, ConsumerConfig
18
+ from .consumers import (
19
+ SocketIOConsumer,
20
+ LoggingConsumer,
21
+ MetricsConsumer,
22
+ DeadLetterConsumer,
23
+ )
24
+ from .producers import HookEventProducer, SystemEventProducer
25
+
26
+ __all__ = [
27
+ # Core
28
+ "Event",
29
+ "EventBus",
30
+ "EventMetadata",
31
+ "EventPriority",
32
+ # Interfaces
33
+ "IEventConsumer",
34
+ "IEventProducer",
35
+ "ConsumerConfig",
36
+ # Consumers
37
+ "SocketIOConsumer",
38
+ "LoggingConsumer",
39
+ "MetricsConsumer",
40
+ "DeadLetterConsumer",
41
+ # Producers
42
+ "HookEventProducer",
43
+ "SystemEventProducer",
44
+ ]
@@ -0,0 +1,18 @@
1
+ """
2
+ Event Bus Consumers
3
+ ==================
4
+
5
+ Various consumer implementations for processing events from the event bus.
6
+ """
7
+
8
+ from .dead_letter import DeadLetterConsumer
9
+ from .logging import LoggingConsumer
10
+ from .metrics import MetricsConsumer
11
+ from .socketio import SocketIOConsumer
12
+
13
+ __all__ = [
14
+ "SocketIOConsumer",
15
+ "LoggingConsumer",
16
+ "MetricsConsumer",
17
+ "DeadLetterConsumer",
18
+ ]