hud-python 0.3.4__py3-none-any.whl → 0.4.0__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.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,388 @@
1
+ """Tests for hud.cli.utils module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ from hud.cli.utils import HINT_REGISTRY, CaptureLogger, Colors, analyze_error_for_hints
11
+
12
+
13
+ class TestColors:
14
+ """Test ANSI color codes."""
15
+
16
+ def test_color_constants(self) -> None:
17
+ """Test that color constants are defined."""
18
+ assert Colors.HEADER == "\033[95m"
19
+ assert Colors.BLUE == "\033[94m"
20
+ assert Colors.CYAN == "\033[96m"
21
+ assert Colors.GREEN == "\033[92m"
22
+ assert Colors.YELLOW == "\033[93m"
23
+ assert Colors.GOLD == "\033[33m"
24
+ assert Colors.RED == "\033[91m"
25
+ assert Colors.GRAY == "\033[90m"
26
+ assert Colors.ENDC == "\033[0m"
27
+ assert Colors.BOLD == "\033[1m"
28
+
29
+
30
+ class TestCaptureLogger:
31
+ """Test CaptureLogger functionality."""
32
+
33
+ def test_logger_print_mode(self) -> None:
34
+ """Test logger in print mode."""
35
+ logger = CaptureLogger(print_output=True)
36
+
37
+ with patch("builtins.print") as mock_print:
38
+ logger._log("Test message", Colors.GREEN)
39
+ mock_print.assert_called_once_with(f"{Colors.GREEN}Test message{Colors.ENDC}")
40
+
41
+ def test_logger_capture_mode(self) -> None:
42
+ """Test logger in capture-only mode."""
43
+ logger = CaptureLogger(print_output=False)
44
+
45
+ with patch("builtins.print") as mock_print:
46
+ logger._log("Test message", Colors.GREEN)
47
+ mock_print.assert_not_called()
48
+
49
+ output = logger.get_output()
50
+ assert "Test message" in output
51
+
52
+ def test_strip_ansi(self) -> None:
53
+ """Test ANSI code stripping."""
54
+ logger = CaptureLogger(print_output=False)
55
+
56
+ # Test various ANSI sequences
57
+ text_with_ansi = (
58
+ f"{Colors.GREEN}Green text{Colors.ENDC} normal {Colors.BOLD}bold{Colors.ENDC}"
59
+ )
60
+ clean_text = logger._strip_ansi(text_with_ansi)
61
+ assert clean_text == "Green text normal bold"
62
+
63
+ def test_timestamp(self) -> None:
64
+ """Test timestamp generation."""
65
+ logger = CaptureLogger(print_output=False)
66
+
67
+ timestamp = logger.timestamp()
68
+ # Should be in HH:MM:SS format
69
+ assert len(timestamp) == 8
70
+ assert timestamp[2] == ":"
71
+ assert timestamp[5] == ":"
72
+
73
+ def test_phase_logging(self) -> None:
74
+ """Test phase header logging."""
75
+ logger = CaptureLogger(print_output=False)
76
+
77
+ logger.phase(1, "Test Phase")
78
+ output = logger.get_output()
79
+
80
+ assert "=" * 80 in output
81
+ assert "PHASE 1: Test Phase" in output
82
+
83
+ def test_command_logging(self) -> None:
84
+ """Test command logging."""
85
+ logger = CaptureLogger(print_output=False)
86
+
87
+ logger.command(["python", "script.py", "--arg", "value"])
88
+ output = logger.get_output()
89
+
90
+ assert "$ python script.py --arg value" in output
91
+
92
+ def test_success_logging(self) -> None:
93
+ """Test success message logging."""
94
+ logger = CaptureLogger(print_output=False)
95
+
96
+ logger.success("Operation completed")
97
+ output = logger.get_output()
98
+
99
+ assert "✅ Operation completed" in output
100
+
101
+ def test_error_logging(self) -> None:
102
+ """Test error message logging."""
103
+ logger = CaptureLogger(print_output=False)
104
+
105
+ logger.error("Operation failed")
106
+ output = logger.get_output()
107
+
108
+ assert "❌ Operation failed" in output
109
+
110
+ def test_info_logging(self) -> None:
111
+ """Test info message logging with timestamp."""
112
+ logger = CaptureLogger(print_output=False)
113
+
114
+ with patch.object(logger, "timestamp", return_value="12:34:56"):
115
+ logger.info("Information message")
116
+ output = logger.get_output()
117
+
118
+ assert "[12:34:56] Information message" in output
119
+
120
+ def test_stdio_logging(self) -> None:
121
+ """Test STDIO communication logging."""
122
+ logger = CaptureLogger(print_output=False)
123
+
124
+ logger.stdio("JSON-RPC message")
125
+ output = logger.get_output()
126
+
127
+ assert "[STDIO] JSON-RPC message" in output
128
+
129
+ def test_stderr_logging(self) -> None:
130
+ """Test STDERR output logging."""
131
+ logger = CaptureLogger(print_output=False)
132
+
133
+ logger.stderr("Error output from server")
134
+ output = logger.get_output()
135
+
136
+ assert "[STDERR] Error output from server" in output
137
+
138
+ def test_hint_logging(self) -> None:
139
+ """Test hint message logging."""
140
+ logger = CaptureLogger(print_output=False)
141
+
142
+ logger.hint("Try checking the configuration")
143
+ output = logger.get_output()
144
+
145
+ assert "💡 Hint: Try checking the configuration" in output
146
+
147
+ def test_progress_bar(self) -> None:
148
+ """Test progress bar visualization."""
149
+ logger = CaptureLogger(print_output=False)
150
+
151
+ # Test partial progress
152
+ logger.progress_bar(3, 5)
153
+ output = logger.get_output()
154
+
155
+ assert "Progress: [███░░] 3/5 phases (60%)" in output
156
+ assert "Failed at Phase 4" in output
157
+
158
+ # Test complete progress
159
+ logger = CaptureLogger(print_output=False)
160
+ logger.progress_bar(5, 5)
161
+ output = logger.get_output()
162
+
163
+ assert "Progress: [█████] 5/5 phases (100%)" in output
164
+ assert "All phases completed successfully!" in output
165
+
166
+ def test_progress_bar_failure_messages(self) -> None:
167
+ """Test progress bar failure messages at different phases."""
168
+ test_cases = [
169
+ (0, "Failed at Phase 1 - Server startup"),
170
+ (1, "Failed at Phase 2 - MCP initialization"),
171
+ (2, "Failed at Phase 3 - Tool discovery"),
172
+ (3, "Failed at Phase 4 - Remote deployment readiness"),
173
+ (4, "Failed at Phase 5 - Concurrent clients & resources"),
174
+ ]
175
+
176
+ for completed, expected_msg in test_cases:
177
+ logger = CaptureLogger(print_output=False)
178
+ logger.progress_bar(completed, 5)
179
+ output = logger.get_output()
180
+ assert expected_msg in output
181
+
182
+ def test_get_output(self) -> None:
183
+ """Test getting accumulated output."""
184
+ logger = CaptureLogger(print_output=False)
185
+
186
+ logger.info("First message")
187
+ logger.error("Second message")
188
+ logger.success("Third message")
189
+
190
+ output = logger.get_output()
191
+ assert "First message" in output
192
+ assert "Second message" in output
193
+ assert "Third message" in output
194
+
195
+
196
+ class TestAnalyzeErrorForHints:
197
+ """Test error analysis and hint generation."""
198
+
199
+ def test_x11_display_errors(self) -> None:
200
+ """Test X11/display related error hints."""
201
+ errors = [
202
+ "Can't connect to display :0",
203
+ "X11 connection rejected",
204
+ "DISPLAY environment variable not set",
205
+ "Xlib.error.DisplayConnectionError",
206
+ ]
207
+
208
+ for error in errors:
209
+ hint = analyze_error_for_hints(error)
210
+ assert hint is not None
211
+ assert "GUI environment needs X11" in hint
212
+ assert "Xvfb" in hint
213
+
214
+ def test_import_errors(self) -> None:
215
+ """Test import/module error hints."""
216
+ errors = [
217
+ "ModuleNotFoundError: No module named 'requests'",
218
+ "ImportError: cannot import name 'api'",
219
+ "No module named numpy",
220
+ ]
221
+
222
+ for error in errors:
223
+ hint = analyze_error_for_hints(error)
224
+ assert hint is not None
225
+ assert "Missing Python dependencies" in hint
226
+ assert "pyproject.toml" in hint
227
+
228
+ def test_json_errors(self) -> None:
229
+ """Test JSON parsing error hints."""
230
+ errors = [
231
+ "json.decoder.JSONDecodeError: Expecting value",
232
+ "JSONDecodeError: Expecting value: line 1 column 1",
233
+ ]
234
+
235
+ for error in errors:
236
+ hint = analyze_error_for_hints(error)
237
+ assert hint is not None
238
+ assert "Invalid JSON-RPC communication" in hint
239
+ assert "proper JSON-RPC format" in hint
240
+
241
+ def test_permission_errors(self) -> None:
242
+ """Test permission error hints."""
243
+ errors = [
244
+ "Permission denied: /var/log/app.log",
245
+ "EACCES: permission denied",
246
+ "Operation not permitted",
247
+ ]
248
+
249
+ for error in errors:
250
+ hint = analyze_error_for_hints(error)
251
+ assert hint is not None
252
+ assert "Permission issues" in hint
253
+ assert "Check file permissions" in hint
254
+
255
+ def test_memory_errors(self) -> None:
256
+ """Test memory/resource error hints."""
257
+ errors = ["Cannot allocate memory", "Process killed", "Container OOMKilled"]
258
+
259
+ for error in errors:
260
+ hint = analyze_error_for_hints(error)
261
+ assert hint is not None
262
+ assert "Resource limits exceeded" in hint
263
+ assert "memory limits" in hint
264
+
265
+ def test_port_errors(self) -> None:
266
+ """Test port binding error hints."""
267
+ errors = [
268
+ "bind: address already in use",
269
+ "EADDRINUSE: address already in use",
270
+ "port 8080 already allocated",
271
+ ]
272
+
273
+ for error in errors:
274
+ hint = analyze_error_for_hints(error)
275
+ assert hint is not None
276
+ assert "Port conflict detected" in hint
277
+ assert "different port" in hint
278
+
279
+ def test_file_not_found_errors(self) -> None:
280
+ """Test file not found error hints."""
281
+ errors = [
282
+ "FileNotFoundError: [Errno 2] No such file or directory",
283
+ "No such file or directory: config.json",
284
+ ]
285
+
286
+ for error in errors:
287
+ hint = analyze_error_for_hints(error)
288
+ assert hint is not None
289
+ assert "File or directory missing" in hint
290
+ assert "required files exist" in hint
291
+
292
+ def test_traceback_errors(self) -> None:
293
+ """Test general traceback error hints."""
294
+ error = """Traceback (most recent call last):
295
+ File "app.py", line 10, in <module>
296
+ import missing_module
297
+ ImportError: No module named missing_module"""
298
+
299
+ hint = analyze_error_for_hints(error)
300
+ assert hint is not None
301
+ # Should match both traceback and import patterns
302
+ # Import has higher priority
303
+ assert "Missing Python dependencies" in hint
304
+
305
+ def test_timeout_errors(self) -> None:
306
+ """Test timeout error hints."""
307
+ errors = ["Operation timed out after 30 seconds", "Connection timeout", "Request timed out"]
308
+
309
+ for error in errors:
310
+ hint = analyze_error_for_hints(error)
311
+ assert hint is not None
312
+ assert "Server taking too long to start" in hint
313
+ assert "slow operations" in hint
314
+
315
+ def test_no_error_text(self) -> None:
316
+ """Test with empty or None error text."""
317
+ assert analyze_error_for_hints("") is None
318
+ assert analyze_error_for_hints(None) is None
319
+
320
+ def test_no_matching_pattern(self) -> None:
321
+ """Test with error that doesn't match any pattern."""
322
+ hint = analyze_error_for_hints("Some random error message")
323
+ assert hint is None
324
+
325
+ def test_priority_ordering(self) -> None:
326
+ """Test that higher priority hints are returned."""
327
+ # This error matches both "No module" (priority 9) and "Exception" (priority 2)
328
+ error = "Exception: No module named requests"
329
+ hint = analyze_error_for_hints(error)
330
+ assert hint is not None
331
+ # Should get the higher priority hint (import error)
332
+ assert "Missing Python dependencies" in hint
333
+
334
+ def test_case_insensitive_matching(self) -> None:
335
+ """Test that pattern matching is case insensitive."""
336
+ errors = ["PERMISSION DENIED", "permission denied", "Permission Denied"]
337
+
338
+ for error in errors:
339
+ hint = analyze_error_for_hints(error)
340
+ assert hint is not None
341
+ assert "Permission issues" in hint
342
+
343
+
344
+ class TestHintRegistry:
345
+ """Test the hint registry structure."""
346
+
347
+ def test_hint_registry_structure(self) -> None:
348
+ """Test that HINT_REGISTRY has correct structure."""
349
+ assert isinstance(HINT_REGISTRY, list)
350
+ assert len(HINT_REGISTRY) > 0
351
+
352
+ for hint_data in HINT_REGISTRY:
353
+ assert "patterns" in hint_data
354
+ assert "priority" in hint_data
355
+ assert "hint" in hint_data
356
+
357
+ assert isinstance(hint_data["patterns"], list)
358
+ assert isinstance(hint_data["priority"], int)
359
+ assert isinstance(hint_data["hint"], str)
360
+
361
+ # All patterns should be strings
362
+ for pattern in hint_data["patterns"]:
363
+ assert isinstance(pattern, str)
364
+
365
+ def test_hint_priorities_unique(self) -> None:
366
+ """Test that hint priorities are reasonable."""
367
+ priorities = [hint["priority"] for hint in HINT_REGISTRY]
368
+
369
+ # Priorities should be positive
370
+ assert all(p > 0 for p in priorities)
371
+
372
+ # Should have a range of priorities
373
+ assert max(priorities) > min(priorities)
374
+
375
+
376
+ class TestWindowsSupport:
377
+ """Test Windows-specific functionality."""
378
+
379
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows only test")
380
+ def test_windows_ansi_enable(self) -> None:
381
+ """Test that ANSI is enabled on Windows."""
382
+ # The module should call os.system("") on import
383
+ # This is hard to test directly, but we can check platform detection
384
+ assert sys.platform == "win32"
385
+
386
+
387
+ if __name__ == "__main__":
388
+ pytest.main([__file__])
hud/cli/utils.py ADDED
@@ -0,0 +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