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 +1 -1
- claude_mpm/cli/commands/dashboard.py +94 -37
- claude_mpm/cli/parsers/__init__.py +0 -29
- claude_mpm/services/dashboard/stable_server.py +536 -56
- {claude_mpm-4.2.5.dist-info → claude_mpm-4.2.7.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.5.dist-info → claude_mpm-4.2.7.dist-info}/RECORD +10 -10
- {claude_mpm-4.2.5.dist-info → claude_mpm-4.2.7.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.5.dist-info → claude_mpm-4.2.7.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.5.dist-info → claude_mpm-4.2.7.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.5.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
|
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
print("\
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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=
|
|
193
|
-
engineio_logger=
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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.
|
|
791
|
+
"version": "4.2.3",
|
|
405
792
|
"server": "stable",
|
|
406
|
-
"features": ["http", "socketio", "
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
420
|
-
print("
|
|
421
|
-
print(" -
|
|
422
|
-
print(" -
|
|
423
|
-
print("
|
|
424
|
-
print(" -
|
|
425
|
-
print(" -
|
|
426
|
-
print("
|
|
427
|
-
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)")
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
916
|
+
return True
|
|
440
917
|
except OSError as e:
|
|
441
|
-
|
|
442
|
-
|
|
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"⚠️
|
|
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"❌
|
|
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,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
|