claude-mpm 4.2.6__py3-none-any.whl → 4.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
claude_mpm/VERSION CHANGED
@@ -1 +1 @@
1
- 4.2.6
1
+ 4.2.7
@@ -127,10 +127,14 @@ class DashboardCommand(BaseCommand):
127
127
  signal.signal(signal.SIGTERM, signal_handler)
128
128
 
129
129
  # Run the server (blocking)
130
- if stable_server.run():
130
+ result = stable_server.run()
131
+ if result:
132
+ # Server ran successfully and stopped normally
131
133
  server_started = True
132
134
  return CommandResult.success_result("Dashboard server stopped")
133
135
  else:
136
+ # Server failed to start (e.g., couldn't find templates)
137
+ server_started = False
134
138
  self.logger.warning("Stable server failed to start, trying advanced server...")
135
139
 
136
140
  except KeyboardInterrupt:
@@ -1,29 +0,0 @@
1
- """
2
- CLI parsers package for claude-mpm.
3
-
4
- This package contains modular parser components that were extracted from the
5
- monolithic parser.py file to improve maintainability and organization.
6
-
7
- WHY: The original parser.py was 1,166 lines with a single 961-line function.
8
- Breaking it into focused modules makes it easier to maintain and test.
9
-
10
- DESIGN DECISION: Each parser module handles a specific command domain:
11
- - base_parser.py: Common arguments and main parser setup
12
- - run_parser.py: Run command arguments
13
- - agent_parser.py: Agent management commands
14
- - memory_parser.py: Memory management commands
15
- - tickets_parser.py: Ticket management commands
16
- - config_parser.py: Configuration commands
17
- - monitor_parser.py: Monitoring commands
18
- - mcp_parser.py: MCP Gateway commands
19
- """
20
-
21
- from .base_parser import add_common_arguments, create_parser, preprocess_args
22
- from .run_parser import add_run_arguments
23
-
24
- __all__ = [
25
- "add_common_arguments",
26
- "add_run_arguments",
27
- "create_parser",
28
- "preprocess_args",
29
- ]
@@ -12,11 +12,18 @@ DESIGN DECISIONS:
12
12
  - Graceful fallbacks for missing dependencies
13
13
  """
14
14
 
15
+ import asyncio
15
16
  import glob
17
+ import json
18
+ import logging
16
19
  import os
17
20
  import sys
21
+ import time
22
+ import traceback
23
+ from collections import deque
24
+ from datetime import datetime
18
25
  from pathlib import Path
19
- from typing import Any, Dict, Optional
26
+ from typing import Any, Deque, Dict, Optional
20
27
 
21
28
  try:
22
29
  import aiohttp
@@ -30,6 +37,13 @@ except ImportError:
30
37
  aiohttp = None
31
38
  web = None
32
39
 
40
+ # Set up logging
41
+ logging.basicConfig(
42
+ level=logging.INFO,
43
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
44
+ )
45
+ logger = logging.getLogger(__name__)
46
+
33
47
 
34
48
  def find_dashboard_files() -> Optional[Path]:
35
49
  """Find dashboard files across different installation methods."""
@@ -170,6 +184,25 @@ class StableDashboardServer:
170
184
  self.sio = None
171
185
  self.server_runner = None
172
186
  self.server_site = None
187
+
188
+ # Event storage with circular buffer (keep last 500 events)
189
+ self.event_history: Deque[Dict[str, Any]] = deque(maxlen=500)
190
+ self.event_count = 0
191
+ self.server_start_time = time.time()
192
+ self.last_event_time = None
193
+ self.connected_clients = set()
194
+
195
+ # Resilience features
196
+ self.retry_count = 0
197
+ self.max_retries = 3
198
+ self.health_check_failures = 0
199
+ self.is_healthy = True
200
+
201
+ # Persistent event storage (optional)
202
+ self.persist_events = os.environ.get('CLAUDE_MPM_PERSIST_EVENTS', 'false').lower() == 'true'
203
+ self.event_log_path = Path.home() / '.claude' / 'dashboard_events.jsonl'
204
+ if self.persist_events:
205
+ self.event_log_path.parent.mkdir(parents=True, exist_ok=True)
173
206
 
174
207
  def setup(self) -> bool:
175
208
  """Set up the server components."""
@@ -179,11 +212,26 @@ class StableDashboardServer:
179
212
  )
180
213
  return False
181
214
 
182
- # Find dashboard files
183
- self.dashboard_path = find_dashboard_files()
215
+ # Find dashboard files only if not already set (for testing)
184
216
  if not self.dashboard_path:
185
- print("❌ Error: Could not find dashboard files")
186
- print("Please ensure Claude MPM is properly installed")
217
+ self.dashboard_path = find_dashboard_files()
218
+ if not self.dashboard_path:
219
+ print("❌ Error: Could not find dashboard files")
220
+ print("Please ensure Claude MPM is properly installed")
221
+ return False
222
+
223
+ # Validate that the dashboard path has the required files
224
+ template_path = self.dashboard_path / "templates" / "index.html"
225
+ static_path = self.dashboard_path / "static"
226
+
227
+ if not template_path.exists():
228
+ print(f"❌ Error: Dashboard template not found at {template_path}")
229
+ print("Please ensure Claude MPM dashboard files are properly installed")
230
+ return False
231
+
232
+ if not static_path.exists():
233
+ print(f"❌ Error: Dashboard static files not found at {static_path}")
234
+ print("Please ensure Claude MPM dashboard files are properly installed")
187
235
  return False
188
236
 
189
237
  if self.debug:
@@ -206,6 +254,8 @@ class StableDashboardServer:
206
254
  ping_timeout=60, # Match client's 60 second timeout
207
255
  max_http_buffer_size=1e8, # Allow larger messages
208
256
  )
257
+ # Create app WITHOUT any static file handlers to prevent directory listing
258
+ # This is critical - we only want explicit routes we define
209
259
  self.app = web.Application()
210
260
  self.sio.attach(self.app)
211
261
  print("✅ SocketIO server created and attached")
@@ -220,17 +270,29 @@ class StableDashboardServer:
220
270
 
221
271
  def _setup_routes(self):
222
272
  """Set up HTTP routes."""
273
+ # IMPORTANT: Only add explicit routes, never add static file serving for root
274
+ # This prevents aiohttp from serving directory listings
223
275
  self.app.router.add_get("/", self._serve_dashboard)
276
+ self.app.router.add_get("/index.html", self._serve_dashboard) # Also handle /index.html
224
277
  self.app.router.add_get("/static/{path:.*}", self._serve_static)
225
278
  self.app.router.add_get("/api/directory/list", self._list_directory)
226
279
  self.app.router.add_get("/api/file/read", self._read_file)
227
280
  self.app.router.add_get("/version.json", self._serve_version)
281
+
282
+ # New resilience endpoints
283
+ self.app.router.add_get("/health", self._health_check)
284
+ self.app.router.add_get("/api/status", self._serve_status)
285
+ self.app.router.add_get("/api/events/history", self._serve_event_history)
286
+
287
+ # CRITICAL: Add the missing /api/events endpoint for receiving events
288
+ self.app.router.add_post("/api/events", self._receive_event)
228
289
 
229
290
  def _setup_socketio_events(self):
230
291
  """Set up SocketIO event handlers."""
231
292
 
232
293
  @self.sio.event
233
294
  async def connect(sid, environ):
295
+ self.connected_clients.add(sid)
234
296
  if self.debug:
235
297
  print(f"✅ SocketIO client connected: {sid}")
236
298
  user_agent = environ.get('HTTP_USER_AGENT', 'Unknown')
@@ -238,13 +300,22 @@ class StableDashboardServer:
238
300
  if len(user_agent) > 80:
239
301
  user_agent = user_agent[:77] + "..."
240
302
  print(f" Client info: {user_agent}")
241
- # Send a test message to confirm connection
303
+
304
+ # Send connection confirmation
242
305
  await self.sio.emit(
243
306
  "connection_test", {"status": "connected", "server": "stable"}, room=sid
244
307
  )
308
+
309
+ # Send recent event history to new client
310
+ if self.event_history:
311
+ # Send last 20 events to catch up new client
312
+ recent_events = list(self.event_history)[-20:]
313
+ for event in recent_events:
314
+ await self.sio.emit("claude_event", event, room=sid)
245
315
 
246
316
  @self.sio.event
247
317
  async def disconnect(sid):
318
+ self.connected_clients.discard(sid)
248
319
  if self.debug:
249
320
  print(f"📤 SocketIO client disconnected: {sid}")
250
321
 
@@ -314,15 +385,187 @@ class StableDashboardServer:
314
385
  if self.debug:
315
386
  print(f"📡 Received top-level discovery request from {sid}")
316
387
  await self.sio.emit("code:top_level:discovered", {"status": "ok"}, room=sid)
388
+
389
+ # Mock event generator when no real events
390
+ @self.sio.event
391
+ async def request_mock_event(sid, data):
392
+ """Generate a mock event for testing."""
393
+ if self.debug:
394
+ print(f"📡 Mock event requested by {sid}")
395
+
396
+ mock_event = self._create_mock_event()
397
+ # Store and broadcast like a real event
398
+ self.event_count += 1
399
+ self.last_event_time = datetime.now()
400
+ self.event_history.append(mock_event)
401
+ await self.sio.emit("claude_event", mock_event)
402
+
403
+ def _create_mock_event(self) -> Dict[str, Any]:
404
+ """Create a mock event for testing/demo purposes."""
405
+ import random
406
+
407
+ event_types = ["file", "command", "test", "build", "deploy"]
408
+ event_subtypes = ["start", "progress", "complete", "error", "warning"]
409
+
410
+ return {
411
+ "type": random.choice(event_types),
412
+ "subtype": random.choice(event_subtypes),
413
+ "timestamp": datetime.now().isoformat(),
414
+ "source": "mock",
415
+ "data": {
416
+ "message": f"Mock {random.choice(['operation', 'task', 'process'])} {random.choice(['started', 'completed', 'in progress'])}",
417
+ "file": f"/path/to/file_{random.randint(1, 100)}.py",
418
+ "line": random.randint(1, 500),
419
+ "progress": random.randint(0, 100)
420
+ },
421
+ "session_id": "mock-session",
422
+ "server_event_id": self.event_count + 1
423
+ }
424
+
425
+ async def _start_mock_event_generator(self):
426
+ """Start generating mock events if no real events for a while."""
427
+ try:
428
+ while True:
429
+ await asyncio.sleep(30) # Check every 30 seconds
430
+
431
+ # If no events in last 60 seconds and clients connected, generate mock
432
+ if self.connected_clients and (
433
+ not self.last_event_time or
434
+ (datetime.now() - self.last_event_time).total_seconds() > 60
435
+ ):
436
+ if self.debug:
437
+ print("⏰ No recent events, generating mock event")
438
+
439
+ mock_event = self._create_mock_event()
440
+ self.event_count += 1
441
+ self.last_event_time = datetime.now()
442
+ self.event_history.append(mock_event)
443
+
444
+ await self.sio.emit("claude_event", mock_event)
445
+ except asyncio.CancelledError:
446
+ pass
447
+ except Exception as e:
448
+ logger.error(f"Mock event generator error: {e}")
317
449
 
318
450
  async def _serve_dashboard(self, request):
319
- """Serve the main dashboard HTML."""
320
- dashboard_file = self.dashboard_path / "templates" / "index.html"
321
- if dashboard_file.exists():
322
- with open(dashboard_file) as f:
323
- content = f.read()
324
- return web.Response(text=content, content_type="text/html")
325
- return web.Response(text="Dashboard not found", status=404)
451
+ """Serve the main dashboard HTML with fallback."""
452
+ dashboard_file = self.dashboard_path / "templates" / "index.html" if self.dashboard_path else None
453
+
454
+ # Try to serve actual dashboard
455
+ if dashboard_file and dashboard_file.exists():
456
+ try:
457
+ with open(dashboard_file, 'r', encoding='utf-8') as f:
458
+ content = f.read()
459
+ return web.Response(text=content, content_type="text/html")
460
+ except Exception as e:
461
+ logger.error(f"Error reading dashboard template: {e}")
462
+ # Fall through to fallback HTML
463
+
464
+ # Fallback HTML if template missing or error
465
+ fallback_html = '''
466
+ <!DOCTYPE html>
467
+ <html lang="en">
468
+ <head>
469
+ <meta charset="UTF-8">
470
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
471
+ <title>Claude MPM Dashboard - Fallback Mode</title>
472
+ <style>
473
+ body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 20px; background: #1e1e1e; color: #e0e0e0; }
474
+ .container { max-width: 1200px; margin: 0 auto; }
475
+ .header { background: #2d2d2d; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
476
+ .status { background: #2d2d2d; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
477
+ .status.healthy { border-left: 4px solid #4caf50; }
478
+ .status.degraded { border-left: 4px solid #ff9800; }
479
+ .events { background: #2d2d2d; padding: 20px; border-radius: 8px; }
480
+ .event { background: #1e1e1e; padding: 10px; margin: 10px 0; border-radius: 4px; }
481
+ h1 { color: #fff; margin: 0; }
482
+ .subtitle { color: #999; margin-top: 5px; }
483
+ .metric { display: inline-block; margin-right: 20px; }
484
+ .metric-label { color: #999; font-size: 12px; }
485
+ .metric-value { color: #fff; font-size: 20px; font-weight: bold; }
486
+ </style>
487
+ </head>
488
+ <body>
489
+ <div class="container">
490
+ <div class="header">
491
+ <h1>Claude MPM Dashboard</h1>
492
+ <div class="subtitle">Fallback Mode - Template not found</div>
493
+ </div>
494
+
495
+ <div id="status" class="status healthy">
496
+ <h3>Server Status</h3>
497
+ <div class="metric">
498
+ <div class="metric-label">Health</div>
499
+ <div class="metric-value" id="health">Loading...</div>
500
+ </div>
501
+ <div class="metric">
502
+ <div class="metric-label">Uptime</div>
503
+ <div class="metric-value" id="uptime">Loading...</div>
504
+ </div>
505
+ <div class="metric">
506
+ <div class="metric-label">Events</div>
507
+ <div class="metric-value" id="events">Loading...</div>
508
+ </div>
509
+ </div>
510
+
511
+ <div class="events">
512
+ <h3>Recent Events</h3>
513
+ <div id="event-list">
514
+ <div class="event">Waiting for events...</div>
515
+ </div>
516
+ </div>
517
+ </div>
518
+
519
+ <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
520
+ <script>
521
+ // Fallback dashboard JavaScript
522
+ const socket = io();
523
+
524
+ // Update status periodically
525
+ async function updateStatus() {
526
+ try {
527
+ const response = await fetch('/api/status');
528
+ const data = await response.json();
529
+
530
+ document.getElementById('health').textContent = data.status;
531
+ document.getElementById('uptime').textContent = data.uptime.human;
532
+ document.getElementById('events').textContent = data.events.total;
533
+
534
+ const statusDiv = document.getElementById('status');
535
+ statusDiv.className = data.status === 'running' ? 'status healthy' : 'status degraded';
536
+ } catch (e) {
537
+ console.error('Failed to fetch status:', e);
538
+ }
539
+ }
540
+
541
+ // Listen for events
542
+ socket.on('claude_event', (event) => {
543
+ const eventList = document.getElementById('event-list');
544
+ const eventDiv = document.createElement('div');
545
+ eventDiv.className = 'event';
546
+ eventDiv.textContent = JSON.stringify(event, null, 2);
547
+ eventList.insertBefore(eventDiv, eventList.firstChild);
548
+
549
+ // Keep only last 10 events
550
+ while (eventList.children.length > 10) {
551
+ eventList.removeChild(eventList.lastChild);
552
+ }
553
+ });
554
+
555
+ socket.on('connect', () => {
556
+ console.log('Connected to dashboard server');
557
+ });
558
+
559
+ // Initial load and periodic updates
560
+ updateStatus();
561
+ setInterval(updateStatus, 5000);
562
+ </script>
563
+ </body>
564
+ </html>
565
+ '''
566
+
567
+ logger.warning("Serving fallback dashboard HTML")
568
+ return web.Response(text=fallback_html, content_type="text/html")
326
569
 
327
570
  async def _serve_static(self, request):
328
571
  """Serve static files."""
@@ -423,36 +666,230 @@ class StableDashboardServer:
423
666
  except Exception as e:
424
667
  return web.json_response({"error": str(e)}, status=500)
425
668
 
669
+ async def _health_check(self, request):
670
+ """Health check endpoint for monitoring."""
671
+ uptime = time.time() - self.server_start_time
672
+ status = "healthy" if self.is_healthy else "degraded"
673
+
674
+ health_info = {
675
+ "status": status,
676
+ "uptime_seconds": round(uptime, 2),
677
+ "connected_clients": len(self.connected_clients),
678
+ "event_count": self.event_count,
679
+ "last_event": self.last_event_time.isoformat() if self.last_event_time else None,
680
+ "retry_count": self.retry_count,
681
+ "health_check_failures": self.health_check_failures,
682
+ "event_history_size": len(self.event_history)
683
+ }
684
+
685
+ status_code = 200 if self.is_healthy else 503
686
+ return web.json_response(health_info, status=status_code)
687
+
688
+ async def _serve_status(self, request):
689
+ """Detailed server status endpoint."""
690
+ uptime = time.time() - self.server_start_time
691
+
692
+ status_info = {
693
+ "server": "stable",
694
+ "version": "4.2.3",
695
+ "status": "running" if self.is_healthy else "degraded",
696
+ "uptime": {
697
+ "seconds": round(uptime, 2),
698
+ "human": self._format_uptime(uptime)
699
+ },
700
+ "connections": {
701
+ "active": len(self.connected_clients),
702
+ "clients": list(self.connected_clients)
703
+ },
704
+ "events": {
705
+ "total": self.event_count,
706
+ "buffered": len(self.event_history),
707
+ "last_received": self.last_event_time.isoformat() if self.last_event_time else None
708
+ },
709
+ "features": [
710
+ "http", "socketio", "event_bridge", "health_monitoring",
711
+ "auto_retry", "event_history", "graceful_degradation"
712
+ ],
713
+ "resilience": {
714
+ "retry_count": self.retry_count,
715
+ "max_retries": self.max_retries,
716
+ "health_failures": self.health_check_failures,
717
+ "persist_events": self.persist_events
718
+ }
719
+ }
720
+ return web.json_response(status_info)
721
+
722
+ async def _serve_event_history(self, request):
723
+ """Serve recent event history."""
724
+ limit = int(request.query.get('limit', '100'))
725
+ events = list(self.event_history)[-limit:]
726
+ return web.json_response({
727
+ "events": events,
728
+ "count": len(events),
729
+ "total_events": self.event_count
730
+ })
731
+
732
+ async def _receive_event(self, request):
733
+ """Receive events from hook system via HTTP POST."""
734
+ try:
735
+ # Parse event data
736
+ data = await request.json()
737
+
738
+ # Add server metadata
739
+ event = {
740
+ **data,
741
+ "received_at": datetime.now().isoformat(),
742
+ "server_event_id": self.event_count + 1
743
+ }
744
+
745
+ # Update tracking
746
+ self.event_count += 1
747
+ self.last_event_time = datetime.now()
748
+
749
+ # Store in circular buffer
750
+ self.event_history.append(event)
751
+
752
+ # Persist to disk if enabled
753
+ if self.persist_events:
754
+ try:
755
+ with open(self.event_log_path, 'a') as f:
756
+ f.write(json.dumps(event) + '\n')
757
+ except Exception as e:
758
+ logger.error(f"Failed to persist event: {e}")
759
+
760
+ # Emit to all connected SocketIO clients
761
+ if self.sio and self.connected_clients:
762
+ await self.sio.emit("claude_event", event)
763
+ if self.debug:
764
+ print(f"📡 Forwarded event to {len(self.connected_clients)} clients")
765
+
766
+ # Return success response
767
+ return web.json_response({
768
+ "status": "received",
769
+ "event_id": event["server_event_id"],
770
+ "clients_notified": len(self.connected_clients)
771
+ })
772
+
773
+ except json.JSONDecodeError as e:
774
+ logger.error(f"Invalid JSON in event request: {e}")
775
+ return web.json_response(
776
+ {"error": "Invalid JSON", "details": str(e)},
777
+ status=400
778
+ )
779
+ except Exception as e:
780
+ logger.error(f"Error processing event: {e}")
781
+ if self.debug:
782
+ traceback.print_exc()
783
+ return web.json_response(
784
+ {"error": "Failed to process event", "details": str(e)},
785
+ status=500
786
+ )
787
+
426
788
  async def _serve_version(self, request):
427
789
  """Serve version information."""
428
790
  version_info = {
429
- "version": "4.2.2",
791
+ "version": "4.2.3",
430
792
  "server": "stable",
431
- "features": ["http", "socketio", "mock_ast"],
432
- "status": "running",
793
+ "features": ["http", "socketio", "event_bridge", "resilience"],
794
+ "status": "running" if self.is_healthy else "degraded",
433
795
  }
434
796
  return web.json_response(version_info)
797
+
798
+ def _format_uptime(self, seconds: float) -> str:
799
+ """Format uptime in human-readable format."""
800
+ days = int(seconds // 86400)
801
+ hours = int((seconds % 86400) // 3600)
802
+ minutes = int((seconds % 3600) // 60)
803
+ secs = int(seconds % 60)
804
+
805
+ parts = []
806
+ if days > 0:
807
+ parts.append(f"{days}d")
808
+ if hours > 0:
809
+ parts.append(f"{hours}h")
810
+ if minutes > 0:
811
+ parts.append(f"{minutes}m")
812
+ parts.append(f"{secs}s")
813
+
814
+ return " ".join(parts)
435
815
 
436
816
  def run(self):
437
- """Run the server with automatic port conflict resolution."""
438
- print("🔧 Setting up server...")
439
- if not self.setup():
440
- print("❌ Server setup failed")
441
- return False
817
+ """Run the server with automatic restart on crash."""
818
+ restart_attempts = 0
819
+ max_restart_attempts = 5
820
+
821
+ while restart_attempts < max_restart_attempts:
822
+ try:
823
+ print(f"🔧 Setting up server... (attempt {restart_attempts + 1}/{max_restart_attempts})")
824
+
825
+ # Reset health status on restart
826
+ self.is_healthy = True
827
+ self.health_check_failures = 0
828
+
829
+ if not self.setup():
830
+ if not DEPENDENCIES_AVAILABLE:
831
+ print("❌ Missing required dependencies")
832
+ return False
833
+
834
+ # Continue with fallback mode even if dashboard files not found
835
+ print("⚠️ Dashboard files not found - running in fallback mode")
836
+ print(" Server will provide basic functionality and receive events")
837
+
838
+ # Set up minimal server without dashboard files
839
+ self.sio = socketio.AsyncServer(
840
+ cors_allowed_origins="*",
841
+ logger=self.debug,
842
+ engineio_logger=self.debug,
843
+ ping_interval=30,
844
+ ping_timeout=60,
845
+ max_http_buffer_size=1e8,
846
+ )
847
+ self.app = web.Application()
848
+ self.sio.attach(self.app)
849
+ self._setup_routes()
850
+ self._setup_socketio_events()
851
+
852
+ return self._run_with_resilience()
853
+
854
+ except Exception as e:
855
+ restart_attempts += 1
856
+ logger.error(f"Server crashed: {e}")
857
+ if self.debug:
858
+ traceback.print_exc()
859
+
860
+ if restart_attempts < max_restart_attempts:
861
+ wait_time = min(2 ** restart_attempts, 30) # Exponential backoff, max 30s
862
+ print(f"🔄 Restarting server in {wait_time} seconds...")
863
+ time.sleep(wait_time)
864
+ else:
865
+ print(f"❌ Server failed after {max_restart_attempts} restart attempts")
866
+ return False
867
+
868
+ return False
869
+
870
+ def _run_with_resilience(self):
871
+ """Run server with port conflict resolution and error handling."""
442
872
 
443
873
  print(f"🚀 Starting stable dashboard server at http://{self.host}:{self.port}")
444
- print("✅ Server ready: HTTP + SocketIO on same port")
445
- print("🎯 This is a standalone server - no monitor service required")
446
- print("📡 SocketIO events registered:")
447
- print(" - connect/disconnect")
874
+ print("✅ Server ready: HTTP + SocketIO with resilience features")
875
+ print("🛡️ Resilience features enabled:")
876
+ print(" - Automatic restart on crash")
877
+ print(" - Health monitoring endpoint (/health)")
878
+ print(" - Event history buffer (500 events)")
879
+ print(" - Graceful degradation")
880
+ print(" - Connection retry logic")
881
+ print("📡 SocketIO events:")
882
+ print(" - claude_event (real-time events from hooks)")
448
883
  print(" - code:analyze:file (code analysis)")
449
- print(" - Various dashboard events")
450
- print("🌐 HTTP endpoints available:")
451
- print(" - GET / (dashboard)")
452
- print(" - GET /static/* (static files)")
453
- print(" - GET /api/directory/list (directory listing)")
454
- print(" - GET /api/file/read (file content)")
455
- print(" - GET /version.json (version info)")
884
+ print(" - connection management")
885
+ print("🌐 HTTP endpoints:")
886
+ print(" - GET / (dashboard)")
887
+ print(" - GET /health (health check)")
888
+ print(" - POST /api/events (receive hook events)")
889
+ print(" - GET /api/status (detailed status)")
890
+ print(" - GET /api/events/history (event history)")
891
+ print(" - GET /api/directory/list")
892
+ print(" - GET /api/file/read")
456
893
  print(f"\n🔗 Open in browser: http://{self.host}:{self.port}")
457
894
  print("\n Press Ctrl+C to stop the server\n")
458
895
 
@@ -473,10 +910,10 @@ class StableDashboardServer:
473
910
  access_log=None,
474
911
  print=lambda *args: None # Suppress startup messages in non-debug mode
475
912
  )
476
- break # Server started successfully
913
+ return True # Server started successfully
477
914
  except KeyboardInterrupt:
478
915
  print("\n🛑 Server stopped by user")
479
- break
916
+ return True
480
917
  except OSError as e:
481
918
  error_str = str(e)
482
919
  if "[Errno 48]" in error_str or "Address already in use" in error_str or "address already in use" in error_str.lower():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.2.6
3
+ Version: 4.2.7
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team
@@ -1,5 +1,5 @@
1
1
  claude_mpm/BUILD_NUMBER,sha256=toytnNjkIKPgQaGwDqQdC1rpNTAdSEc6Vja50d7Ovug,4
2
- claude_mpm/VERSION,sha256=y3hGbi5SzvubCW71VVd9sntScYZDVej66mVma8oVl-k,6
2
+ claude_mpm/VERSION,sha256=Ja49LMTSh-SA00LxW0FQuEWoJY55Ua7HEZO5p1DeXIM,6
3
3
  claude_mpm/__init__.py,sha256=lyTZAYGH4DTaFGLRNWJKk5Q5oTjzN5I6AXmfVX-Jff0,1512
4
4
  claude_mpm/__main__.py,sha256=Ro5UBWBoQaSAIoSqWAr7zkbLyvi4sSy28WShqAhKJG0,723
5
5
  claude_mpm/constants.py,sha256=I946iCQzIIPRZVVJ8aO7lA4euiyDnNw2IX7EelAOkIE,5915
@@ -71,7 +71,7 @@ claude_mpm/cli/commands/cleanup_orphaned_agents.py,sha256=JR8crvgrz7Sa6d-SI-gKyw
71
71
  claude_mpm/cli/commands/config.py,sha256=Yfi8WO-10_MYz2QipFw-yEzVvHKNQ6iSQXeyW5J85Cg,18559
72
72
  claude_mpm/cli/commands/configure.py,sha256=OsuISDoctmQyJpd0wn4LUFRR1AdoVLveT-pU6JJdW60,55439
73
73
  claude_mpm/cli/commands/configure_tui.py,sha256=VYLlm2B7QN8P3HtKMEO5Z7A9TpRicWvsojtNh4NxVWQ,66760
74
- claude_mpm/cli/commands/dashboard.py,sha256=68IZaSMFKnroaclKsORYaVu7jYV9jWH9sAeD1QHolIo,14097
74
+ claude_mpm/cli/commands/dashboard.py,sha256=N6LFEx7azVAexvsAXM2RLfKvTRKrEDN3dwUEQmw0Pd4,14316
75
75
  claude_mpm/cli/commands/debug.py,sha256=lupNJRzpPHeisREYmbQ-9E3A3pvKjSHsapYFJVH8sbc,47056
76
76
  claude_mpm/cli/commands/doctor.py,sha256=bOZzDNxEMNMZYrJnu_V82tyZ12r5FiBRQNLVfSVvRIQ,5774
77
77
  claude_mpm/cli/commands/info.py,sha256=_hWH7uNv9VLO0eZh9Ua6qc5L1Z66kYj9ATzU4Q8UkSM,7377
@@ -89,7 +89,7 @@ claude_mpm/cli/commands/mpm_init_handler.py,sha256=-pCB0XL3KipqGtnta8CC7Lg5TPMws
89
89
  claude_mpm/cli/commands/run.py,sha256=HrqRWCxmtCKcOxygXRM4KYlN1FDxBXrt4lxz0WunPTs,43390
90
90
  claude_mpm/cli/commands/socketio_monitor.py,sha256=GHHY5pKg0XCffoqLoO0l0Nxa9HQY4gdrpYebLVahzl4,9540
91
91
  claude_mpm/cli/commands/tickets.py,sha256=kl2dklTBnG3Y4jUUJ_PcEVsTx4CtVJfkGWboWBx_mQM,21234
92
- claude_mpm/cli/parsers/__init__.py,sha256=f0Fm1DDXorlVOZPLxUpjC-GIvLh01G-FZOK7TEV1L3I,1005
92
+ claude_mpm/cli/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
93
  claude_mpm/cli/parsers/agent_manager_parser.py,sha256=8HuGpTnHSOnTOqOHriBTzi8EzKLMSfqH2eFHs0dEuu4,8009
94
94
  claude_mpm/cli/parsers/agents_parser.py,sha256=DxAZMotptyaJbROqbRbTipOKLLJ96ATrXhwiFK6Dbm0,5450
95
95
  claude_mpm/cli/parsers/analyze_code_parser.py,sha256=cpJSMFbc3mqB4qrMBIEZiikzPekC2IQX-cjt9U2fHW4,5356
@@ -433,7 +433,7 @@ claude_mpm/services/core/interfaces/agent.py,sha256=EaPYn6k9HjB2DRTiZIMJwEIBABZF
433
433
  claude_mpm/services/core/interfaces/communication.py,sha256=evwtLbYCFa3Zb8kEfL10LOBVdwP4-n3a3wa7NqIHmKQ,8887
434
434
  claude_mpm/services/core/interfaces/infrastructure.py,sha256=eLtr_dFhA3Ux3mPOV_4DbWhGjHpfpGnj6xOhfQcgZGk,10037
435
435
  claude_mpm/services/core/interfaces/service.py,sha256=hNfHXe45LcPCp_dToOmZCfnUZBF5axMf_TdxqCSm2-I,11536
436
- claude_mpm/services/dashboard/stable_server.py,sha256=151lEl_uz28szFpUNc9lSCsNoTBP48IqxXuF_KpmYHU,19984
436
+ claude_mpm/services/dashboard/stable_server.py,sha256=xksZD-iYEvq-MQFnKPZRKFJku4HLiHTMF_5ufP00HyE,38157
437
437
  claude_mpm/services/diagnostics/__init__.py,sha256=WTRucANR9EwNi53rotjkeE4k75s18RjHJ8s1BfBj7ic,614
438
438
  claude_mpm/services/diagnostics/diagnostic_runner.py,sha256=cpCZ7JBvRIpGEchiwYsojmiGaI99Wf-hGxk8eem7xXQ,9164
439
439
  claude_mpm/services/diagnostics/doctor_reporter.py,sha256=Z8hYLqUbGC02gL7nX9kZKbLWRzOmTORRINfCr7EHbBI,10537
@@ -615,9 +615,9 @@ claude_mpm/utils/subprocess_utils.py,sha256=zgiwLqh_17WxHpySvUPH65pb4bzIeUGOAYUJ
615
615
  claude_mpm/validation/__init__.py,sha256=YZhwE3mhit-lslvRLuwfX82xJ_k4haZeKmh4IWaVwtk,156
616
616
  claude_mpm/validation/agent_validator.py,sha256=3Lo6LK-Mw9IdnL_bd3zl_R6FkgSVDYKUUM7EeVVD3jc,20865
617
617
  claude_mpm/validation/frontmatter_validator.py,sha256=u8g4Eyd_9O6ugj7Un47oSGh3kqv4wMkuks2i_CtWRvM,7028
618
- claude_mpm-4.2.6.dist-info/licenses/LICENSE,sha256=lpaivOlPuBZW1ds05uQLJJswy8Rp_HMNieJEbFlqvLk,1072
619
- claude_mpm-4.2.6.dist-info/METADATA,sha256=w4lTcx_Man2orXEGeND6EOxlXQQb6kc1UcF6-XaDeeQ,13776
620
- claude_mpm-4.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
621
- claude_mpm-4.2.6.dist-info/entry_points.txt,sha256=FDPZgz8JOvD-6iuXY2l9Zbo9zYVRuE4uz4Qr0vLeGOk,471
622
- claude_mpm-4.2.6.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
623
- claude_mpm-4.2.6.dist-info/RECORD,,
618
+ claude_mpm-4.2.7.dist-info/licenses/LICENSE,sha256=lpaivOlPuBZW1ds05uQLJJswy8Rp_HMNieJEbFlqvLk,1072
619
+ claude_mpm-4.2.7.dist-info/METADATA,sha256=sXlWAEREXV1KNO3rmseVO7v1T2wbcvgCByu9WzrrexU,13776
620
+ claude_mpm-4.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
621
+ claude_mpm-4.2.7.dist-info/entry_points.txt,sha256=FDPZgz8JOvD-6iuXY2l9Zbo9zYVRuE4uz4Qr0vLeGOk,471
622
+ claude_mpm-4.2.7.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
623
+ claude_mpm-4.2.7.dist-info/RECORD,,