hud-python 0.4.35__py3-none-any.whl → 0.4.37__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/agents/__init__.py +2 -0
- hud/agents/lite_llm.py +72 -0
- hud/agents/openai_chat_generic.py +21 -7
- hud/agents/tests/test_claude.py +32 -7
- hud/agents/tests/test_openai.py +29 -6
- hud/cli/__init__.py +228 -79
- hud/cli/build.py +26 -6
- hud/cli/dev.py +21 -40
- hud/cli/eval.py +96 -15
- hud/cli/flows/tasks.py +198 -65
- hud/cli/init.py +222 -629
- hud/cli/pull.py +6 -0
- hud/cli/push.py +11 -1
- hud/cli/rl/__init__.py +14 -4
- hud/cli/rl/celebrate.py +187 -0
- hud/cli/rl/config.py +15 -8
- hud/cli/rl/local_runner.py +44 -20
- hud/cli/rl/remote_runner.py +166 -87
- hud/cli/rl/viewer.py +141 -0
- hud/cli/rl/wait_utils.py +89 -0
- hud/cli/tests/test_build.py +3 -27
- hud/cli/tests/test_mcp_server.py +1 -12
- hud/cli/utils/config.py +85 -0
- hud/cli/utils/docker.py +21 -39
- hud/cli/utils/env_check.py +196 -0
- hud/cli/utils/environment.py +4 -3
- hud/cli/utils/interactive.py +2 -1
- hud/cli/utils/local_runner.py +204 -0
- hud/cli/utils/metadata.py +3 -1
- hud/cli/utils/package_runner.py +292 -0
- hud/cli/utils/remote_runner.py +4 -1
- hud/cli/utils/source_hash.py +108 -0
- hud/clients/base.py +1 -1
- hud/clients/fastmcp.py +1 -1
- hud/clients/mcp_use.py +30 -7
- hud/datasets/parallel.py +3 -1
- hud/datasets/runner.py +4 -1
- hud/otel/config.py +1 -1
- hud/otel/context.py +40 -6
- hud/rl/buffer.py +3 -0
- hud/rl/tests/test_learner.py +1 -1
- hud/rl/vllm_adapter.py +1 -1
- hud/server/server.py +234 -7
- hud/server/tests/test_add_tool.py +60 -0
- hud/server/tests/test_context.py +128 -0
- hud/server/tests/test_mcp_server_handlers.py +44 -0
- hud/server/tests/test_mcp_server_integration.py +405 -0
- hud/server/tests/test_mcp_server_more.py +247 -0
- hud/server/tests/test_run_wrapper.py +53 -0
- hud/server/tests/test_server_extra.py +166 -0
- hud/server/tests/test_sigterm_runner.py +78 -0
- hud/settings.py +38 -0
- hud/shared/hints.py +2 -2
- hud/telemetry/job.py +2 -2
- hud/types.py +9 -2
- hud/utils/tasks.py +32 -24
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.35.dist-info → hud_python-0.4.37.dist-info}/METADATA +43 -23
- {hud_python-0.4.35.dist-info → hud_python-0.4.37.dist-info}/RECORD +63 -46
- {hud_python-0.4.35.dist-info → hud_python-0.4.37.dist-info}/WHEEL +0 -0
- {hud_python-0.4.35.dist-info → hud_python-0.4.37.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.35.dist-info → hud_python-0.4.37.dist-info}/licenses/LICENSE +0 -0
hud/cli/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextlib
|
|
6
7
|
import json
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
@@ -28,6 +29,7 @@ from .init import create_environment
|
|
|
28
29
|
from .pull import pull_command
|
|
29
30
|
from .push import push_command
|
|
30
31
|
from .remove import remove_command
|
|
32
|
+
from .utils.config import set_env_values
|
|
31
33
|
from .utils.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
|
|
32
34
|
from .utils.logging import CaptureLogger
|
|
33
35
|
|
|
@@ -116,7 +118,9 @@ def analyze(
|
|
|
116
118
|
image, *docker_args = params
|
|
117
119
|
if live or docker_args: # If docker args provided, assume live mode
|
|
118
120
|
# Build Docker command from image and args
|
|
119
|
-
|
|
121
|
+
from .utils.docker import build_run_command
|
|
122
|
+
|
|
123
|
+
docker_cmd = build_run_command(image, docker_args)
|
|
120
124
|
asyncio.run(analyze_environment(docker_cmd, output_format, verbose))
|
|
121
125
|
else:
|
|
122
126
|
# Fast mode - analyze from metadata
|
|
@@ -239,11 +243,15 @@ def debug(
|
|
|
239
243
|
raise typer.Exit(1)
|
|
240
244
|
|
|
241
245
|
# Build Docker command
|
|
242
|
-
|
|
246
|
+
from .utils.docker import build_run_command
|
|
247
|
+
|
|
248
|
+
command = build_run_command(image_name, docker_args)
|
|
243
249
|
else:
|
|
244
250
|
# Assume it's an image name
|
|
245
251
|
image = first_param
|
|
246
|
-
|
|
252
|
+
from .utils.docker import build_run_command
|
|
253
|
+
|
|
254
|
+
command = build_run_command(image, docker_args)
|
|
247
255
|
else:
|
|
248
256
|
console.print(
|
|
249
257
|
"[red]Error: Must specify a directory, Docker image, --config, or --cursor[/red]"
|
|
@@ -370,12 +378,10 @@ def dev(
|
|
|
370
378
|
False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
|
|
371
379
|
),
|
|
372
380
|
) -> None:
|
|
373
|
-
"""🔥 Development mode
|
|
381
|
+
"""🔥 Development mode - interactive MCP environment.
|
|
374
382
|
|
|
375
|
-
Runs your MCP environment in Docker with
|
|
376
|
-
|
|
377
|
-
The container's last command (typically the MCP server) will be wrapped
|
|
378
|
-
with watchfiles for hot-reload functionality.
|
|
383
|
+
Runs your MCP environment in Docker with mounted source for development.
|
|
384
|
+
The container's CMD determines reload behavior.
|
|
379
385
|
|
|
380
386
|
Examples:
|
|
381
387
|
hud dev # Auto-detect in current directory
|
|
@@ -388,13 +394,12 @@ def dev(
|
|
|
388
394
|
hud dev . --inspector # Launch MCP Inspector (HTTP mode only)
|
|
389
395
|
hud dev . --interactive # Launch interactive testing mode (HTTP mode only)
|
|
390
396
|
hud dev . --no-logs # Disable Docker log streaming
|
|
391
|
-
hud dev . --full-reload # Restart entire container on file changes (instead of just server)
|
|
392
397
|
|
|
393
398
|
# With Docker arguments (after all options):
|
|
394
399
|
hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
|
|
395
400
|
hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
|
|
396
401
|
hud dev . --build -e DEBUG=true --memory 2g
|
|
397
|
-
"""
|
|
402
|
+
"""
|
|
398
403
|
# Parse directory and Docker arguments
|
|
399
404
|
if params:
|
|
400
405
|
directory = params[0]
|
|
@@ -424,7 +429,7 @@ def dev(
|
|
|
424
429
|
def run(
|
|
425
430
|
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
426
431
|
None,
|
|
427
|
-
help="Docker image followed by optional arguments
|
|
432
|
+
help="Python file/module/package or Docker image followed by optional arguments",
|
|
428
433
|
),
|
|
429
434
|
local: bool = typer.Option(
|
|
430
435
|
False,
|
|
@@ -474,32 +479,152 @@ def run(
|
|
|
474
479
|
"--interactive",
|
|
475
480
|
help="Launch interactive testing mode (HTTP transport only)",
|
|
476
481
|
),
|
|
482
|
+
reload: bool = typer.Option(
|
|
483
|
+
False,
|
|
484
|
+
"--reload",
|
|
485
|
+
help="Enable auto-reload on file changes (local Python files only)",
|
|
486
|
+
),
|
|
487
|
+
watch: list[str] = typer.Option( # noqa: B008
|
|
488
|
+
None,
|
|
489
|
+
"--watch",
|
|
490
|
+
help="Directories to watch for changes (can be used multiple times). Defaults to current directory.", # noqa: E501
|
|
491
|
+
),
|
|
492
|
+
cmd: str | None = typer.Option(
|
|
493
|
+
None,
|
|
494
|
+
"--cmd",
|
|
495
|
+
help="Command to run as MCP server (e.g., 'python -m controller')",
|
|
496
|
+
),
|
|
477
497
|
) -> None:
|
|
478
|
-
"""🚀 Run MCP server
|
|
479
|
-
|
|
480
|
-
By default, runs remotely via mcp.hud.so. Use --local for Docker.
|
|
498
|
+
"""🚀 Run MCP server.
|
|
481
499
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
500
|
+
Modes:
|
|
501
|
+
- Python (decorator-based): pass a dotted module path. Example: hud run controller
|
|
502
|
+
The module is imported, decorators register implicitly, and the server runs.
|
|
503
|
+
Use --reload to watch the module/package directory.
|
|
486
504
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
hud run --local my-server:v1 -e API_KEY=xxx
|
|
490
|
-
hud run --local my-server:v1 --transport http
|
|
505
|
+
- Command: use --cmd to run any command as an MCP server. Example: hud run --cmd "python -m controller"
|
|
506
|
+
Works with Docker, binaries, or any executable. Supports --reload.
|
|
491
507
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if not params:
|
|
497
|
-
typer.echo("❌ Docker image is required")
|
|
508
|
+
- Docker image: pass a Docker image name (optionally with --local to run locally).
|
|
509
|
+
""" # noqa: E501
|
|
510
|
+
if not params and not cmd:
|
|
511
|
+
typer.echo("❌ Dotted module path, Docker image, or --cmd is required")
|
|
498
512
|
raise typer.Exit(1)
|
|
499
513
|
|
|
500
|
-
#
|
|
501
|
-
|
|
502
|
-
|
|
514
|
+
# Handle --cmd mode
|
|
515
|
+
if cmd:
|
|
516
|
+
import asyncio
|
|
517
|
+
|
|
518
|
+
from .utils.package_runner import run_package_as_mcp
|
|
519
|
+
|
|
520
|
+
asyncio.run(
|
|
521
|
+
run_package_as_mcp(
|
|
522
|
+
cmd, # Pass command string
|
|
523
|
+
transport=transport,
|
|
524
|
+
port=port,
|
|
525
|
+
verbose=verbose,
|
|
526
|
+
reload=reload,
|
|
527
|
+
watch_paths=watch if watch else None,
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
first_param = params[0]
|
|
533
|
+
extra_args = params[1:] if len(params) > 1 else []
|
|
534
|
+
|
|
535
|
+
# Guard: strip accidental nested 'run' token from positional args,
|
|
536
|
+
# which can happen with nested invocations or reload wrappers.
|
|
537
|
+
if first_param == "run" and extra_args:
|
|
538
|
+
first_param, extra_args = extra_args[0], extra_args[1:]
|
|
539
|
+
|
|
540
|
+
# Try to interpret first_param as module[:attr] or file[:attr]
|
|
541
|
+
target = first_param
|
|
542
|
+
server_attr = "mcp"
|
|
543
|
+
if ":" in target:
|
|
544
|
+
target, server_attr = target.split(":", 1)
|
|
545
|
+
|
|
546
|
+
# Only allow dotted import paths or python files for Python mode
|
|
547
|
+
import importlib.util as _importlib_util
|
|
548
|
+
|
|
549
|
+
# Ensure current working directory is importable for local packages like 'controller'
|
|
550
|
+
try:
|
|
551
|
+
import sys as _sys
|
|
552
|
+
from pathlib import Path as _Path
|
|
553
|
+
|
|
554
|
+
cwd_str = str(_Path.cwd())
|
|
555
|
+
if cwd_str not in _sys.path:
|
|
556
|
+
_sys.path.insert(0, cwd_str)
|
|
557
|
+
except Exception: # noqa: S110
|
|
558
|
+
pass
|
|
559
|
+
try:
|
|
560
|
+
# If given a file path, detect and import via file spec
|
|
561
|
+
from pathlib import Path as _Path
|
|
562
|
+
|
|
563
|
+
if target.endswith(".py") and _Path(target).exists():
|
|
564
|
+
spec = _importlib_util.spec_from_file_location("_hud_module", target)
|
|
565
|
+
else:
|
|
566
|
+
spec = _importlib_util.find_spec(target)
|
|
567
|
+
except Exception:
|
|
568
|
+
spec = None
|
|
569
|
+
|
|
570
|
+
# Fallback: treat a local package directory (e.g. 'controller') as a module target
|
|
571
|
+
from pathlib import Path as _Path
|
|
572
|
+
|
|
573
|
+
pkg_dir = _Path(target)
|
|
574
|
+
is_pkg_dir = pkg_dir.is_dir() and (pkg_dir / "__init__.py").exists()
|
|
575
|
+
|
|
576
|
+
is_python_target = (spec is not None) or is_pkg_dir
|
|
577
|
+
|
|
578
|
+
if is_python_target and not (local or remote):
|
|
579
|
+
# Python file/package mode - use implicit MCP server
|
|
580
|
+
import asyncio
|
|
581
|
+
|
|
582
|
+
from .utils.package_runner import run_package_as_mcp, run_with_reload
|
|
583
|
+
|
|
584
|
+
if reload:
|
|
585
|
+
# Run with watchfiles reload
|
|
586
|
+
# Use user-provided watch paths or compute from module
|
|
587
|
+
if watch:
|
|
588
|
+
watch_paths = watch
|
|
589
|
+
else:
|
|
590
|
+
# Compute a watch path that works for dotted modules as well
|
|
591
|
+
watch_paths = [target]
|
|
592
|
+
if spec is not None:
|
|
593
|
+
origin = getattr(spec, "origin", None)
|
|
594
|
+
sublocs = getattr(spec, "submodule_search_locations", None)
|
|
595
|
+
if origin:
|
|
596
|
+
p = _Path(origin)
|
|
597
|
+
# If package __init__.py, watch the package directory
|
|
598
|
+
watch_paths = [str(p.parent if p.name == "__init__.py" else p)]
|
|
599
|
+
elif sublocs:
|
|
600
|
+
with contextlib.suppress(Exception):
|
|
601
|
+
watch_paths = [next(iter(sublocs))]
|
|
602
|
+
|
|
603
|
+
# Always run as subprocess when using reload to enable proper file watching
|
|
604
|
+
# This ensures the parent process can watch files while the child runs the server
|
|
605
|
+
run_with_reload(
|
|
606
|
+
None, # This forces subprocess mode for both stdio and http
|
|
607
|
+
watch_paths,
|
|
608
|
+
verbose=verbose,
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
# Run normally (but still pass reload=False for consistency)
|
|
612
|
+
asyncio.run(
|
|
613
|
+
run_package_as_mcp(
|
|
614
|
+
target,
|
|
615
|
+
transport=transport,
|
|
616
|
+
port=port,
|
|
617
|
+
verbose=verbose,
|
|
618
|
+
server_attr=server_attr,
|
|
619
|
+
reload=False, # Explicitly pass reload state
|
|
620
|
+
watch_paths=None,
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
# Docker image mode
|
|
626
|
+
image = first_param
|
|
627
|
+
docker_args = extra_args
|
|
503
628
|
|
|
504
629
|
# Handle conflicting flags
|
|
505
630
|
if local and remote:
|
|
@@ -741,6 +866,12 @@ def remove(
|
|
|
741
866
|
@app.command()
|
|
742
867
|
def init(
|
|
743
868
|
name: str = typer.Argument(None, help="Environment name (default: current directory name)"),
|
|
869
|
+
preset: str | None = typer.Option(
|
|
870
|
+
None,
|
|
871
|
+
"--preset",
|
|
872
|
+
"-p",
|
|
873
|
+
help="Preset to use: blank, deep-research, browser. If omitted, you'll choose interactively.", # noqa: E501
|
|
874
|
+
),
|
|
744
875
|
directory: str = typer.Option(".", "--dir", "-d", help="Target directory"),
|
|
745
876
|
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
746
877
|
) -> None:
|
|
@@ -757,7 +888,7 @@ def init(
|
|
|
757
888
|
hud init my-env # Create in ./my-env/
|
|
758
889
|
hud init my-env --dir /tmp # Create in /tmp/my-env/
|
|
759
890
|
"""
|
|
760
|
-
create_environment(name, directory, force)
|
|
891
|
+
create_environment(name, directory, force, preset)
|
|
761
892
|
|
|
762
893
|
|
|
763
894
|
@app.command()
|
|
@@ -774,14 +905,14 @@ def eval(
|
|
|
774
905
|
source: str | None = typer.Argument(
|
|
775
906
|
None,
|
|
776
907
|
help=(
|
|
777
|
-
"HuggingFace dataset
|
|
908
|
+
"HuggingFace dataset (e.g. 'hud-evals/SheetBench-50') or task JSON file. "
|
|
778
909
|
"If not provided, looks for task.json in current directory."
|
|
779
910
|
),
|
|
780
911
|
),
|
|
781
912
|
agent: str | None = typer.Argument(
|
|
782
913
|
None,
|
|
783
914
|
help=(
|
|
784
|
-
"Agent backend to use (claude, openai, or
|
|
915
|
+
"Agent backend to use (claude, openai, vllm, or litellm). If not provided, will prompt interactively." # noqa: E501
|
|
785
916
|
),
|
|
786
917
|
),
|
|
787
918
|
full: bool = typer.Option(
|
|
@@ -829,6 +960,12 @@ def eval(
|
|
|
829
960
|
"--verbose",
|
|
830
961
|
help="Enable verbose output from the agent",
|
|
831
962
|
),
|
|
963
|
+
very_verbose: bool = typer.Option(
|
|
964
|
+
False,
|
|
965
|
+
"--very-verbose",
|
|
966
|
+
"-vv",
|
|
967
|
+
help="Enable debug-level logs for maximum visibility",
|
|
968
|
+
),
|
|
832
969
|
vllm_base_url: str | None = typer.Option(
|
|
833
970
|
None,
|
|
834
971
|
"--vllm-base-url",
|
|
@@ -846,54 +983,21 @@ def eval(
|
|
|
846
983
|
|
|
847
984
|
hud_console = HUDConsole()
|
|
848
985
|
|
|
849
|
-
# If no source provided,
|
|
986
|
+
# If no source provided, reuse RL helper to find a tasks file interactively
|
|
850
987
|
if source is None:
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
"
|
|
855
|
-
"
|
|
856
|
-
|
|
857
|
-
"*Eval*.json",
|
|
858
|
-
"*TASK*.json",
|
|
859
|
-
"*EVAL*.json",
|
|
860
|
-
]
|
|
861
|
-
|
|
862
|
-
# First check current directory
|
|
863
|
-
for pattern in patterns:
|
|
864
|
-
json_files.extend(Path(".").glob(pattern))
|
|
865
|
-
|
|
866
|
-
# If no files found, search recursively (but limit depth to avoid deep searches)
|
|
867
|
-
if not json_files:
|
|
868
|
-
for pattern in patterns:
|
|
869
|
-
# Search up to 2 levels deep
|
|
870
|
-
json_files.extend(Path(".").glob(f"*/{pattern}"))
|
|
871
|
-
json_files.extend(Path(".").glob(f"*/*/{pattern}"))
|
|
872
|
-
|
|
873
|
-
# Remove duplicates and sort
|
|
874
|
-
json_files = sorted(set(json_files))
|
|
875
|
-
|
|
876
|
-
if not json_files:
|
|
988
|
+
try:
|
|
989
|
+
from hud.cli.utils.tasks import find_tasks_file
|
|
990
|
+
|
|
991
|
+
source = find_tasks_file(None, msg="Select a tasks file to run")
|
|
992
|
+
hud_console.success(f"Selected: {source}")
|
|
993
|
+
except Exception as e:
|
|
877
994
|
hud_console.error(
|
|
878
995
|
"No source provided and no task/eval JSON files found in current directory"
|
|
879
996
|
)
|
|
880
997
|
hud_console.info(
|
|
881
|
-
"Usage: hud eval <source> or create a task JSON file "
|
|
882
|
-
"(e.g., task.json, eval_config.json)"
|
|
883
|
-
)
|
|
884
|
-
raise typer.Exit(1)
|
|
885
|
-
elif len(json_files) == 1:
|
|
886
|
-
source = str(json_files[0])
|
|
887
|
-
hud_console.info(f"Found task file: {source}")
|
|
888
|
-
else:
|
|
889
|
-
# Multiple files found, let user choose
|
|
890
|
-
hud_console.info("Multiple task files found:")
|
|
891
|
-
file_choice = hud_console.select(
|
|
892
|
-
"Select a task file to run:",
|
|
893
|
-
choices=[str(f) for f in json_files],
|
|
998
|
+
"Usage: hud eval <source> or create a task JSON file (e.g., task.json, tasks.jsonl)"
|
|
894
999
|
)
|
|
895
|
-
|
|
896
|
-
hud_console.success(f"Selected: {source}")
|
|
1000
|
+
raise typer.Exit(1) from e
|
|
897
1001
|
|
|
898
1002
|
# Import eval_command lazily to avoid importing agent dependencies
|
|
899
1003
|
try:
|
|
@@ -927,13 +1031,14 @@ def eval(
|
|
|
927
1031
|
{"name": "Claude 4 Sonnet", "value": "claude"},
|
|
928
1032
|
{"name": "OpenAI Computer Use", "value": "openai"},
|
|
929
1033
|
{"name": "vLLM (Local Server)", "value": "vllm"},
|
|
1034
|
+
{"name": "LiteLLM (Multi-provider)", "value": "litellm"},
|
|
930
1035
|
]
|
|
931
1036
|
)
|
|
932
1037
|
|
|
933
1038
|
agent = hud_console.select("Select an agent to use:", choices=choices, default=0)
|
|
934
1039
|
|
|
935
1040
|
# Handle HUD model selection
|
|
936
|
-
if agent and agent not in ["claude", "openai", "vllm"]:
|
|
1041
|
+
if agent and agent not in ["claude", "openai", "vllm", "litellm"]:
|
|
937
1042
|
# Find remote model name
|
|
938
1043
|
model = agent
|
|
939
1044
|
if not vllm_base_url:
|
|
@@ -954,7 +1059,7 @@ def eval(
|
|
|
954
1059
|
hud_console.info(f"Using HUD model: {model} (trained on {base_model})")
|
|
955
1060
|
|
|
956
1061
|
# Validate agent choice
|
|
957
|
-
valid_agents = ["claude", "openai", "vllm"]
|
|
1062
|
+
valid_agents = ["claude", "openai", "vllm", "litellm"]
|
|
958
1063
|
if agent not in valid_agents:
|
|
959
1064
|
hud_console.error(f"Invalid agent: {agent}. Must be one of: {', '.join(valid_agents)}")
|
|
960
1065
|
raise typer.Exit(1)
|
|
@@ -972,6 +1077,7 @@ def eval(
|
|
|
972
1077
|
max_workers=max_workers,
|
|
973
1078
|
max_concurrent_per_worker=max_concurrent_per_worker,
|
|
974
1079
|
verbose=verbose,
|
|
1080
|
+
very_verbose=very_verbose,
|
|
975
1081
|
vllm_base_url=vllm_base_url,
|
|
976
1082
|
group_size=group_size,
|
|
977
1083
|
)
|
|
@@ -1021,7 +1127,7 @@ def rl(
|
|
|
1021
1127
|
),
|
|
1022
1128
|
model: str | None = typer.Argument(
|
|
1023
1129
|
None,
|
|
1024
|
-
help="Model to train (default: interactive selection)",
|
|
1130
|
+
help="Model to train from https://hud.so/models (default: interactive selection)",
|
|
1025
1131
|
),
|
|
1026
1132
|
config_file: Path | None = typer.Option( # noqa: B008
|
|
1027
1133
|
None,
|
|
@@ -1061,6 +1167,12 @@ def rl(
|
|
|
1061
1167
|
"--ddp-gpus",
|
|
1062
1168
|
help="Specific GPUs for DDP (e.g., '0,1,2,3')",
|
|
1063
1169
|
),
|
|
1170
|
+
yes: bool = typer.Option(
|
|
1171
|
+
False,
|
|
1172
|
+
"--yes",
|
|
1173
|
+
"-y",
|
|
1174
|
+
help="Auto-accept all prompts and use defaults (lazy mode)",
|
|
1175
|
+
),
|
|
1064
1176
|
vllm_gpu: int | None = typer.Option(
|
|
1065
1177
|
None,
|
|
1066
1178
|
"--vllm-gpu",
|
|
@@ -1082,9 +1194,46 @@ def rl(
|
|
|
1082
1194
|
no_ddp=no_ddp,
|
|
1083
1195
|
ddp_gpus=ddp_gpus,
|
|
1084
1196
|
vllm_gpu=vllm_gpu,
|
|
1197
|
+
yes=yes,
|
|
1085
1198
|
)
|
|
1086
1199
|
|
|
1087
1200
|
|
|
1201
|
+
@app.command()
|
|
1202
|
+
def set(
|
|
1203
|
+
assignments: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
1204
|
+
..., help="One or more KEY=VALUE pairs to persist in ~/.hud/.env"
|
|
1205
|
+
),
|
|
1206
|
+
) -> None:
|
|
1207
|
+
"""Persist API keys or other variables for HUD to use by default.
|
|
1208
|
+
|
|
1209
|
+
Examples:
|
|
1210
|
+
hud set ANTHROPIC_API_KEY=sk-... OPENAI_API_KEY=sk-...
|
|
1211
|
+
|
|
1212
|
+
Values are stored in ~/.hud/.env and are loaded by hud.settings with
|
|
1213
|
+
the lowest precedence (overridden by process env and project .env).
|
|
1214
|
+
"""
|
|
1215
|
+
from hud.utils.hud_console import HUDConsole
|
|
1216
|
+
|
|
1217
|
+
hud_console = HUDConsole()
|
|
1218
|
+
|
|
1219
|
+
updates: dict[str, str] = {}
|
|
1220
|
+
for item in assignments:
|
|
1221
|
+
if "=" not in item:
|
|
1222
|
+
hud_console.error(f"Invalid assignment (expected KEY=VALUE): {item}")
|
|
1223
|
+
raise typer.Exit(1)
|
|
1224
|
+
key, value = item.split("=", 1)
|
|
1225
|
+
key = key.strip()
|
|
1226
|
+
value = value.strip()
|
|
1227
|
+
if not key:
|
|
1228
|
+
hud_console.error(f"Invalid key in assignment: {item}")
|
|
1229
|
+
raise typer.Exit(1)
|
|
1230
|
+
updates[key] = value
|
|
1231
|
+
|
|
1232
|
+
path = set_env_values(updates)
|
|
1233
|
+
hud_console.success("Saved credentials to user config")
|
|
1234
|
+
hud_console.info(f"Location: {path}")
|
|
1235
|
+
|
|
1236
|
+
|
|
1088
1237
|
def main() -> None:
|
|
1089
1238
|
"""Main entry point for the CLI."""
|
|
1090
1239
|
# Handle --version flag before Typer parses args
|
hud/cli/build.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextlib
|
|
6
7
|
import hashlib
|
|
7
8
|
import subprocess
|
|
8
9
|
import time
|
|
@@ -13,6 +14,7 @@ from typing import Any
|
|
|
13
14
|
import typer
|
|
14
15
|
import yaml
|
|
15
16
|
|
|
17
|
+
from hud.cli.utils.source_hash import compute_source_hash, list_source_files
|
|
16
18
|
from hud.clients import MCPClient
|
|
17
19
|
from hud.utils.hud_console import HUDConsole
|
|
18
20
|
from hud.version import __version__ as hud_version
|
|
@@ -236,10 +238,13 @@ def build_docker_image(
|
|
|
236
238
|
hud_console.error(f"No Dockerfile found in {directory}")
|
|
237
239
|
return False
|
|
238
240
|
|
|
241
|
+
# Default platform to match RL pipeline unless explicitly overridden
|
|
242
|
+
effective_platform = platform if platform is not None else "linux/amd64"
|
|
243
|
+
|
|
239
244
|
# Build command
|
|
240
245
|
cmd = ["docker", "build"]
|
|
241
|
-
if
|
|
242
|
-
cmd.extend(["--platform",
|
|
246
|
+
if effective_platform:
|
|
247
|
+
cmd.extend(["--platform", effective_platform])
|
|
243
248
|
cmd.extend(["-t", tag])
|
|
244
249
|
if no_cache:
|
|
245
250
|
cmd.append("--no-cache")
|
|
@@ -338,10 +343,11 @@ def build_environment(
|
|
|
338
343
|
required_env, optional_env = extract_env_vars_from_dockerfile(dockerfile_path)
|
|
339
344
|
|
|
340
345
|
# Merge user-provided env vars with detected ones
|
|
341
|
-
provided_env_vars = {}
|
|
346
|
+
provided_env_vars: dict[str, str] = {}
|
|
342
347
|
missing_required = []
|
|
343
348
|
if env_vars:
|
|
344
|
-
|
|
349
|
+
# Use placeholders in lock file for any provided values to avoid storing secrets
|
|
350
|
+
provided_env_vars = {k: f"${{{k}}}" for k in env_vars}
|
|
345
351
|
# Track which required vars are still missing
|
|
346
352
|
missing_required = [e for e in required_env if e not in env_vars]
|
|
347
353
|
|
|
@@ -381,6 +387,8 @@ def build_environment(
|
|
|
381
387
|
"hudVersion": hud_version,
|
|
382
388
|
"directory": str(env_dir.name),
|
|
383
389
|
"version": new_version, # Internal environment version
|
|
390
|
+
# Fast source fingerprint for change detection
|
|
391
|
+
"sourceHash": compute_source_hash(env_dir),
|
|
384
392
|
},
|
|
385
393
|
"environment": {
|
|
386
394
|
"initializeMs": analysis["initializeMs"],
|
|
@@ -421,6 +429,16 @@ def build_environment(
|
|
|
421
429
|
with open(lock_path, "w") as f:
|
|
422
430
|
yaml.dump(lock_content, f, default_flow_style=False, sort_keys=False)
|
|
423
431
|
|
|
432
|
+
# Also write the file list we hashed for transparency (non-essential)
|
|
433
|
+
with contextlib.suppress(Exception):
|
|
434
|
+
files = [
|
|
435
|
+
str(p.resolve().relative_to(env_dir)).replace("\\", "/")
|
|
436
|
+
for p in list_source_files(env_dir)
|
|
437
|
+
]
|
|
438
|
+
lock_content["build"]["sourceFiles"] = files
|
|
439
|
+
with open(lock_path, "w") as f:
|
|
440
|
+
yaml.dump(lock_content, f, default_flow_style=False, sort_keys=False)
|
|
441
|
+
|
|
424
442
|
hud_console.success("Created lock file: hud.lock.yaml")
|
|
425
443
|
|
|
426
444
|
# Calculate lock file hash
|
|
@@ -437,8 +455,10 @@ def build_environment(
|
|
|
437
455
|
version_tag = f"{base_name}:{new_version}"
|
|
438
456
|
|
|
439
457
|
label_cmd = ["docker", "build"]
|
|
440
|
-
|
|
441
|
-
|
|
458
|
+
# Use same defaulting for the second build step
|
|
459
|
+
label_platform = platform if platform is not None else "linux/amd64"
|
|
460
|
+
if label_platform:
|
|
461
|
+
label_cmd.extend(["--platform", label_platform])
|
|
442
462
|
label_cmd.extend(
|
|
443
463
|
[
|
|
444
464
|
"--label",
|
hud/cli/dev.py
CHANGED
|
@@ -14,7 +14,7 @@ from fastmcp import FastMCP
|
|
|
14
14
|
|
|
15
15
|
from hud.utils.hud_console import HUDConsole
|
|
16
16
|
|
|
17
|
-
from .utils.docker import get_docker_cmd
|
|
17
|
+
from .utils.docker import get_docker_cmd
|
|
18
18
|
from .utils.environment import (
|
|
19
19
|
build_environment,
|
|
20
20
|
get_image_name,
|
|
@@ -42,7 +42,7 @@ def create_proxy_server(
|
|
|
42
42
|
interactive: bool = False,
|
|
43
43
|
) -> FastMCP:
|
|
44
44
|
"""Create an HTTP proxy server that forwards to Docker container with hot-reload."""
|
|
45
|
-
|
|
45
|
+
project_path = Path(directory)
|
|
46
46
|
|
|
47
47
|
# Get the original CMD from the image
|
|
48
48
|
original_cmd = get_docker_cmd(image_name)
|
|
@@ -66,15 +66,22 @@ def create_proxy_server(
|
|
|
66
66
|
"--name",
|
|
67
67
|
container_name,
|
|
68
68
|
"-v",
|
|
69
|
-
f"{
|
|
69
|
+
f"{project_path.absolute()}:/app:rw",
|
|
70
70
|
"-e",
|
|
71
|
-
"PYTHONPATH=/app
|
|
71
|
+
"PYTHONPATH=/app",
|
|
72
|
+
"-e",
|
|
73
|
+
"PYTHONUNBUFFERED=1", # Ensure Python output is not buffered
|
|
72
74
|
]
|
|
73
75
|
|
|
74
76
|
# Add user-provided Docker arguments
|
|
75
77
|
if docker_args:
|
|
76
78
|
docker_cmd.extend(docker_args)
|
|
77
79
|
|
|
80
|
+
# Append the image name and CMD
|
|
81
|
+
docker_cmd.append(image_name)
|
|
82
|
+
if original_cmd:
|
|
83
|
+
docker_cmd.extend(original_cmd)
|
|
84
|
+
|
|
78
85
|
# Disable hot-reload if interactive mode is enabled
|
|
79
86
|
if interactive:
|
|
80
87
|
no_reload = True
|
|
@@ -84,17 +91,6 @@ def create_proxy_server(
|
|
|
84
91
|
hud_console.warning("Cannot use --full-reload with --no-reload, ignoring --full-reload")
|
|
85
92
|
full_reload = False
|
|
86
93
|
|
|
87
|
-
if not no_reload and not full_reload:
|
|
88
|
-
# Standard hot-reload: inject supervisor for server restart within container
|
|
89
|
-
modified_cmd = inject_supervisor(original_cmd)
|
|
90
|
-
docker_cmd.extend(["--entrypoint", modified_cmd[0]])
|
|
91
|
-
docker_cmd.append(image_name)
|
|
92
|
-
docker_cmd.extend(modified_cmd[1:])
|
|
93
|
-
else:
|
|
94
|
-
# No reload or full reload: use original CMD without supervisor
|
|
95
|
-
# Note: Full reload logic (container restart) would be implemented here in the future
|
|
96
|
-
docker_cmd.append(image_name)
|
|
97
|
-
|
|
98
94
|
# Create configuration following MCPConfig schema
|
|
99
95
|
config = {
|
|
100
96
|
"mcpServers": {
|
|
@@ -108,17 +104,12 @@ def create_proxy_server(
|
|
|
108
104
|
|
|
109
105
|
# Debug output - only if verbose
|
|
110
106
|
if verbose:
|
|
111
|
-
if
|
|
112
|
-
hud_console.info("Mode: Hot-reload (server restart within container)")
|
|
113
|
-
hud_console.info("Watching: /app/src for changes")
|
|
114
|
-
elif full_reload:
|
|
107
|
+
if full_reload:
|
|
115
108
|
hud_console.info("Mode: Full reload (container restart on file changes)")
|
|
116
|
-
hud_console.info(
|
|
117
|
-
"Note: Full container restart not yet implemented, using no-reload mode"
|
|
118
|
-
)
|
|
109
|
+
hud_console.info("Note: Full container restart not yet implemented")
|
|
119
110
|
else:
|
|
120
|
-
hud_console.info("Mode:
|
|
121
|
-
hud_console.info("
|
|
111
|
+
hud_console.info("Mode: Container manages its own reload")
|
|
112
|
+
hud_console.info("The container's CMD determines reload behavior")
|
|
122
113
|
hud_console.command_example(f"docker logs -f {container_name}", "View container logs")
|
|
123
114
|
|
|
124
115
|
# Show the full Docker command if there are environment variables
|
|
@@ -227,10 +218,10 @@ async def start_mcp_proxy(
|
|
|
227
218
|
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
228
219
|
root_logger.addHandler(stderr_handler)
|
|
229
220
|
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
if not
|
|
233
|
-
hud_console.error(f"
|
|
221
|
+
# Validate project directory exists
|
|
222
|
+
project_path = Path(directory)
|
|
223
|
+
if not project_path.exists():
|
|
224
|
+
hud_console.error(f"Project directory not found: {project_path}")
|
|
234
225
|
raise click.Abort
|
|
235
226
|
|
|
236
227
|
# Extract container name from the proxy configuration (must match create_proxy_server naming)
|
|
@@ -539,7 +530,7 @@ async def start_mcp_proxy(
|
|
|
539
530
|
stderr=asyncio.subprocess.DEVNULL,
|
|
540
531
|
)
|
|
541
532
|
await stop_result.communicate()
|
|
542
|
-
hud_console.success("
|
|
533
|
+
hud_console.success("Container stopped successfully")
|
|
543
534
|
container_stopped = True
|
|
544
535
|
except Exception as e:
|
|
545
536
|
hud_console.warning(f"Failed to stop container: {e}")
|
|
@@ -774,16 +765,6 @@ def run_mcp_dev_server(
|
|
|
774
765
|
elif not interactive:
|
|
775
766
|
hud_console.progress_message("🧪 Run with --interactive for interactive testing mode")
|
|
776
767
|
|
|
777
|
-
# Disable logs and hot-reload if interactive mode is enabled
|
|
778
|
-
if interactive and not no_logs:
|
|
779
|
-
hud_console.warning("Docker logs disabled in interactive mode for better UI experience")
|
|
780
|
-
no_logs = True
|
|
781
|
-
# if not no_reload:
|
|
782
|
-
# hud_console.warning(
|
|
783
|
-
# "Hot-reload disabled in interactive mode to prevent output interference"
|
|
784
|
-
# )
|
|
785
|
-
# no_reload = True
|
|
786
|
-
|
|
787
768
|
# Show configuration as JSON (just the server config, not wrapped)
|
|
788
769
|
full_config = {}
|
|
789
770
|
full_config[server_name] = server_config
|
|
@@ -796,9 +777,9 @@ def run_mcp_dev_server(
|
|
|
796
777
|
"Connect to Cursor (be careful with multiple windows as that may interfere with the proxy)"
|
|
797
778
|
)
|
|
798
779
|
hud_console.link(deeplink)
|
|
780
|
+
|
|
799
781
|
hud_console.info("") # Empty line
|
|
800
782
|
|
|
801
|
-
# Start the proxy (pass original port, start_mcp_proxy will find actual port again)
|
|
802
783
|
try:
|
|
803
784
|
asyncio.run(
|
|
804
785
|
start_mcp_proxy(
|