hud-python 0.4.56__py3-none-any.whl → 0.4.58__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

hud/cli/__init__.py CHANGED
@@ -253,10 +253,23 @@ def debug(
253
253
  else:
254
254
  # Assume it's an image name
255
255
  image = first_param
256
- from .utils.docker import build_run_command
256
+ from .utils.docker import create_docker_run_command
257
+
258
+ # For image mode, check if there's a .env file in current directory
259
+ # and use it if available (similar to hud dev behavior)
260
+ cwd = Path.cwd()
261
+ if (cwd / ".env").exists():
262
+ # Use create_docker_run_command to load .env from current directory
263
+ command = create_docker_run_command(
264
+ image,
265
+ docker_args=docker_args,
266
+ env_dir=cwd, # Load .env from current directory
267
+ )
268
+ else:
269
+ # No .env file, use basic command without env loading
270
+ from .utils.docker import build_run_command
257
271
 
258
- # Image-only mode: do not auto-inject local .env
259
- command = build_run_command(image, docker_args)
272
+ command = build_run_command(image, docker_args)
260
273
  else:
261
274
  console.print(
262
275
  "[red]Error: Must specify a directory, Docker image, --config, or --cursor[/red]"
@@ -741,14 +754,14 @@ def remove(
741
754
 
742
755
  @app.command()
743
756
  def init(
744
- name: str = typer.Argument(None, help="Environment name (default: current directory name)"),
757
+ name: str = typer.Argument(None, help="Environment name (default: chosen preset name)"),
745
758
  preset: str | None = typer.Option(
746
759
  None,
747
760
  "--preset",
748
761
  "-p",
749
762
  help="Preset to use: blank, deep-research, browser, rubrics. If omitted, you'll choose interactively.", # noqa: E501
750
763
  ),
751
- directory: str = typer.Option(".", "--dir", "-d", help="Target directory"),
764
+ directory: str = typer.Option(".", "--dir", "-d", help="Parent directory for the environment"),
752
765
  force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
753
766
  ) -> None:
754
767
  """🚀 Initialize a new HUD environment with minimal boilerplate.
@@ -760,8 +773,8 @@ def init(
760
773
  - Required setup/evaluate tools
761
774
 
762
775
  Examples:
763
- hud init # Use current directory name
764
- hud init my-env # Create in ./my-env/
776
+ hud init # Choose preset interactively, create ./preset-name/
777
+ hud init my-env # Create new directory ./my-env/
765
778
  hud init my-env --dir /tmp # Create in /tmp/my-env/
766
779
  """
767
780
  create_environment(name, directory, force, preset)
hud/cli/dev.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import contextlib
6
7
  import importlib
7
8
  import importlib.util
8
9
  import logging
@@ -13,6 +14,8 @@ import threading
13
14
  from pathlib import Path
14
15
  from typing import Any
15
16
 
17
+ import typer
18
+
16
19
  from hud.utils.hud_console import HUDConsole
17
20
 
18
21
  hud_console = HUDConsole()
@@ -26,6 +29,7 @@ def show_dev_server_info(
26
29
  interactive: bool,
27
30
  env_dir: Path | None = None,
28
31
  new: bool = False,
32
+ docker_mode: bool = False,
29
33
  ) -> str:
30
34
  """Show consistent server info for both Python and Docker modes.
31
35
 
@@ -54,7 +58,15 @@ def show_dev_server_info(
54
58
  if transport == "http":
55
59
  hud_console.section_title("Quick Links")
56
60
  hud_console.info(f"{hud_console.sym.ITEM} Docs: http://localhost:{port}/docs")
57
- hud_console.info(f"{hud_console.sym.ITEM} Cursor: {cursor_deeplink}")
61
+ hud_console.info(f"{hud_console.sym.ITEM} Cursor:")
62
+ # Display the Cursor link on its own line to prevent wrapping
63
+ hud_console.link(cursor_deeplink)
64
+
65
+ # Show eval endpoint if in Docker mode
66
+ if docker_mode:
67
+ hud_console.info(
68
+ f"{hud_console.sym.ITEM} Eval API: http://localhost:{port}/eval (POST)"
69
+ )
58
70
 
59
71
  # Check for VNC (browser environment)
60
72
  if env_dir and (env_dir / "environment" / "server.py").exists():
@@ -237,7 +249,7 @@ async def run_mcp_module(
237
249
 
238
250
  from hud.cli.flows.dev import create_dynamic_trace
239
251
 
240
- live_trace_url = await create_dynamic_trace(
252
+ _, live_trace_url = await create_dynamic_trace(
241
253
  mcp_config=local_mcp_config,
242
254
  build_status=False,
243
255
  environment_name=mcp_server.name or "mcp-server",
@@ -510,6 +522,9 @@ def run_docker_dev_server(
510
522
  new: bool = False,
511
523
  ) -> None:
512
524
  """Run MCP server in Docker with volume mounts, expose via local HTTP proxy."""
525
+ import atexit
526
+ import signal
527
+
513
528
  import typer
514
529
  import yaml
515
530
 
@@ -522,6 +537,69 @@ def run_docker_dev_server(
522
537
 
523
538
  cwd = Path.cwd()
524
539
 
540
+ # Container name will be set later and used for cleanup
541
+ container_name: str | None = None
542
+ cleanup_done = False
543
+
544
+ def cleanup_container() -> None:
545
+ """Clean up Docker container on exit."""
546
+ nonlocal cleanup_done
547
+ if cleanup_done or not container_name:
548
+ return
549
+
550
+ cleanup_done = True
551
+ hud_console.debug(f"Cleaning up container: {container_name}")
552
+
553
+ # Check if container is still running
554
+ try:
555
+ result = subprocess.run( # noqa: S603
556
+ ["docker", "ps", "-q", "-f", f"name={container_name}"], # noqa: S607
557
+ stdout=subprocess.PIPE,
558
+ stderr=subprocess.DEVNULL,
559
+ text=True,
560
+ timeout=5,
561
+ )
562
+ if not result.stdout.strip():
563
+ # Container is not running, just try to remove it
564
+ subprocess.run( # noqa: S603
565
+ ["docker", "rm", "-f", container_name], # noqa: S607
566
+ stdout=subprocess.DEVNULL,
567
+ stderr=subprocess.DEVNULL,
568
+ timeout=5,
569
+ )
570
+ return
571
+ except Exception: # noqa: S110
572
+ pass
573
+
574
+ try:
575
+ # First try to stop gracefully
576
+ subprocess.run( # noqa: S603
577
+ ["docker", "stop", container_name], # noqa: S607
578
+ stdout=subprocess.DEVNULL,
579
+ stderr=subprocess.DEVNULL,
580
+ timeout=10,
581
+ )
582
+ hud_console.debug(f"Container {container_name} stopped successfully")
583
+ except subprocess.TimeoutExpired:
584
+ # Force kill if stop times out
585
+ hud_console.debug(f"Container {container_name} stop timeout, forcing kill")
586
+ with contextlib.suppress(Exception):
587
+ subprocess.run( # noqa: S603
588
+ ["docker", "kill", container_name], # noqa: S607
589
+ stdout=subprocess.DEVNULL,
590
+ stderr=subprocess.DEVNULL,
591
+ timeout=5,
592
+ )
593
+
594
+ # Set up signal handlers for cleanup
595
+ def signal_handler(signum: int, frame: Any) -> None:
596
+ cleanup_container()
597
+ sys.exit(0)
598
+
599
+ signal.signal(signal.SIGTERM, signal_handler)
600
+ if sys.platform != "win32":
601
+ signal.signal(signal.SIGHUP, signal_handler)
602
+
525
603
  # Find environment directory (current or parent with hud.lock.yaml)
526
604
  env_dir = cwd
527
605
  lock_path = env_dir / "hud.lock.yaml"
@@ -562,10 +640,14 @@ def run_docker_dev_server(
562
640
  base_name = image_name.replace(":", "-").replace("/", "-")
563
641
  container_name = f"{base_name}-dev-{pid}"
564
642
 
643
+ # Register cleanup function with atexit
644
+ atexit.register(cleanup_container)
645
+
565
646
  # Build docker run command with volume mounts and folder-mode envs
566
647
  from .utils.docker import create_docker_run_command
567
648
 
568
649
  base_args = [
650
+ "--rm", # Automatically remove container when it stops
569
651
  "--name",
570
652
  container_name,
571
653
  "-v",
@@ -608,7 +690,7 @@ def run_docker_dev_server(
608
690
  "headers": {},
609
691
  }
610
692
  }
611
- live_trace_url = _asy.run(
693
+ _, live_trace_url = _asy.run(
612
694
  create_dynamic_trace(
613
695
  mcp_config=local_mcp_config,
614
696
  build_status=True,
@@ -643,6 +725,7 @@ def run_docker_dev_server(
643
725
  interactive=interactive,
644
726
  env_dir=env_dir,
645
727
  new=new,
728
+ docker_mode=True,
646
729
  )
647
730
  hud_console.dim_info(
648
731
  "",
@@ -661,13 +744,38 @@ def run_docker_dev_server(
661
744
  # Create and run proxy with HUD helpers
662
745
  async def run_proxy() -> None:
663
746
  from fastmcp import FastMCP
747
+ from fastmcp.server.proxy import ProxyClient
748
+
749
+ # Create ProxyClient without custom log handler since we capture Docker logs directly
750
+ proxy_client = ProxyClient(mcp_config, name="HUD Docker Dev Proxy")
751
+
752
+ # Extract container name from docker args and store for logs endpoint
753
+ docker_cmd = mcp_config["docker"]["args"]
754
+ container_name = None
755
+ for i, arg in enumerate(docker_cmd):
756
+ if arg == "--name" and i + 1 < len(docker_cmd):
757
+ container_name = docker_cmd[i + 1]
758
+ break
664
759
 
665
- # Create FastMCP proxy to Docker stdio
666
- fastmcp_proxy = FastMCP.as_proxy(mcp_config, name="HUD Docker Dev Proxy")
760
+ if container_name:
761
+ # Store container name for logs endpoint to use
762
+ os.environ["_HUD_DEV_DOCKER_CONTAINER"] = container_name
763
+ hud_console.debug(f"Docker container: {container_name}")
764
+
765
+ # Store the docker mcp_config for the eval endpoint
766
+ import json
767
+
768
+ os.environ["_HUD_DEV_DOCKER_MCP_CONFIG"] = json.dumps(mcp_config)
769
+
770
+ # Create FastMCP proxy using the ProxyClient
771
+ fastmcp_proxy = FastMCP.as_proxy(proxy_client)
667
772
 
668
773
  # Wrap in MCPServer to get /docs and REST wrappers
669
774
  proxy = MCPServer(name="HUD Docker Dev Proxy")
670
775
 
776
+ # Enable logs endpoint on HTTP server
777
+ os.environ["_HUD_DEV_LOGS_PROVIDER"] = "enabled"
778
+
671
779
  # Import all tools from the FastMCP proxy
672
780
  await proxy.import_server(fastmcp_proxy)
673
781
 
@@ -693,7 +801,15 @@ def run_docker_dev_server(
693
801
  asyncio.run(run_proxy())
694
802
  except KeyboardInterrupt:
695
803
  hud_console.info("\n\nStopping...")
804
+ cleanup_container()
696
805
  raise typer.Exit(0) from None
806
+ except Exception:
807
+ # Ensure cleanup happens on any exception
808
+ cleanup_container()
809
+ raise
810
+ finally:
811
+ # Final cleanup attempt
812
+ cleanup_container()
697
813
 
698
814
 
699
815
  def run_mcp_dev_server(
@@ -712,6 +828,20 @@ def run_mcp_dev_server(
712
828
  docker_args = docker_args or []
713
829
  cwd = Path.cwd()
714
830
 
831
+ # Find an available port if not using stdio transport
832
+ if not stdio:
833
+ from hud.cli.utils.logging import find_free_port
834
+
835
+ actual_port = find_free_port(port)
836
+ if actual_port is None:
837
+ hud_console.error(f"No available ports found starting from {port}")
838
+ raise typer.Exit(1)
839
+
840
+ if actual_port != port:
841
+ hud_console.info(f"Port {port} is in use, using port {actual_port} instead")
842
+
843
+ port = actual_port
844
+
715
845
  # Auto-detect Docker mode if Dockerfile present and no module specified
716
846
  if not docker and module is None and should_use_docker_mode(cwd):
717
847
  hud_console.note("Detected Dockerfile - using Docker mode with volume mounts")
hud/cli/eval.py CHANGED
@@ -53,7 +53,7 @@ def get_available_models() -> list[dict[str, str | None]]:
53
53
  try:
54
54
  from hud.cli.rl import rl_api
55
55
 
56
- hud_console.info("Fetching your models from https://hud.so/models")
56
+ hud_console.info("Fetching your models from https://hud.ai/models")
57
57
  models = rl_api.list_models()
58
58
 
59
59
  # Filter for ready models only and sort by recency
@@ -771,7 +771,7 @@ def eval_command(
771
771
  # Check for HUD_API_KEY if using HUD services
772
772
  if not settings.api_key:
773
773
  hud_console.warning("HUD_API_KEY not set. Some features may be limited.")
774
- hud_console.info("Get your API key at: https://hud.so")
774
+ hud_console.info("Get your API key at: https://hud.ai")
775
775
  hud_console.info("Set it in your environment or run: hud set HUD_API_KEY=your-key-here")
776
776
 
777
777
  # Parse allowed tools
hud/cli/flows/dev.py CHANGED
@@ -18,7 +18,7 @@ async def create_dynamic_trace(
18
18
  mcp_config: dict[str, dict[str, Any]],
19
19
  build_status: bool,
20
20
  environment_name: str,
21
- ) -> str | None:
21
+ ) -> tuple[str | None, str | None]:
22
22
  """
23
23
  Create a dynamic trace for HUD dev sessions when running in HTTP mode.
24
24
 
@@ -43,27 +43,16 @@ async def create_dynamic_trace(
43
43
  api_key = settings.api_key
44
44
  if not api_key:
45
45
  logger.warning("Skipping dynamic trace creation; missing HUD_API_KEY")
46
- return None
46
+ return None, None
47
47
 
48
48
  try:
49
49
  resp = await make_request("POST", url=url, json=payload, api_key=api_key)
50
- # New API returns an id; construct the URL as https://hud.so/trace/{id}
51
- trace_id = None
52
- if isinstance(resp, dict):
53
- trace_id = resp.get("id")
54
- if trace_id is None:
55
- data = resp.get("data", {}) or {}
56
- if isinstance(data, dict):
57
- trace_id = data.get("id")
58
- # Backcompat: if url is provided directly
59
- if not trace_id:
60
- direct_url = resp.get("url") or (resp.get("data", {}) or {}).get("url")
61
- if isinstance(direct_url, str) and direct_url:
62
- return direct_url
50
+ # New API returns an id; construct the URL as https://hud.ai/trace/{id}
51
+ trace_id = resp.get("id")
63
52
 
64
53
  if isinstance(trace_id, str) and trace_id:
65
- return f"https://hud.so/trace/{trace_id}"
66
- return None
54
+ return trace_id, f"https://hud.ai/trace/{trace_id}"
55
+ return None, None
67
56
  except Exception as e:
68
57
  # Do not interrupt dev flow
69
58
  try:
@@ -71,7 +60,7 @@ async def create_dynamic_trace(
71
60
  logger.warning("Failed to create dynamic dev trace: %s | payload=%s", e, preview)
72
61
  except Exception:
73
62
  logger.warning("Failed to create dynamic dev trace: %s", e)
74
- return None
63
+ return None, None
75
64
 
76
65
 
77
66
  def show_dev_ui(
@@ -125,7 +114,9 @@ def show_dev_ui(
125
114
  label = "Base image" if is_docker else "Server"
126
115
  hud_console.info("")
127
116
  hud_console.info(f"{hud_console.sym.ITEM} {label}: {server_name}")
128
- hud_console.info(f"{hud_console.sym.ITEM} Cursor: {cursor_deeplink}")
117
+ hud_console.info(f"{hud_console.sym.ITEM} Cursor:")
118
+ # Display the Cursor link on its own line to prevent wrapping
119
+ hud_console.link(cursor_deeplink)
129
120
  hud_console.info("")
130
121
  hud_console.info(f"{hud_console.sym.SUCCESS} Hot-reload enabled")
131
122
  if is_docker:
hud/cli/init.py CHANGED
@@ -182,17 +182,17 @@ def create_environment(
182
182
 
183
183
  hud_console = HUDConsole()
184
184
 
185
- # Determine environment name/target directory
186
- if name is None:
187
- current_dir = Path.cwd()
188
- name = current_dir.name
189
- target_dir = current_dir
190
- hud_console.info(f"Using current directory name: {name}")
191
- else:
192
- target_dir = Path(directory) / name
193
-
194
185
  # Choose preset
195
186
  preset_normalized = (preset or "").strip().lower() if preset else _prompt_for_preset()
187
+
188
+ # If no name is provided, use the preset name as the environment name
189
+ if name is None:
190
+ name = preset_normalized
191
+ hud_console.info(f"Using preset name as environment name: {name}")
192
+
193
+ # Always create a new directory based on the name
194
+ target_dir = Path.cwd() / name if directory == "." else Path(directory) / name
195
+
196
196
  if preset_normalized not in PRESET_MAP:
197
197
  hud_console.warning(
198
198
  f"Unknown preset '{preset_normalized}', defaulting to 'blank' "
@@ -263,14 +263,10 @@ def create_environment(
263
263
  hud_console.status_item(entry, "added")
264
264
 
265
265
  hud_console.section_title("Next steps")
266
- if target_dir == Path.cwd():
267
- hud_console.info("1. Start development server (with MCP inspector):")
268
- hud_console.command_example("hud dev --inspector")
269
- else:
270
- hud_console.info("1. Enter the directory:")
271
- hud_console.command_example(f"cd {target_dir}")
272
- hud_console.info("\n2. Start development server (with MCP inspector):")
273
- hud_console.command_example("hud dev --inspector")
274
-
266
+ # Since we now almost always create a new directory, show cd command
267
+ hud_console.info("1. Enter the directory:")
268
+ hud_console.command_example(f"cd {target_dir.name}")
269
+ hud_console.info("\n2. Start development server (with MCP inspector):")
270
+ hud_console.command_example("hud dev --inspector")
275
271
  hud_console.info("\n3. Review the README in this preset for specific instructions.")
276
272
  hud_console.info("\n4. Customize as needed.")
hud/cli/push.py CHANGED
@@ -152,7 +152,7 @@ def push_environment(
152
152
  hud_console.error("No HUD API key found")
153
153
  hud_console.warning("A HUD API key is required to push environments.")
154
154
  hud_console.info("\nTo get started:")
155
- hud_console.info("1. Get your API key at: https://hud.so/settings")
155
+ hud_console.info("1. Get your API key at: https://hud.ai/settings")
156
156
  hud_console.info("Set it in your environment or run: hud set HUD_API_KEY=your-key-here")
157
157
  hud_console.command_example("hud push", "Try again")
158
158
  hud_console.info("")
@@ -440,7 +440,7 @@ def push_environment(
440
440
  elif response.status_code == 401:
441
441
  hud_console.error("Authentication failed")
442
442
  hud_console.info("Check your HUD_API_KEY is valid")
443
- hud_console.info("Get a new key at: https://hud.so/settings")
443
+ hud_console.info("Get a new key at: https://hud.ai/settings")
444
444
  hud_console.info("Set it in your environment or run: hud set HUD_API_KEY=your-key-here")
445
445
  elif response.status_code == 403:
446
446
  hud_console.error("Permission denied")
hud/cli/rl/__init__.py CHANGED
@@ -25,7 +25,7 @@ def rl_command(
25
25
  ),
26
26
  model: str | None = typer.Argument(
27
27
  None,
28
- help="Model to train from https://hud.so/models (default: interactive selection)",
28
+ help="Model to train from https://hud.ai/models (default: interactive selection)",
29
29
  ),
30
30
  config_file: Path | None = typer.Option( # noqa: B008
31
31
  None,
hud/cli/rl/celebrate.py CHANGED
@@ -133,7 +133,7 @@ def show_confetti(console: Console, seconds: float = 2.5) -> None:
133
133
  """
134
134
  # Show celebratory message first
135
135
  console.print(
136
- "[bold green]🎉 Starting training! See your model on https://hud.so/models[/bold green]"
136
+ "[bold green]🎉 Starting training! See your model on https://hud.ai/models[/bold green]"
137
137
  )
138
138
  time.sleep(0.3) # Brief pause to see the message
139
139
 
@@ -55,7 +55,7 @@ def ensure_vllm_deployed(
55
55
  hud_console.info("Waiting for vLLM server to be ready...")
56
56
  start_time = time.time()
57
57
  with hud_console.progress() as progress:
58
- progress.update("Checking deployment status (see live status on https://hud.so/models)")
58
+ progress.update("Checking deployment status (see live status on https://hud.ai/models)")
59
59
  while True:
60
60
  if time.time() - start_time > timeout:
61
61
  hud_console.error("Timeout waiting for vLLM deployment")
@@ -139,7 +139,7 @@ def run_remote_training(
139
139
  hud_console.section_title("Model Selection")
140
140
 
141
141
  # Fetch existing models
142
- hud_console.info("Fetching your models from https://hud.so/models")
142
+ hud_console.info("Fetching your models from https://hud.ai/models")
143
143
 
144
144
  try:
145
145
  models = rl_api.list_models()
@@ -312,7 +312,7 @@ def run_remote_training(
312
312
  # gpu_table.add_column("Price/hr", style="yellow")
313
313
 
314
314
  # for gpu, info in GPU_PRICING.items():
315
- # gpu_table.add_row(gpu, info["memory"], "see pricing on hud.so")
315
+ # gpu_table.add_row(gpu, info["memory"], "see pricing on hud.ai")
316
316
 
317
317
  # console.print(gpu_table)
318
318