hud-python 0.4.11__py3-none-any.whl → 0.4.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.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (63) hide show
  1. hud/__main__.py +8 -0
  2. hud/agents/base.py +7 -8
  3. hud/agents/langchain.py +2 -2
  4. hud/agents/tests/test_openai.py +3 -1
  5. hud/cli/__init__.py +114 -52
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +101 -38
  9. hud/cli/eval.py +175 -90
  10. hud/cli/init.py +442 -64
  11. hud/cli/list_func.py +72 -71
  12. hud/cli/pull.py +1 -2
  13. hud/cli/push.py +35 -23
  14. hud/cli/remove.py +35 -41
  15. hud/cli/tests/test_analyze.py +2 -1
  16. hud/cli/tests/test_analyze_metadata.py +42 -49
  17. hud/cli/tests/test_build.py +28 -52
  18. hud/cli/tests/test_cursor.py +1 -1
  19. hud/cli/tests/test_debug.py +1 -1
  20. hud/cli/tests/test_list_func.py +75 -64
  21. hud/cli/tests/test_main_module.py +30 -0
  22. hud/cli/tests/test_mcp_server.py +3 -3
  23. hud/cli/tests/test_pull.py +30 -61
  24. hud/cli/tests/test_push.py +70 -89
  25. hud/cli/tests/test_registry.py +36 -38
  26. hud/cli/tests/test_utils.py +1 -1
  27. hud/cli/utils/__init__.py +1 -0
  28. hud/cli/{docker_utils.py → utils/docker.py} +36 -0
  29. hud/cli/{env_utils.py → utils/environment.py} +7 -7
  30. hud/cli/{interactive.py → utils/interactive.py} +91 -19
  31. hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
  32. hud/cli/{registry.py → utils/registry.py} +28 -30
  33. hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
  34. hud/cli/utils/runner.py +134 -0
  35. hud/cli/utils/server.py +250 -0
  36. hud/clients/base.py +1 -1
  37. hud/clients/fastmcp.py +5 -13
  38. hud/clients/mcp_use.py +6 -10
  39. hud/server/server.py +35 -5
  40. hud/shared/exceptions.py +11 -0
  41. hud/shared/tests/test_exceptions.py +22 -0
  42. hud/telemetry/tests/__init__.py +0 -0
  43. hud/telemetry/tests/test_replay.py +40 -0
  44. hud/telemetry/tests/test_trace.py +63 -0
  45. hud/tools/base.py +20 -3
  46. hud/tools/computer/hud.py +15 -6
  47. hud/tools/executors/tests/test_base_executor.py +27 -0
  48. hud/tools/response.py +12 -8
  49. hud/tools/tests/test_response.py +60 -0
  50. hud/tools/tests/test_tools_init.py +49 -0
  51. hud/utils/design.py +19 -8
  52. hud/utils/mcp.py +17 -5
  53. hud/utils/tests/test_mcp.py +112 -0
  54. hud/utils/tests/test_version.py +1 -1
  55. hud/version.py +1 -1
  56. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/METADATA +16 -13
  57. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/RECORD +62 -52
  58. hud/cli/runner.py +0 -160
  59. /hud/cli/{cursor.py → utils/cursor.py} +0 -0
  60. /hud/cli/{utils.py → utils/logging.py} +0 -0
  61. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -9,12 +9,17 @@ import subprocess
9
9
  from pathlib import Path
10
10
 
11
11
  import click
12
- import toml
13
12
  from fastmcp import FastMCP
14
13
 
15
14
  from hud.utils.design import HUDDesign
16
- from .docker_utils import get_docker_cmd, inject_supervisor
17
- from .env_utils import get_image_name, update_pyproject_toml, build_environment, image_exists
15
+
16
+ from .utils.docker import get_docker_cmd, inject_supervisor
17
+ from .utils.environment import (
18
+ build_environment,
19
+ get_image_name,
20
+ image_exists,
21
+ update_pyproject_toml,
22
+ )
18
23
 
19
24
  # Global design instance
20
25
  design = HUDDesign()
@@ -30,6 +35,7 @@ def create_proxy_server(
30
35
  directory: str | Path,
31
36
  image_name: str,
32
37
  no_reload: bool = False,
38
+ full_reload: bool = False,
33
39
  verbose: bool = False,
34
40
  docker_args: list[str] | None = None,
35
41
  interactive: bool = False,
@@ -43,8 +49,12 @@ def create_proxy_server(
43
49
  design.warning(f"Could not extract CMD from {image_name}, using default")
44
50
  original_cmd = ["python", "-m", "hud_controller.server"]
45
51
 
46
- # Generate container name from image
47
- container_name = f"{image_name.replace(':', '-').replace('/', '-')}"
52
+ # Generate unique container name from image to avoid conflicts between multiple instances
53
+ import os
54
+
55
+ pid = str(os.getpid())[-6:] # Last 6 digits of process ID for uniqueness
56
+ base_name = image_name.replace(":", "-").replace("/", "-")
57
+ container_name = f"{base_name}-{pid}"
48
58
 
49
59
  # Build the docker run command
50
60
  docker_cmd = [
@@ -68,14 +78,20 @@ def create_proxy_server(
68
78
  if interactive:
69
79
  no_reload = True
70
80
 
71
- if not no_reload:
72
- # Inject our supervisor into the CMD
81
+ # Validate reload options
82
+ if no_reload and full_reload:
83
+ design.warning("Cannot use --full-reload with --no-reload, ignoring --full-reload")
84
+ full_reload = False
85
+
86
+ if not no_reload and not full_reload:
87
+ # Standard hot-reload: inject supervisor for server restart within container
73
88
  modified_cmd = inject_supervisor(original_cmd)
74
89
  docker_cmd.extend(["--entrypoint", modified_cmd[0]])
75
90
  docker_cmd.append(image_name)
76
91
  docker_cmd.extend(modified_cmd[1:])
77
92
  else:
78
- # No reload - use original CMD
93
+ # No reload or full reload: use original CMD without supervisor
94
+ # Note: Full reload logic (container restart) would be implemented here in the future
79
95
  docker_cmd.append(image_name)
80
96
 
81
97
  # Create configuration following MCPConfig schema
@@ -91,12 +107,23 @@ def create_proxy_server(
91
107
 
92
108
  # Debug output - only if verbose
93
109
  if verbose:
94
- if not no_reload:
110
+ if not no_reload and not full_reload:
111
+ design.info("Mode: Hot-reload (server restart within container)")
95
112
  design.info("Watching: /app/src for changes")
113
+ elif full_reload:
114
+ design.info("Mode: Full reload (container restart on file changes)")
115
+ design.info("Note: Full container restart not yet implemented, using no-reload mode")
96
116
  else:
117
+ design.info("Mode: No reload")
97
118
  design.info("Container will run without hot-reload")
98
119
  design.command_example(f"docker logs -f {container_name}", "View container logs")
99
120
 
121
+ # Show the full Docker command if there are environment variables
122
+ if docker_args and any(arg == "-e" or arg.startswith("--env") for arg in docker_args):
123
+ design.info("")
124
+ design.info("Docker command with environment variables:")
125
+ design.info(" ".join(docker_cmd))
126
+
100
127
  # Create the HTTP proxy server using config
101
128
  try:
102
129
  proxy = FastMCP.as_proxy(config, name=f"HUD Dev Proxy - {image_name}")
@@ -116,6 +143,7 @@ async def start_mcp_proxy(
116
143
  transport: str,
117
144
  port: int,
118
145
  no_reload: bool = False,
146
+ full_reload: bool = False,
119
147
  verbose: bool = False,
120
148
  inspector: bool = False,
121
149
  no_logs: bool = False,
@@ -127,10 +155,9 @@ async def start_mcp_proxy(
127
155
  import asyncio
128
156
  import logging
129
157
  import os
130
- import subprocess
131
158
  import sys
132
159
 
133
- from .utils import find_free_port
160
+ from .utils.logging import find_free_port
134
161
 
135
162
  # Always disable the banner - we have our own output
136
163
  os.environ["FASTMCP_DISABLE_BANNER"] = "1"
@@ -202,20 +229,24 @@ async def start_mcp_proxy(
202
229
  design.error(f"Source directory not found: {src_path}")
203
230
  raise click.Abort
204
231
 
205
- # Extract container name from the proxy configuration
206
- container_name = f"{image_name.replace(':', '-').replace('/', '-')}"
232
+ # Extract container name from the proxy configuration (must match create_proxy_server naming)
233
+ import os
234
+
235
+ pid = str(os.getpid())[-6:] # Last 6 digits of process ID for uniqueness
236
+ base_name = image_name.replace(":", "-").replace("/", "-")
237
+ container_name = f"{base_name}-{pid}"
207
238
 
208
239
  # Remove any existing container with the same name (silently)
209
240
  # Note: The proxy creates containers on-demand when clients connect
210
- try:
241
+ try: # noqa: SIM105
211
242
  subprocess.run( # noqa: S603, ASYNC221
212
243
  ["docker", "rm", "-f", container_name], # noqa: S607
213
244
  stdout=subprocess.DEVNULL,
214
245
  stderr=subprocess.DEVNULL,
215
246
  check=False, # Don't raise error if container doesn't exist
216
247
  )
217
- except Exception:
218
- pass # Silent failure, container might not exist
248
+ except Exception: # noqa: S110
249
+ pass
219
250
 
220
251
  if transport == "stdio":
221
252
  if verbose:
@@ -281,13 +312,11 @@ async def start_mcp_proxy(
281
312
  design.error("Failed to launch inspector")
282
313
 
283
314
  # Launch inspector asynchronously so it doesn't block
284
- asyncio.create_task(launch_inspector())
315
+ asyncio.create_task(launch_inspector()) # noqa: RUF006
285
316
 
286
317
  # Launch interactive mode if requested
287
318
  if interactive:
288
319
  if transport != "http":
289
- from hud.utils.design import HUDDesign
290
-
291
320
  design.warning("Interactive mode only works with HTTP transport")
292
321
  else:
293
322
  server_url = f"http://localhost:{actual_port}/mcp"
@@ -306,7 +335,7 @@ async def start_mcp_proxy(
306
335
  design.info("Press Ctrl+C in the interactive session to exit")
307
336
 
308
337
  # Import and run interactive mode in a new event loop
309
- from .interactive import run_interactive_mode
338
+ from .utils.interactive import run_interactive_mode
310
339
 
311
340
  # Create a new event loop for the thread
312
341
  loop = asyncio.new_event_loop()
@@ -329,12 +358,17 @@ async def start_mcp_proxy(
329
358
 
330
359
  # Function to stream Docker logs
331
360
  async def stream_docker_logs() -> None:
332
- """Stream Docker container logs asynchronously."""
361
+ """Stream Docker container logs asynchronously.
362
+
363
+ Note: The Docker container is created on-demand when the first client connects.
364
+ Any environment variables passed via -e flags are included when the container starts.
365
+ """
333
366
  log_design = design
334
367
 
335
368
  # Always show waiting message
336
369
  log_design.info("") # Empty line for spacing
337
370
  log_design.progress_message("⏳ Waiting for first client connection to start container...")
371
+ log_design.info(f"📋 Looking for container: {container_name}") # noqa: G004
338
372
 
339
373
  # Keep trying to stream logs - container is created on demand
340
374
  has_shown_started = False
@@ -385,7 +419,8 @@ async def start_mcp_proxy(
385
419
 
386
420
  # Show all logs with gold formatting like hud debug
387
421
  # Format all logs in gold/dim style like hud debug's stderr
388
- log_design.console.print(
422
+ # Use stdout console to avoid stderr redirection when not verbose
423
+ log_design._stdout_console.print(
389
424
  f"[rgb(192,150,12)]■[/rgb(192,150,12)] {decoded_line}", highlight=False
390
425
  )
391
426
 
@@ -396,16 +431,19 @@ async def start_mcp_proxy(
396
431
  await asyncio.sleep(1)
397
432
  continue # Loop back to check if container exists
398
433
 
399
- except Exception:
400
- # Some unexpected error
434
+ except Exception as e:
435
+ # Some unexpected error - show it so we can debug
436
+ log_design.warning(f"Failed to stream Docker logs: {e}") # noqa: G004
401
437
  if verbose:
402
- log_design.warning("Failed to stream logs")
438
+ import traceback
439
+
440
+ log_design.warning(f"Traceback: {traceback.format_exc()}") # noqa: G004
403
441
  await asyncio.sleep(1)
404
442
 
405
443
  # CRITICAL: Create proxy AFTER all logging setup to prevent it from resetting logging config
406
444
  # This is important because FastMCP might initialize loggers during creation
407
445
  proxy = create_proxy_server(
408
- directory, image_name, no_reload, verbose, docker_args or [], interactive
446
+ directory, image_name, no_reload, full_reload, verbose, docker_args or [], interactive
409
447
  )
410
448
 
411
449
  # One more attempt to suppress the FastMCP server log
@@ -505,9 +543,7 @@ async def start_mcp_proxy(
505
543
  design.info("")
506
544
  design.info("Then you can:")
507
545
  design.info(" • Test locally: [cyan]hud run <image>[/cyan]")
508
- design.info(
509
- " • Push to registry: [cyan]hud push --image <registry/name>[/cyan]"
510
- )
546
+ design.info(" • Push to registry: [cyan]hud push --image <registry/name>[/cyan]")
511
547
  except Exception as e:
512
548
  # Suppress the graceful shutdown error and other FastMCP/uvicorn internal errors
513
549
  error_msg = str(e)
@@ -527,7 +563,7 @@ async def start_mcp_proxy(
527
563
  try:
528
564
  await log_task
529
565
  except asyncio.CancelledError:
530
- pass # Log streaming cancelled, normal shutdown
566
+ contextlib.suppress(asyncio.CancelledError)
531
567
 
532
568
 
533
569
  def run_mcp_dev_server(
@@ -538,6 +574,7 @@ def run_mcp_dev_server(
538
574
  transport: str = "http",
539
575
  port: int = 8765,
540
576
  no_reload: bool = False,
577
+ full_reload: bool = False,
541
578
  verbose: bool = False,
542
579
  inspector: bool = False,
543
580
  no_logs: bool = False,
@@ -588,7 +625,7 @@ def run_mcp_dev_server(
588
625
  # For HTTP transport, find available port first
589
626
  actual_port = port
590
627
  if transport == "http":
591
- from .utils import find_free_port
628
+ from .utils.logging import find_free_port
592
629
 
593
630
  actual_port = find_free_port(port)
594
631
  if actual_port is None:
@@ -600,8 +637,13 @@ def run_mcp_dev_server(
600
637
  # Create config
601
638
  if transport == "stdio":
602
639
  server_config = {"command": "hud", "args": ["dev", directory, "--transport", "stdio"]}
640
+ # For stdio, include docker args in the command
641
+ if docker_args:
642
+ server_config["args"].extend(docker_args)
603
643
  else:
604
644
  server_config = {"url": f"http://localhost:{actual_port}/mcp"}
645
+ # Note: Environment variables are passed to the Docker container via the proxy,
646
+ # not included in the client configuration
605
647
 
606
648
  # For the deeplink, we only need the server config
607
649
  server_config_json = json.dumps(server_config, indent=2)
@@ -627,7 +669,27 @@ def run_mcp_dev_server(
627
669
  else:
628
670
  design.info(f"🐳 {resolved_image}")
629
671
 
630
- design.progress_message(f"❗ If any issues arise, run `hud debug {resolved_image}` to debug the container")
672
+ design.progress_message(
673
+ f"❗ If any issues arise, run `hud debug {resolved_image}` to debug the container"
674
+ )
675
+
676
+ # Show environment variables if provided
677
+ if docker_args and any(arg == "-e" or arg.startswith("--env") for arg in docker_args):
678
+ design.section_title("Environment Variables")
679
+ design.info("The following environment variables will be passed to the Docker container:")
680
+ i = 0
681
+ while i < len(docker_args):
682
+ if docker_args[i] == "-e" and i + 1 < len(docker_args):
683
+ design.info(f" • {docker_args[i + 1]}")
684
+ i += 2
685
+ elif docker_args[i].startswith("--env="):
686
+ design.info(f" • {docker_args[i][6:]}")
687
+ i += 1
688
+ elif docker_args[i] == "--env" and i + 1 < len(docker_args):
689
+ design.info(f" • {docker_args[i + 1]}")
690
+ i += 2
691
+ else:
692
+ i += 1
631
693
 
632
694
  # Show hints about inspector and interactive mode
633
695
  if transport == "http":
@@ -671,6 +733,7 @@ def run_mcp_dev_server(
671
733
  transport,
672
734
  port,
673
735
  no_reload,
736
+ full_reload,
674
737
  verbose,
675
738
  inspector,
676
739
  no_logs,
@@ -679,10 +742,10 @@ def run_mcp_dev_server(
679
742
  )
680
743
  )
681
744
  except Exception as e:
682
- d.error(f"Failed to start MCP server: {e}")
683
- d.info("")
684
- d.info("💡 Tip: Run the following command to debug the container:")
685
- d.info(f" hud debug {resolved_image}")
686
- d.info("")
687
- d.info("This will help identify connection issues or initialization failures.")
745
+ design.error(f"Failed to start MCP server: {e}")
746
+ design.info("")
747
+ design.info("💡 Tip: Run the following command to debug the container:")
748
+ design.info(f" hud debug {resolved_image}")
749
+ design.info("")
750
+ design.info("This will help identify connection issues or initialization failures.")
688
751
  raise