hud-python 0.4.48__py3-none-any.whl → 0.4.50__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/base.py +40 -34
- hud/agents/grounded_openai.py +1 -1
- hud/cli/__init__.py +78 -213
- hud/cli/build.py +105 -45
- hud/cli/dev.py +614 -743
- hud/cli/flows/tasks.py +98 -17
- hud/cli/init.py +18 -14
- hud/cli/push.py +27 -9
- hud/cli/rl/local_runner.py +3 -3
- hud/cli/tests/test_eval.py +168 -119
- hud/cli/tests/test_mcp_server.py +6 -95
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/source_hash.py +1 -1
- hud/server/__init__.py +2 -1
- hud/server/router.py +160 -0
- hud/server/server.py +246 -79
- hud/tools/base.py +9 -1
- hud/tools/bash.py +2 -2
- hud/tools/edit.py +3 -7
- hud/utils/hud_console.py +43 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/METADATA +1 -1
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/RECORD +27 -26
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/WHEEL +0 -0
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/licenses/LICENSE +0 -0
hud/agents/base.py
CHANGED
|
@@ -7,7 +7,7 @@ import fnmatch
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
from abc import ABC, abstractmethod
|
|
10
|
-
from typing import TYPE_CHECKING, Any, ClassVar,
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
11
11
|
|
|
12
12
|
import mcp.types as types
|
|
13
13
|
|
|
@@ -97,9 +97,9 @@ class MCPAgent(ABC):
|
|
|
97
97
|
self.console.set_verbose(True)
|
|
98
98
|
|
|
99
99
|
# User filtering
|
|
100
|
-
self.allowed_tools:
|
|
101
|
-
self.disallowed_tools:
|
|
102
|
-
self._available_tools:
|
|
100
|
+
self.allowed_tools: list[str] | None = allowed_tools
|
|
101
|
+
self.disallowed_tools: list[str] | None = disallowed_tools
|
|
102
|
+
self._available_tools: list[types.Tool] | None = None
|
|
103
103
|
|
|
104
104
|
# Messages
|
|
105
105
|
self.system_prompt = system_prompt
|
|
@@ -144,28 +144,30 @@ class MCPAgent(ABC):
|
|
|
144
144
|
self._handle_connection_error(e)
|
|
145
145
|
|
|
146
146
|
# If task is provided, apply agent_config and add lifecycle tools
|
|
147
|
-
if isinstance(task, Task):
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
147
|
+
if isinstance(task, Task) and task.agent_config:
|
|
148
|
+
if task.agent_config.get("system_prompt"):
|
|
149
|
+
self.system_prompt += "\n\n" + task.agent_config["system_prompt"]
|
|
150
|
+
if "append_setup_output" in task.agent_config:
|
|
151
|
+
self.append_setup_output = task.agent_config["append_setup_output"]
|
|
152
|
+
if "initial_screenshot" in task.agent_config:
|
|
153
|
+
self.initial_screenshot = task.agent_config["initial_screenshot"]
|
|
154
|
+
if "allowed_tools" in task.agent_config:
|
|
155
|
+
# If allowed_tools has already been set, we take the intersection of the two
|
|
156
|
+
# If the list had been empty, we were allowing all tools, so we overwrite this
|
|
157
|
+
if isinstance(self.allowed_tools, list) and len(self.allowed_tools) > 0:
|
|
158
|
+
self.allowed_tools = [
|
|
159
|
+
tool
|
|
160
|
+
for tool in self.allowed_tools
|
|
161
|
+
if tool in task.agent_config["allowed_tools"]
|
|
162
|
+
]
|
|
163
|
+
else: # If allowed_tools is None, we overwrite it
|
|
164
|
+
self.allowed_tools = task.agent_config["allowed_tools"]
|
|
165
|
+
if "disallowed_tools" in task.agent_config:
|
|
166
|
+
# If disallowed_tools has already been set, we take the union of the two
|
|
167
|
+
if isinstance(self.disallowed_tools, list):
|
|
168
|
+
self.disallowed_tools.extend(task.agent_config["disallowed_tools"])
|
|
169
|
+
else: # If disallowed_tools is None, we overwrite it
|
|
170
|
+
self.disallowed_tools = task.agent_config["disallowed_tools"]
|
|
169
171
|
|
|
170
172
|
all_tools = await self.mcp_client.list_tools()
|
|
171
173
|
self._available_tools = []
|
|
@@ -174,14 +176,16 @@ class MCPAgent(ABC):
|
|
|
174
176
|
# No allowed tools and no disallowed tools -> we accept all tools
|
|
175
177
|
# No allowed tools and disallowed tools -> we accept all tools except the disallowed ones
|
|
176
178
|
for tool in all_tools:
|
|
177
|
-
if self.allowed_tools is not None
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
if self.allowed_tools is not None and not any(
|
|
180
|
+
fnmatch.fnmatch(tool.name, pattern) for pattern in self.allowed_tools
|
|
181
|
+
):
|
|
182
|
+
continue
|
|
183
|
+
if self.disallowed_tools is not None and any(
|
|
184
|
+
fnmatch.fnmatch(tool.name, pattern) for pattern in self.disallowed_tools
|
|
185
|
+
):
|
|
186
|
+
continue
|
|
183
187
|
self._available_tools.append(tool)
|
|
184
|
-
|
|
188
|
+
|
|
185
189
|
self.console.info(
|
|
186
190
|
f"Agent initialized with {len(self.get_available_tools())} tools: {', '.join([t.name for t in self.get_available_tools()])}" # noqa: E501
|
|
187
191
|
)
|
|
@@ -622,7 +626,9 @@ class MCPAgent(ABC):
|
|
|
622
626
|
def get_available_tools(self) -> list[types.Tool]:
|
|
623
627
|
"""Get list of available MCP tools for LLM use (excludes lifecycle tools)."""
|
|
624
628
|
if self._available_tools is None:
|
|
625
|
-
raise RuntimeError(
|
|
629
|
+
raise RuntimeError(
|
|
630
|
+
"Tools have not been initialized. Call initialize() before accessing available tools." # noqa: E501
|
|
631
|
+
)
|
|
626
632
|
return self._available_tools
|
|
627
633
|
|
|
628
634
|
def get_tool_schemas(self) -> list[dict]:
|
hud/agents/grounded_openai.py
CHANGED
|
@@ -169,7 +169,7 @@ class GroundedOpenAIChatAgent(GenericOpenAIChatAgent):
|
|
|
169
169
|
protected_keys = {"model", "messages", "tools", "parallel_tool_calls"}
|
|
170
170
|
extra = {k: v for k, v in (self.completion_kwargs or {}).items() if k not in protected_keys}
|
|
171
171
|
|
|
172
|
-
response = await self.oai.chat.completions.create(
|
|
172
|
+
response = await self.oai.chat.completions.create( # type: ignore
|
|
173
173
|
model=self.model_name,
|
|
174
174
|
messages=messages,
|
|
175
175
|
tools=tool_schemas,
|
hud/cli/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import contextlib
|
|
7
6
|
import json
|
|
8
7
|
import sys
|
|
9
8
|
from pathlib import Path
|
|
@@ -39,6 +38,7 @@ app = typer.Typer(
|
|
|
39
38
|
help="🚀 HUD CLI for MCP environment analysis and debugging",
|
|
40
39
|
add_completion=False,
|
|
41
40
|
rich_markup_mode="rich",
|
|
41
|
+
pretty_exceptions_enable=False, # Disable Rich's verbose tracebacks
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
console = Console()
|
|
@@ -352,76 +352,71 @@ def version() -> None:
|
|
|
352
352
|
def dev(
|
|
353
353
|
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
354
354
|
None,
|
|
355
|
-
help="
|
|
355
|
+
help="Module path or extra Docker args (when using --docker)",
|
|
356
356
|
),
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild without cache"),
|
|
362
|
-
transport: str = typer.Option(
|
|
363
|
-
"http", "--transport", "-t", help="Transport protocol: http (default) or stdio"
|
|
357
|
+
docker: bool = typer.Option(
|
|
358
|
+
False,
|
|
359
|
+
"--docker",
|
|
360
|
+
help="Run in Docker with volume mounts for hot-reload (for complex environments)",
|
|
364
361
|
),
|
|
365
|
-
|
|
366
|
-
no_reload: bool = typer.Option(False, "--no-reload", help="Disable hot-reload"),
|
|
367
|
-
full_reload: bool = typer.Option(
|
|
362
|
+
stdio: bool = typer.Option(
|
|
368
363
|
False,
|
|
369
|
-
"--
|
|
370
|
-
help="
|
|
364
|
+
"--stdio",
|
|
365
|
+
help="Use stdio transport (default: HTTP)",
|
|
371
366
|
),
|
|
372
|
-
|
|
367
|
+
port: int = typer.Option(8765, "--port", "-p", help="HTTP server port (ignored for stdio)"),
|
|
368
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed logs"),
|
|
373
369
|
inspector: bool = typer.Option(
|
|
374
370
|
False, "--inspector", help="Launch MCP Inspector (HTTP mode only)"
|
|
375
371
|
),
|
|
376
|
-
no_logs: bool = typer.Option(False, "--no-logs", help="Disable streaming Docker logs"),
|
|
377
372
|
interactive: bool = typer.Option(
|
|
378
373
|
False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
|
|
379
374
|
),
|
|
375
|
+
watch: list[str] = typer.Option( # noqa: B008
|
|
376
|
+
None,
|
|
377
|
+
"--watch",
|
|
378
|
+
help="Additional directories to watch for changes (default: current directory)",
|
|
379
|
+
),
|
|
380
380
|
) -> None:
|
|
381
|
-
"""🔥 Development mode -
|
|
381
|
+
"""🔥 Development mode - run MCP server with hot-reload.
|
|
382
|
+
|
|
383
|
+
TWO MODES:
|
|
384
|
+
|
|
385
|
+
1. Python Module:
|
|
386
|
+
hud dev # Auto-detects module
|
|
387
|
+
hud dev server.main # Explicit module
|
|
382
388
|
|
|
383
|
-
|
|
384
|
-
|
|
389
|
+
2. Docker with Volume Mounts (Complex environments like 'browser'):
|
|
390
|
+
hud dev --docker # Auto-detects image from hud.lock.yaml
|
|
391
|
+
hud dev --docker -p 8080:8080 # With extra Docker args
|
|
392
|
+
|
|
393
|
+
The server must define 'mcp' in its __init__.py or main.py.
|
|
385
394
|
|
|
386
395
|
Examples:
|
|
387
396
|
hud dev # Auto-detect in current directory
|
|
388
|
-
hud dev
|
|
389
|
-
hud dev
|
|
390
|
-
hud dev
|
|
391
|
-
hud dev
|
|
392
|
-
hud dev
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
hud dev . --no-logs # Disable Docker log streaming
|
|
397
|
-
|
|
398
|
-
# With Docker arguments (after all options):
|
|
399
|
-
hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
|
|
400
|
-
hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
|
|
401
|
-
hud dev . --build -e DEBUG=true --memory 2g
|
|
397
|
+
hud dev controller # Run specific module
|
|
398
|
+
hud dev --inspector # Launch MCP Inspector
|
|
399
|
+
hud dev --interactive # Launch interactive testing mode
|
|
400
|
+
hud dev --stdio # Use stdio transport
|
|
401
|
+
hud dev --watch ../shared # Watch additional directories
|
|
402
|
+
|
|
403
|
+
For environment backend servers, use uvicorn directly:
|
|
404
|
+
uvicorn server:app --reload
|
|
402
405
|
"""
|
|
403
|
-
#
|
|
404
|
-
if params
|
|
405
|
-
|
|
406
|
-
docker_args = params[1:] if len(params) > 1 else []
|
|
407
|
-
else:
|
|
408
|
-
directory = "."
|
|
409
|
-
docker_args = []
|
|
406
|
+
# Extract module from params if provided (first param when not --docker)
|
|
407
|
+
module = params[0] if params and not docker else None
|
|
408
|
+
docker_args = params if docker else []
|
|
410
409
|
|
|
411
410
|
run_mcp_dev_server(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
build,
|
|
415
|
-
no_cache,
|
|
416
|
-
transport,
|
|
411
|
+
module,
|
|
412
|
+
stdio,
|
|
417
413
|
port,
|
|
418
|
-
no_reload,
|
|
419
|
-
full_reload,
|
|
420
414
|
verbose,
|
|
421
415
|
inspector,
|
|
422
|
-
no_logs,
|
|
423
416
|
interactive,
|
|
424
|
-
|
|
417
|
+
watch,
|
|
418
|
+
docker=docker,
|
|
419
|
+
docker_args=docker_args,
|
|
425
420
|
)
|
|
426
421
|
|
|
427
422
|
|
|
@@ -429,18 +424,14 @@ def dev(
|
|
|
429
424
|
def run(
|
|
430
425
|
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
431
426
|
None,
|
|
432
|
-
help="
|
|
427
|
+
help="Docker image followed by optional Docker run arguments "
|
|
428
|
+
"(e.g., 'my-image:latest -e KEY=value')",
|
|
433
429
|
),
|
|
434
430
|
local: bool = typer.Option(
|
|
435
431
|
False,
|
|
436
432
|
"--local",
|
|
437
433
|
help="Run locally with Docker (default: remote via mcp.hud.so)",
|
|
438
434
|
),
|
|
439
|
-
remote: bool = typer.Option(
|
|
440
|
-
False,
|
|
441
|
-
"--remote",
|
|
442
|
-
help="Run remotely via mcp.hud.so (default)",
|
|
443
|
-
),
|
|
444
435
|
transport: str = typer.Option(
|
|
445
436
|
"stdio",
|
|
446
437
|
"--transport",
|
|
@@ -474,180 +465,54 @@ def run(
|
|
|
474
465
|
"-v",
|
|
475
466
|
help="Show detailed output",
|
|
476
467
|
),
|
|
477
|
-
interactive: bool = typer.Option(
|
|
478
|
-
False,
|
|
479
|
-
"--interactive",
|
|
480
|
-
help="Launch interactive testing mode (HTTP transport only)",
|
|
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
|
-
),
|
|
497
468
|
) -> None:
|
|
498
|
-
"""🚀 Run MCP server.
|
|
469
|
+
"""🚀 Run Docker image as MCP server.
|
|
499
470
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
The module is imported, decorators register implicitly, and the server runs.
|
|
503
|
-
Use --reload to watch the module/package directory.
|
|
471
|
+
A simple wrapper around 'docker run' that can launch images locally or remotely.
|
|
472
|
+
By default, runs remotely via mcp.hud.so. Use --local to run with local Docker.
|
|
504
473
|
|
|
505
|
-
|
|
506
|
-
Works with Docker, binaries, or any executable. Supports --reload.
|
|
474
|
+
For local Python development with hot-reload, use 'hud dev' instead.
|
|
507
475
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
476
|
+
Examples:
|
|
477
|
+
hud run my-image:latest # Run remotely (default)
|
|
478
|
+
hud run my-image:latest --local # Run with local Docker
|
|
479
|
+
hud run my-image:latest -e KEY=value # Remote with env vars
|
|
480
|
+
hud run my-image:latest --local -e KEY=val # Local with env vars
|
|
481
|
+
hud run my-image:latest --transport http # Use HTTP transport
|
|
482
|
+
"""
|
|
483
|
+
if not params:
|
|
484
|
+
console.print("[red]❌ Docker image is required[/red]")
|
|
485
|
+
console.print("\nExamples:")
|
|
486
|
+
console.print(" hud run my-image:latest # Run remotely (default)")
|
|
487
|
+
console.print(" hud run my-image:latest --local # Run with local Docker")
|
|
488
|
+
console.print("\n[yellow]For local Python development:[/yellow]")
|
|
489
|
+
console.print(" hud dev # Run with hot-reload")
|
|
512
490
|
raise typer.Exit(1)
|
|
513
491
|
|
|
514
|
-
|
|
515
|
-
if
|
|
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
|
|
492
|
+
image = params[0]
|
|
493
|
+
docker_args = params[1:] if len(params) > 1 else []
|
|
548
494
|
|
|
549
|
-
#
|
|
550
|
-
|
|
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
|
|
495
|
+
# Check if user accidentally passed a module path
|
|
496
|
+
from pathlib import Path
|
|
628
497
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
498
|
+
if not any(c in image for c in [":", "/"]) and (
|
|
499
|
+
Path(image).is_dir() or Path(image).is_file() or "." in image
|
|
500
|
+
):
|
|
501
|
+
console.print(f"[yellow]⚠️ '{image}' looks like a module path, not a Docker image[/yellow]")
|
|
502
|
+
console.print("\n[green]For local Python development, use:[/green]")
|
|
503
|
+
console.print(f" hud dev {image}")
|
|
504
|
+
console.print("\n[green]For Docker images:[/green]")
|
|
505
|
+
console.print(" hud run my-image:latest")
|
|
632
506
|
raise typer.Exit(1)
|
|
633
507
|
|
|
634
508
|
# Default to remote if not explicitly local
|
|
635
|
-
is_local = local
|
|
636
|
-
|
|
637
|
-
# Check for interactive mode restrictions
|
|
638
|
-
if interactive:
|
|
639
|
-
if transport != "http":
|
|
640
|
-
typer.echo("❌ Interactive mode requires HTTP transport (use --transport http)")
|
|
641
|
-
raise typer.Exit(1)
|
|
642
|
-
if not is_local:
|
|
643
|
-
typer.echo("❌ Interactive mode is only available for local execution (use --local)")
|
|
644
|
-
raise typer.Exit(1)
|
|
509
|
+
is_local = local
|
|
645
510
|
|
|
646
511
|
if is_local:
|
|
647
512
|
# Local Docker execution
|
|
648
513
|
from .utils.runner import run_mcp_server
|
|
649
514
|
|
|
650
|
-
run_mcp_server(image, docker_args, transport, port, verbose, interactive)
|
|
515
|
+
run_mcp_server(image, docker_args, transport, port, verbose, interactive=False)
|
|
651
516
|
else:
|
|
652
517
|
# Remote execution via proxy
|
|
653
518
|
from .utils.remote_runner import run_remote_server
|