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 +1 -1
- claude_mpm/cli/commands/dashboard.py +5 -1
- claude_mpm/cli/parsers/__init__.py +0 -29
- claude_mpm/services/dashboard/stable_server.py +471 -34
- {claude_mpm-4.2.6.dist-info → claude_mpm-4.2.7.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.6.dist-info → claude_mpm-4.2.7.dist-info}/RECORD +10 -10
- {claude_mpm-4.2.6.dist-info → claude_mpm-4.2.7.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.6.dist-info → claude_mpm-4.2.7.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.6.dist-info → claude_mpm-4.2.7.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.6.dist-info → claude_mpm-4.2.7.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.2.
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
|
791
|
+
"version": "4.2.3",
|
|
430
792
|
"server": "stable",
|
|
431
|
-
"features": ["http", "socketio", "
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
445
|
-
print("
|
|
446
|
-
print("
|
|
447
|
-
print(" -
|
|
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(" -
|
|
450
|
-
print("🌐 HTTP endpoints
|
|
451
|
-
print(" - GET /
|
|
452
|
-
print(" - GET /
|
|
453
|
-
print(" -
|
|
454
|
-
print(" - GET /api/
|
|
455
|
-
print(" - GET /
|
|
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
|
-
|
|
913
|
+
return True # Server started successfully
|
|
477
914
|
except KeyboardInterrupt:
|
|
478
915
|
print("\n🛑 Server stopped by user")
|
|
479
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
claude_mpm/BUILD_NUMBER,sha256=toytnNjkIKPgQaGwDqQdC1rpNTAdSEc6Vja50d7Ovug,4
|
|
2
|
-
claude_mpm/VERSION,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
619
|
-
claude_mpm-4.2.
|
|
620
|
-
claude_mpm-4.2.
|
|
621
|
-
claude_mpm-4.2.
|
|
622
|
-
claude_mpm-4.2.
|
|
623
|
-
claude_mpm-4.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|