hud-python 0.4.1__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/utils.py
CHANGED
|
@@ -1,263 +1,263 @@
|
|
|
1
|
-
"""CLI utilities - logging, colors, and error analysis."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
import sys
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from io import StringIO
|
|
9
|
-
|
|
10
|
-
# Enable ANSI colors on Windows
|
|
11
|
-
if sys.platform == "win32":
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
os.system("") # Enable ANSI escape sequences on Windows # noqa: S607 S605
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Colors:
|
|
18
|
-
"""ANSI color codes for terminal output."""
|
|
19
|
-
|
|
20
|
-
HEADER = "\033[95m"
|
|
21
|
-
BLUE = "\033[94m"
|
|
22
|
-
CYAN = "\033[96m"
|
|
23
|
-
GREEN = "\033[92m"
|
|
24
|
-
YELLOW = "\033[93m"
|
|
25
|
-
GOLD = "\033[33m"
|
|
26
|
-
RED = "\033[91m"
|
|
27
|
-
GRAY = "\033[90m"
|
|
28
|
-
ENDC = "\033[0m"
|
|
29
|
-
BOLD = "\033[1m"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class CaptureLogger:
|
|
33
|
-
"""Logger that can both print and capture output."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, print_output: bool = True) -> None:
|
|
36
|
-
self.print_output = print_output
|
|
37
|
-
self.buffer = StringIO()
|
|
38
|
-
|
|
39
|
-
def _log(self, message: str, color: str = "") -> None:
|
|
40
|
-
"""Internal log method that handles both printing and capturing."""
|
|
41
|
-
if self.print_output:
|
|
42
|
-
if color:
|
|
43
|
-
print(f"{color}{message}{Colors.ENDC}") # noqa: T201
|
|
44
|
-
else:
|
|
45
|
-
print(message) # noqa: T201
|
|
46
|
-
|
|
47
|
-
# Always capture (without ANSI codes)
|
|
48
|
-
clean_msg = self._strip_ansi(message)
|
|
49
|
-
self.buffer.write(clean_msg + "\n")
|
|
50
|
-
|
|
51
|
-
def _strip_ansi(self, text: str) -> str:
|
|
52
|
-
"""Remove ANSI escape codes from text."""
|
|
53
|
-
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
54
|
-
return ansi_escape.sub("", text)
|
|
55
|
-
|
|
56
|
-
def timestamp(self) -> str:
|
|
57
|
-
"""Get minimal timestamp HH:MM:SS."""
|
|
58
|
-
return datetime.now().strftime("%H:%M:%S")
|
|
59
|
-
|
|
60
|
-
def phase(self, phase_num: int, title: str) -> None:
|
|
61
|
-
"""Log a phase header."""
|
|
62
|
-
self._log(f"\n{'=' * 80}", Colors.GOLD if self.print_output else "")
|
|
63
|
-
self._log(
|
|
64
|
-
f"PHASE {phase_num}: {title}", Colors.BOLD + Colors.GOLD if self.print_output else ""
|
|
65
|
-
)
|
|
66
|
-
self._log(f"{'=' * 80}\n", Colors.GOLD if self.print_output else "")
|
|
67
|
-
|
|
68
|
-
def command(self, cmd: list) -> None:
|
|
69
|
-
"""Log the command being executed."""
|
|
70
|
-
self._log(f"$ {' '.join(cmd)}", Colors.BOLD if self.print_output else "")
|
|
71
|
-
|
|
72
|
-
def success(self, message: str) -> None:
|
|
73
|
-
"""Log a success message."""
|
|
74
|
-
self._log(f"✅ {message}", Colors.GREEN if self.print_output else "")
|
|
75
|
-
|
|
76
|
-
def error(self, message: str) -> None:
|
|
77
|
-
"""Log an error message."""
|
|
78
|
-
self._log(f"❌ {message}", Colors.RED if self.print_output else "")
|
|
79
|
-
|
|
80
|
-
def info(self, message: str) -> None:
|
|
81
|
-
"""Log an info message."""
|
|
82
|
-
self._log(f"[{self.timestamp()}] {message}")
|
|
83
|
-
|
|
84
|
-
def stdio(self, message: str) -> None:
|
|
85
|
-
"""Log STDIO communication."""
|
|
86
|
-
self._log(f"[STDIO] {message}", Colors.GOLD if self.print_output else "")
|
|
87
|
-
|
|
88
|
-
def stderr(self, message: str) -> None:
|
|
89
|
-
"""Log STDERR output."""
|
|
90
|
-
self._log(f"[STDERR] {message}", Colors.GRAY if self.print_output else "")
|
|
91
|
-
|
|
92
|
-
def hint(self, hint: str) -> None:
|
|
93
|
-
"""Log a hint message."""
|
|
94
|
-
self._log(f"\n💡 Hint: {hint}", Colors.YELLOW if self.print_output else "")
|
|
95
|
-
|
|
96
|
-
def progress_bar(self, completed: int, total: int) -> None:
|
|
97
|
-
"""Show a visual progress bar."""
|
|
98
|
-
filled = "█" * completed
|
|
99
|
-
empty = "░" * (total - completed)
|
|
100
|
-
percentage = (completed / total) * 100
|
|
101
|
-
|
|
102
|
-
self._log(
|
|
103
|
-
f"\nProgress: [{filled}{empty}] {completed}/{total} phases ({percentage:.0f}%)",
|
|
104
|
-
Colors.BOLD if self.print_output else "",
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
phase_messages = {
|
|
108
|
-
0: ("Failed at Phase 1 - Server startup", Colors.RED),
|
|
109
|
-
1: ("Failed at Phase 2 - MCP initialization", Colors.YELLOW),
|
|
110
|
-
2: ("Failed at Phase 3 - Tool discovery", Colors.YELLOW),
|
|
111
|
-
3: ("Failed at Phase 4 - Remote deployment readiness", Colors.YELLOW),
|
|
112
|
-
4: ("Failed at Phase 5 - Concurrent clients & resources", Colors.YELLOW),
|
|
113
|
-
5: ("All phases completed successfully!", Colors.GREEN),
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if completed in phase_messages:
|
|
117
|
-
msg, color = phase_messages[completed]
|
|
118
|
-
self._log(msg, color if self.print_output else "")
|
|
119
|
-
|
|
120
|
-
def get_output(self) -> str:
|
|
121
|
-
"""Get the captured output."""
|
|
122
|
-
return self.buffer.getvalue()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Hint registry with patterns and priorities
|
|
126
|
-
HINT_REGISTRY = [
|
|
127
|
-
{
|
|
128
|
-
"patterns": [r"Can't connect to display", r"X11", r"DISPLAY.*not set", r"Xlib.*error"],
|
|
129
|
-
"priority": 10,
|
|
130
|
-
"hint": """GUI environment needs X11. Common fixes:
|
|
131
|
-
- Start Xvfb before importing GUI libraries in your entrypoint
|
|
132
|
-
- Use a base image with X11 pre-configured (e.g., hudpython/novnc-base)
|
|
133
|
-
- Delay GUI imports until after X11 is running""",
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"patterns": [r"ModuleNotFoundError", r"ImportError", r"No module named"],
|
|
137
|
-
"priority": 9,
|
|
138
|
-
"hint": """Missing Python dependencies. Check:
|
|
139
|
-
- Is pyproject.toml complete with all dependencies?
|
|
140
|
-
- Did 'pip install' run successfully?
|
|
141
|
-
- For editable installs, is the package structure correct?""",
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
"patterns": [r"json\.decoder\.JSONDecodeError", r"Expecting value.*line.*column"],
|
|
145
|
-
"priority": 8,
|
|
146
|
-
"hint": """Invalid JSON-RPC communication. Check:
|
|
147
|
-
- MCP server is using proper JSON-RPC format
|
|
148
|
-
- No debug prints are corrupting stdout
|
|
149
|
-
- Character encoding is UTF-8""",
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
"patterns": [r"Permission denied", r"EACCES", r"Operation not permitted"],
|
|
153
|
-
"priority": 7,
|
|
154
|
-
"hint": """Permission issues. Try:
|
|
155
|
-
- Check file permissions in container/environment
|
|
156
|
-
- Running with appropriate user
|
|
157
|
-
- Using --privileged flag if absolutely needed (Docker)""",
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
"patterns": [r"Cannot allocate memory", r"killed", r"OOMKilled"],
|
|
161
|
-
"priority": 6,
|
|
162
|
-
"hint": """Resource limits exceeded. Consider:
|
|
163
|
-
- Increasing memory limits
|
|
164
|
-
- Optimizing memory usage in your code
|
|
165
|
-
- Checking for memory leaks""",
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
"patterns": [r"bind.*address already in use", r"EADDRINUSE", r"port.*already allocated"],
|
|
169
|
-
"priority": 5,
|
|
170
|
-
"hint": """Port conflict detected. Options:
|
|
171
|
-
- Use a different port
|
|
172
|
-
- Check if another process is running
|
|
173
|
-
- Ensure proper cleanup in previous runs""",
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
"patterns": [r"FileNotFoundError", r"No such file or directory"],
|
|
177
|
-
"priority": 4,
|
|
178
|
-
"hint": """File or directory missing. Check:
|
|
179
|
-
- All required files exist
|
|
180
|
-
- Working directory is set correctly
|
|
181
|
-
- File paths are correct for the environment""",
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
"patterns": [r"Traceback.*most recent call last", r"Exception"],
|
|
185
|
-
"priority": 2,
|
|
186
|
-
"hint": """Server crashed during startup. Common causes:
|
|
187
|
-
- Missing environment variables
|
|
188
|
-
- Import errors in your module
|
|
189
|
-
- Initialization code failing""",
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
"patterns": [r"timeout", r"timed out"],
|
|
193
|
-
"priority": 1,
|
|
194
|
-
"hint": """Server taking too long to start. Consider:
|
|
195
|
-
- Using initialization wrappers for heavy setup
|
|
196
|
-
- Moving slow operations to setup() tool
|
|
197
|
-
- Checking for deadlocks or infinite loops""",
|
|
198
|
-
},
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def analyze_error_for_hints(error_text: str | None) -> str | None:
|
|
203
|
-
"""Analyze error text and return the highest priority matching hint."""
|
|
204
|
-
if not error_text:
|
|
205
|
-
return None
|
|
206
|
-
|
|
207
|
-
matches = []
|
|
208
|
-
for hint_data in HINT_REGISTRY:
|
|
209
|
-
for pattern in hint_data["patterns"]:
|
|
210
|
-
if re.search(pattern, error_text, re.IGNORECASE):
|
|
211
|
-
matches.append((hint_data["priority"], hint_data["hint"]))
|
|
212
|
-
break
|
|
213
|
-
|
|
214
|
-
if matches:
|
|
215
|
-
matches.sort(key=lambda x: x[0], reverse=True)
|
|
216
|
-
return matches[0][1]
|
|
217
|
-
|
|
218
|
-
return None
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int | None:
|
|
222
|
-
"""Find a free port starting from the given port.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
start_port: Port to start searching from
|
|
226
|
-
max_attempts: Maximum number of ports to try
|
|
227
|
-
|
|
228
|
-
Returns:
|
|
229
|
-
Available port number or None if no ports found
|
|
230
|
-
"""
|
|
231
|
-
import socket
|
|
232
|
-
|
|
233
|
-
for port in range(start_port, start_port + max_attempts):
|
|
234
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
235
|
-
try:
|
|
236
|
-
# Try to bind to the port
|
|
237
|
-
s.bind(("", port))
|
|
238
|
-
s.close()
|
|
239
|
-
return port
|
|
240
|
-
except OSError:
|
|
241
|
-
# Port is in use, try next one
|
|
242
|
-
continue
|
|
243
|
-
return None
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def is_port_free(port: int) -> bool:
|
|
247
|
-
"""Check if a specific port is free.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
port: Port number to check
|
|
251
|
-
|
|
252
|
-
Returns:
|
|
253
|
-
True if port is free, False otherwise
|
|
254
|
-
"""
|
|
255
|
-
import socket
|
|
256
|
-
|
|
257
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
258
|
-
try:
|
|
259
|
-
s.bind(("", port))
|
|
260
|
-
s.close()
|
|
261
|
-
return True
|
|
262
|
-
except OSError:
|
|
263
|
-
return False
|
|
1
|
+
"""CLI utilities - logging, colors, and error analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from io import StringIO
|
|
9
|
+
|
|
10
|
+
# Enable ANSI colors on Windows
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
os.system("") # Enable ANSI escape sequences on Windows # noqa: S607 S605
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Colors:
|
|
18
|
+
"""ANSI color codes for terminal output."""
|
|
19
|
+
|
|
20
|
+
HEADER = "\033[95m"
|
|
21
|
+
BLUE = "\033[94m"
|
|
22
|
+
CYAN = "\033[96m"
|
|
23
|
+
GREEN = "\033[92m"
|
|
24
|
+
YELLOW = "\033[93m"
|
|
25
|
+
GOLD = "\033[33m"
|
|
26
|
+
RED = "\033[91m"
|
|
27
|
+
GRAY = "\033[90m"
|
|
28
|
+
ENDC = "\033[0m"
|
|
29
|
+
BOLD = "\033[1m"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CaptureLogger:
|
|
33
|
+
"""Logger that can both print and capture output."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, print_output: bool = True) -> None:
|
|
36
|
+
self.print_output = print_output
|
|
37
|
+
self.buffer = StringIO()
|
|
38
|
+
|
|
39
|
+
def _log(self, message: str, color: str = "") -> None:
|
|
40
|
+
"""Internal log method that handles both printing and capturing."""
|
|
41
|
+
if self.print_output:
|
|
42
|
+
if color:
|
|
43
|
+
print(f"{color}{message}{Colors.ENDC}") # noqa: T201
|
|
44
|
+
else:
|
|
45
|
+
print(message) # noqa: T201
|
|
46
|
+
|
|
47
|
+
# Always capture (without ANSI codes)
|
|
48
|
+
clean_msg = self._strip_ansi(message)
|
|
49
|
+
self.buffer.write(clean_msg + "\n")
|
|
50
|
+
|
|
51
|
+
def _strip_ansi(self, text: str) -> str:
|
|
52
|
+
"""Remove ANSI escape codes from text."""
|
|
53
|
+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
54
|
+
return ansi_escape.sub("", text)
|
|
55
|
+
|
|
56
|
+
def timestamp(self) -> str:
|
|
57
|
+
"""Get minimal timestamp HH:MM:SS."""
|
|
58
|
+
return datetime.now().strftime("%H:%M:%S")
|
|
59
|
+
|
|
60
|
+
def phase(self, phase_num: int, title: str) -> None:
|
|
61
|
+
"""Log a phase header."""
|
|
62
|
+
self._log(f"\n{'=' * 80}", Colors.GOLD if self.print_output else "")
|
|
63
|
+
self._log(
|
|
64
|
+
f"PHASE {phase_num}: {title}", Colors.BOLD + Colors.GOLD if self.print_output else ""
|
|
65
|
+
)
|
|
66
|
+
self._log(f"{'=' * 80}\n", Colors.GOLD if self.print_output else "")
|
|
67
|
+
|
|
68
|
+
def command(self, cmd: list) -> None:
|
|
69
|
+
"""Log the command being executed."""
|
|
70
|
+
self._log(f"$ {' '.join(cmd)}", Colors.BOLD if self.print_output else "")
|
|
71
|
+
|
|
72
|
+
def success(self, message: str) -> None:
|
|
73
|
+
"""Log a success message."""
|
|
74
|
+
self._log(f"✅ {message}", Colors.GREEN if self.print_output else "")
|
|
75
|
+
|
|
76
|
+
def error(self, message: str) -> None:
|
|
77
|
+
"""Log an error message."""
|
|
78
|
+
self._log(f"❌ {message}", Colors.RED if self.print_output else "")
|
|
79
|
+
|
|
80
|
+
def info(self, message: str) -> None:
|
|
81
|
+
"""Log an info message."""
|
|
82
|
+
self._log(f"[{self.timestamp()}] {message}")
|
|
83
|
+
|
|
84
|
+
def stdio(self, message: str) -> None:
|
|
85
|
+
"""Log STDIO communication."""
|
|
86
|
+
self._log(f"[STDIO] {message}", Colors.GOLD if self.print_output else "")
|
|
87
|
+
|
|
88
|
+
def stderr(self, message: str) -> None:
|
|
89
|
+
"""Log STDERR output."""
|
|
90
|
+
self._log(f"[STDERR] {message}", Colors.GRAY if self.print_output else "")
|
|
91
|
+
|
|
92
|
+
def hint(self, hint: str) -> None:
|
|
93
|
+
"""Log a hint message."""
|
|
94
|
+
self._log(f"\n💡 Hint: {hint}", Colors.YELLOW if self.print_output else "")
|
|
95
|
+
|
|
96
|
+
def progress_bar(self, completed: int, total: int) -> None:
|
|
97
|
+
"""Show a visual progress bar."""
|
|
98
|
+
filled = "█" * completed
|
|
99
|
+
empty = "░" * (total - completed)
|
|
100
|
+
percentage = (completed / total) * 100
|
|
101
|
+
|
|
102
|
+
self._log(
|
|
103
|
+
f"\nProgress: [{filled}{empty}] {completed}/{total} phases ({percentage:.0f}%)",
|
|
104
|
+
Colors.BOLD if self.print_output else "",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
phase_messages = {
|
|
108
|
+
0: ("Failed at Phase 1 - Server startup", Colors.RED),
|
|
109
|
+
1: ("Failed at Phase 2 - MCP initialization", Colors.YELLOW),
|
|
110
|
+
2: ("Failed at Phase 3 - Tool discovery", Colors.YELLOW),
|
|
111
|
+
3: ("Failed at Phase 4 - Remote deployment readiness", Colors.YELLOW),
|
|
112
|
+
4: ("Failed at Phase 5 - Concurrent clients & resources", Colors.YELLOW),
|
|
113
|
+
5: ("All phases completed successfully!", Colors.GREEN),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if completed in phase_messages:
|
|
117
|
+
msg, color = phase_messages[completed]
|
|
118
|
+
self._log(msg, color if self.print_output else "")
|
|
119
|
+
|
|
120
|
+
def get_output(self) -> str:
|
|
121
|
+
"""Get the captured output."""
|
|
122
|
+
return self.buffer.getvalue()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Hint registry with patterns and priorities
|
|
126
|
+
HINT_REGISTRY = [
|
|
127
|
+
{
|
|
128
|
+
"patterns": [r"Can't connect to display", r"X11", r"DISPLAY.*not set", r"Xlib.*error"],
|
|
129
|
+
"priority": 10,
|
|
130
|
+
"hint": """GUI environment needs X11. Common fixes:
|
|
131
|
+
- Start Xvfb before importing GUI libraries in your entrypoint
|
|
132
|
+
- Use a base image with X11 pre-configured (e.g., hudpython/novnc-base)
|
|
133
|
+
- Delay GUI imports until after X11 is running""",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"patterns": [r"ModuleNotFoundError", r"ImportError", r"No module named"],
|
|
137
|
+
"priority": 9,
|
|
138
|
+
"hint": """Missing Python dependencies. Check:
|
|
139
|
+
- Is pyproject.toml complete with all dependencies?
|
|
140
|
+
- Did 'pip install' run successfully?
|
|
141
|
+
- For editable installs, is the package structure correct?""",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"patterns": [r"json\.decoder\.JSONDecodeError", r"Expecting value.*line.*column"],
|
|
145
|
+
"priority": 8,
|
|
146
|
+
"hint": """Invalid JSON-RPC communication. Check:
|
|
147
|
+
- MCP server is using proper JSON-RPC format
|
|
148
|
+
- No debug prints are corrupting stdout
|
|
149
|
+
- Character encoding is UTF-8""",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"patterns": [r"Permission denied", r"EACCES", r"Operation not permitted"],
|
|
153
|
+
"priority": 7,
|
|
154
|
+
"hint": """Permission issues. Try:
|
|
155
|
+
- Check file permissions in container/environment
|
|
156
|
+
- Running with appropriate user
|
|
157
|
+
- Using --privileged flag if absolutely needed (Docker)""",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"patterns": [r"Cannot allocate memory", r"killed", r"OOMKilled"],
|
|
161
|
+
"priority": 6,
|
|
162
|
+
"hint": """Resource limits exceeded. Consider:
|
|
163
|
+
- Increasing memory limits
|
|
164
|
+
- Optimizing memory usage in your code
|
|
165
|
+
- Checking for memory leaks""",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"patterns": [r"bind.*address already in use", r"EADDRINUSE", r"port.*already allocated"],
|
|
169
|
+
"priority": 5,
|
|
170
|
+
"hint": """Port conflict detected. Options:
|
|
171
|
+
- Use a different port
|
|
172
|
+
- Check if another process is running
|
|
173
|
+
- Ensure proper cleanup in previous runs""",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"patterns": [r"FileNotFoundError", r"No such file or directory"],
|
|
177
|
+
"priority": 4,
|
|
178
|
+
"hint": """File or directory missing. Check:
|
|
179
|
+
- All required files exist
|
|
180
|
+
- Working directory is set correctly
|
|
181
|
+
- File paths are correct for the environment""",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"patterns": [r"Traceback.*most recent call last", r"Exception"],
|
|
185
|
+
"priority": 2,
|
|
186
|
+
"hint": """Server crashed during startup. Common causes:
|
|
187
|
+
- Missing environment variables
|
|
188
|
+
- Import errors in your module
|
|
189
|
+
- Initialization code failing""",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"patterns": [r"timeout", r"timed out"],
|
|
193
|
+
"priority": 1,
|
|
194
|
+
"hint": """Server taking too long to start. Consider:
|
|
195
|
+
- Using initialization wrappers for heavy setup
|
|
196
|
+
- Moving slow operations to setup() tool
|
|
197
|
+
- Checking for deadlocks or infinite loops""",
|
|
198
|
+
},
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def analyze_error_for_hints(error_text: str | None) -> str | None:
|
|
203
|
+
"""Analyze error text and return the highest priority matching hint."""
|
|
204
|
+
if not error_text:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
matches = []
|
|
208
|
+
for hint_data in HINT_REGISTRY:
|
|
209
|
+
for pattern in hint_data["patterns"]:
|
|
210
|
+
if re.search(pattern, error_text, re.IGNORECASE):
|
|
211
|
+
matches.append((hint_data["priority"], hint_data["hint"]))
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if matches:
|
|
215
|
+
matches.sort(key=lambda x: x[0], reverse=True)
|
|
216
|
+
return matches[0][1]
|
|
217
|
+
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int | None:
|
|
222
|
+
"""Find a free port starting from the given port.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
start_port: Port to start searching from
|
|
226
|
+
max_attempts: Maximum number of ports to try
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Available port number or None if no ports found
|
|
230
|
+
"""
|
|
231
|
+
import socket
|
|
232
|
+
|
|
233
|
+
for port in range(start_port, start_port + max_attempts):
|
|
234
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
235
|
+
try:
|
|
236
|
+
# Try to bind to the port
|
|
237
|
+
s.bind(("", port))
|
|
238
|
+
s.close()
|
|
239
|
+
return port
|
|
240
|
+
except OSError:
|
|
241
|
+
# Port is in use, try next one
|
|
242
|
+
continue
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def is_port_free(port: int) -> bool:
|
|
247
|
+
"""Check if a specific port is free.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
port: Port number to check
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
True if port is free, False otherwise
|
|
254
|
+
"""
|
|
255
|
+
import socket
|
|
256
|
+
|
|
257
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
258
|
+
try:
|
|
259
|
+
s.bind(("", port))
|
|
260
|
+
s.close()
|
|
261
|
+
return True
|
|
262
|
+
except OSError:
|
|
263
|
+
return False
|