claude-mpm 3.1.3__py3-none-any.whl → 3.3.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 (80) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +149 -17
  4. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  5. claude_mpm/agents/base_agent.json +1 -1
  6. claude_mpm/agents/templates/pm.json +25 -0
  7. claude_mpm/agents/templates/research.json +2 -1
  8. claude_mpm/cli/__init__.py +19 -23
  9. claude_mpm/cli/commands/__init__.py +3 -1
  10. claude_mpm/cli/commands/agents.py +7 -18
  11. claude_mpm/cli/commands/info.py +5 -10
  12. claude_mpm/cli/commands/memory.py +232 -0
  13. claude_mpm/cli/commands/run.py +501 -28
  14. claude_mpm/cli/commands/tickets.py +10 -17
  15. claude_mpm/cli/commands/ui.py +15 -37
  16. claude_mpm/cli/parser.py +91 -1
  17. claude_mpm/cli/utils.py +9 -28
  18. claude_mpm/config/socketio_config.py +256 -0
  19. claude_mpm/constants.py +9 -0
  20. claude_mpm/core/__init__.py +2 -2
  21. claude_mpm/core/agent_registry.py +4 -4
  22. claude_mpm/core/claude_runner.py +919 -0
  23. claude_mpm/core/config.py +21 -1
  24. claude_mpm/core/factories.py +1 -1
  25. claude_mpm/core/hook_manager.py +196 -0
  26. claude_mpm/core/pm_hook_interceptor.py +205 -0
  27. claude_mpm/core/service_registry.py +1 -1
  28. claude_mpm/core/simple_runner.py +323 -33
  29. claude_mpm/core/socketio_pool.py +582 -0
  30. claude_mpm/core/websocket_handler.py +233 -0
  31. claude_mpm/deployment_paths.py +261 -0
  32. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +667 -679
  34. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  35. claude_mpm/hooks/memory_integration_hook.py +312 -0
  36. claude_mpm/models/__init__.py +9 -91
  37. claude_mpm/orchestration/__init__.py +1 -1
  38. claude_mpm/scripts/claude-mpm-socketio +32 -0
  39. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  40. claude_mpm/scripts/install_socketio_server.py +407 -0
  41. claude_mpm/scripts/launch_monitor.py +132 -0
  42. claude_mpm/scripts/launch_socketio_dashboard.py +261 -0
  43. claude_mpm/scripts/manage_version.py +479 -0
  44. claude_mpm/scripts/socketio_daemon.py +181 -0
  45. claude_mpm/scripts/socketio_server_manager.py +428 -0
  46. claude_mpm/services/__init__.py +5 -0
  47. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  48. claude_mpm/services/agent_memory_manager.py +684 -0
  49. claude_mpm/services/agent_modification_tracker.py +98 -17
  50. claude_mpm/services/agent_persistence_service.py +33 -13
  51. claude_mpm/services/agent_registry.py +82 -43
  52. claude_mpm/services/hook_service.py +362 -0
  53. claude_mpm/services/socketio_client_manager.py +474 -0
  54. claude_mpm/services/socketio_server.py +922 -0
  55. claude_mpm/services/standalone_socketio_server.py +631 -0
  56. claude_mpm/services/ticket_manager.py +4 -5
  57. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  58. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  59. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  60. claude_mpm/services/websocket_server.py +376 -0
  61. claude_mpm/utils/dependency_manager.py +211 -0
  62. claude_mpm/utils/import_migration_example.py +80 -0
  63. claude_mpm/utils/path_operations.py +0 -20
  64. claude_mpm/web/open_dashboard.py +34 -0
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/METADATA +20 -9
  66. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/RECORD +71 -50
  67. claude_mpm-3.3.0.dist-info/entry_points.txt +7 -0
  68. claude_mpm/cli_old.py +0 -728
  69. claude_mpm/models/common.py +0 -41
  70. claude_mpm/models/lifecycle.py +0 -97
  71. claude_mpm/models/modification.py +0 -126
  72. claude_mpm/models/persistence.py +0 -57
  73. claude_mpm/models/registry.py +0 -91
  74. claude_mpm/security/__init__.py +0 -8
  75. claude_mpm/security/bash_validator.py +0 -393
  76. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  77. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/WHEEL +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/licenses/LICENSE +0 -0
  80. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,631 @@
1
+ """Standalone Socket.IO server with independent versioning and deployment agnostic design.
2
+
3
+ This server is designed to run independently of claude-mpm and maintain its own versioning.
4
+ It provides a persistent Socket.IO service that can handle multiple claude-mpm client connections.
5
+
6
+ KEY DESIGN PRINCIPLES:
7
+ 1. Single server per machine - Only one instance should run
8
+ 2. Persistent across sessions - Server keeps running when code is pushed
9
+ 3. Separate versioning - Server has its own version schema independent of claude-mpm
10
+ 4. Version compatibility mapping - Track which server versions work with which claude-mpm versions
11
+ 5. Deployment agnostic - Works with local script, PyPI, npm installations
12
+
13
+ WHY standalone architecture:
14
+ - Allows server evolution independent of claude-mpm releases
15
+ - Enables persistent monitoring across multiple claude-mpm sessions
16
+ - Provides better resource management (one server vs multiple)
17
+ - Simplifies debugging and maintenance
18
+ - Supports different installation methods (PyPI, local, Docker, etc.)
19
+ """
20
+
21
+ import asyncio
22
+ import json
23
+ import logging
24
+ import os
25
+ import signal
26
+ import socket
27
+ import sys
28
+ import threading
29
+ import time
30
+ import uuid
31
+ from datetime import datetime
32
+ from pathlib import Path
33
+ from typing import Dict, Any, Optional, List, Set
34
+ from collections import deque
35
+ import importlib.metadata
36
+
37
+ try:
38
+ import socketio
39
+ from aiohttp import web
40
+ SOCKETIO_AVAILABLE = True
41
+
42
+ # Get Socket.IO version
43
+ try:
44
+ SOCKETIO_VERSION = importlib.metadata.version('python-socketio')
45
+ except Exception:
46
+ SOCKETIO_VERSION = 'unknown'
47
+
48
+ except ImportError:
49
+ SOCKETIO_AVAILABLE = False
50
+ socketio = None
51
+ web = None
52
+ SOCKETIO_VERSION = 'not-installed'
53
+
54
+ # Standalone server version - independent of claude-mpm
55
+ STANDALONE_SERVER_VERSION = "1.0.0"
56
+
57
+ # Compatibility matrix - which server versions work with which claude-mpm versions
58
+ COMPATIBILITY_MATRIX = {
59
+ "1.0.0": {
60
+ "claude_mpm_versions": [">=0.7.0"],
61
+ "min_python": "3.8",
62
+ "socketio_min": "5.11.0",
63
+ "features": [
64
+ "persistent_server",
65
+ "version_compatibility",
66
+ "process_isolation",
67
+ "health_monitoring",
68
+ "event_namespacing"
69
+ ]
70
+ }
71
+ }
72
+
73
+
74
+ class StandaloneSocketIOServer:
75
+ """Standalone Socket.IO server with independent lifecycle and versioning.
76
+
77
+ This server runs independently of claude-mpm processes and provides:
78
+ - Version compatibility checking
79
+ - Process isolation and management
80
+ - Persistent operation across claude-mpm sessions
81
+ - Health monitoring and diagnostics
82
+ - Event namespacing and routing
83
+ """
84
+
85
+ def __init__(self, host: str = "localhost", port: int = 8765,
86
+ server_id: Optional[str] = None):
87
+ self.server_version = STANDALONE_SERVER_VERSION
88
+ self.server_id = server_id or f"socketio-{uuid.uuid4().hex[:8]}"
89
+ self.host = host
90
+ self.port = port
91
+ self.start_time = datetime.utcnow()
92
+
93
+ # Setup logging
94
+ self.logger = self._setup_logging()
95
+
96
+ # Server state
97
+ self.running = False
98
+ self.clients: Set[str] = set()
99
+ self.event_history: deque = deque(maxlen=10000) # Larger history for standalone server
100
+ self.client_versions: Dict[str, str] = {} # Track client claude-mpm versions
101
+ self.health_stats = {
102
+ "events_processed": 0,
103
+ "clients_served": 0,
104
+ "errors": 0,
105
+ "last_activity": None
106
+ }
107
+
108
+ # Asyncio components
109
+ self.loop = None
110
+ self.app = None
111
+ self.sio = None
112
+ self.runner = None
113
+ self.site = None
114
+
115
+ # Process management
116
+ self.pid = os.getpid()
117
+ self.pidfile_path = self._get_pidfile_path()
118
+
119
+ if not SOCKETIO_AVAILABLE:
120
+ self.logger.error("Socket.IO dependencies not available. Install with: pip install python-socketio aiohttp")
121
+ return
122
+
123
+ self.logger.info(f"Standalone Socket.IO server v{self.server_version} initialized")
124
+ self.logger.info(f"Server ID: {self.server_id}, PID: {self.pid}")
125
+ self.logger.info(f"Using python-socketio v{SOCKETIO_VERSION}")
126
+
127
+ def _setup_logging(self) -> logging.Logger:
128
+ """Setup dedicated logging for standalone server."""
129
+ logger = logging.getLogger(f"socketio_standalone_{self.server_id}")
130
+
131
+ if not logger.handlers:
132
+ handler = logging.StreamHandler()
133
+ formatter = logging.Formatter(
134
+ f'%(asctime)s - StandaloneSocketIO[{self.server_id}] - %(levelname)s - %(message)s'
135
+ )
136
+ handler.setFormatter(formatter)
137
+ logger.addHandler(handler)
138
+ logger.setLevel(logging.INFO)
139
+
140
+ return logger
141
+
142
+ def _get_pidfile_path(self) -> Path:
143
+ """Get path for PID file to track running server."""
144
+ # Use system temp directory or user home
145
+ if os.name == 'nt': # Windows
146
+ temp_dir = Path(os.environ.get('TEMP', os.path.expanduser('~')))
147
+ else: # Unix-like
148
+ temp_dir = Path('/tmp') if Path('/tmp').exists() else Path.home()
149
+
150
+ return temp_dir / f"claude_mpm_socketio_{self.port}.pid"
151
+
152
+ def check_compatibility(self, client_version: str) -> Dict[str, Any]:
153
+ """Check if client version is compatible with this server version.
154
+
155
+ Returns compatibility info including warnings and supported features.
156
+ """
157
+ server_compat = COMPATIBILITY_MATRIX.get(self.server_version, {})
158
+
159
+ result = {
160
+ "compatible": False,
161
+ "server_version": self.server_version,
162
+ "client_version": client_version,
163
+ "warnings": [],
164
+ "supported_features": server_compat.get("features", []),
165
+ "requirements": {
166
+ "min_python": server_compat.get("min_python", "3.8"),
167
+ "socketio_min": server_compat.get("socketio_min", "5.11.0")
168
+ }
169
+ }
170
+
171
+ # Simple version compatibility check
172
+ # In production, you'd use proper semantic versioning
173
+ try:
174
+ if client_version >= "0.7.0": # Minimum supported
175
+ result["compatible"] = True
176
+ else:
177
+ result["warnings"].append(f"Client version {client_version} may not be fully supported")
178
+ result["compatible"] = False
179
+ except Exception as e:
180
+ result["warnings"].append(f"Could not parse client version: {e}")
181
+ result["compatible"] = False
182
+
183
+ return result
184
+
185
+ def is_already_running(self) -> bool:
186
+ """Check if another server instance is already running on this port."""
187
+ try:
188
+ # Check PID file
189
+ if self.pidfile_path.exists():
190
+ with open(self.pidfile_path, 'r') as f:
191
+ old_pid = int(f.read().strip())
192
+
193
+ # Check if process is still running
194
+ try:
195
+ os.kill(old_pid, 0) # Signal 0 just checks if process exists
196
+ self.logger.info(f"Found existing server with PID {old_pid}")
197
+ return True
198
+ except OSError:
199
+ # Process doesn't exist, remove stale PID file
200
+ self.pidfile_path.unlink()
201
+
202
+ # Check if port is in use
203
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
204
+ s.settimeout(1.0)
205
+ result = s.connect_ex((self.host, self.port))
206
+ if result == 0:
207
+ self.logger.info(f"Port {self.port} is already in use")
208
+ return True
209
+
210
+ except Exception as e:
211
+ self.logger.debug(f"Error checking for existing server: {e}")
212
+
213
+ return False
214
+
215
+ def create_pidfile(self):
216
+ """Create PID file to track this server instance."""
217
+ try:
218
+ self.pidfile_path.parent.mkdir(parents=True, exist_ok=True)
219
+ with open(self.pidfile_path, 'w') as f:
220
+ f.write(str(self.pid))
221
+ self.logger.info(f"Created PID file: {self.pidfile_path}")
222
+ except Exception as e:
223
+ self.logger.error(f"Failed to create PID file: {e}")
224
+
225
+ def remove_pidfile(self):
226
+ """Remove PID file on shutdown."""
227
+ try:
228
+ if self.pidfile_path.exists():
229
+ self.pidfile_path.unlink()
230
+ self.logger.info(f"Removed PID file: {self.pidfile_path}")
231
+ except Exception as e:
232
+ self.logger.error(f"Failed to remove PID file: {e}")
233
+
234
+ def setup_signal_handlers(self):
235
+ """Setup signal handlers for graceful shutdown."""
236
+ def signal_handler(signum, frame):
237
+ self.logger.info(f"Received signal {signum}, initiating shutdown...")
238
+ self.stop()
239
+ sys.exit(0)
240
+
241
+ signal.signal(signal.SIGINT, signal_handler)
242
+ signal.signal(signal.SIGTERM, signal_handler)
243
+
244
+ if hasattr(signal, 'SIGHUP'):
245
+ signal.signal(signal.SIGHUP, signal_handler)
246
+
247
+ async def start_async(self):
248
+ """Start the server asynchronously."""
249
+ if not SOCKETIO_AVAILABLE:
250
+ raise RuntimeError("Socket.IO dependencies not available")
251
+
252
+ self.logger.info(f"Starting standalone Socket.IO server v{self.server_version}")
253
+
254
+ # Create Socket.IO server with production settings
255
+ self.sio = socketio.AsyncServer(
256
+ cors_allowed_origins="*", # Configure appropriately for production
257
+ async_mode='aiohttp',
258
+ ping_timeout=60,
259
+ ping_interval=25,
260
+ max_http_buffer_size=1000000,
261
+ logger=False, # Use our own logger
262
+ engineio_logger=False
263
+ )
264
+
265
+ # Create aiohttp application
266
+ self.app = web.Application()
267
+ self.sio.attach(self.app)
268
+
269
+ # Setup routes and event handlers
270
+ self._setup_routes()
271
+ self._setup_event_handlers()
272
+
273
+ # Start the server
274
+ self.runner = web.AppRunner(self.app)
275
+ await self.runner.setup()
276
+
277
+ self.site = web.TCPSite(self.runner, self.host, self.port)
278
+ await self.site.start()
279
+
280
+ self.running = True
281
+ self.create_pidfile()
282
+
283
+ self.logger.info(f"🚀 Standalone Socket.IO server STARTED on http://{self.host}:{self.port}")
284
+ self.logger.info(f"🔧 Server ID: {self.server_id}")
285
+ self.logger.info(f"💾 PID file: {self.pidfile_path}")
286
+
287
+ def start(self):
288
+ """Start the server in the main thread (for standalone execution)."""
289
+ if self.is_already_running():
290
+ self.logger.error("Server is already running. Use stop() first or choose a different port.")
291
+ return False
292
+
293
+ self.setup_signal_handlers()
294
+
295
+ # Run in main thread for standalone operation
296
+ try:
297
+ self.loop = asyncio.new_event_loop()
298
+ asyncio.set_event_loop(self.loop)
299
+ self.loop.run_until_complete(self._run_forever())
300
+ except KeyboardInterrupt:
301
+ self.logger.info("Received KeyboardInterrupt, shutting down...")
302
+ except Exception as e:
303
+ self.logger.error(f"Server error: {e}")
304
+ raise
305
+ finally:
306
+ self.stop()
307
+
308
+ return True
309
+
310
+ async def _run_forever(self):
311
+ """Run the server until stopped."""
312
+ await self.start_async()
313
+
314
+ try:
315
+ # Keep server running with periodic health checks
316
+ last_health_check = time.time()
317
+
318
+ while self.running:
319
+ await asyncio.sleep(1)
320
+
321
+ # Periodic health check and stats update
322
+ now = time.time()
323
+ if now - last_health_check > 30: # Every 30 seconds
324
+ self._update_health_stats()
325
+ last_health_check = now
326
+
327
+ except Exception as e:
328
+ self.logger.error(f"Error in server loop: {e}")
329
+ raise
330
+
331
+ def stop(self):
332
+ """Stop the server gracefully."""
333
+ self.logger.info("Stopping standalone Socket.IO server...")
334
+ self.running = False
335
+
336
+ if self.loop and self.loop.is_running():
337
+ # Schedule shutdown in the event loop
338
+ self.loop.create_task(self._shutdown_async())
339
+ else:
340
+ # Direct shutdown
341
+ asyncio.run(self._shutdown_async())
342
+
343
+ self.remove_pidfile()
344
+ self.logger.info("Server stopped")
345
+
346
+ async def _shutdown_async(self):
347
+ """Async shutdown process."""
348
+ try:
349
+ # Close all client connections
350
+ if self.sio:
351
+ await self.sio.shutdown()
352
+
353
+ # Stop the web server
354
+ if self.site:
355
+ await self.site.stop()
356
+ if self.runner:
357
+ await self.runner.cleanup()
358
+
359
+ except Exception as e:
360
+ self.logger.error(f"Error during shutdown: {e}")
361
+
362
+ def _setup_routes(self):
363
+ """Setup HTTP routes for health checks and admin endpoints."""
364
+
365
+ async def version_endpoint(request):
366
+ """Version discovery endpoint."""
367
+ compatibility_info = {
368
+ "server_version": self.server_version,
369
+ "server_id": self.server_id,
370
+ "socketio_version": SOCKETIO_VERSION,
371
+ "compatibility_matrix": COMPATIBILITY_MATRIX,
372
+ "supported_client_versions": COMPATIBILITY_MATRIX[self.server_version].get("claude_mpm_versions", []),
373
+ "features": COMPATIBILITY_MATRIX[self.server_version].get("features", [])
374
+ }
375
+ return web.json_response(compatibility_info)
376
+
377
+ async def health_endpoint(request):
378
+ """Health check endpoint with detailed diagnostics."""
379
+ uptime = (datetime.utcnow() - self.start_time).total_seconds()
380
+
381
+ health_info = {
382
+ "status": "healthy" if self.running else "stopped",
383
+ "server_version": self.server_version,
384
+ "server_id": self.server_id,
385
+ "pid": self.pid,
386
+ "uptime_seconds": uptime,
387
+ "start_time": self.start_time.isoformat() + "Z",
388
+ "timestamp": datetime.utcnow().isoformat() + "Z",
389
+ "clients_connected": len(self.clients),
390
+ "client_versions": dict(self.client_versions),
391
+ "health_stats": dict(self.health_stats),
392
+ "port": self.port,
393
+ "host": self.host,
394
+ "dependencies": {
395
+ "socketio_version": SOCKETIO_VERSION,
396
+ "python_version": sys.version.split()[0]
397
+ }
398
+ }
399
+ return web.json_response(health_info)
400
+
401
+ async def compatibility_check(request):
402
+ """Check compatibility with a specific client version."""
403
+ data = await request.json()
404
+ client_version = data.get("client_version", "unknown")
405
+
406
+ compatibility = self.check_compatibility(client_version)
407
+ return web.json_response(compatibility)
408
+
409
+ async def stats_endpoint(request):
410
+ """Server statistics endpoint."""
411
+ stats = {
412
+ "server_info": {
413
+ "version": self.server_version,
414
+ "id": self.server_id,
415
+ "uptime": (datetime.utcnow() - self.start_time).total_seconds()
416
+ },
417
+ "connections": {
418
+ "current_clients": len(self.clients),
419
+ "total_served": self.health_stats["clients_served"],
420
+ "client_versions": dict(self.client_versions)
421
+ },
422
+ "events": {
423
+ "total_processed": self.health_stats["events_processed"],
424
+ "history_size": len(self.event_history),
425
+ "last_activity": self.health_stats["last_activity"]
426
+ },
427
+ "errors": self.health_stats["errors"]
428
+ }
429
+ return web.json_response(stats)
430
+
431
+ # Register routes
432
+ self.app.router.add_get('/version', version_endpoint)
433
+ self.app.router.add_get('/health', health_endpoint)
434
+ self.app.router.add_get('/status', health_endpoint) # Alias
435
+ self.app.router.add_post('/compatibility', compatibility_check)
436
+ self.app.router.add_get('/stats', stats_endpoint)
437
+
438
+ # Serve Socket.IO client library
439
+ self.app.router.add_static('/socket.io/',
440
+ path=Path(__file__).parent / 'static',
441
+ name='socketio_static')
442
+
443
+ def _setup_event_handlers(self):
444
+ """Setup Socket.IO event handlers."""
445
+
446
+ @self.sio.event
447
+ async def connect(sid, environ, auth):
448
+ """Handle client connection with version compatibility checking."""
449
+ self.clients.add(sid)
450
+ client_addr = environ.get('REMOTE_ADDR', 'unknown')
451
+
452
+ # Extract client version from auth if provided
453
+ client_version = "unknown"
454
+ if auth and isinstance(auth, dict):
455
+ client_version = auth.get('claude_mpm_version', 'unknown')
456
+
457
+ self.client_versions[sid] = client_version
458
+ self.health_stats["clients_served"] += 1
459
+ self.health_stats["last_activity"] = datetime.utcnow().isoformat() + "Z"
460
+
461
+ self.logger.info(f"🔗 Client {sid} connected from {client_addr}")
462
+ self.logger.info(f"📋 Client version: {client_version}")
463
+ self.logger.info(f"📊 Total clients: {len(self.clients)}")
464
+
465
+ # Check version compatibility
466
+ compatibility = self.check_compatibility(client_version)
467
+
468
+ # Send connection acknowledgment with compatibility info
469
+ await self.sio.emit('connection_ack', {
470
+ "server_version": self.server_version,
471
+ "server_id": self.server_id,
472
+ "compatibility": compatibility,
473
+ "timestamp": datetime.utcnow().isoformat() + "Z"
474
+ }, room=sid)
475
+
476
+ # Send current server status
477
+ await self._send_server_status(sid)
478
+
479
+ if not compatibility["compatible"]:
480
+ self.logger.warning(f"⚠️ Client {sid} version {client_version} has compatibility issues")
481
+ await self.sio.emit('compatibility_warning', compatibility, room=sid)
482
+
483
+ @self.sio.event
484
+ async def disconnect(sid):
485
+ """Handle client disconnection."""
486
+ if sid in self.clients:
487
+ self.clients.remove(sid)
488
+ if sid in self.client_versions:
489
+ del self.client_versions[sid]
490
+
491
+ self.logger.info(f"🔌 Client {sid} disconnected")
492
+ self.logger.info(f"📊 Remaining clients: {len(self.clients)}")
493
+
494
+ @self.sio.event
495
+ async def ping(sid, data=None):
496
+ """Handle ping requests."""
497
+ await self.sio.emit('pong', {
498
+ "timestamp": datetime.utcnow().isoformat() + "Z",
499
+ "server_id": self.server_id
500
+ }, room=sid)
501
+
502
+ @self.sio.event
503
+ async def get_version(sid):
504
+ """Handle version info requests."""
505
+ version_info = {
506
+ "server_version": self.server_version,
507
+ "server_id": self.server_id,
508
+ "socketio_version": SOCKETIO_VERSION,
509
+ "compatibility_matrix": COMPATIBILITY_MATRIX
510
+ }
511
+ await self.sio.emit('version_info', version_info, room=sid)
512
+
513
+ @self.sio.event
514
+ async def claude_event(sid, data):
515
+ """Handle events from claude-mpm clients and broadcast to other clients."""
516
+ try:
517
+ # Add server metadata
518
+ enhanced_data = {
519
+ **data,
520
+ "server_id": self.server_id,
521
+ "received_at": datetime.utcnow().isoformat() + "Z"
522
+ }
523
+
524
+ # Store in event history
525
+ self.event_history.append(enhanced_data)
526
+ self.health_stats["events_processed"] += 1
527
+ self.health_stats["last_activity"] = datetime.utcnow().isoformat() + "Z"
528
+
529
+ # Broadcast to all other clients
530
+ await self.sio.emit('claude_event', enhanced_data, skip_sid=sid)
531
+
532
+ self.logger.debug(f"📤 Broadcasted claude_event from {sid} to {len(self.clients)-1} clients")
533
+
534
+ except Exception as e:
535
+ self.logger.error(f"Error handling claude_event: {e}")
536
+ self.health_stats["errors"] += 1
537
+
538
+ @self.sio.event
539
+ async def get_history(sid, data=None):
540
+ """Handle event history requests."""
541
+ params = data or {}
542
+ limit = min(params.get("limit", 100), len(self.event_history))
543
+
544
+ history = list(self.event_history)[-limit:] if limit > 0 else []
545
+
546
+ await self.sio.emit('event_history', {
547
+ "events": history,
548
+ "total_available": len(self.event_history),
549
+ "returned": len(history)
550
+ }, room=sid)
551
+
552
+ async def _send_server_status(self, sid: str):
553
+ """Send current server status to a client."""
554
+ status = {
555
+ "server_version": self.server_version,
556
+ "server_id": self.server_id,
557
+ "uptime": (datetime.utcnow() - self.start_time).total_seconds(),
558
+ "clients_connected": len(self.clients),
559
+ "events_processed": self.health_stats["events_processed"],
560
+ "timestamp": datetime.utcnow().isoformat() + "Z"
561
+ }
562
+ await self.sio.emit('server_status', status, room=sid)
563
+
564
+ def _update_health_stats(self):
565
+ """Update health statistics."""
566
+ self.logger.debug(f"🏥 Health check - Clients: {len(self.clients)}, "
567
+ f"Events: {self.health_stats['events_processed']}, "
568
+ f"Errors: {self.health_stats['errors']}")
569
+
570
+
571
+ def main():
572
+ """Main entry point for standalone server execution."""
573
+ import argparse
574
+
575
+ parser = argparse.ArgumentParser(description="Standalone Claude MPM Socket.IO Server")
576
+ parser.add_argument("--host", default="localhost", help="Host to bind to")
577
+ parser.add_argument("--port", type=int, default=8765, help="Port to bind to")
578
+ parser.add_argument("--server-id", help="Custom server ID")
579
+ parser.add_argument("--check-running", action="store_true",
580
+ help="Check if server is already running and exit")
581
+ parser.add_argument("--stop", action="store_true", help="Stop running server")
582
+ parser.add_argument("--version", action="store_true", help="Show version info")
583
+
584
+ args = parser.parse_args()
585
+
586
+ if args.version:
587
+ print(f"Standalone Socket.IO Server v{STANDALONE_SERVER_VERSION}")
588
+ print(f"Socket.IO v{SOCKETIO_VERSION}")
589
+ print(f"Compatibility: {COMPATIBILITY_MATRIX[STANDALONE_SERVER_VERSION]['claude_mpm_versions']}")
590
+ return
591
+
592
+ server = StandaloneSocketIOServer(
593
+ host=args.host,
594
+ port=args.port,
595
+ server_id=args.server_id
596
+ )
597
+
598
+ if args.check_running:
599
+ if server.is_already_running():
600
+ print(f"Server is running on {args.host}:{args.port}")
601
+ sys.exit(0)
602
+ else:
603
+ print(f"No server running on {args.host}:{args.port}")
604
+ sys.exit(1)
605
+
606
+ if args.stop:
607
+ if server.is_already_running():
608
+ # Send termination signal to running server
609
+ try:
610
+ with open(server.pidfile_path, 'r') as f:
611
+ pid = int(f.read().strip())
612
+ os.kill(pid, signal.SIGTERM)
613
+ print(f"Sent stop signal to server (PID: {pid})")
614
+ except Exception as e:
615
+ print(f"Error stopping server: {e}")
616
+ sys.exit(1)
617
+ else:
618
+ print("No server running to stop")
619
+ sys.exit(1)
620
+ return
621
+
622
+ # Start the server
623
+ try:
624
+ server.start()
625
+ except Exception as e:
626
+ print(f"Failed to start server: {e}")
627
+ sys.exit(1)
628
+
629
+
630
+ if __name__ == "__main__":
631
+ main()
@@ -4,11 +4,10 @@ from pathlib import Path
4
4
  from typing import Optional, Dict, Any, List
5
5
  from datetime import datetime
6
6
 
7
- from claude_mpm.utils.imports import safe_import
8
-
9
- # Import logger using safe_import pattern
10
- # Note: safe_import cannot handle relative imports as primary, use absolute import first
11
- get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
7
+ try:
8
+ from ..core.logger import get_logger
9
+ except ImportError:
10
+ from core.logger import get_logger
12
11
 
13
12
 
14
13
  class TicketManager: