claude-mpm 4.0.32__py3-none-any.whl → 4.1.0__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 (82) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +70 -2
  3. claude_mpm/agents/OUTPUT_STYLE.md +0 -11
  4. claude_mpm/agents/WORKFLOW.md +14 -2
  5. claude_mpm/agents/templates/documentation.json +51 -34
  6. claude_mpm/agents/templates/research.json +0 -11
  7. claude_mpm/cli/__init__.py +111 -33
  8. claude_mpm/cli/commands/agent_manager.py +10 -8
  9. claude_mpm/cli/commands/agents.py +82 -0
  10. claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
  11. claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
  12. claude_mpm/cli/parsers/agents_parser.py +27 -0
  13. claude_mpm/cli/parsers/base_parser.py +6 -0
  14. claude_mpm/cli/startup_logging.py +75 -0
  15. claude_mpm/core/framework_loader.py +173 -84
  16. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  17. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  18. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  19. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  20. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  21. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  22. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  23. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  24. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  25. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  26. claude_mpm/dashboard/static/js/components/build-tracker.js +323 -0
  27. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  28. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  29. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  30. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  31. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  32. claude_mpm/dashboard/static/js/socket-client.js +92 -11
  33. claude_mpm/dashboard/templates/index.html +1 -0
  34. claude_mpm/hooks/claude_hooks/connection_pool.py +25 -4
  35. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  36. claude_mpm/hooks/claude_hooks/hook_handler.py +125 -163
  37. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  38. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  39. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  40. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
  41. claude_mpm/services/agents/deployment/agent_template_builder.py +20 -11
  42. claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
  43. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  44. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +396 -13
  45. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  46. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  47. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  48. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  49. claude_mpm/services/agents/memory/content_manager.py +98 -105
  50. claude_mpm/services/event_bus/__init__.py +18 -0
  51. claude_mpm/services/event_bus/config.py +165 -0
  52. claude_mpm/services/event_bus/event_bus.py +349 -0
  53. claude_mpm/services/event_bus/relay.py +297 -0
  54. claude_mpm/services/events/__init__.py +44 -0
  55. claude_mpm/services/events/consumers/__init__.py +18 -0
  56. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  57. claude_mpm/services/events/consumers/logging.py +183 -0
  58. claude_mpm/services/events/consumers/metrics.py +242 -0
  59. claude_mpm/services/events/consumers/socketio.py +376 -0
  60. claude_mpm/services/events/core.py +470 -0
  61. claude_mpm/services/events/interfaces.py +230 -0
  62. claude_mpm/services/events/producers/__init__.py +14 -0
  63. claude_mpm/services/events/producers/hook.py +269 -0
  64. claude_mpm/services/events/producers/system.py +327 -0
  65. claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
  66. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  67. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  68. claude_mpm/services/monitor_build_service.py +345 -0
  69. claude_mpm/services/socketio/event_normalizer.py +667 -0
  70. claude_mpm/services/socketio/handlers/connection.py +81 -23
  71. claude_mpm/services/socketio/handlers/hook.py +14 -5
  72. claude_mpm/services/socketio/migration_utils.py +329 -0
  73. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  74. claude_mpm/services/socketio/server/core.py +29 -5
  75. claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
  76. claude_mpm/services/socketio/server/main.py +25 -0
  77. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +28 -9
  78. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +82 -56
  79. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
  80. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
  81. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
  82. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,349 @@
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.asyncio 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 to regular handlers (pyee handles thread safety)
180
+ self._emitter.emit(event_type, data)
181
+
182
+ # Also emit to wildcard handlers
183
+ if hasattr(self, '_wildcard_handlers'):
184
+ for prefix, handlers in self._wildcard_handlers.items():
185
+ if event_type.startswith(prefix):
186
+ for handler in handlers:
187
+ try:
188
+ # Call with event_type and data for wildcard handlers
189
+ if asyncio.iscoroutinefunction(handler):
190
+ # Schedule async handlers
191
+ try:
192
+ loop = asyncio.get_event_loop()
193
+ if loop.is_running():
194
+ asyncio.create_task(handler(event_type, data))
195
+ else:
196
+ loop.run_until_complete(handler(event_type, data))
197
+ except RuntimeError:
198
+ # No event loop, skip async handler
199
+ pass
200
+ else:
201
+ handler(event_type, data)
202
+ except Exception as e:
203
+ if self._debug:
204
+ logger.debug(f"Wildcard handler error: {e}")
205
+
206
+ # Update stats
207
+ self._stats["events_published"] += 1
208
+ self._stats["last_event_time"] = datetime.now().isoformat()
209
+
210
+ if self._debug:
211
+ logger.debug(f"Published event: {event_type}")
212
+
213
+ return True
214
+
215
+ except Exception as e:
216
+ self._stats["events_failed"] += 1
217
+ logger.error(f"Failed to publish event {event_type}: {e}")
218
+ return False
219
+
220
+ async def publish_async(self, event_type: str, data: Any) -> bool:
221
+ """Publish an event from an async context.
222
+
223
+ Args:
224
+ event_type: The event type
225
+ data: The event data
226
+
227
+ Returns:
228
+ bool: True if event was published
229
+ """
230
+ # Just delegate to sync publish (pyee handles both)
231
+ return self.publish(event_type, data)
232
+
233
+ def on(self, event_type: str, handler: Callable) -> None:
234
+ """Register an event handler.
235
+
236
+ The handler can be sync or async. For async handlers,
237
+ they will be scheduled on the event loop.
238
+
239
+ Args:
240
+ event_type: The event type to listen for (supports wildcards)
241
+ handler: The handler function
242
+ """
243
+ if event_type.endswith("*"):
244
+ # Store wildcard handlers separately
245
+ if not hasattr(self, '_wildcard_handlers'):
246
+ self._wildcard_handlers = {}
247
+
248
+ prefix = event_type[:-1]
249
+ if prefix not in self._wildcard_handlers:
250
+ self._wildcard_handlers[prefix] = []
251
+ self._wildcard_handlers[prefix].append(handler)
252
+
253
+ logger.debug(f"Registered wildcard handler for: {event_type}")
254
+ else:
255
+ # Regular event registration
256
+ self._emitter.on(event_type, handler)
257
+ logger.debug(f"Registered handler for: {event_type}")
258
+
259
+ def once(self, event_type: str, handler: Callable) -> None:
260
+ """Register a one-time event handler.
261
+
262
+ Args:
263
+ event_type: The event type to listen for
264
+ handler: The handler function
265
+ """
266
+ self._emitter.once(event_type, handler)
267
+ logger.debug(f"Registered one-time handler for: {event_type}")
268
+
269
+ def remove_listener(self, event_type: str, handler: Callable) -> None:
270
+ """Remove an event handler.
271
+
272
+ Args:
273
+ event_type: The event type
274
+ handler: The handler to remove
275
+ """
276
+ self._emitter.remove_listener(event_type, handler)
277
+ logger.debug(f"Removed handler for: {event_type}")
278
+
279
+ def remove_all_listeners(self, event_type: Optional[str] = None) -> None:
280
+ """Remove all listeners for an event type, or all listeners.
281
+
282
+ Args:
283
+ event_type: Optional event type. If None, removes all listeners.
284
+ """
285
+ if event_type:
286
+ self._emitter.remove_all_listeners(event_type)
287
+ logger.debug(f"Removed all handlers for: {event_type}")
288
+ else:
289
+ self._emitter.remove_all_listeners()
290
+ logger.debug("Removed all event handlers")
291
+
292
+ def _record_event(self, event_type: str, data: Any) -> None:
293
+ """Record event in history for debugging.
294
+
295
+ Args:
296
+ event_type: The event type
297
+ data: The event data
298
+ """
299
+ event_record = {
300
+ "timestamp": datetime.now().isoformat(),
301
+ "type": event_type,
302
+ "data": data
303
+ }
304
+
305
+ self._event_history.append(event_record)
306
+
307
+ # Trim history if too large
308
+ if len(self._event_history) > self._max_history_size:
309
+ self._event_history = self._event_history[-self._max_history_size:]
310
+
311
+ def get_stats(self) -> Dict[str, Any]:
312
+ """Get event bus statistics.
313
+
314
+ Returns:
315
+ dict: Statistics about event processing
316
+ """
317
+ return {
318
+ **self._stats,
319
+ "enabled": self._enabled,
320
+ "filters_active": len(self._event_filters) > 0,
321
+ "filter_count": len(self._event_filters),
322
+ "history_size": len(self._event_history)
323
+ }
324
+
325
+ def get_recent_events(self, limit: int = 10) -> List[Dict[str, Any]]:
326
+ """Get recent events from history.
327
+
328
+ Args:
329
+ limit: Maximum number of events to return
330
+
331
+ Returns:
332
+ list: Recent events
333
+ """
334
+ return self._event_history[-limit:]
335
+
336
+ def clear_history(self) -> None:
337
+ """Clear the event history."""
338
+ self._event_history.clear()
339
+ logger.debug("Cleared event history")
340
+
341
+ def reset_stats(self) -> None:
342
+ """Reset event statistics."""
343
+ self._stats = {
344
+ "events_published": 0,
345
+ "events_filtered": 0,
346
+ "events_failed": 0,
347
+ "last_event_time": None
348
+ }
349
+ logger.debug("Reset event statistics")
@@ -0,0 +1,297 @@
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 with better connection settings
110
+ self.client = socketio.Client(
111
+ reconnection=True,
112
+ reconnection_attempts=5,
113
+ reconnection_delay=2,
114
+ reconnection_delay_max=10,
115
+ logger=False,
116
+ engineio_logger=False
117
+ )
118
+
119
+ # Connect to server with longer timeout
120
+ self.client.connect(
121
+ f"http://localhost:{self.port}",
122
+ wait=True,
123
+ wait_timeout=10.0, # Increase timeout for stability
124
+ transports=['websocket', 'polling']
125
+ )
126
+
127
+ self.connected = True
128
+ logger.info(f"SocketIO relay connected to port {self.port}")
129
+ return True
130
+
131
+ except Exception as e:
132
+ self.stats["connection_failures"] += 1
133
+ if self.debug:
134
+ logger.debug(f"Failed to connect to Socket.IO server: {e}")
135
+ self.connected = False
136
+ self.client = None
137
+ return False
138
+
139
+ def _ensure_connection(self) -> bool:
140
+ """Ensure Socket.IO client is connected.
141
+
142
+ Returns:
143
+ bool: True if connected or reconnected
144
+ """
145
+ if not self.enabled:
146
+ return False
147
+
148
+ # Check existing connection
149
+ if self.client and self.connected:
150
+ try:
151
+ # Verify connection is still alive
152
+ if self.client.connected:
153
+ return True
154
+ except:
155
+ pass
156
+
157
+ # Need to create or reconnect
158
+ return self._create_client()
159
+
160
+ async def relay_event(self, event_type: str, data: Any) -> bool:
161
+ """Relay an event to Socket.IO.
162
+
163
+ Args:
164
+ event_type: The event type
165
+ data: The event data
166
+
167
+ Returns:
168
+ bool: True if successfully relayed
169
+ """
170
+ if not self.enabled:
171
+ return False
172
+
173
+ # Ensure we have a connection
174
+ if not self._ensure_connection():
175
+ self.stats["events_failed"] += 1
176
+ return False
177
+
178
+ try:
179
+ # Emit to Socket.IO
180
+ self.client.emit("claude_event", {
181
+ "event": "claude_event",
182
+ "type": event_type.split(".")[0] if "." in event_type else event_type,
183
+ "subtype": event_type.split(".", 1)[1] if "." in event_type else "generic",
184
+ "timestamp": data.get("timestamp", datetime.now().isoformat()),
185
+ "data": data,
186
+ "source": "event_bus"
187
+ })
188
+
189
+ # Update statistics
190
+ self.stats["events_relayed"] += 1
191
+ self.stats["last_relay_time"] = datetime.now().isoformat()
192
+
193
+ if self.debug:
194
+ logger.debug(f"Relayed event to Socket.IO: {event_type}")
195
+
196
+ return True
197
+
198
+ except Exception as e:
199
+ self.stats["events_failed"] += 1
200
+ if self.debug:
201
+ logger.debug(f"Failed to relay event {event_type}: {e}")
202
+ # Mark connection as failed for retry
203
+ self.connected = False
204
+ return False
205
+
206
+ def start(self) -> None:
207
+ """Start the relay by subscribing to EventBus events.
208
+
209
+ This sets up listeners for all hook events and relays them
210
+ to Socket.IO clients.
211
+ """
212
+ if not self.enabled:
213
+ logger.warning("Cannot start relay: disabled or Socket.IO not available")
214
+ return
215
+
216
+ # Define async handler for events
217
+ async def handle_hook_event(event_type: str, data: Any):
218
+ """Handle events from the event bus."""
219
+ # Only relay hook events by default
220
+ if event_type.startswith("hook."):
221
+ await self.relay_event(event_type, data)
222
+
223
+ # Subscribe to all hook events via wildcard
224
+ # This will catch ALL hook.* events
225
+ self.event_bus.on("hook.*", handle_hook_event)
226
+
227
+ logger.info("SocketIO relay started and subscribed to events")
228
+
229
+ def stop(self) -> None:
230
+ """Stop the relay and clean up resources."""
231
+ # Disconnect Socket.IO client
232
+ if self.client and self.connected:
233
+ try:
234
+ self.client.disconnect()
235
+ except:
236
+ pass
237
+
238
+ # Could remove event bus listeners here if needed
239
+ # For now, let them be cleaned up naturally
240
+
241
+ self.connected = False
242
+ self.client = None
243
+ logger.info("SocketIO relay stopped")
244
+
245
+ def get_stats(self) -> Dict[str, Any]:
246
+ """Get relay statistics.
247
+
248
+ Returns:
249
+ dict: Statistics about relay operation
250
+ """
251
+ return {
252
+ **self.stats,
253
+ "enabled": self.enabled,
254
+ "connected": self.connected,
255
+ "port": self.port
256
+ }
257
+
258
+
259
+ # Global relay instance
260
+ _relay_instance: Optional[SocketIORelay] = None
261
+
262
+
263
+ def get_relay(port: Optional[int] = None) -> SocketIORelay:
264
+ """Get or create the global SocketIO relay instance.
265
+
266
+ Args:
267
+ port: Optional port number
268
+
269
+ Returns:
270
+ SocketIORelay: The relay instance
271
+ """
272
+ global _relay_instance
273
+ if _relay_instance is None:
274
+ _relay_instance = SocketIORelay(port)
275
+ return _relay_instance
276
+
277
+
278
+ def start_relay(port: Optional[int] = None) -> SocketIORelay:
279
+ """Start the global SocketIO relay.
280
+
281
+ Args:
282
+ port: Optional port number
283
+
284
+ Returns:
285
+ SocketIORelay: The started relay instance
286
+ """
287
+ relay = get_relay(port)
288
+ relay.start()
289
+ return relay
290
+
291
+
292
+ def stop_relay() -> None:
293
+ """Stop the global SocketIO relay."""
294
+ global _relay_instance
295
+ if _relay_instance:
296
+ _relay_instance.stop()
297
+ _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
+ ]