hud-python 0.4.45__py3-none-any.whl → 0.5.13__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.
Files changed (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
hud/utils/hud_console.py CHANGED
@@ -20,9 +20,8 @@ import time
20
20
  import traceback
21
21
  from typing import TYPE_CHECKING, Any, Literal, Self
22
22
 
23
- import questionary
24
- import typer
25
23
  from rich.console import Console
24
+ from rich.markup import escape
26
25
  from rich.panel import Panel
27
26
  from rich.table import Table
28
27
 
@@ -38,9 +37,26 @@ TEXT = "bright_white" # Off-white that's readable on dark, not too bright on li
38
37
  SECONDARY = "rgb(108,113,196)" # Muted blue-purple for secondary text
39
38
 
40
39
 
40
+ # HUD Symbol System - Minimal 3-category system with default colors
41
+ class Symbols:
42
+ """Unicode symbols for consistent CLI output with default colors."""
43
+
44
+ # Info/Items - Use for all informational lines (gold)
45
+ ITEM = f"[{GOLD}]•[/{GOLD}]"
46
+
47
+ # Status - Use for state/completion (green)
48
+ SUCCESS = f"[{GREEN}]●[/{GREEN}]"
49
+
50
+ # Flow/Special - Use for transitions and important notes (gold)
51
+ FLOW = f"[{GOLD}]⟿[/{GOLD}]"
52
+
53
+
41
54
  class HUDConsole:
42
55
  """Design system for HUD CLI output."""
43
56
 
57
+ # Make symbols easily accessible
58
+ sym = Symbols
59
+
44
60
  def __init__(self, logger: logging.Logger | None = None) -> None:
45
61
  """Initialize the design system.
46
62
 
@@ -80,7 +96,7 @@ class HUDConsole:
80
96
  stderr: If True, output to stderr (default), otherwise stdout
81
97
  """
82
98
  console = self._stderr_console if stderr else self._stdout_console
83
- console.print(f"[{GREEN}]✅ {message}[/{GREEN}]")
99
+ console.print(f"[{GREEN}]✅ {escape(message)}[/{GREEN}]")
84
100
 
85
101
  def error(self, message: str, stderr: bool = True) -> None:
86
102
  """Print an error message.
@@ -91,10 +107,12 @@ class HUDConsole:
91
107
  """
92
108
  console = self._stderr_console if stderr else self._stdout_console
93
109
  tb = traceback.format_exc()
110
+ escaped_message = escape(message)
94
111
  if "NoneType: None" not in tb:
95
- console.print(f"[{RED} not bold]❌ {message}\n{tb}[/{RED} not bold]")
112
+ escaped_tb = escape(tb)
113
+ console.print(f"[{RED} not bold]❌ {escaped_message}\n{escaped_tb}[/{RED} not bold]")
96
114
  else:
97
- console.print(f"[{RED} not bold]❌ {message}[/{RED} not bold]")
115
+ console.print(f"[{RED} not bold]❌ {escaped_message}[/{RED} not bold]")
98
116
 
99
117
  def warning(self, message: str, stderr: bool = True) -> None:
100
118
  """Print a warning message.
@@ -104,7 +122,7 @@ class HUDConsole:
104
122
  stderr: If True, output to stderr (default), otherwise stdout
105
123
  """
106
124
  console = self._stderr_console if stderr else self._stdout_console
107
- console.print(f"⚠️ [{YELLOW} not bold]{message}[/{YELLOW} not bold]")
125
+ console.print(f"⚠️ [{YELLOW} not bold]{escape(message)}[/{YELLOW} not bold]")
108
126
 
109
127
  def info(self, message: str, stderr: bool = True) -> None:
110
128
  """Print an info message.
@@ -114,7 +132,7 @@ class HUDConsole:
114
132
  stderr: If True, output to stderr (default), otherwise stdout
115
133
  """
116
134
  console = self._stderr_console if stderr else self._stdout_console
117
- console.print(f"[{TEXT} not bold]{message}[/{TEXT} not bold]")
135
+ console.print(f"[{TEXT} not bold]{escape(message)}[/{TEXT} not bold]")
118
136
 
119
137
  def print(self, message: str, stderr: bool = True) -> None:
120
138
  """Print a message.
@@ -136,7 +154,7 @@ class HUDConsole:
136
154
  """
137
155
  console = self._stderr_console if stderr else self._stdout_console
138
156
  console.print(
139
- f"[{DIM} not bold][default]{label}[/default][/{DIM} not bold] [default]{value}[/default]" # noqa: E501
157
+ f"[{DIM} not bold][default]{escape(label)}[/default][/{DIM} not bold] [default]{escape(value)}[/default]" # noqa: E501
140
158
  )
141
159
 
142
160
  def link(self, url: str, stderr: bool = True) -> None:
@@ -147,7 +165,7 @@ class HUDConsole:
147
165
  stderr: If True, output to stderr (default), otherwise stdout
148
166
  """
149
167
  console = self._stderr_console if stderr else self._stdout_console
150
- console.print(f"[{SECONDARY} underline]{url}[/{SECONDARY} underline]")
168
+ console.print(f"[{SECONDARY} underline]{escape(url)}[/{SECONDARY} underline]")
151
169
 
152
170
  def json_config(self, json_str: str, stderr: bool = True) -> None:
153
171
  """Print JSON configuration with neutral theme.
@@ -158,7 +176,7 @@ class HUDConsole:
158
176
  """
159
177
  # Print JSON with neutral grey text
160
178
  console = self._stderr_console if stderr else self._stdout_console
161
- console.print(f"[{TEXT}]{json_str}[/{TEXT}]")
179
+ console.print(f"[{TEXT}]{escape(json_str)}[/{TEXT}]")
162
180
 
163
181
  def key_value_table(
164
182
  self, data: dict[str, str | int | float], show_header: bool = False, stderr: bool = True
@@ -188,7 +206,7 @@ class HUDConsole:
188
206
  stderr: If True, output to stderr (default), otherwise stdout
189
207
  """
190
208
  console = self._stderr_console if stderr else self._stdout_console
191
- console.print(f"[{DIM}]{message}[/{DIM}]")
209
+ console.print(f"[{DIM}]{escape(message)}[/{DIM}]")
192
210
 
193
211
  def phase(self, phase_num: int, title: str, stderr: bool = True) -> None:
194
212
  """Print a phase header (for debug command).
@@ -221,7 +239,7 @@ class HUDConsole:
221
239
  stderr: If True, output to stderr (default), otherwise stdout
222
240
  """
223
241
  console = self._stderr_console if stderr else self._stdout_console
224
- console.print(f"[rgb(181,137,0)]💡 Hint: {hint}[/rgb(181,137,0)]")
242
+ console.print(f"[rgb(181,137,0)]💡 Hint: {escape(hint)}[/rgb(181,137,0)]")
225
243
 
226
244
  def status_item(
227
245
  self,
@@ -250,10 +268,14 @@ class HUDConsole:
250
268
  indicator = indicators.get(status, indicators["info"])
251
269
  console = self._stderr_console if stderr else self._stdout_console
252
270
 
271
+ escaped_label = escape(label)
272
+ escaped_value = escape(value)
253
273
  if primary:
254
- console.print(f"{indicator} {label}: [bold {SECONDARY}]{value}[/bold {SECONDARY}]")
274
+ console.print(
275
+ f"{indicator} {escaped_label}: [bold {SECONDARY}]{escaped_value}[/bold {SECONDARY}]"
276
+ )
255
277
  else:
256
- console.print(f"{indicator} {label}: [{TEXT}]{value}[/{TEXT}]")
278
+ console.print(f"{indicator} {escaped_label}: [{TEXT}]{escaped_value}[/{TEXT}]")
257
279
 
258
280
  def command_example(
259
281
  self, command: str, description: str | None = None, stderr: bool = True
@@ -470,6 +492,9 @@ class HUDConsole:
470
492
  Returns:
471
493
  The selected choice value
472
494
  """
495
+ import questionary
496
+ from questionary import Style
497
+
473
498
  # Convert choices to questionary format
474
499
  q_choices = []
475
500
 
@@ -481,15 +506,27 @@ class HUDConsole:
481
506
  else:
482
507
  q_choices.append(choice)
483
508
 
509
+ # Custom style for better visibility of selection
510
+ custom_style = Style(
511
+ [
512
+ ("qmark", "fg:cyan bold"),
513
+ ("question", "bold"),
514
+ ("pointer", "fg:cyan bold"),
515
+ ("highlighted", "fg:cyan bold"),
516
+ ]
517
+ )
518
+
484
519
  result = questionary.select(
485
520
  message,
486
521
  choices=q_choices,
487
- default=q_choices[default] if default is not None else None,
488
522
  instruction="(Use ↑/↓ arrows, Enter to select)",
523
+ style=custom_style,
489
524
  ).ask()
490
525
 
491
526
  # If no selection made (Ctrl+C or ESC), exit
492
527
  if result is None:
528
+ import typer
529
+
493
530
  raise typer.Exit(1)
494
531
 
495
532
  return result
@@ -516,7 +553,12 @@ class HUDConsole:
516
553
  except (TypeError, ValueError):
517
554
  args_str = str(arguments)[:60]
518
555
 
519
- return f"[{GOLD}]→[/{GOLD}] [bold {TEXT}]{name}[/bold {TEXT}][{DIM}]({args_str})[/{DIM}]"
556
+ escaped_name = escape(name)
557
+ escaped_args = escape(args_str)
558
+ return (
559
+ f"[{GOLD}]→[/{GOLD}] [bold {TEXT}]{escaped_name}[/bold {TEXT}]"
560
+ f"[{DIM}]({escaped_args})[/{DIM}]"
561
+ )
520
562
 
521
563
  def format_tool_result(self, content: str, is_error: bool = False) -> str:
522
564
  """Format a tool result in compact HUD style.
@@ -532,11 +574,12 @@ class HUDConsole:
532
574
  if len(content) > 80:
533
575
  content = content[:77] + "..."
534
576
 
577
+ escaped_content = escape(content)
535
578
  # Format with status using HUD colors
536
579
  if is_error:
537
- return f" [{RED}]✗[/{RED}] [{DIM}]{content}[/{DIM}]"
580
+ return f" [{RED}]✗[/{RED}] [{DIM}]{escaped_content}[/{DIM}]"
538
581
  else:
539
- return f" [{GREEN}]✓[/{GREEN}] [{TEXT}]{content}[/{TEXT}]"
582
+ return f" [{GREEN}]✓[/{GREEN}] [{TEXT}]{escaped_content}[/{TEXT}]"
540
583
 
541
584
  def confirm(self, message: str, default: bool = True) -> bool:
542
585
  """Print a confirmation message.
@@ -545,8 +588,36 @@ class HUDConsole:
545
588
  message: The confirmation message
546
589
  default: If True, the default choice is True
547
590
  """
591
+ import questionary
592
+
548
593
  return questionary.confirm(message, default=default).ask()
549
594
 
595
+ # Symbol-based output methods
596
+ def symbol(self, symbol: str, message: str, color: str = GOLD, stderr: bool = True) -> None:
597
+ """Print a message with a colored symbol prefix.
598
+
599
+ Args:
600
+ symbol: Symbol to use (use Symbols.* constants)
601
+ message: Message text
602
+ color: Color for the symbol (default: gold)
603
+ stderr: If True, output to stderr
604
+ """
605
+ console = self._stderr_console if stderr else self._stdout_console
606
+ console.print(f"[{color}]{symbol}[/{color}] {escape(message)}")
607
+
608
+ def detail(self, message: str, stderr: bool = True) -> None:
609
+ """Print an indented detail line with gold pointer symbol."""
610
+ console = self._stderr_console if stderr else self._stdout_console
611
+ console.print(f" [{GOLD}]{Symbols.ITEM}[/{GOLD}] {escape(message)}")
612
+
613
+ def flow(self, message: str, stderr: bool = True) -> None:
614
+ """Print a flow/transition message with wave symbol."""
615
+ self.symbol(Symbols.FLOW, message, GOLD, stderr)
616
+
617
+ def note(self, message: str, stderr: bool = True) -> None:
618
+ """Print an important note with asterism symbol."""
619
+ self.symbol(Symbols.ITEM, message, GOLD, stderr)
620
+
550
621
 
551
622
  # Global design instance for convenience
552
623
  class _ProgressContext:
hud/utils/mcp.py CHANGED
@@ -5,8 +5,6 @@ from typing import Any
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
- from hud.settings import settings
9
-
10
8
  logger = logging.getLogger(__name__)
11
9
 
12
10
 
@@ -17,15 +15,27 @@ class MCPConfigPatch(BaseModel):
17
15
  meta: dict[str, Any] | None = Field(default_factory=dict, alias="meta")
18
16
 
19
17
 
18
+ def _is_hud_server(url: str) -> bool:
19
+ """Check if a URL is a HUD MCP server.
20
+
21
+ Matches:
22
+ - Any mcp.hud.* domain (including .ai, .so, and future domains)
23
+ - Staging servers (orcstaging.hud.so)
24
+ - Any *.hud.ai or *.hud.so domain
25
+ """
26
+ if not url:
27
+ return False
28
+ url_lower = url.lower()
29
+ return "mcp.hud." in url_lower or ".hud.ai" in url_lower or ".hud.so" in url_lower
30
+
31
+
20
32
  def patch_mcp_config(mcp_config: dict[str, dict[str, Any]], patch: MCPConfigPatch) -> None:
21
33
  """Patch MCP config with additional values."""
22
- hud_mcp_url = settings.hud_mcp_url
23
-
24
34
  for server_cfg in mcp_config.values():
25
35
  url = server_cfg.get("url", "")
26
36
 
27
37
  # 1) HTTP header lane (only for hud MCP servers)
28
- if hud_mcp_url in url and patch.headers:
38
+ if _is_hud_server(url) and patch.headers:
29
39
  for key, value in patch.headers.items():
30
40
  headers = server_cfg.setdefault("headers", {})
31
41
  headers.setdefault(key, value)
@@ -35,56 +45,3 @@ def patch_mcp_config(mcp_config: dict[str, dict[str, Any]], patch: MCPConfigPatc
35
45
  for key, value in patch.meta.items():
36
46
  meta = server_cfg.setdefault("meta", {})
37
47
  meta.setdefault(key, value)
38
-
39
-
40
- def setup_hud_telemetry(
41
- mcp_config: dict[str, dict[str, Any]], auto_trace: bool = True
42
- ) -> Any | None:
43
- """Setup telemetry for hud servers.
44
-
45
- Returns:
46
- The auto-created trace context manager if one was created, None otherwise.
47
- Caller is responsible for exiting the context manager.
48
- """
49
- if not mcp_config:
50
- raise ValueError("Please run initialize() before setting up client-side telemetry")
51
-
52
- # Check if there are any HUD servers to setup telemetry for
53
- hud_mcp_url = settings.hud_mcp_url
54
- has_hud_servers = any(
55
- hud_mcp_url in server_cfg.get("url", "") for server_cfg in mcp_config.values()
56
- )
57
-
58
- # If no HUD servers, no need for telemetry setup
59
- if not has_hud_servers:
60
- return None
61
-
62
- from hud.otel import get_current_task_run_id
63
- from hud.telemetry import trace
64
-
65
- run_id = get_current_task_run_id()
66
- auto_trace_cm = None
67
-
68
- if not run_id and auto_trace:
69
- # Start an auto trace and capture its ID for headers/metadata
70
- auto_trace_cm = trace("My Trace")
71
- _trace_obj = auto_trace_cm.__enter__()
72
- try:
73
- run_id = getattr(_trace_obj, "id", None) or str(_trace_obj)
74
- except Exception: # pragma: no cover - fallback shouldn't fail lint
75
- run_id = None
76
-
77
- # Patch HUD servers with run-id (works whether auto or user trace)
78
- if run_id:
79
- patch_mcp_config(
80
- mcp_config,
81
- MCPConfigPatch(headers={"Run-Id": run_id}, meta={"run_id": run_id}),
82
- )
83
-
84
- if settings.api_key:
85
- patch_mcp_config(
86
- mcp_config,
87
- MCPConfigPatch(headers={"Authorization": f"Bearer {settings.api_key}"}),
88
- )
89
-
90
- return auto_trace_cm
@@ -0,0 +1,162 @@
1
+ """Utilities to convert JSON schemas into OpenAI's strict format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeGuard
6
+
7
+ _EMPTY_SCHEMA = {
8
+ "additionalProperties": False,
9
+ "type": "object",
10
+ "properties": {},
11
+ "required": [],
12
+ }
13
+
14
+
15
+ def ensure_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
16
+ """Ensure a JSON schema conforms to OpenAI's strict requirements.
17
+
18
+ This mutates the provided schema in-place and returns it for convenience.
19
+ """
20
+ if schema == {}:
21
+ return _EMPTY_SCHEMA.copy()
22
+ return _ensure_strict_json_schema(schema, path=(), root=schema)
23
+
24
+
25
+ def _ensure_strict_json_schema(
26
+ json_schema: object,
27
+ *,
28
+ path: tuple[str, ...],
29
+ root: dict[str, Any],
30
+ ) -> dict[str, Any]:
31
+ if not _is_dict(json_schema):
32
+ raise TypeError(f"Expected {json_schema} to be a dictionary; path={path}")
33
+
34
+ defs = json_schema.get("$defs")
35
+ if _is_dict(defs):
36
+ for def_name, def_schema in defs.items():
37
+ _ensure_strict_json_schema(def_schema, path=(*path, "$defs", def_name), root=root)
38
+
39
+ definitions = json_schema.get("definitions")
40
+ if _is_dict(definitions):
41
+ for definition_name, definition_schema in definitions.items():
42
+ _ensure_strict_json_schema(
43
+ definition_schema, path=(*path, "definitions", definition_name), root=root
44
+ )
45
+
46
+ typ = json_schema.get("type")
47
+ if typ == "object":
48
+ if "additionalProperties" not in json_schema or json_schema["additionalProperties"] is True:
49
+ json_schema["additionalProperties"] = False
50
+ elif (
51
+ json_schema["additionalProperties"] and json_schema["additionalProperties"] is not False
52
+ ):
53
+ raise ValueError(
54
+ "additionalProperties should not be set for object types in strict mode."
55
+ )
56
+
57
+ properties = json_schema.get("properties")
58
+ if _is_dict(properties):
59
+ json_schema["required"] = list(properties.keys())
60
+ json_schema["properties"] = {
61
+ key: _ensure_strict_json_schema(prop_schema, path=(*path, "properties", key), root=root)
62
+ for key, prop_schema in properties.items()
63
+ }
64
+
65
+ items = json_schema.get("items")
66
+ if _is_dict(items):
67
+ json_schema["items"] = _ensure_strict_json_schema(items, path=(*path, "items"), root=root)
68
+
69
+ prefix_items = json_schema.get("prefixItems")
70
+ if _is_list(prefix_items) and prefix_items:
71
+ item_types = set()
72
+ for item in prefix_items:
73
+ if _is_dict(item) and "type" in item:
74
+ item_types.add(item["type"])
75
+
76
+ if len(item_types) == 1:
77
+ item_type = item_types.pop()
78
+ json_schema["items"] = {"type": item_type}
79
+ else:
80
+ json_schema["items"] = {"type": "integer"}
81
+
82
+ tuple_length = len(prefix_items)
83
+ json_schema["minItems"] = tuple_length
84
+ json_schema["maxItems"] = tuple_length
85
+ json_schema.pop("prefixItems")
86
+
87
+ any_of = json_schema.get("anyOf")
88
+ if _is_list(any_of):
89
+ json_schema["anyOf"] = [
90
+ _ensure_strict_json_schema(variant, path=(*path, "anyOf", str(i)), root=root)
91
+ for i, variant in enumerate(any_of)
92
+ ]
93
+
94
+ one_of = json_schema.get("oneOf")
95
+ if _is_list(one_of):
96
+ existing_any_of = json_schema.get("anyOf", [])
97
+ if not _is_list(existing_any_of):
98
+ existing_any_of = []
99
+ json_schema["anyOf"] = existing_any_of + [
100
+ _ensure_strict_json_schema(variant, path=(*path, "oneOf", str(i)), root=root)
101
+ for i, variant in enumerate(one_of)
102
+ ]
103
+ json_schema.pop("oneOf")
104
+
105
+ all_of = json_schema.get("allOf")
106
+ if _is_list(all_of):
107
+ if len(all_of) == 1:
108
+ json_schema.update(
109
+ _ensure_strict_json_schema(all_of[0], path=(*path, "allOf", "0"), root=root)
110
+ )
111
+ json_schema.pop("allOf")
112
+ else:
113
+ json_schema["allOf"] = [
114
+ _ensure_strict_json_schema(entry, path=(*path, "allOf", str(i)), root=root)
115
+ for i, entry in enumerate(all_of)
116
+ ]
117
+
118
+ if "default" in json_schema:
119
+ json_schema.pop("default")
120
+
121
+ for keyword in ("title", "examples", "format"):
122
+ json_schema.pop(keyword, None)
123
+
124
+ ref = json_schema.get("$ref")
125
+ if ref and _has_more_than_n_keys(json_schema, 1):
126
+ if not isinstance(ref, str):
127
+ raise ValueError(f"Received non-string $ref - {ref}")
128
+ resolved = _resolve_ref(root=root, ref=ref)
129
+ if not _is_dict(resolved):
130
+ raise ValueError(
131
+ f"Expected `$ref: {ref}` to resolve to a dictionary but got {resolved}"
132
+ )
133
+ json_schema.update({**resolved, **json_schema})
134
+ json_schema.pop("$ref")
135
+ return _ensure_strict_json_schema(json_schema, path=path, root=root)
136
+
137
+ return json_schema
138
+
139
+
140
+ def _resolve_ref(*, root: dict[str, Any], ref: str) -> object:
141
+ if not ref.startswith("#/"):
142
+ raise ValueError(f"Unexpected $ref format {ref!r}; does not start with #/")
143
+
144
+ path = ref[2:].split("/")
145
+ resolved: object = root
146
+ for key in path:
147
+ assert _is_dict(resolved), f"Encountered non-dictionary entry while resolving {ref}"
148
+ resolved = resolved[key]
149
+
150
+ return resolved
151
+
152
+
153
+ def _is_dict(obj: object) -> TypeGuard[dict[str, Any]]:
154
+ return isinstance(obj, dict)
155
+
156
+
157
+ def _is_list(obj: object) -> TypeGuard[list[object]]:
158
+ return isinstance(obj, list)
159
+
160
+
161
+ def _has_more_than_n_keys(obj: dict[str, object], n: int) -> bool:
162
+ return any(count > n for count, _ in enumerate(obj, start=1))
@@ -11,7 +11,6 @@ def test_utils_imports():
11
11
  assert hud.utils is not None
12
12
 
13
13
  # Try importing submodules
14
- from hud.utils import progress, telemetry
14
+ from hud.utils import telemetry
15
15
 
16
- assert progress is not None
17
16
  assert telemetry is not None
@@ -2,9 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import pytest
6
-
7
- from hud.utils.mcp import MCPConfigPatch, patch_mcp_config, setup_hud_telemetry
5
+ from hud.utils.mcp import MCPConfigPatch, patch_mcp_config
8
6
 
9
7
 
10
8
  class TestPatchMCPConfig:
@@ -85,28 +83,3 @@ class TestPatchMCPConfig:
85
83
  # Existing meta should be preserved, new one added
86
84
  assert mcp_config["test_server"]["meta"]["existing_key"] == "existing_value"
87
85
  assert mcp_config["test_server"]["meta"]["test_key"] == "test_value"
88
-
89
-
90
- class TestSetupHUDTelemetry:
91
- """Tests for setup_hud_telemetry function."""
92
-
93
- def test_empty_config_raises_error(self):
94
- """Test that empty config raises ValueError."""
95
- with pytest.raises(
96
- ValueError, match="Please run initialize\\(\\) before setting up client-side telemetry"
97
- ):
98
- setup_hud_telemetry({})
99
-
100
- def test_none_config_raises_error(self):
101
- """Test that None config raises ValueError."""
102
- with pytest.raises(
103
- ValueError, match="Please run initialize\\(\\) before setting up client-side telemetry"
104
- ):
105
- setup_hud_telemetry(None) # type: ignore[arg-type]
106
-
107
- def test_valid_config_returns_none_when_no_hud_servers(self):
108
- """Test that valid config with no HUD servers returns None."""
109
- mcp_config = {"test_server": {"url": "http://example.com"}}
110
-
111
- result = setup_hud_telemetry(mcp_config)
112
- assert result is None