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.
- hud/__main__.py +8 -0
- hud/agents/base.py +7 -8
- hud/agents/langchain.py +2 -2
- hud/agents/tests/test_openai.py +3 -1
- hud/cli/__init__.py +114 -52
- hud/cli/build.py +121 -71
- hud/cli/debug.py +2 -2
- hud/cli/{mcp_server.py → dev.py} +101 -38
- hud/cli/eval.py +175 -90
- hud/cli/init.py +442 -64
- hud/cli/list_func.py +72 -71
- hud/cli/pull.py +1 -2
- hud/cli/push.py +35 -23
- hud/cli/remove.py +35 -41
- hud/cli/tests/test_analyze.py +2 -1
- hud/cli/tests/test_analyze_metadata.py +42 -49
- hud/cli/tests/test_build.py +28 -52
- hud/cli/tests/test_cursor.py +1 -1
- hud/cli/tests/test_debug.py +1 -1
- hud/cli/tests/test_list_func.py +75 -64
- hud/cli/tests/test_main_module.py +30 -0
- hud/cli/tests/test_mcp_server.py +3 -3
- hud/cli/tests/test_pull.py +30 -61
- hud/cli/tests/test_push.py +70 -89
- hud/cli/tests/test_registry.py +36 -38
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/utils/__init__.py +1 -0
- hud/cli/{docker_utils.py → utils/docker.py} +36 -0
- hud/cli/{env_utils.py → utils/environment.py} +7 -7
- hud/cli/{interactive.py → utils/interactive.py} +91 -19
- hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
- hud/cli/{registry.py → utils/registry.py} +28 -30
- hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
- hud/cli/utils/runner.py +134 -0
- hud/cli/utils/server.py +250 -0
- hud/clients/base.py +1 -1
- hud/clients/fastmcp.py +5 -13
- hud/clients/mcp_use.py +6 -10
- hud/server/server.py +35 -5
- hud/shared/exceptions.py +11 -0
- hud/shared/tests/test_exceptions.py +22 -0
- hud/telemetry/tests/__init__.py +0 -0
- hud/telemetry/tests/test_replay.py +40 -0
- hud/telemetry/tests/test_trace.py +63 -0
- hud/tools/base.py +20 -3
- hud/tools/computer/hud.py +15 -6
- hud/tools/executors/tests/test_base_executor.py +27 -0
- hud/tools/response.py +12 -8
- hud/tools/tests/test_response.py +60 -0
- hud/tools/tests/test_tools_init.py +49 -0
- hud/utils/design.py +19 -8
- hud/utils/mcp.py +17 -5
- hud/utils/tests/test_mcp.py +112 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/METADATA +16 -13
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/RECORD +62 -52
- hud/cli/runner.py +0 -160
- /hud/cli/{cursor.py → utils/cursor.py} +0 -0
- /hud/cli/{utils.py → utils/logging.py} +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/WHEEL +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/licenses/LICENSE +0 -0
hud/cli/{mcp_server.py → dev.py}
RENAMED
|
@@ -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
|
-
|
|
17
|
-
from .
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|