claude-mpm 4.2.1__py3-none-any.whl → 4.2.3__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/agents/templates/agent-manager.json +1 -1
- claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
- claude_mpm/agents/templates/api_qa.json +1 -1
- claude_mpm/agents/templates/code_analyzer.json +1 -1
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
- claude_mpm/agents/templates/imagemagick.json +1 -1
- claude_mpm/agents/templates/memory_manager.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/project_organizer.json +1 -1
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +1 -1
- claude_mpm/agents/templates/research.json +3 -3
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/test-non-mpm.json +20 -0
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +3 -8
- claude_mpm/agents/templates/web_ui.json +1 -1
- claude_mpm/cli/commands/agents.py +3 -0
- claude_mpm/cli/commands/dashboard.py +3 -3
- claude_mpm/cli/commands/monitor.py +227 -64
- claude_mpm/core/config.py +25 -0
- claude_mpm/core/unified_agent_registry.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/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
- claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
- claude_mpm/dashboard/static/js/socket-client.js +5 -2
- claude_mpm/dashboard/templates/code_simple.html +79 -0
- claude_mpm/dashboard/templates/index.html +42 -41
- claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
- claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
- claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
- claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
- claude_mpm/services/agents/deployment/agent_validator.py +11 -6
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
- claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
- claude_mpm/services/cli/agent_listing_service.py +2 -2
- claude_mpm/services/dashboard/stable_server.py +389 -0
- claude_mpm/services/socketio/client_proxy.py +16 -0
- claude_mpm/services/socketio/dashboard_server.py +360 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
- claude_mpm/services/socketio/monitor_client.py +366 -0
- claude_mpm/services/socketio/monitor_server.py +505 -0
- claude_mpm/tools/code_tree_analyzer.py +95 -17
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/top_level.txt +0 -0
|
@@ -31,7 +31,7 @@ class TemplateValidator:
|
|
|
31
31
|
"agent_type": str,
|
|
32
32
|
"metadata": dict,
|
|
33
33
|
"capabilities": dict,
|
|
34
|
-
"instructions":
|
|
34
|
+
"instructions": str,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
# Required metadata fields
|
|
@@ -45,13 +45,10 @@ class TemplateValidator:
|
|
|
45
45
|
# Required capabilities fields
|
|
46
46
|
self.required_capabilities_fields = {
|
|
47
47
|
"model": str,
|
|
48
|
-
"
|
|
48
|
+
"resource_tier": str,
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
#
|
|
52
|
-
self.required_instructions_fields = {
|
|
53
|
-
"system_prompt": str,
|
|
54
|
-
}
|
|
51
|
+
# Instructions is now a string field, not a dictionary
|
|
55
52
|
|
|
56
53
|
def validate_template_file(self, template_file: Path) -> ValidationResult:
|
|
57
54
|
"""Validate a template file.
|
|
@@ -90,9 +87,11 @@ class TemplateValidator:
|
|
|
90
87
|
if "capabilities" in template_data:
|
|
91
88
|
self._validate_capabilities(template_data["capabilities"], result)
|
|
92
89
|
|
|
93
|
-
# Validate instructions
|
|
90
|
+
# Validate instructions (now a string field)
|
|
94
91
|
if "instructions" in template_data:
|
|
95
|
-
self.
|
|
92
|
+
self._validate_instructions_string(
|
|
93
|
+
template_data["instructions"], result
|
|
94
|
+
)
|
|
96
95
|
|
|
97
96
|
# Validate agent ID format
|
|
98
97
|
if "agent_id" in template_data:
|
|
@@ -230,7 +229,7 @@ class TemplateValidator:
|
|
|
230
229
|
suggestion=f"Use one of: {', '.join(valid_models)}",
|
|
231
230
|
)
|
|
232
231
|
|
|
233
|
-
# Validate tools
|
|
232
|
+
# Validate tools (optional field but validate if present)
|
|
234
233
|
if "tools" in capabilities:
|
|
235
234
|
tools = capabilities["tools"]
|
|
236
235
|
if not isinstance(tools, list):
|
|
@@ -242,43 +241,55 @@ class TemplateValidator:
|
|
|
242
241
|
"No tools specified", field_name="capabilities.tools"
|
|
243
242
|
)
|
|
244
243
|
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
# Validate resource_tier
|
|
245
|
+
if "resource_tier" in capabilities:
|
|
246
|
+
resource_tier = capabilities["resource_tier"]
|
|
247
|
+
valid_tiers = ["basic", "standard", "intensive", "lightweight", "high"]
|
|
248
|
+
if resource_tier not in valid_tiers:
|
|
249
|
+
result.add_warning(
|
|
250
|
+
f"Unknown resource tier '{resource_tier}'",
|
|
251
|
+
field_name="capabilities.resource_tier",
|
|
252
|
+
suggestion=f"Use one of: {', '.join(valid_tiers)}",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _validate_instructions_string(
|
|
256
|
+
self, instructions: str, result: ValidationResult
|
|
247
257
|
) -> None:
|
|
248
|
-
"""Validate instructions
|
|
258
|
+
"""Validate instructions string.
|
|
249
259
|
|
|
250
260
|
Args:
|
|
251
|
-
instructions: Instructions
|
|
261
|
+
instructions: Instructions string
|
|
252
262
|
result: ValidationResult to update
|
|
253
263
|
"""
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
value = instructions[field]
|
|
262
|
-
if not isinstance(value, expected_type):
|
|
263
|
-
result.add_error(
|
|
264
|
-
f"Instructions field '{field}' should be {expected_type.__name__}, got {type(value).__name__}",
|
|
265
|
-
field_name=f"instructions.{field}",
|
|
266
|
-
)
|
|
264
|
+
# Check if instructions is actually a string
|
|
265
|
+
if not isinstance(instructions, str):
|
|
266
|
+
result.add_error(
|
|
267
|
+
f"Instructions should be a string, got {type(instructions).__name__}",
|
|
268
|
+
field_name="instructions",
|
|
269
|
+
)
|
|
270
|
+
return
|
|
267
271
|
|
|
268
|
-
#
|
|
269
|
-
if
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
272
|
+
# Check if instructions is not empty
|
|
273
|
+
if not instructions or not instructions.strip():
|
|
274
|
+
result.add_error(
|
|
275
|
+
"Instructions cannot be empty",
|
|
276
|
+
field_name="instructions",
|
|
277
|
+
)
|
|
278
|
+
elif len(instructions) < 20:
|
|
279
|
+
result.add_warning(
|
|
280
|
+
"Instructions are very short",
|
|
281
|
+
field_name="instructions",
|
|
282
|
+
suggestion="Provide more detailed instructions",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Check for file references that might be invalid
|
|
286
|
+
if instructions.startswith("file:"):
|
|
287
|
+
file_ref = instructions[5:] # Remove "file:" prefix
|
|
288
|
+
result.add_warning(
|
|
289
|
+
f"Instructions reference external file: {file_ref}",
|
|
290
|
+
field_name="instructions",
|
|
291
|
+
suggestion="Consider embedding instructions directly or ensure the referenced file exists",
|
|
292
|
+
)
|
|
282
293
|
|
|
283
294
|
def _validate_agent_id(self, agent_id: str, result: ValidationResult) -> None:
|
|
284
295
|
"""Validate agent ID format.
|
|
@@ -250,11 +250,11 @@ class AgentListingService(IAgentListingService):
|
|
|
250
250
|
type=agent_data.get("type", "agent"),
|
|
251
251
|
tier=agent_data.get("tier", "system"),
|
|
252
252
|
path=agent_data.get("path", ""),
|
|
253
|
-
description=agent_data.get("description")
|
|
253
|
+
description=agent_data.get("description"),
|
|
254
254
|
specializations=(
|
|
255
255
|
agent_data.get("specializations") if verbose else None
|
|
256
256
|
),
|
|
257
|
-
version=agent_data.get("version")
|
|
257
|
+
version=agent_data.get("version"),
|
|
258
258
|
deployed=True,
|
|
259
259
|
)
|
|
260
260
|
agents.append(agent)
|
|
@@ -0,0 +1,389 @@
|
|
|
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
|
|
190
|
+
self.sio = socketio.AsyncServer(
|
|
191
|
+
cors_allowed_origins="*", logger=True, engineio_logger=True
|
|
192
|
+
)
|
|
193
|
+
self.app = web.Application()
|
|
194
|
+
self.sio.attach(self.app)
|
|
195
|
+
print("✅ SocketIO server created and attached")
|
|
196
|
+
|
|
197
|
+
# Set up routes
|
|
198
|
+
self._setup_routes()
|
|
199
|
+
self._setup_socketio_events()
|
|
200
|
+
|
|
201
|
+
print("✅ Server setup complete!")
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def _setup_routes(self):
|
|
206
|
+
"""Set up HTTP routes."""
|
|
207
|
+
self.app.router.add_get("/", self._serve_dashboard)
|
|
208
|
+
self.app.router.add_get("/static/{path:.*}", self._serve_static)
|
|
209
|
+
self.app.router.add_get("/api/directory/list", self._list_directory)
|
|
210
|
+
self.app.router.add_get("/version.json", self._serve_version)
|
|
211
|
+
|
|
212
|
+
def _setup_socketio_events(self):
|
|
213
|
+
"""Set up SocketIO event handlers."""
|
|
214
|
+
|
|
215
|
+
@self.sio.event
|
|
216
|
+
async def connect(sid, environ):
|
|
217
|
+
print(f"✅ SocketIO client connected: {sid}")
|
|
218
|
+
print(f" Client info: {environ.get('HTTP_USER_AGENT', 'Unknown')}")
|
|
219
|
+
# Send a test message to confirm connection
|
|
220
|
+
await self.sio.emit(
|
|
221
|
+
"connection_test", {"status": "connected", "server": "stable"}, room=sid
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@self.sio.event
|
|
225
|
+
async def disconnect(sid):
|
|
226
|
+
print(f"❌ SocketIO client disconnected: {sid}")
|
|
227
|
+
|
|
228
|
+
@self.sio.event
|
|
229
|
+
async def code_analyze_file(sid, data):
|
|
230
|
+
print(
|
|
231
|
+
f"📡 Received file analysis request from {sid}: {data.get('path', 'unknown')}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
file_path = data.get("path", "")
|
|
235
|
+
file_name = file_path.split("/")[-1] if file_path else "unknown"
|
|
236
|
+
|
|
237
|
+
# Create mock response
|
|
238
|
+
response = create_mock_ast_data(file_path, file_name)
|
|
239
|
+
|
|
240
|
+
print(f"📤 Sending analysis response: {len(response['elements'])} elements")
|
|
241
|
+
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
242
|
+
|
|
243
|
+
# CRITICAL: Handle the actual event name with colons that the client sends
|
|
244
|
+
@self.sio.on("code:analyze:file")
|
|
245
|
+
async def handle_code_analyze_file(sid, data):
|
|
246
|
+
print(
|
|
247
|
+
f"📡 Received code:analyze:file from {sid}: {data.get('path', 'unknown')}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
file_path = data.get("path", "")
|
|
251
|
+
file_name = file_path.split("/")[-1] if file_path else "unknown"
|
|
252
|
+
|
|
253
|
+
# Create mock response
|
|
254
|
+
response = create_mock_ast_data(file_path, file_name)
|
|
255
|
+
|
|
256
|
+
print(f"📤 Sending analysis response: {len(response['elements'])} elements")
|
|
257
|
+
await self.sio.emit("code:file:analyzed", response, room=sid)
|
|
258
|
+
|
|
259
|
+
# Handle other events the dashboard sends
|
|
260
|
+
@self.sio.event
|
|
261
|
+
async def get_git_branch(sid, data):
|
|
262
|
+
print(f"📡 Received git branch request from {sid}: {data}")
|
|
263
|
+
await self.sio.emit(
|
|
264
|
+
"git_branch_response", {"branch": "main", "path": data}, room=sid
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@self.sio.event
|
|
268
|
+
async def request_status(sid, data):
|
|
269
|
+
print(f"📡 Received status request from {sid}")
|
|
270
|
+
await self.sio.emit(
|
|
271
|
+
"status_response", {"status": "running", "server": "stable"}, room=sid
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Handle the event with dots (SocketIO converts colons to dots sometimes)
|
|
275
|
+
@self.sio.event
|
|
276
|
+
async def request_dot_status(sid, data):
|
|
277
|
+
print(f"📡 Received request.status from {sid}")
|
|
278
|
+
await self.sio.emit(
|
|
279
|
+
"status_response", {"status": "running", "server": "stable"}, room=sid
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@self.sio.event
|
|
283
|
+
async def code_discover_top_level(sid, data):
|
|
284
|
+
print(f"📡 Received top-level discovery request from {sid}")
|
|
285
|
+
await self.sio.emit("code:top_level:discovered", {"status": "ok"}, room=sid)
|
|
286
|
+
|
|
287
|
+
async def _serve_dashboard(self, request):
|
|
288
|
+
"""Serve the main dashboard HTML."""
|
|
289
|
+
dashboard_file = self.dashboard_path / "templates" / "index.html"
|
|
290
|
+
if dashboard_file.exists():
|
|
291
|
+
with open(dashboard_file) as f:
|
|
292
|
+
content = f.read()
|
|
293
|
+
return web.Response(text=content, content_type="text/html")
|
|
294
|
+
return web.Response(text="Dashboard not found", status=404)
|
|
295
|
+
|
|
296
|
+
async def _serve_static(self, request):
|
|
297
|
+
"""Serve static files."""
|
|
298
|
+
file_path = request.match_info["path"]
|
|
299
|
+
static_file = self.dashboard_path / "static" / file_path
|
|
300
|
+
|
|
301
|
+
if static_file.exists() and static_file.is_file():
|
|
302
|
+
content_type = (
|
|
303
|
+
"text/javascript"
|
|
304
|
+
if file_path.endswith(".js")
|
|
305
|
+
else "text/css" if file_path.endswith(".css") else "text/plain"
|
|
306
|
+
)
|
|
307
|
+
with open(static_file) as f:
|
|
308
|
+
content = f.read()
|
|
309
|
+
return web.Response(text=content, content_type=content_type)
|
|
310
|
+
return web.Response(text="File not found", status=404)
|
|
311
|
+
|
|
312
|
+
async def _list_directory(self, request):
|
|
313
|
+
"""List directory contents."""
|
|
314
|
+
path = request.query.get("path", ".")
|
|
315
|
+
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
316
|
+
|
|
317
|
+
result = {"path": abs_path, "exists": os.path.exists(abs_path), "contents": []}
|
|
318
|
+
|
|
319
|
+
if os.path.exists(abs_path) and os.path.isdir(abs_path):
|
|
320
|
+
try:
|
|
321
|
+
for item in sorted(os.listdir(abs_path)):
|
|
322
|
+
item_path = os.path.join(abs_path, item)
|
|
323
|
+
result["contents"].append(
|
|
324
|
+
{
|
|
325
|
+
"name": item,
|
|
326
|
+
"path": item_path,
|
|
327
|
+
"is_directory": os.path.isdir(item_path),
|
|
328
|
+
"is_file": os.path.isfile(item_path),
|
|
329
|
+
"is_code_file": item.endswith(
|
|
330
|
+
(".py", ".js", ".ts", ".jsx", ".tsx")
|
|
331
|
+
),
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
except PermissionError:
|
|
335
|
+
result["error"] = "Permission denied"
|
|
336
|
+
|
|
337
|
+
return web.json_response(result)
|
|
338
|
+
|
|
339
|
+
async def _serve_version(self, request):
|
|
340
|
+
"""Serve version information."""
|
|
341
|
+
version_info = {
|
|
342
|
+
"version": "4.2.2",
|
|
343
|
+
"server": "stable",
|
|
344
|
+
"features": ["http", "socketio", "mock_ast"],
|
|
345
|
+
"status": "running",
|
|
346
|
+
}
|
|
347
|
+
return web.json_response(version_info)
|
|
348
|
+
|
|
349
|
+
def run(self):
|
|
350
|
+
"""Run the server."""
|
|
351
|
+
print("🔧 Setting up server...")
|
|
352
|
+
if not self.setup():
|
|
353
|
+
print("❌ Server setup failed")
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
print(f"🚀 Starting stable dashboard server at http://{self.host}:{self.port}")
|
|
357
|
+
print("✅ Server ready: HTTP + SocketIO on same port")
|
|
358
|
+
print("📡 SocketIO events registered:")
|
|
359
|
+
print(" - connect/disconnect")
|
|
360
|
+
print(" - code_analyze_file (from 'code:analyze:file')")
|
|
361
|
+
print("🌐 HTTP endpoints available:")
|
|
362
|
+
print(" - GET / (dashboard)")
|
|
363
|
+
print(" - GET /static/* (static files)")
|
|
364
|
+
print(" - GET /api/directory/list (directory API)")
|
|
365
|
+
print(f"🔗 Open in browser: http://{self.host}:{self.port}")
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
web.run_app(self.app, host=self.host, port=self.port, access_log=None)
|
|
369
|
+
except KeyboardInterrupt:
|
|
370
|
+
print("\n🛑 Server stopped by user")
|
|
371
|
+
except Exception as e:
|
|
372
|
+
print(f"❌ Server error: {e}")
|
|
373
|
+
if self.debug:
|
|
374
|
+
import traceback
|
|
375
|
+
|
|
376
|
+
traceback.print_exc()
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def create_stable_server(
|
|
383
|
+
dashboard_path: Optional[Path] = None, **kwargs
|
|
384
|
+
) -> StableDashboardServer:
|
|
385
|
+
"""Create a stable dashboard server instance."""
|
|
386
|
+
server = StableDashboardServer(**kwargs)
|
|
387
|
+
if dashboard_path:
|
|
388
|
+
server.dashboard_path = dashboard_path
|
|
389
|
+
return server
|
|
@@ -45,6 +45,22 @@ class SocketIOClientProxy:
|
|
|
45
45
|
self._client_thread = None
|
|
46
46
|
self._client_loop = None
|
|
47
47
|
|
|
48
|
+
def start(self):
|
|
49
|
+
"""Start the Socket.IO client connection (compatibility wrapper).
|
|
50
|
+
|
|
51
|
+
This method exists for backward compatibility with code that expects
|
|
52
|
+
a start() method. It simply calls start_sync().
|
|
53
|
+
"""
|
|
54
|
+
return self.start_sync()
|
|
55
|
+
|
|
56
|
+
def stop(self):
|
|
57
|
+
"""Stop the Socket.IO client connection (compatibility wrapper).
|
|
58
|
+
|
|
59
|
+
This method exists for backward compatibility with code that expects
|
|
60
|
+
a stop() method. It simply calls stop_sync().
|
|
61
|
+
"""
|
|
62
|
+
return self.stop_sync()
|
|
63
|
+
|
|
48
64
|
def start_sync(self):
|
|
49
65
|
"""Start the Socket.IO client connection to the persistent server."""
|
|
50
66
|
self.logger.debug(
|