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.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {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