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/build.py
CHANGED
|
@@ -17,18 +17,18 @@ from hud.clients import MCPClient
|
|
|
17
17
|
from hud.utils.design import HUDDesign
|
|
18
18
|
from hud.version import __version__ as hud_version
|
|
19
19
|
|
|
20
|
-
from .registry import save_to_registry
|
|
20
|
+
from .utils.registry import save_to_registry
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def parse_version(version_str: str) -> tuple[int, int, int]:
|
|
24
24
|
"""Parse version string like '1.0.0' or '1.0' into tuple of integers."""
|
|
25
25
|
# Remove 'v' prefix if present
|
|
26
|
-
version_str = version_str.lstrip(
|
|
27
|
-
|
|
26
|
+
version_str = version_str.lstrip("v")
|
|
27
|
+
|
|
28
28
|
# Split by dots and pad with zeros if needed
|
|
29
|
-
parts = version_str.split(
|
|
30
|
-
parts.extend([
|
|
31
|
-
|
|
29
|
+
parts = version_str.split(".")
|
|
30
|
+
parts.extend(["0"] * (3 - len(parts))) # Ensure we have at least 3 parts
|
|
31
|
+
|
|
32
32
|
try:
|
|
33
33
|
return (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
34
34
|
except (ValueError, IndexError):
|
|
@@ -39,7 +39,7 @@ def parse_version(version_str: str) -> tuple[int, int, int]:
|
|
|
39
39
|
def increment_version(version_str: str, increment_type: str = "patch") -> str:
|
|
40
40
|
"""Increment version string. increment_type can be 'major', 'minor', or 'patch'."""
|
|
41
41
|
major, minor, patch = parse_version(version_str)
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
if increment_type == "major":
|
|
44
44
|
return f"{major + 1}.0.0"
|
|
45
45
|
elif increment_type == "minor":
|
|
@@ -52,11 +52,11 @@ def get_existing_version(lock_path: Path) -> str | None:
|
|
|
52
52
|
"""Get the internal version from existing lock file if it exists."""
|
|
53
53
|
if not lock_path.exists():
|
|
54
54
|
return None
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
try:
|
|
57
57
|
with open(lock_path) as f:
|
|
58
58
|
lock_data = yaml.safe_load(f)
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
# Look for internal version in build metadata
|
|
61
61
|
build_data = lock_data.get("build", {})
|
|
62
62
|
return build_data.get("version", None)
|
|
@@ -81,7 +81,7 @@ def get_docker_image_digest(image: str) -> str | None:
|
|
|
81
81
|
if digest_list:
|
|
82
82
|
# Return full image reference with digest
|
|
83
83
|
return digest_list[0]
|
|
84
|
-
except Exception:
|
|
84
|
+
except Exception: # noqa: S110
|
|
85
85
|
# Don't print error here, let calling code handle it
|
|
86
86
|
pass
|
|
87
87
|
return None
|
|
@@ -113,30 +113,60 @@ def extract_env_vars_from_dockerfile(dockerfile_path: Path) -> tuple[list[str],
|
|
|
113
113
|
if not dockerfile_path.exists():
|
|
114
114
|
return required, optional
|
|
115
115
|
|
|
116
|
-
#
|
|
117
|
-
# This is a basic implementation - could be enhanced
|
|
116
|
+
# Parse both ENV and ARG directives
|
|
118
117
|
content = dockerfile_path.read_text()
|
|
118
|
+
arg_vars = set() # Track ARG variables
|
|
119
|
+
|
|
119
120
|
for line in content.splitlines():
|
|
120
121
|
line = line.strip()
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
|
|
123
|
+
# Look for ARG directives (build-time variables)
|
|
124
|
+
if line.startswith("ARG "):
|
|
123
125
|
parts = line[4:].strip().split("=", 1)
|
|
124
|
-
|
|
126
|
+
var_name = parts[0].strip()
|
|
127
|
+
if len(parts) == 1 or not parts[1].strip():
|
|
125
128
|
# No default value = required
|
|
126
|
-
|
|
129
|
+
arg_vars.add(var_name)
|
|
130
|
+
if var_name not in required:
|
|
131
|
+
required.append(var_name)
|
|
132
|
+
|
|
133
|
+
# Look for ENV directives (runtime variables)
|
|
134
|
+
elif line.startswith("ENV "):
|
|
135
|
+
parts = line[4:].strip().split("=", 1)
|
|
136
|
+
var_name = parts[0].strip()
|
|
137
|
+
|
|
138
|
+
# Check if it references an ARG variable (e.g., ENV MY_VAR=$MY_VAR)
|
|
139
|
+
if len(parts) == 2 and parts[1].strip().startswith("$"):
|
|
140
|
+
ref_var = parts[1].strip()[1:]
|
|
141
|
+
if ref_var in arg_vars and var_name not in required:
|
|
142
|
+
required.append(var_name)
|
|
143
|
+
elif len(parts) == 2 and not parts[1].strip():
|
|
144
|
+
# No default value = required
|
|
145
|
+
if var_name not in required:
|
|
146
|
+
required.append(var_name)
|
|
127
147
|
elif len(parts) == 1:
|
|
128
148
|
# No equals sign = required
|
|
129
|
-
required
|
|
149
|
+
if var_name not in required:
|
|
150
|
+
required.append(var_name)
|
|
130
151
|
|
|
131
152
|
return required, optional
|
|
132
153
|
|
|
133
154
|
|
|
134
|
-
async def analyze_mcp_environment(
|
|
155
|
+
async def analyze_mcp_environment(
|
|
156
|
+
image: str, verbose: bool = False, env_vars: dict[str, str] | None = None
|
|
157
|
+
) -> dict[str, Any]:
|
|
135
158
|
"""Analyze an MCP environment to extract metadata."""
|
|
136
159
|
design = HUDDesign()
|
|
160
|
+
env_vars = env_vars or {}
|
|
137
161
|
|
|
138
162
|
# Build Docker command to run the image
|
|
139
|
-
docker_cmd = ["docker", "run", "--rm", "-i"
|
|
163
|
+
docker_cmd = ["docker", "run", "--rm", "-i"]
|
|
164
|
+
|
|
165
|
+
# Add environment variables
|
|
166
|
+
for key, value in env_vars.items():
|
|
167
|
+
docker_cmd.extend(["-e", f"{key}={value}"])
|
|
168
|
+
|
|
169
|
+
docker_cmd.append(image)
|
|
140
170
|
|
|
141
171
|
# Create MCP config
|
|
142
172
|
config = {
|
|
@@ -209,10 +239,15 @@ async def analyze_mcp_environment(image: str, verbose: bool = False) -> dict[str
|
|
|
209
239
|
|
|
210
240
|
|
|
211
241
|
def build_docker_image(
|
|
212
|
-
directory: Path,
|
|
242
|
+
directory: Path,
|
|
243
|
+
tag: str,
|
|
244
|
+
no_cache: bool = False,
|
|
245
|
+
verbose: bool = False,
|
|
246
|
+
build_args: dict[str, str] | None = None,
|
|
213
247
|
) -> bool:
|
|
214
248
|
"""Build a Docker image from a directory."""
|
|
215
249
|
design = HUDDesign()
|
|
250
|
+
build_args = build_args or {}
|
|
216
251
|
|
|
217
252
|
# Check if Dockerfile exists
|
|
218
253
|
dockerfile = directory / "Dockerfile"
|
|
@@ -224,39 +259,35 @@ def build_docker_image(
|
|
|
224
259
|
cmd = ["docker", "build", "-t", tag]
|
|
225
260
|
if no_cache:
|
|
226
261
|
cmd.append("--no-cache")
|
|
262
|
+
|
|
263
|
+
# Add build args
|
|
264
|
+
for key, value in build_args.items():
|
|
265
|
+
cmd.extend(["--build-arg", f"{key}={value}"])
|
|
266
|
+
|
|
227
267
|
cmd.append(str(directory))
|
|
228
268
|
|
|
229
269
|
# Always show build output
|
|
230
270
|
design.info(f"Running: {' '.join(cmd)}")
|
|
231
271
|
|
|
232
272
|
try:
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
stdout=subprocess.PIPE,
|
|
237
|
-
stderr=subprocess.STDOUT,
|
|
238
|
-
text=True,
|
|
239
|
-
encoding="utf-8",
|
|
240
|
-
errors="replace", # Replace invalid chars instead of failing
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
# Stream output
|
|
244
|
-
for line in process.stdout or []:
|
|
245
|
-
design.info(line.rstrip())
|
|
246
|
-
|
|
247
|
-
process.wait()
|
|
248
|
-
|
|
249
|
-
return process.returncode == 0
|
|
273
|
+
# Use Docker's native output formatting - no capture, let Docker handle display
|
|
274
|
+
result = subprocess.run(cmd, check=False) # noqa: S603
|
|
275
|
+
return result.returncode == 0
|
|
250
276
|
except Exception as e:
|
|
251
277
|
design.error(f"Build error: {e}")
|
|
252
278
|
return False
|
|
253
279
|
|
|
254
280
|
|
|
255
281
|
def build_environment(
|
|
256
|
-
directory: str = ".",
|
|
282
|
+
directory: str = ".",
|
|
283
|
+
tag: str | None = None,
|
|
284
|
+
no_cache: bool = False,
|
|
285
|
+
verbose: bool = False,
|
|
286
|
+
env_vars: dict[str, str] | None = None,
|
|
257
287
|
) -> None:
|
|
258
288
|
"""Build a HUD environment and generate lock file."""
|
|
259
289
|
design = HUDDesign()
|
|
290
|
+
env_vars = env_vars or {}
|
|
260
291
|
design.header("HUD Environment Build")
|
|
261
292
|
|
|
262
293
|
# Resolve directory
|
|
@@ -292,7 +323,7 @@ def build_environment(
|
|
|
292
323
|
|
|
293
324
|
design.progress_message(f"Building Docker image: {temp_tag}")
|
|
294
325
|
|
|
295
|
-
# Build the image
|
|
326
|
+
# Build the image (env vars are for runtime, not build time)
|
|
296
327
|
if not build_docker_image(env_dir, temp_tag, no_cache, verbose):
|
|
297
328
|
design.error("Docker build failed")
|
|
298
329
|
raise typer.Exit(1)
|
|
@@ -305,7 +336,7 @@ def build_environment(
|
|
|
305
336
|
loop = asyncio.new_event_loop()
|
|
306
337
|
asyncio.set_event_loop(loop)
|
|
307
338
|
try:
|
|
308
|
-
analysis = loop.run_until_complete(analyze_mcp_environment(temp_tag, verbose))
|
|
339
|
+
analysis = loop.run_until_complete(analyze_mcp_environment(temp_tag, verbose, env_vars))
|
|
309
340
|
finally:
|
|
310
341
|
loop.close()
|
|
311
342
|
|
|
@@ -316,9 +347,9 @@ def build_environment(
|
|
|
316
347
|
|
|
317
348
|
# Provide helpful debugging tips
|
|
318
349
|
design.section_title("Debugging Tips")
|
|
319
|
-
design.info("1.
|
|
320
|
-
design.command_example(
|
|
321
|
-
design.dim_info("
|
|
350
|
+
design.info("1. Debug your environment build:")
|
|
351
|
+
design.command_example("hud debug . --build")
|
|
352
|
+
design.dim_info(" This will", "test MCP server connection and show detailed logs")
|
|
322
353
|
design.info("")
|
|
323
354
|
design.info("2. Check for common issues:")
|
|
324
355
|
design.info(" - Server crashes on startup")
|
|
@@ -336,10 +367,28 @@ def build_environment(
|
|
|
336
367
|
dockerfile_path = env_dir / "Dockerfile"
|
|
337
368
|
required_env, optional_env = extract_env_vars_from_dockerfile(dockerfile_path)
|
|
338
369
|
|
|
370
|
+
# Merge user-provided env vars with detected ones
|
|
371
|
+
provided_env_vars = {}
|
|
372
|
+
missing_required = []
|
|
373
|
+
if env_vars:
|
|
374
|
+
provided_env_vars = env_vars.copy()
|
|
375
|
+
# Track which required vars are still missing
|
|
376
|
+
missing_required = [e for e in required_env if e not in env_vars]
|
|
377
|
+
|
|
378
|
+
# Show what env vars were provided
|
|
379
|
+
design.success(f"Using provided environment variables: {', '.join(env_vars.keys())}")
|
|
380
|
+
else:
|
|
381
|
+
missing_required = required_env[:]
|
|
382
|
+
|
|
383
|
+
# Warn about missing required variables
|
|
384
|
+
if missing_required:
|
|
385
|
+
design.warning(f"Missing required environment variables: {', '.join(missing_required)}")
|
|
386
|
+
design.info("These can be added to the lock file after build or provided with -e flags")
|
|
387
|
+
|
|
339
388
|
# Check for existing version and increment
|
|
340
389
|
lock_path = env_dir / "hud.lock.yaml"
|
|
341
390
|
existing_version = get_existing_version(lock_path)
|
|
342
|
-
|
|
391
|
+
|
|
343
392
|
if existing_version:
|
|
344
393
|
# Increment existing version
|
|
345
394
|
new_version = increment_version(existing_version)
|
|
@@ -365,11 +414,20 @@ def build_environment(
|
|
|
365
414
|
},
|
|
366
415
|
}
|
|
367
416
|
|
|
368
|
-
#
|
|
369
|
-
if
|
|
417
|
+
# Add environment variables section if any exist
|
|
418
|
+
if missing_required or optional_env or provided_env_vars:
|
|
370
419
|
lock_content["environment"]["variables"] = {}
|
|
371
|
-
|
|
372
|
-
|
|
420
|
+
|
|
421
|
+
# Add note about editing environment variables
|
|
422
|
+
lock_content["environment"]["variables"]["_note"] = (
|
|
423
|
+
"You can edit this section to add or modify environment variables. "
|
|
424
|
+
"Provided variables will be used when running the environment."
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if provided_env_vars:
|
|
428
|
+
lock_content["environment"]["variables"]["provided"] = provided_env_vars
|
|
429
|
+
if missing_required:
|
|
430
|
+
lock_content["environment"]["variables"]["required"] = missing_required
|
|
373
431
|
if optional_env:
|
|
374
432
|
lock_content["environment"]["variables"]["optional"] = optional_env
|
|
375
433
|
|
|
@@ -399,7 +457,7 @@ def build_environment(
|
|
|
399
457
|
# Also tag with version
|
|
400
458
|
base_name = tag.split(":")[0] if tag and ":" in tag else tag
|
|
401
459
|
version_tag = f"{base_name}:{new_version}"
|
|
402
|
-
|
|
460
|
+
|
|
403
461
|
label_cmd = [
|
|
404
462
|
"docker",
|
|
405
463
|
"build",
|
|
@@ -411,30 +469,21 @@ def build_environment(
|
|
|
411
469
|
tag,
|
|
412
470
|
"-t",
|
|
413
471
|
version_tag,
|
|
414
|
-
str(env_dir),
|
|
415
472
|
]
|
|
416
473
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
stdout=subprocess.PIPE,
|
|
421
|
-
stderr=subprocess.STDOUT,
|
|
422
|
-
text=True,
|
|
423
|
-
encoding="utf-8",
|
|
424
|
-
errors="replace",
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
# Stream output if verbose
|
|
474
|
+
label_cmd.append(str(env_dir))
|
|
475
|
+
|
|
476
|
+
# Run rebuild using Docker's native output formatting
|
|
428
477
|
if verbose:
|
|
429
|
-
|
|
430
|
-
|
|
478
|
+
# Show Docker's native output when verbose
|
|
479
|
+
result = subprocess.run(label_cmd, check=False) # noqa: S603
|
|
431
480
|
else:
|
|
432
|
-
#
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
481
|
+
# Hide output when not verbose
|
|
482
|
+
result = subprocess.run( # noqa: S603
|
|
483
|
+
label_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False
|
|
484
|
+
)
|
|
436
485
|
|
|
437
|
-
if
|
|
486
|
+
if result.returncode != 0:
|
|
438
487
|
design.error("Failed to rebuild with label")
|
|
439
488
|
raise typer.Exit(1)
|
|
440
489
|
|
|
@@ -475,8 +524,8 @@ def build_environment(
|
|
|
475
524
|
design.status_item("Also tagged", tag)
|
|
476
525
|
design.status_item("Version", new_version)
|
|
477
526
|
design.status_item("Lock file", "hud.lock.yaml")
|
|
478
|
-
design.status_item("Tools found", str(analysis[
|
|
479
|
-
|
|
527
|
+
design.status_item("Tools found", str(analysis["toolCount"]))
|
|
528
|
+
|
|
480
529
|
# Show the digest info separately if we have it
|
|
481
530
|
if image_id:
|
|
482
531
|
design.dim_info("\nImage digest", image_id)
|
|
@@ -500,6 +549,7 @@ def build_command(
|
|
|
500
549
|
),
|
|
501
550
|
no_cache: bool = typer.Option(False, "--no-cache", help="Build without Docker cache"),
|
|
502
551
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
552
|
+
env_vars: dict[str, str] | None = None,
|
|
503
553
|
) -> None:
|
|
504
554
|
"""Build a HUD environment and generate lock file."""
|
|
505
|
-
build_environment(directory, tag, no_cache, verbose)
|
|
555
|
+
build_environment(directory, tag, no_cache, verbose, env_vars)
|
hud/cli/debug.py
CHANGED
|
@@ -14,7 +14,7 @@ from rich.console import Console
|
|
|
14
14
|
from hud.clients import MCPClient
|
|
15
15
|
from hud.utils.design import HUDDesign
|
|
16
16
|
|
|
17
|
-
from .utils import CaptureLogger, Colors, analyze_error_for_hints
|
|
17
|
+
from .utils.logging import CaptureLogger, Colors, analyze_error_for_hints
|
|
18
18
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
@@ -167,7 +167,7 @@ async def debug_mcp_stdio(command: list[str], logger: CaptureLogger, max_phase:
|
|
|
167
167
|
break
|
|
168
168
|
except Exception as e:
|
|
169
169
|
logger.error(f"Failed to parse MCP response: {e}")
|
|
170
|
-
logger.error(f"Raw output that caused the error: {
|
|
170
|
+
logger.error(f"Raw output that caused the error: {line!r}")
|
|
171
171
|
logger.hint("This usually means non-JSON output is being sent to STDOUT")
|
|
172
172
|
logger.hint("Common causes:")
|
|
173
173
|
logger.hint(" - Print statements in your server code")
|