claude-mpm 4.2.2__py3-none-any.whl → 4.2.4__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/monitor.py +1 -1
- claude_mpm/core/config.py +2 -2
- claude_mpm/dashboard/static/css/code-tree.css +220 -1
- claude_mpm/dashboard/static/css/dashboard.css +286 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1833 -89
- claude_mpm/dashboard/static/js/socket-client.js +11 -8
- claude_mpm/dashboard/templates/index.html +41 -40
- claude_mpm/services/agents/deployment/agent_template_builder.py +17 -4
- claude_mpm/services/dashboard/stable_server.py +482 -0
- claude_mpm/services/socketio/client_proxy.py +16 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
- claude_mpm/services/socketio/monitor_server.py +2 -2
- claude_mpm/services/socketio/server/core.py +62 -0
- claude_mpm/tools/code_tree_analyzer.py +95 -17
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/RECORD +21 -20
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/top_level.txt +0 -0
|
@@ -248,7 +248,7 @@ class SocketClient {
|
|
|
248
248
|
* @private
|
|
249
249
|
* @const
|
|
250
250
|
*/
|
|
251
|
-
this.pingTimeout =
|
|
251
|
+
this.pingTimeout = 120000; // 120 seconds for health check (more lenient for stability)
|
|
252
252
|
|
|
253
253
|
/**
|
|
254
254
|
* Health check interval timer.
|
|
@@ -346,13 +346,13 @@ class SocketClient {
|
|
|
346
346
|
autoConnect: true,
|
|
347
347
|
reconnection: true,
|
|
348
348
|
reconnectionDelay: 1000,
|
|
349
|
-
reconnectionDelayMax:
|
|
350
|
-
reconnectionAttempts:
|
|
351
|
-
timeout:
|
|
349
|
+
reconnectionDelayMax: 10000, // Increased max delay for stability
|
|
350
|
+
reconnectionAttempts: 10, // Increased attempts for better resilience
|
|
351
|
+
timeout: 30000, // Increased connection timeout to 30 seconds
|
|
352
352
|
forceNew: true,
|
|
353
353
|
transports: ['websocket', 'polling'],
|
|
354
|
-
pingInterval:
|
|
355
|
-
pingTimeout:
|
|
354
|
+
pingInterval: 30000, // Increased ping interval for stability
|
|
355
|
+
pingTimeout: 60000 // Much longer timeout for better stability
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
this.setupSocketHandlers();
|
|
@@ -1218,8 +1218,11 @@ class SocketClient {
|
|
|
1218
1218
|
transformedEvent[key] = eventData.data[key];
|
|
1219
1219
|
}
|
|
1220
1220
|
} else {
|
|
1221
|
-
// Log
|
|
1222
|
-
|
|
1221
|
+
// Log debug info if data field would overwrite a protected field
|
|
1222
|
+
// Only log for non-timestamp fields to reduce noise
|
|
1223
|
+
if (key !== 'timestamp') {
|
|
1224
|
+
console.debug(`Protected field '${key}' in data object was not copied to top level to preserve event structure`);
|
|
1225
|
+
}
|
|
1223
1226
|
}
|
|
1224
1227
|
});
|
|
1225
1228
|
|
|
@@ -247,7 +247,6 @@
|
|
|
247
247
|
<button class="tab-button" data-tab="files">📁 Files</button>
|
|
248
248
|
<button class="tab-button" data-tab="activity">🌳 Activity</button>
|
|
249
249
|
<button class="tab-button" data-tab="code">🧬 Code</button>
|
|
250
|
-
<a href="/code-simple" class="tab-button" style="background: #f7fafc; color: #4a5568; text-decoration: none; border-left: 2px solid #e2e8f0;">📁 Simple View</a>
|
|
251
250
|
</div>
|
|
252
251
|
|
|
253
252
|
<!-- Events Tab -->
|
|
@@ -389,45 +388,52 @@
|
|
|
389
388
|
<!-- Code Tab -->
|
|
390
389
|
<div class="tab-content" id="code-tab">
|
|
391
390
|
<div class="code-container">
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
<div class="
|
|
395
|
-
<
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<span class="stat-compact">⚡ <span id="function-count">0</span></span>
|
|
404
|
-
<span class="stat-compact">📝 <span id="line-count">0</span></span>
|
|
391
|
+
<div id="code-tree-container" class="code-tree-container">
|
|
392
|
+
<!-- Top-left corner: Language selector -->
|
|
393
|
+
<div class="tree-corner-controls top-left">
|
|
394
|
+
<div class="control-group">
|
|
395
|
+
<label class="control-label">Languages:</label>
|
|
396
|
+
<div class="checkbox-group">
|
|
397
|
+
<label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="python" checked> Python</label>
|
|
398
|
+
<label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="javascript" checked> JS</label>
|
|
399
|
+
<label class="checkbox-label"><input type="checkbox" class="language-checkbox" value="typescript" checked> TS</label>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
405
402
|
</div>
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
403
|
+
|
|
404
|
+
<!-- Top-right corner: Layout and search -->
|
|
405
|
+
<div class="tree-corner-controls top-right">
|
|
406
|
+
<div class="control-group">
|
|
407
|
+
<select id="code-layout" class="select-compact">
|
|
408
|
+
<option value="tree">Tree</option>
|
|
409
|
+
<option value="radial">Radial</option>
|
|
410
|
+
</select>
|
|
411
|
+
<input type="text" id="code-search" placeholder="Search..." class="search-compact">
|
|
412
|
+
</div>
|
|
414
413
|
</div>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
<
|
|
421
|
-
<
|
|
422
|
-
<
|
|
423
|
-
<label><input type="checkbox" class="language-checkbox" value="typescript" checked> TS</label>
|
|
414
|
+
|
|
415
|
+
<!-- Bottom-left corner: Stats and Status -->
|
|
416
|
+
<div class="tree-corner-controls bottom-left">
|
|
417
|
+
<div class="stats-display" id="code-stats">
|
|
418
|
+
<span id="stats-files">0 files</span> •
|
|
419
|
+
<span id="stats-classes">0 classes</span> •
|
|
420
|
+
<span id="stats-functions">0 functions</span> •
|
|
421
|
+
<span id="stats-methods">0 methods</span>
|
|
424
422
|
</div>
|
|
425
|
-
<div class="
|
|
426
|
-
<
|
|
423
|
+
<div class="status-display" id="code-breadcrumb">
|
|
424
|
+
<div class="breadcrumb-ticker" id="breadcrumb-ticker">
|
|
425
|
+
<span id="breadcrumb-content">Ready to analyze...</span>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<!-- Bottom-right corner: Ignore patterns -->
|
|
431
|
+
<div class="tree-corner-controls bottom-right">
|
|
432
|
+
<div class="control-group">
|
|
433
|
+
<label class="control-label">Ignore:</label>
|
|
434
|
+
<input type="text" id="ignore-patterns" placeholder="test*, *.spec.js, node_modules" class="input-compact">
|
|
427
435
|
</div>
|
|
428
436
|
</div>
|
|
429
|
-
</div>
|
|
430
|
-
<div id="code-tree-container" class="code-tree-container">
|
|
431
437
|
<div id="code-tree"></div>
|
|
432
438
|
<!-- Collapsible legend -->
|
|
433
439
|
<div class="tree-legend collapsed" id="tree-legend" style="display: none;">
|
|
@@ -447,11 +453,6 @@
|
|
|
447
453
|
</div>
|
|
448
454
|
</div>
|
|
449
455
|
</div>
|
|
450
|
-
<div class="code-breadcrumb" id="code-breadcrumb">
|
|
451
|
-
<div class="breadcrumb-ticker" id="breadcrumb-ticker">
|
|
452
|
-
<span id="breadcrumb-content">Ready to analyze...</span>
|
|
453
|
-
</div>
|
|
454
|
-
</div>
|
|
455
456
|
</div>
|
|
456
457
|
</div>
|
|
457
458
|
|
|
@@ -704,16 +704,29 @@ tools:
|
|
|
704
704
|
if triggers and not examples:
|
|
705
705
|
# Convert first trigger to example with commentary
|
|
706
706
|
trigger = triggers[0]
|
|
707
|
+
|
|
708
|
+
# Handle both string and dict trigger formats
|
|
709
|
+
if isinstance(trigger, dict):
|
|
710
|
+
# New format with pattern and confidence
|
|
711
|
+
trigger_text = trigger.get("pattern", "")
|
|
712
|
+
else:
|
|
713
|
+
# Old format with simple string
|
|
714
|
+
trigger_text = str(trigger)
|
|
715
|
+
|
|
716
|
+
# Skip if we don't have valid trigger text
|
|
717
|
+
if not trigger_text:
|
|
718
|
+
return examples
|
|
719
|
+
|
|
707
720
|
agent_type = template_data.get("agent_type", "general")
|
|
708
721
|
|
|
709
722
|
examples.extend(
|
|
710
723
|
[
|
|
711
724
|
"<example>",
|
|
712
|
-
f"Context: When user needs {
|
|
713
|
-
f'user: "{
|
|
714
|
-
f'assistant: "I\'ll use the {agent_name} agent for {
|
|
725
|
+
f"Context: When user needs {trigger_text}",
|
|
726
|
+
f'user: "{trigger_text}"',
|
|
727
|
+
f'assistant: "I\'ll use the {agent_name} agent for {trigger_text}."',
|
|
715
728
|
"<commentary>",
|
|
716
|
-
f"This {agent_type} agent is appropriate because it has specialized capabilities for {
|
|
729
|
+
f"This {agent_type} agent is appropriate because it has specialized capabilities for {trigger_text.lower()} tasks.",
|
|
717
730
|
"</commentary>",
|
|
718
731
|
"</example>",
|
|
719
732
|
]
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stable Dashboard Server for claude-mpm.
|
|
3
|
+
|
|
4
|
+
WHY: This module provides a simple, stable HTTP + SocketIO server that works
|
|
5
|
+
across all installation methods (direct, pip, pipx, homebrew, npm).
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Uses proven python-socketio + aiohttp combination
|
|
9
|
+
- Automatically finds dashboard files across installation methods
|
|
10
|
+
- Provides both HTTP endpoints and SocketIO real-time features
|
|
11
|
+
- Simple mock AST analysis to avoid complex backend dependencies
|
|
12
|
+
- Graceful fallbacks for missing dependencies
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import glob
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import aiohttp
|
|
23
|
+
import socketio
|
|
24
|
+
from aiohttp import web
|
|
25
|
+
|
|
26
|
+
DEPENDENCIES_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
DEPENDENCIES_AVAILABLE = False
|
|
29
|
+
socketio = None
|
|
30
|
+
aiohttp = None
|
|
31
|
+
web = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_dashboard_files() -> Optional[Path]:
|
|
35
|
+
"""Find dashboard files across different installation methods."""
|
|
36
|
+
# Try different possible locations
|
|
37
|
+
possible_locations = [
|
|
38
|
+
# Development/direct install
|
|
39
|
+
Path(__file__).parent.parent.parent / "dashboard",
|
|
40
|
+
# Current working directory (for development)
|
|
41
|
+
Path.cwd() / "src" / "claude_mpm" / "dashboard",
|
|
42
|
+
# Pip install in current Python environment
|
|
43
|
+
Path(sys.prefix)
|
|
44
|
+
/ "lib"
|
|
45
|
+
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
46
|
+
/ "site-packages"
|
|
47
|
+
/ "claude_mpm"
|
|
48
|
+
/ "dashboard",
|
|
49
|
+
# User site-packages
|
|
50
|
+
Path.home()
|
|
51
|
+
/ ".local"
|
|
52
|
+
/ "lib"
|
|
53
|
+
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
54
|
+
/ "site-packages"
|
|
55
|
+
/ "claude_mpm"
|
|
56
|
+
/ "dashboard",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Add glob patterns for different Python versions
|
|
60
|
+
python_patterns = [
|
|
61
|
+
f"/opt/homebrew/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/claude_mpm/dashboard",
|
|
62
|
+
f"/usr/local/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/claude_mpm/dashboard",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Check direct paths first
|
|
66
|
+
for location in possible_locations:
|
|
67
|
+
if location.exists() and (location / "templates" / "index.html").exists():
|
|
68
|
+
return location
|
|
69
|
+
|
|
70
|
+
# Check pattern-based paths
|
|
71
|
+
for pattern in python_patterns:
|
|
72
|
+
matches = glob.glob(pattern)
|
|
73
|
+
for match in matches:
|
|
74
|
+
path = Path(match)
|
|
75
|
+
if path.exists() and (path / "templates" / "index.html").exists():
|
|
76
|
+
return path
|
|
77
|
+
|
|
78
|
+
# Fallback: try to find via module import
|
|
79
|
+
try:
|
|
80
|
+
import claude_mpm.dashboard
|
|
81
|
+
|
|
82
|
+
module_path = Path(claude_mpm.dashboard.__file__).parent
|
|
83
|
+
if (module_path / "templates" / "index.html").exists():
|
|
84
|
+
return module_path
|
|
85
|
+
except ImportError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def create_mock_ast_data(file_path: str, file_name: str) -> Dict[str, Any]:
|
|
92
|
+
"""Create mock AST analysis data."""
|
|
93
|
+
ext = file_name.split(".")[-1].lower() if "." in file_name else ""
|
|
94
|
+
|
|
95
|
+
elements = []
|
|
96
|
+
if ext == "py":
|
|
97
|
+
elements = [
|
|
98
|
+
{
|
|
99
|
+
"name": "MockClass",
|
|
100
|
+
"type": "class",
|
|
101
|
+
"line": 10,
|
|
102
|
+
"complexity": 2,
|
|
103
|
+
"docstring": "Mock class for demonstration",
|
|
104
|
+
"methods": [
|
|
105
|
+
{"name": "__init__", "type": "method", "line": 11, "complexity": 1},
|
|
106
|
+
{
|
|
107
|
+
"name": "mock_method",
|
|
108
|
+
"type": "method",
|
|
109
|
+
"line": 15,
|
|
110
|
+
"complexity": 1,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "mock_function",
|
|
116
|
+
"type": "function",
|
|
117
|
+
"line": 20,
|
|
118
|
+
"complexity": 1,
|
|
119
|
+
"docstring": "Mock function for demonstration",
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
elif ext in ["js", "ts", "jsx", "tsx"]:
|
|
123
|
+
elements = [
|
|
124
|
+
{
|
|
125
|
+
"name": "MockClass",
|
|
126
|
+
"type": "class",
|
|
127
|
+
"line": 5,
|
|
128
|
+
"complexity": 2,
|
|
129
|
+
"methods": [
|
|
130
|
+
{
|
|
131
|
+
"name": "constructor",
|
|
132
|
+
"type": "method",
|
|
133
|
+
"line": 6,
|
|
134
|
+
"complexity": 1,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "mockMethod",
|
|
138
|
+
"type": "method",
|
|
139
|
+
"line": 10,
|
|
140
|
+
"complexity": 1,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{"name": "mockFunction", "type": "function", "line": 15, "complexity": 1},
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"path": file_path,
|
|
149
|
+
"elements": elements,
|
|
150
|
+
"complexity": sum(e.get("complexity", 1) for e in elements),
|
|
151
|
+
"lines": 50,
|
|
152
|
+
"stats": {
|
|
153
|
+
"classes": len([e for e in elements if e["type"] == "class"]),
|
|
154
|
+
"functions": len([e for e in elements if e["type"] == "function"]),
|
|
155
|
+
"methods": sum(len(e.get("methods", [])) for e in elements),
|
|
156
|
+
"lines": 50,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class StableDashboardServer:
|
|
162
|
+
"""Stable dashboard server that works across all installation methods."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, host: str = "localhost", port: int = 8765, debug: bool = False):
|
|
165
|
+
self.host = host
|
|
166
|
+
self.port = port
|
|
167
|
+
self.debug = debug
|
|
168
|
+
self.dashboard_path = None
|
|
169
|
+
self.app = None
|
|
170
|
+
self.sio = None
|
|
171
|
+
|
|
172
|
+
def setup(self) -> bool:
|
|
173
|
+
"""Set up the server components."""
|
|
174
|
+
if not DEPENDENCIES_AVAILABLE:
|
|
175
|
+
print(
|
|
176
|
+
"❌ Error: Missing dependencies. Install with: pip install aiohttp python-socketio"
|
|
177
|
+
)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
# Find dashboard files
|
|
181
|
+
self.dashboard_path = find_dashboard_files()
|
|
182
|
+
if not self.dashboard_path:
|
|
183
|
+
print("❌ Error: Could not find dashboard files")
|
|
184
|
+
print("Please ensure Claude MPM is properly installed")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
print(f"📁 Using dashboard files from: {self.dashboard_path}")
|
|
188
|
+
|
|
189
|
+
# Create SocketIO server with improved timeout settings
|
|
190
|
+
self.sio = socketio.AsyncServer(
|
|
191
|
+
cors_allowed_origins="*",
|
|
192
|
+
logger=True,
|
|
193
|
+
engineio_logger=True,
|
|
194
|
+
ping_interval=30, # Match client's 30 second ping interval
|
|
195
|
+
ping_timeout=60, # Match client's 60 second timeout
|
|
196
|
+
max_http_buffer_size=1e8, # Allow larger messages
|
|
197
|
+
)
|
|
198
|
+
self.app = web.Application()
|
|
199
|
+
self.sio.attach(self.app)
|
|
200
|
+
print("✅ SocketIO server created and attached")
|
|
201
|
+
|
|
202
|
+
# Set up routes
|
|
203
|
+
self._setup_routes()
|
|
204
|
+
self._setup_socketio_events()
|
|
205
|
+
|
|
206
|
+
print("✅ Server setup complete!")
|
|
207
|
+
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
def _setup_routes(self):
|
|
211
|
+
"""Set up HTTP routes."""
|
|
212
|
+
self.app.router.add_get("/", self._serve_dashboard)
|
|
213
|
+
self.app.router.add_get("/static/{path:.*}", self._serve_static)
|
|
214
|
+
self.app.router.add_get("/api/directory/list", self._list_directory)
|
|
215
|
+
self.app.router.add_get("/api/file/read", self._read_file)
|
|
216
|
+
self.app.router.add_get("/version.json", self._serve_version)
|
|
217
|
+
|
|
218
|
+
def _setup_socketio_events(self):
|
|
219
|
+
"""Set up SocketIO event handlers."""
|
|
220
|
+
|
|
221
|
+
@self.sio.event
|
|
222
|
+
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
|
|
226
|
+
await self.sio.emit(
|
|
227
|
+
"connection_test", {"status": "connected", "server": "stable"}, room=sid
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@self.sio.event
|
|
231
|
+
async def disconnect(sid):
|
|
232
|
+
print(f"❌ SocketIO client disconnected: {sid}")
|
|
233
|
+
|
|
234
|
+
@self.sio.event
|
|
235
|
+
async def code_analyze_file(sid, data):
|
|
236
|
+
print(
|
|
237
|
+
f"📡 Received file analysis request from {sid}: {data.get('path', 'unknown')}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
file_path = data.get("path", "")
|
|
241
|
+
file_name = file_path.split("/")[-1] if file_path else "unknown"
|
|
242
|
+
|
|
243
|
+
# Create mock response
|
|
244
|
+
response = create_mock_ast_data(file_path, file_name)
|
|
245
|
+
|
|
246
|
+
print(f"📤 Sending analysis response: {len(response['elements'])} elements")
|
|
247
|
+
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
248
|
+
|
|
249
|
+
# CRITICAL: Handle the actual event name with colons that the client sends
|
|
250
|
+
@self.sio.on("code:analyze:file")
|
|
251
|
+
async def handle_code_analyze_file(sid, data):
|
|
252
|
+
print(
|
|
253
|
+
f"📡 Received code:analyze:file from {sid}: {data.get('path', 'unknown')}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
file_path = data.get("path", "")
|
|
257
|
+
file_name = file_path.split("/")[-1] if file_path else "unknown"
|
|
258
|
+
|
|
259
|
+
# Create mock response
|
|
260
|
+
response = create_mock_ast_data(file_path, file_name)
|
|
261
|
+
|
|
262
|
+
print(f"📤 Sending analysis response: {len(response['elements'])} elements")
|
|
263
|
+
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
264
|
+
|
|
265
|
+
# Handle other events the dashboard sends
|
|
266
|
+
@self.sio.event
|
|
267
|
+
async def get_git_branch(sid, data):
|
|
268
|
+
print(f"📡 Received git branch request from {sid}: {data}")
|
|
269
|
+
await self.sio.emit(
|
|
270
|
+
"git_branch_response", {"branch": "main", "path": data}, room=sid
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
@self.sio.event
|
|
274
|
+
async def request_status(sid, data):
|
|
275
|
+
print(f"📡 Received status request from {sid}")
|
|
276
|
+
await self.sio.emit(
|
|
277
|
+
"status_response", {"status": "running", "server": "stable"}, room=sid
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Handle the event with dots (SocketIO converts colons to dots sometimes)
|
|
281
|
+
@self.sio.event
|
|
282
|
+
async def request_dot_status(sid, data):
|
|
283
|
+
print(f"📡 Received request.status from {sid}")
|
|
284
|
+
await self.sio.emit(
|
|
285
|
+
"status_response", {"status": "running", "server": "stable"}, room=sid
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@self.sio.event
|
|
289
|
+
async def code_discover_top_level(sid, data):
|
|
290
|
+
print(f"📡 Received top-level discovery request from {sid}")
|
|
291
|
+
await self.sio.emit("code:top_level:discovered", {"status": "ok"}, room=sid)
|
|
292
|
+
|
|
293
|
+
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)
|
|
301
|
+
|
|
302
|
+
async def _serve_static(self, request):
|
|
303
|
+
"""Serve static files."""
|
|
304
|
+
file_path = request.match_info["path"]
|
|
305
|
+
static_file = self.dashboard_path / "static" / file_path
|
|
306
|
+
|
|
307
|
+
if static_file.exists() and static_file.is_file():
|
|
308
|
+
content_type = (
|
|
309
|
+
"text/javascript"
|
|
310
|
+
if file_path.endswith(".js")
|
|
311
|
+
else "text/css" if file_path.endswith(".css") else "text/plain"
|
|
312
|
+
)
|
|
313
|
+
with open(static_file) as f:
|
|
314
|
+
content = f.read()
|
|
315
|
+
return web.Response(text=content, content_type=content_type)
|
|
316
|
+
return web.Response(text="File not found", status=404)
|
|
317
|
+
|
|
318
|
+
async def _list_directory(self, request):
|
|
319
|
+
"""List directory contents."""
|
|
320
|
+
path = request.query.get("path", ".")
|
|
321
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
322
|
+
|
|
323
|
+
result = {"path": abs_path, "exists": os.path.exists(abs_path), "contents": []}
|
|
324
|
+
|
|
325
|
+
if os.path.exists(abs_path) and os.path.isdir(abs_path):
|
|
326
|
+
try:
|
|
327
|
+
for item in sorted(os.listdir(abs_path)):
|
|
328
|
+
item_path = os.path.join(abs_path, item)
|
|
329
|
+
result["contents"].append(
|
|
330
|
+
{
|
|
331
|
+
"name": item,
|
|
332
|
+
"path": item_path,
|
|
333
|
+
"is_directory": os.path.isdir(item_path),
|
|
334
|
+
"is_file": os.path.isfile(item_path),
|
|
335
|
+
"is_code_file": item.endswith(
|
|
336
|
+
(".py", ".js", ".ts", ".jsx", ".tsx")
|
|
337
|
+
),
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
except PermissionError:
|
|
341
|
+
result["error"] = "Permission denied"
|
|
342
|
+
|
|
343
|
+
return web.json_response(result)
|
|
344
|
+
|
|
345
|
+
async def _read_file(self, request):
|
|
346
|
+
"""Read file content for source viewer."""
|
|
347
|
+
file_path = request.query.get("path", "")
|
|
348
|
+
|
|
349
|
+
if not file_path:
|
|
350
|
+
return web.json_response({"error": "No path provided"}, status=400)
|
|
351
|
+
|
|
352
|
+
abs_path = os.path.abspath(os.path.expanduser(file_path))
|
|
353
|
+
|
|
354
|
+
# Security check - ensure file is within the project
|
|
355
|
+
try:
|
|
356
|
+
# Get the project root (current working directory)
|
|
357
|
+
project_root = os.getcwd()
|
|
358
|
+
# Ensure the path is within the project
|
|
359
|
+
if not abs_path.startswith(project_root):
|
|
360
|
+
return web.json_response({"error": "Access denied"}, status=403)
|
|
361
|
+
except Exception:
|
|
362
|
+
pass # Allow read if we can't determine project root
|
|
363
|
+
|
|
364
|
+
if not os.path.exists(abs_path):
|
|
365
|
+
return web.json_response({"error": "File not found"}, status=404)
|
|
366
|
+
|
|
367
|
+
if not os.path.isfile(abs_path):
|
|
368
|
+
return web.json_response({"error": "Not a file"}, status=400)
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
# Read file with appropriate encoding
|
|
372
|
+
encodings = ["utf-8", "latin-1", "cp1252"]
|
|
373
|
+
content = None
|
|
374
|
+
|
|
375
|
+
for encoding in encodings:
|
|
376
|
+
try:
|
|
377
|
+
with open(abs_path, encoding=encoding) as f:
|
|
378
|
+
content = f.read()
|
|
379
|
+
break
|
|
380
|
+
except UnicodeDecodeError:
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
if content is None:
|
|
384
|
+
return web.json_response({"error": "Could not decode file"}, status=400)
|
|
385
|
+
|
|
386
|
+
return web.json_response(
|
|
387
|
+
{
|
|
388
|
+
"path": abs_path,
|
|
389
|
+
"name": os.path.basename(abs_path),
|
|
390
|
+
"content": content,
|
|
391
|
+
"lines": len(content.splitlines()),
|
|
392
|
+
"size": os.path.getsize(abs_path),
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
except PermissionError:
|
|
397
|
+
return web.json_response({"error": "Permission denied"}, status=403)
|
|
398
|
+
except Exception as e:
|
|
399
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
400
|
+
|
|
401
|
+
async def _serve_version(self, request):
|
|
402
|
+
"""Serve version information."""
|
|
403
|
+
version_info = {
|
|
404
|
+
"version": "4.2.2",
|
|
405
|
+
"server": "stable",
|
|
406
|
+
"features": ["http", "socketio", "mock_ast"],
|
|
407
|
+
"status": "running",
|
|
408
|
+
}
|
|
409
|
+
return web.json_response(version_info)
|
|
410
|
+
|
|
411
|
+
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
|
|
417
|
+
|
|
418
|
+
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}")
|
|
428
|
+
|
|
429
|
+
# Try to start server with port conflict handling
|
|
430
|
+
max_port_attempts = 10
|
|
431
|
+
original_port = self.port
|
|
432
|
+
|
|
433
|
+
for attempt in range(max_port_attempts):
|
|
434
|
+
try:
|
|
435
|
+
web.run_app(self.app, host=self.host, port=self.port, access_log=None)
|
|
436
|
+
break # Server started successfully
|
|
437
|
+
except KeyboardInterrupt:
|
|
438
|
+
print("\n🛑 Server stopped by user")
|
|
439
|
+
break
|
|
440
|
+
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
|
|
443
|
+
if attempt < max_port_attempts - 1:
|
|
444
|
+
self.port += 1
|
|
445
|
+
print(
|
|
446
|
+
f"⚠️ Port {self.port - 1} in use, trying port {self.port}..."
|
|
447
|
+
)
|
|
448
|
+
# Recreate the app with new port
|
|
449
|
+
self.setup()
|
|
450
|
+
else:
|
|
451
|
+
print(
|
|
452
|
+
f"❌ Could not find available port after {max_port_attempts} attempts"
|
|
453
|
+
)
|
|
454
|
+
print(f" Ports {original_port} to {self.port} are all in use")
|
|
455
|
+
return False
|
|
456
|
+
else:
|
|
457
|
+
# Other OS error
|
|
458
|
+
print(f"❌ Server error: {e}")
|
|
459
|
+
if self.debug:
|
|
460
|
+
import traceback
|
|
461
|
+
|
|
462
|
+
traceback.print_exc()
|
|
463
|
+
return False
|
|
464
|
+
except Exception as e:
|
|
465
|
+
print(f"❌ Server error: {e}")
|
|
466
|
+
if self.debug:
|
|
467
|
+
import traceback
|
|
468
|
+
|
|
469
|
+
traceback.print_exc()
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def create_stable_server(
|
|
476
|
+
dashboard_path: Optional[Path] = None, **kwargs
|
|
477
|
+
) -> StableDashboardServer:
|
|
478
|
+
"""Create a stable dashboard server instance."""
|
|
479
|
+
server = StableDashboardServer(**kwargs)
|
|
480
|
+
if dashboard_path:
|
|
481
|
+
server.dashboard_path = dashboard_path
|
|
482
|
+
return server
|