claude-mpm 4.2.5__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.5
1
+ 4.2.7
@@ -71,9 +71,11 @@ class DashboardCommand(BaseCommand):
71
71
  port = getattr(args, "port", 8765)
72
72
  host = getattr(args, "host", "localhost")
73
73
  background = getattr(args, "background", False)
74
+ use_stable = getattr(args, "stable", True) # Default to stable server
75
+ debug = getattr(args, "debug", False)
74
76
 
75
77
  self.logger.info(
76
- f"Starting dashboard on {host}:{port} (background: {background})"
78
+ f"Starting dashboard on {host}:{port} (background: {background}, stable: {use_stable})"
77
79
  )
78
80
 
79
81
  # Check if dashboard is already running
@@ -100,47 +102,102 @@ class DashboardCommand(BaseCommand):
100
102
  },
101
103
  )
102
104
  return CommandResult.error_result("Failed to start dashboard in background")
103
- # Run in foreground mode - directly start the SocketIO server
104
- try:
105
- print(f"Starting dashboard server on {host}:{port}...")
106
- print("Press Ctrl+C to stop the server")
107
-
108
- # Create and start the Dashboard server (with monitor client)
109
- self.server = DashboardServer(host=host, port=port)
110
-
111
- # Set up signal handlers for graceful shutdown
112
- def signal_handler(signum, frame):
113
- print("\nShutting down dashboard server...")
105
+
106
+ # Run in foreground mode
107
+ server_started = False
108
+
109
+ # Try stable server first (or if explicitly requested)
110
+ if use_stable:
111
+ try:
112
+ self.logger.info("Starting stable dashboard server (no monitor dependency)...")
113
+ print(f"Starting stable dashboard server on {host}:{port}...")
114
+ print("Press Ctrl+C to stop the server")
115
+ print("\n✅ Using stable server - works without monitor service\n")
116
+
117
+ # Create and run the stable server
118
+ from ...services.dashboard.stable_server import StableDashboardServer
119
+ stable_server = StableDashboardServer(host=host, port=port, debug=debug)
120
+
121
+ # Set up signal handlers for graceful shutdown
122
+ def signal_handler(signum, frame):
123
+ print("\nShutting down dashboard server...")
124
+ sys.exit(0)
125
+
126
+ signal.signal(signal.SIGINT, signal_handler)
127
+ signal.signal(signal.SIGTERM, signal_handler)
128
+
129
+ # Run the server (blocking)
130
+ result = stable_server.run()
131
+ if result:
132
+ # Server ran successfully and stopped normally
133
+ server_started = True
134
+ return CommandResult.success_result("Dashboard server stopped")
135
+ else:
136
+ # Server failed to start (e.g., couldn't find templates)
137
+ server_started = False
138
+ self.logger.warning("Stable server failed to start, trying advanced server...")
139
+
140
+ except KeyboardInterrupt:
141
+ print("\nDashboard server stopped by user")
142
+ return CommandResult.success_result("Dashboard server stopped")
143
+ except Exception as e:
144
+ self.logger.warning(f"Stable server failed: {e}")
145
+ if not getattr(args, "no_fallback", False):
146
+ print(f"\n⚠️ Stable server failed: {e}")
147
+ print("Attempting fallback to advanced server...")
148
+ else:
149
+ return CommandResult.error_result(f"Failed to start stable dashboard: {e}")
150
+
151
+ # Fallback to advanced DashboardServer if stable server failed or not requested
152
+ if not server_started and not getattr(args, "stable_only", False):
153
+ try:
154
+ self.logger.info("Attempting to start advanced dashboard server with monitor...")
155
+ print(f"\nStarting advanced dashboard server on {host}:{port}...")
156
+ print("Note: This requires monitor service on port 8766")
157
+ print("Press Ctrl+C to stop the server")
158
+
159
+ # Create and start the Dashboard server (with monitor client)
160
+ self.server = DashboardServer(host=host, port=port)
161
+
162
+ # Set up signal handlers for graceful shutdown
163
+ def signal_handler(signum, frame):
164
+ print("\nShutting down dashboard server...")
165
+ if self.server:
166
+ self.server.stop_sync()
167
+ sys.exit(0)
168
+
169
+ signal.signal(signal.SIGINT, signal_handler)
170
+ signal.signal(signal.SIGTERM, signal_handler)
171
+
172
+ # Start the server (this starts in background thread)
173
+ self.server.start_sync()
174
+
175
+ # Keep the main thread alive while server is running
176
+ # The server runs in a background thread, so we need to block here
177
+ try:
178
+ while self.server.is_running():
179
+ time.sleep(1)
180
+ except KeyboardInterrupt:
181
+ # Ctrl+C pressed, stop the server
182
+ pass
183
+
184
+ # Server has stopped or user interrupted
114
185
  if self.server:
115
186
  self.server.stop_sync()
116
- sys.exit(0)
117
187
 
118
- signal.signal(signal.SIGINT, signal_handler)
119
- signal.signal(signal.SIGTERM, signal_handler)
188
+ return CommandResult.success_result("Dashboard server stopped")
120
189
 
121
- # Start the server (this starts in background thread)
122
- self.server.start_sync()
123
-
124
- # Keep the main thread alive while server is running
125
- # The server runs in a background thread, so we need to block here
126
- try:
127
- while self.server.is_running():
128
- time.sleep(1)
129
190
  except KeyboardInterrupt:
130
- # Ctrl+C pressed, stop the server
131
- pass
132
-
133
- # Server has stopped or user interrupted
134
- if self.server:
135
- self.server.stop_sync()
136
-
137
- return CommandResult.success_result("Dashboard server stopped")
138
-
139
- except KeyboardInterrupt:
140
- print("\nDashboard server stopped by user")
141
- return CommandResult.success_result("Dashboard server stopped")
142
- except Exception as e:
143
- return CommandResult.error_result(f"Failed to start dashboard: {e}")
191
+ print("\nDashboard server stopped by user")
192
+ return CommandResult.success_result("Dashboard server stopped")
193
+ except Exception as e:
194
+ # If both servers fail, provide helpful error message
195
+ error_msg = f"Failed to start dashboard: {e}\n\n"
196
+ error_msg += "💡 Troubleshooting tips:\n"
197
+ error_msg += f" - Check if port {port} is already in use\n"
198
+ error_msg += " - Try running with --stable flag for standalone mode\n"
199
+ error_msg += " - Use --debug flag for more details\n"
200
+ return CommandResult.error_result(error_msg)
144
201
 
145
202
  def _stop_dashboard(self, args) -> CommandResult:
146
203
  """Stop the dashboard server."""
@@ -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."""
@@ -168,6 +182,27 @@ class StableDashboardServer:
168
182
  self.dashboard_path = None
169
183
  self.app = None
170
184
  self.sio = None
185
+ self.server_runner = None
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)
171
206
 
172
207
  def setup(self) -> bool:
173
208
  """Set up the server components."""
@@ -177,24 +212,50 @@ class StableDashboardServer:
177
212
  )
178
213
  return False
179
214
 
180
- # Find dashboard files
181
- self.dashboard_path = find_dashboard_files()
215
+ # Find dashboard files only if not already set (for testing)
182
216
  if not self.dashboard_path:
183
- print("❌ Error: Could not find dashboard files")
184
- 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")
185
235
  return False
186
236
 
237
+ if self.debug:
238
+ print(f"🔍 Debug: Dashboard path resolved to: {self.dashboard_path}")
239
+ print(f"🔍 Debug: Checking for required files...")
240
+ template_exists = (self.dashboard_path / "templates" / "index.html").exists()
241
+ static_exists = (self.dashboard_path / "static").exists()
242
+ print(f" - templates/index.html: {template_exists}")
243
+ print(f" - static directory: {static_exists}")
244
+
187
245
  print(f"📁 Using dashboard files from: {self.dashboard_path}")
188
246
 
189
247
  # Create SocketIO server with improved timeout settings
248
+ logger_enabled = self.debug # Only enable verbose logging in debug mode
190
249
  self.sio = socketio.AsyncServer(
191
250
  cors_allowed_origins="*",
192
- logger=True,
193
- engineio_logger=True,
251
+ logger=logger_enabled,
252
+ engineio_logger=logger_enabled,
194
253
  ping_interval=30, # Match client's 30 second ping interval
195
254
  ping_timeout=60, # Match client's 60 second timeout
196
255
  max_http_buffer_size=1e8, # Allow larger messages
197
256
  )
257
+ # Create app WITHOUT any static file handlers to prevent directory listing
258
+ # This is critical - we only want explicit routes we define
198
259
  self.app = web.Application()
199
260
  self.sio.attach(self.app)
200
261
  print("✅ SocketIO server created and attached")
@@ -209,33 +270,61 @@ class StableDashboardServer:
209
270
 
210
271
  def _setup_routes(self):
211
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
212
275
  self.app.router.add_get("/", self._serve_dashboard)
276
+ self.app.router.add_get("/index.html", self._serve_dashboard) # Also handle /index.html
213
277
  self.app.router.add_get("/static/{path:.*}", self._serve_static)
214
278
  self.app.router.add_get("/api/directory/list", self._list_directory)
215
279
  self.app.router.add_get("/api/file/read", self._read_file)
216
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)
217
289
 
218
290
  def _setup_socketio_events(self):
219
291
  """Set up SocketIO event handlers."""
220
292
 
221
293
  @self.sio.event
222
294
  async def connect(sid, environ):
223
- print(f"✅ SocketIO client connected: {sid}")
224
- print(f" Client info: {environ.get('HTTP_USER_AGENT', 'Unknown')}")
225
- # Send a test message to confirm connection
295
+ self.connected_clients.add(sid)
296
+ if self.debug:
297
+ print(f"✅ SocketIO client connected: {sid}")
298
+ user_agent = environ.get('HTTP_USER_AGENT', 'Unknown')
299
+ # Truncate long user agents for readability
300
+ if len(user_agent) > 80:
301
+ user_agent = user_agent[:77] + "..."
302
+ print(f" Client info: {user_agent}")
303
+
304
+ # Send connection confirmation
226
305
  await self.sio.emit(
227
306
  "connection_test", {"status": "connected", "server": "stable"}, room=sid
228
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)
229
315
 
230
316
  @self.sio.event
231
317
  async def disconnect(sid):
232
- print(f"❌ SocketIO client disconnected: {sid}")
318
+ self.connected_clients.discard(sid)
319
+ if self.debug:
320
+ print(f"📤 SocketIO client disconnected: {sid}")
233
321
 
234
322
  @self.sio.event
235
323
  async def code_analyze_file(sid, data):
236
- print(
237
- f"📡 Received file analysis request from {sid}: {data.get('path', 'unknown')}"
238
- )
324
+ if self.debug:
325
+ print(
326
+ f"📡 Received file analysis request from {sid}: {data.get('path', 'unknown')}"
327
+ )
239
328
 
240
329
  file_path = data.get("path", "")
241
330
  file_name = file_path.split("/")[-1] if file_path else "unknown"
@@ -243,15 +332,17 @@ class StableDashboardServer:
243
332
  # Create mock response
244
333
  response = create_mock_ast_data(file_path, file_name)
245
334
 
246
- print(f"📤 Sending analysis response: {len(response['elements'])} elements")
335
+ if self.debug:
336
+ print(f"📤 Sending analysis response: {len(response['elements'])} elements")
247
337
  await self.sio.emit("code:file:analyzed", response, room=sid)
248
338
 
249
339
  # CRITICAL: Handle the actual event name with colons that the client sends
250
340
  @self.sio.on("code:analyze:file")
251
341
  async def handle_code_analyze_file(sid, data):
252
- print(
253
- f"📡 Received code:analyze:file from {sid}: {data.get('path', 'unknown')}"
254
- )
342
+ if self.debug:
343
+ print(
344
+ f"📡 Received code:analyze:file from {sid}: {data.get('path', 'unknown')}"
345
+ )
255
346
 
256
347
  file_path = data.get("path", "")
257
348
  file_name = file_path.split("/")[-1] if file_path else "unknown"
@@ -259,20 +350,23 @@ class StableDashboardServer:
259
350
  # Create mock response
260
351
  response = create_mock_ast_data(file_path, file_name)
261
352
 
262
- print(f"📤 Sending analysis response: {len(response['elements'])} elements")
353
+ if self.debug:
354
+ print(f"📤 Sending analysis response: {len(response['elements'])} elements")
263
355
  await self.sio.emit("code:file:analyzed", response, room=sid)
264
356
 
265
357
  # Handle other events the dashboard sends
266
358
  @self.sio.event
267
359
  async def get_git_branch(sid, data):
268
- print(f"📡 Received git branch request from {sid}: {data}")
360
+ if self.debug:
361
+ print(f"📡 Received git branch request from {sid}: {data}")
269
362
  await self.sio.emit(
270
363
  "git_branch_response", {"branch": "main", "path": data}, room=sid
271
364
  )
272
365
 
273
366
  @self.sio.event
274
367
  async def request_status(sid, data):
275
- print(f"📡 Received status request from {sid}")
368
+ if self.debug:
369
+ print(f"📡 Received status request from {sid}")
276
370
  await self.sio.emit(
277
371
  "status_response", {"status": "running", "server": "stable"}, room=sid
278
372
  )
@@ -280,24 +374,198 @@ class StableDashboardServer:
280
374
  # Handle the event with dots (SocketIO converts colons to dots sometimes)
281
375
  @self.sio.event
282
376
  async def request_dot_status(sid, data):
283
- print(f"📡 Received request.status from {sid}")
377
+ if self.debug:
378
+ print(f"📡 Received request.status from {sid}")
284
379
  await self.sio.emit(
285
380
  "status_response", {"status": "running", "server": "stable"}, room=sid
286
381
  )
287
382
 
288
383
  @self.sio.event
289
384
  async def code_discover_top_level(sid, data):
290
- print(f"📡 Received top-level discovery request from {sid}")
385
+ if self.debug:
386
+ print(f"📡 Received top-level discovery request from {sid}")
291
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}")
292
449
 
293
450
  async def _serve_dashboard(self, request):
294
- """Serve the main dashboard HTML."""
295
- dashboard_file = self.dashboard_path / "templates" / "index.html"
296
- if dashboard_file.exists():
297
- with open(dashboard_file) as f:
298
- content = f.read()
299
- return web.Response(text=content, content_type="text/html")
300
- 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")
301
569
 
302
570
  async def _serve_static(self, request):
303
571
  """Serve static files."""
@@ -398,33 +666,232 @@ class StableDashboardServer:
398
666
  except Exception as e:
399
667
  return web.json_response({"error": str(e)}, status=500)
400
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
+
401
788
  async def _serve_version(self, request):
402
789
  """Serve version information."""
403
790
  version_info = {
404
- "version": "4.2.2",
791
+ "version": "4.2.3",
405
792
  "server": "stable",
406
- "features": ["http", "socketio", "mock_ast"],
407
- "status": "running",
793
+ "features": ["http", "socketio", "event_bridge", "resilience"],
794
+ "status": "running" if self.is_healthy else "degraded",
408
795
  }
409
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)
410
815
 
411
816
  def run(self):
412
- """Run the server with automatic port conflict resolution."""
413
- print("🔧 Setting up server...")
414
- if not self.setup():
415
- print("❌ Server setup failed")
416
- 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."""
417
872
 
418
873
  print(f"🚀 Starting stable dashboard server at http://{self.host}:{self.port}")
419
- print("✅ Server ready: HTTP + SocketIO on same port")
420
- print("📡 SocketIO events registered:")
421
- print(" - connect/disconnect")
422
- print(" - code_analyze_file (from 'code:analyze:file')")
423
- print("🌐 HTTP endpoints available:")
424
- print(" - GET / (dashboard)")
425
- print(" - GET /static/* (static files)")
426
- print(" - GET /api/directory/list (directory API)")
427
- print(f"🔗 Open in browser: http://{self.host}:{self.port}")
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)")
883
+ print(" - code:analyze:file (code analysis)")
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")
893
+ print(f"\n🔗 Open in browser: http://{self.host}:{self.port}")
894
+ print("\n Press Ctrl+C to stop the server\n")
428
895
 
429
896
  # Try to start server with port conflict handling
430
897
  max_port_attempts = 10
@@ -432,18 +899,29 @@ class StableDashboardServer:
432
899
 
433
900
  for attempt in range(max_port_attempts):
434
901
  try:
435
- web.run_app(self.app, host=self.host, port=self.port, access_log=None)
436
- break # Server started successfully
902
+ # Use the print_func parameter to control access log output
903
+ if self.debug:
904
+ web.run_app(self.app, host=self.host, port=self.port)
905
+ else:
906
+ web.run_app(
907
+ self.app,
908
+ host=self.host,
909
+ port=self.port,
910
+ access_log=None,
911
+ print=lambda *args: None # Suppress startup messages in non-debug mode
912
+ )
913
+ return True # Server started successfully
437
914
  except KeyboardInterrupt:
438
915
  print("\n🛑 Server stopped by user")
439
- break
916
+ return True
440
917
  except OSError as e:
441
- if "[Errno 48]" in str(e) or "Address already in use" in str(e):
442
- # Port is already in use, try next port
918
+ error_str = str(e)
919
+ if "[Errno 48]" in error_str or "Address already in use" in error_str or "address already in use" in error_str.lower():
920
+ # Port is already in use
443
921
  if attempt < max_port_attempts - 1:
444
922
  self.port += 1
445
923
  print(
446
- f"⚠️ Port {self.port - 1} in use, trying port {self.port}..."
924
+ f"⚠️ Port {self.port - 1} is in use, trying port {self.port}..."
447
925
  )
448
926
  # Recreate the app with new port
449
927
  self.setup()
@@ -452,21 +930,23 @@ class StableDashboardServer:
452
930
  f"❌ Could not find available port after {max_port_attempts} attempts"
453
931
  )
454
932
  print(f" Ports {original_port} to {self.port} are all in use")
933
+ print("\n💡 Tip: Check if another dashboard instance is running")
934
+ print(" You can stop it with: claude-mpm dashboard stop")
455
935
  return False
456
936
  else:
457
937
  # Other OS error
458
938
  print(f"❌ Server error: {e}")
459
939
  if self.debug:
460
940
  import traceback
461
-
462
941
  traceback.print_exc()
463
942
  return False
464
943
  except Exception as e:
465
- print(f"❌ Server error: {e}")
944
+ print(f"❌ Unexpected server error: {e}")
466
945
  if self.debug:
467
946
  import traceback
468
-
469
947
  traceback.print_exc()
948
+ else:
949
+ print("\n💡 Run with --debug flag for more details")
470
950
  return False
471
951
 
472
952
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.2.5
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=rwXIWtsOJVrhT-T2eEOfESQ3sLYYzm_WGcjbp4g3dRE,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=dh9B39Da1Y4ppqHZyzXu7vzCIpA7SvoID4Gy5AZkELI,11053
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=vUiWXutYX5kMkSB3_FBuCp9qnlSAT0bnXjNlI9ybe3Q,17752
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.5.dist-info/licenses/LICENSE,sha256=lpaivOlPuBZW1ds05uQLJJswy8Rp_HMNieJEbFlqvLk,1072
619
- claude_mpm-4.2.5.dist-info/METADATA,sha256=y8K0gWlLWR4tW2V8YD-qp3Q8m9MbjNxVYodwr1oLKQs,13776
620
- claude_mpm-4.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
621
- claude_mpm-4.2.5.dist-info/entry_points.txt,sha256=FDPZgz8JOvD-6iuXY2l9Zbo9zYVRuE4uz4Qr0vLeGOk,471
622
- claude_mpm-4.2.5.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
623
- claude_mpm-4.2.5.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,,