hud-python 0.4.1__py3-none-any.whl → 0.4.3__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.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/__init__.py CHANGED
@@ -1,617 +1,617 @@
1
- """HUD CLI - Command-line interface for MCP environment analysis and debugging."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import json
7
- import os
8
- import sys
9
- from pathlib import Path # noqa: TC003
10
-
11
- import typer
12
- from rich.console import Console
13
- from rich.panel import Panel
14
- from rich.table import Table
15
-
16
- from .analyze import (
17
- analyze_environment,
18
- analyze_environment_from_config,
19
- analyze_environment_from_mcp_config,
20
- )
21
- from .build import build_command
22
- from .clone import clone_repository, get_clone_message, print_error, print_tutorial
23
- from .cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
24
- from .debug import debug_mcp_stdio
25
- from .init import create_environment
26
- from .mcp_server import run_mcp_dev_server
27
- from .pull import pull_command
28
- from .push import push_command
29
- from .utils import CaptureLogger
30
-
31
- # Create the main Typer app
32
- app = typer.Typer(
33
- name="hud",
34
- help="🚀 HUD CLI for MCP environment analysis and debugging",
35
- add_completion=False,
36
- rich_markup_mode="rich",
37
- )
38
-
39
- console = Console()
40
-
41
-
42
- # Capture IMAGE and any following Docker args as a single variadic argument list.
43
- @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
44
- def analyze(
45
- params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
46
- None, # Optional positional arguments
47
- help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
48
- ),
49
- config: Path = typer.Option( # noqa: B008
50
- None,
51
- "--config",
52
- "-c",
53
- help="JSON config file with MCP configuration",
54
- exists=True,
55
- file_okay=True,
56
- dir_okay=False,
57
- ),
58
- cursor: str | None = typer.Option(
59
- None,
60
- "--cursor",
61
- help="Analyze a server from Cursor config",
62
- ),
63
- output_format: str = typer.Option(
64
- "interactive",
65
- "--format",
66
- "-f",
67
- help="Output format: interactive, json, markdown",
68
- ),
69
- verbose: bool = typer.Option(
70
- False,
71
- "--verbose",
72
- "-v",
73
- help="Enable verbose output (shows tool schemas)",
74
- ),
75
- live: bool = typer.Option(
76
- False,
77
- "--live",
78
- help="Run container for live analysis (slower but more accurate)",
79
- ),
80
- ) -> None:
81
- """🔍 Analyze MCP environment - discover tools, resources, and capabilities.
82
-
83
- By default, uses cached metadata for instant results.
84
- Use --live to run the container for real-time analysis.
85
-
86
- Examples:
87
- hud analyze hudpython/test_init # Fast metadata inspection
88
- hud analyze my-env --live # Full container analysis
89
- hud analyze --config mcp-config.json # From MCP config
90
- hud analyze --cursor text-2048-dev # From Cursor config
91
- """
92
- if config:
93
- # Load config from JSON file (always live for configs)
94
- asyncio.run(analyze_environment_from_config(config, output_format, verbose))
95
- elif cursor:
96
- # Parse cursor config (always live for cursor)
97
- command, error = parse_cursor_config(cursor)
98
- if error or command is None:
99
- console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
100
- raise typer.Exit(1)
101
- # Convert to MCP config
102
- mcp_config = {
103
- "local": {"command": command[0], "args": command[1:] if len(command) > 1 else []}
104
- }
105
- asyncio.run(analyze_environment_from_mcp_config(mcp_config, output_format, verbose))
106
- elif params:
107
- image, *docker_args = params
108
- if live or docker_args: # If docker args provided, assume live mode
109
- # Build Docker command from image and args
110
- docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
111
- asyncio.run(analyze_environment(docker_cmd, output_format, verbose))
112
- else:
113
- # Fast mode - analyze from metadata
114
- from .analyze_metadata import analyze_from_metadata
115
-
116
- asyncio.run(analyze_from_metadata(image, output_format, verbose))
117
- else:
118
- console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
119
- console.print("\nExamples:")
120
- console.print(" hud analyze hudpython/test_init # Fast metadata analysis")
121
- console.print(" hud analyze my-env --live # Live container analysis")
122
- console.print(" hud analyze --config mcp-config.json # From config file")
123
- console.print(" hud analyze --cursor my-server # From Cursor")
124
- raise typer.Exit(1)
125
-
126
-
127
- # Same variadic approach for debug.
128
- @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
129
- def debug(
130
- params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
131
- None,
132
- help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
133
- ),
134
- config: Path = typer.Option( # noqa: B008
135
- None,
136
- "--config",
137
- "-c",
138
- help="JSON config file with MCP configuration",
139
- exists=True,
140
- file_okay=True,
141
- dir_okay=False,
142
- ),
143
- cursor: str | None = typer.Option(
144
- None,
145
- "--cursor",
146
- help="Debug a server from Cursor config",
147
- ),
148
- max_phase: int = typer.Option(
149
- 5,
150
- "--max-phase",
151
- "-p",
152
- min=1,
153
- max=5,
154
- help="Maximum debug phase (1-5)",
155
- ),
156
- ) -> None:
157
- """🐛 Debug MCP environment - test initialization, tools, and readiness.
158
-
159
- Examples:
160
- hud debug hud-text-2048:latest
161
- hud debug my-mcp-server:v1 -e API_KEY=xxx -p 8080:8080
162
- hud debug --config mcp-config.json
163
- hud debug --cursor text-2048-dev
164
- hud debug hud-browser:dev --max-phase 3
165
- """
166
-
167
- # Determine the command to run
168
- command = None
169
-
170
- if config:
171
- # Load config from JSON file
172
- with open(config) as f:
173
- mcp_config = json.load(f)
174
-
175
- # Extract command from first server in config
176
- server_name = next(iter(mcp_config.keys()))
177
- server_config = mcp_config[server_name]
178
- command = [server_config["command"], *server_config.get("args", [])]
179
- elif cursor:
180
- # Parse cursor config
181
- command, error = parse_cursor_config(cursor)
182
- if error or command is None:
183
- console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
184
- raise typer.Exit(1)
185
- elif params:
186
- image, *docker_args = params
187
- # Build Docker command
188
- command = ["docker", "run", "--rm", "-i", *docker_args, image]
189
- else:
190
- console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
191
- console.print("\nExamples:")
192
- console.print(" hud debug hud-text-2048:latest")
193
- console.print(" hud debug --config mcp-config.json")
194
- console.print(" hud debug --cursor my-server")
195
- raise typer.Exit(1)
196
-
197
- # Create logger and run debug
198
- logger = CaptureLogger(print_output=True)
199
- phases_completed = asyncio.run(debug_mcp_stdio(command, logger, max_phase=max_phase))
200
-
201
- # Show summary using design system
202
- from hud.utils.design import HUDDesign
203
-
204
- design = HUDDesign()
205
-
206
- design.info("") # Empty line
207
- design.section_title("Debug Summary")
208
-
209
- if phases_completed == max_phase:
210
- design.success(f"All {max_phase} phases completed successfully!")
211
- if max_phase == 5:
212
- design.info("Your MCP server is fully functional and ready for production use.")
213
- else:
214
- design.warning(f"Completed {phases_completed} out of {max_phase} phases")
215
- design.info("Check the errors above for troubleshooting.")
216
-
217
- # Exit with appropriate code
218
- if phases_completed < max_phase:
219
- raise typer.Exit(1)
220
-
221
-
222
- @app.command()
223
- def cursor_list() -> None:
224
- """📋 List all MCP servers configured in Cursor."""
225
- console.print(Panel.fit("📋 [bold cyan]Cursor MCP Servers[/bold cyan]", border_style="cyan"))
226
-
227
- servers, error = list_cursor_servers()
228
-
229
- if error:
230
- console.print(f"[red]❌ {error}[/red]")
231
- raise typer.Exit(1)
232
-
233
- if not servers:
234
- console.print("[yellow]No servers found in Cursor config[/yellow]")
235
- return
236
-
237
- # Display servers in a table
238
- table = Table(title="Available Servers")
239
- table.add_column("Server Name", style="cyan")
240
- table.add_column("Command Preview", style="dim")
241
-
242
- config_path = get_cursor_config_path()
243
- if config_path.exists():
244
- with open(config_path) as f:
245
- config = json.load(f)
246
- mcp_servers = config.get("mcpServers", {})
247
-
248
- for server_name in servers:
249
- server_config = mcp_servers.get(server_name, {})
250
- command = server_config.get("command", "")
251
- args = server_config.get("args", [])
252
-
253
- # Create command preview
254
- if args:
255
- preview = f"{command} {' '.join(args[:2])}"
256
- if len(args) > 2:
257
- preview += " ..."
258
- else:
259
- preview = command
260
-
261
- table.add_row(server_name, preview)
262
-
263
- console.print(table)
264
- console.print(f"\n[dim]Config location: {config_path}[/dim]")
265
- console.print(
266
- "\n[green]Tip:[/green] Use [cyan]hud debug --cursor <server-name>[/cyan] to debug a server"
267
- )
268
-
269
-
270
- @app.command()
271
- def version() -> None:
272
- """Show HUD CLI version."""
273
- try:
274
- from hud import __version__
275
-
276
- console.print(f"HUD CLI version: [cyan]{__version__}[/cyan]")
277
- except ImportError:
278
- console.print("HUD CLI version: [cyan]unknown[/cyan]")
279
-
280
-
281
- @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
282
- def dev(
283
- params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
284
- None,
285
- help="Environment directory followed by optional Docker arguments (e.g., '. -e KEY=value')",
286
- ),
287
- image: str | None = typer.Option(
288
- None, "--image", "-i", help="Docker image name (overrides auto-detection)"
289
- ),
290
- build: bool = typer.Option(False, "--build", "-b", help="Build image before starting"),
291
- no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild without cache"),
292
- transport: str = typer.Option(
293
- "http", "--transport", "-t", help="Transport protocol: http (default) or stdio"
294
- ),
295
- port: int = typer.Option(8765, "--port", "-p", help="HTTP server port (ignored for stdio)"),
296
- no_reload: bool = typer.Option(False, "--no-reload", help="Disable hot-reload"),
297
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show server logs"),
298
- inspector: bool = typer.Option(
299
- False, "--inspector", help="Launch MCP Inspector (HTTP mode only)"
300
- ),
301
- no_logs: bool = typer.Option(False, "--no-logs", help="Disable streaming Docker logs"),
302
- interactive: bool = typer.Option(
303
- False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
304
- ),
305
- ) -> None:
306
- """🔥 Development mode with hot-reload.
307
-
308
- Runs your MCP environment in Docker with automatic restart on file changes.
309
-
310
- The container's last command (typically the MCP server) will be wrapped
311
- with watchfiles for hot-reload functionality.
312
-
313
- Examples:
314
- hud dev # Auto-detect in current directory
315
- hud dev environments/browser # Specific directory
316
- hud dev . --build # Build image first
317
- hud dev . --image custom:tag # Use specific image
318
- hud dev . --no-cache # Force clean rebuild
319
- hud dev . --verbose # Show detailed logs
320
- hud dev . --transport stdio # Use stdio proxy for multiple connections
321
- hud dev . --inspector # Launch MCP Inspector (HTTP mode only)
322
- hud dev . --interactive # Launch interactive testing mode (HTTP mode only)
323
- hud dev . --no-logs # Disable Docker log streaming
324
-
325
- # With Docker arguments (after all options):
326
- hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
327
- hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
328
- hud dev . --build -e DEBUG=true --memory 2g
329
- """
330
- # Parse directory and Docker arguments
331
- if params:
332
- directory = params[0]
333
- docker_args = params[1:] if len(params) > 1 else []
334
- else:
335
- directory = "."
336
- docker_args = []
337
-
338
- run_mcp_dev_server(
339
- directory,
340
- image,
341
- build,
342
- no_cache,
343
- transport,
344
- port,
345
- no_reload,
346
- verbose,
347
- inspector,
348
- no_logs,
349
- interactive,
350
- docker_args,
351
- )
352
-
353
-
354
- @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
355
- def run(
356
- params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
357
- None,
358
- help="Docker image followed by optional arguments (e.g., 'hud-image:latest -e KEY=value')",
359
- ),
360
- local: bool = typer.Option(
361
- False,
362
- "--local",
363
- help="Run locally with Docker (default: remote via mcp.hud.so)",
364
- ),
365
- remote: bool = typer.Option(
366
- False,
367
- "--remote",
368
- help="Run remotely via mcp.hud.so (default)",
369
- ),
370
- transport: str = typer.Option(
371
- "stdio",
372
- "--transport",
373
- "-t",
374
- help="Transport protocol: stdio (default) or http",
375
- ),
376
- port: int = typer.Option(
377
- 8765,
378
- "--port",
379
- "-p",
380
- help="Port for HTTP transport (ignored for stdio)",
381
- ),
382
- url: str = typer.Option(
383
- None,
384
- "--url",
385
- help="Remote MCP server URL (default: HUD_MCP_URL or mcp.hud.so)",
386
- ),
387
- api_key: str | None = typer.Option(
388
- None,
389
- "--api-key",
390
- help="API key for remote server (default: HUD_API_KEY env var)",
391
- ),
392
- run_id: str | None = typer.Option(
393
- None,
394
- "--run-id",
395
- help="Run ID for tracking (remote only)",
396
- ),
397
- verbose: bool = typer.Option(
398
- False,
399
- "--verbose",
400
- "-v",
401
- help="Show detailed output",
402
- ),
403
- ) -> None:
404
- """🚀 Run MCP server locally or remotely.
405
-
406
- By default, runs remotely via mcp.hud.so. Use --local for Docker.
407
-
408
- Remote Examples:
409
- hud run hud-text-2048:latest
410
- hud run my-server:v1 -e API_KEY=xxx -h Run-Id:abc123
411
- hud run my-server:v1 --transport http --port 9000
412
-
413
- Local Examples:
414
- hud run --local hud-text-2048:latest
415
- hud run --local my-server:v1 -e API_KEY=xxx
416
- hud run --local my-server:v1 --transport http
417
- """
418
- if not params:
419
- typer.echo("❌ Docker image is required")
420
- raise typer.Exit(1)
421
-
422
- # Parse image and args
423
- image = params[0]
424
- docker_args = params[1:] if len(params) > 1 else []
425
-
426
- # Handle conflicting flags
427
- if local and remote:
428
- typer.echo("❌ Cannot use both --local and --remote")
429
- raise typer.Exit(1)
430
-
431
- # Default to remote if not explicitly local
432
- is_local = local and not remote
433
-
434
- if is_local:
435
- # Local Docker execution
436
- from .runner import run_mcp_server
437
-
438
- run_mcp_server(image, docker_args, transport, port, verbose)
439
- else:
440
- # Remote execution via proxy
441
- from .remote_runner import run_remote_server
442
-
443
- # Get URL from options or environment
444
- if not url:
445
- url = os.getenv("HUD_MCP_URL", "https://mcp.hud.so/v3/mcp")
446
-
447
- run_remote_server(image, docker_args, transport, port, url, api_key, run_id, verbose)
448
-
449
-
450
- @app.command()
451
- def clone(
452
- url: str = typer.Argument(
453
- ...,
454
- help="Git repository URL to clone",
455
- ),
456
- ) -> None:
457
- """🚀 Clone a git repository quietly with a pretty output.
458
-
459
- This command wraps 'git clone' with the --quiet flag and displays
460
- a rich formatted success message. If the repository contains a clone
461
- message in pyproject.toml, it will be displayed as a tutorial.
462
-
463
- Configure clone messages in your repository's pyproject.toml:
464
-
465
- [tool.hud.clone]
466
- title = "🚀 My Project"
467
- message = "Thanks for cloning! Run 'pip install -e .' to get started."
468
-
469
- # Or use markdown format:
470
- # markdown = "## Welcome!\\n\\nHere's how to get started..."
471
- # style = "cyan"
472
-
473
- Examples:
474
- hud clone https://github.com/user/repo.git
475
- """
476
- # Run the clone
477
- success, result = clone_repository(url)
478
-
479
- if success:
480
- # Look for clone message configuration
481
- clone_config = get_clone_message(result)
482
- print_tutorial(clone_config)
483
- else:
484
- print_error(result)
485
- raise typer.Exit(1)
486
-
487
-
488
- @app.command()
489
- def build(
490
- directory: str = typer.Argument(".", help="Environment directory to build"),
491
- tag: str | None = typer.Option(
492
- None, "--tag", "-t", help="Docker image tag (default: from pyproject.toml)"
493
- ),
494
- no_cache: bool = typer.Option(False, "--no-cache", help="Build without Docker cache"),
495
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
496
- ) -> None:
497
- """🏗️ Build a HUD environment and generate lock file.
498
-
499
- This command:
500
- - Builds a Docker image from your environment
501
- - Analyzes the MCP server to extract metadata
502
- - Generates a hud.lock.yaml file for reproducibility
503
-
504
- Examples:
505
- hud build # Build current directory
506
- hud build environments/text_2048
507
- hud build . --tag my-env:v1.0
508
- hud build . --no-cache # Force rebuild
509
- """
510
- build_command(directory, tag, no_cache, verbose)
511
-
512
-
513
- @app.command()
514
- def push(
515
- directory: str = typer.Argument(".", help="Environment directory containing hud.lock.yaml"),
516
- image: str | None = typer.Option(None, "--image", "-i", help="Override registry image name"),
517
- tag: str | None = typer.Option(
518
- None, "--tag", "-t", help="Override tag (e.g., 'v1.0', 'latest')"
519
- ),
520
- sign: bool = typer.Option(
521
- False, "--sign", help="Sign the image with cosign (not yet implemented)"
522
- ),
523
- yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
524
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
525
- ) -> None:
526
- """📤 Push HUD environment to registry.
527
-
528
- Reads hud.lock.yaml from the directory and pushes to registry.
529
- Auto-detects your Docker username if --image not specified.
530
-
531
- Examples:
532
- hud push # Push with auto-detected name
533
- hud push --tag v1.0 # Push with specific tag
534
- hud push . --image myuser/myenv:v1.0
535
- hud push --yes # Skip confirmation
536
- """
537
- push_command(directory, image, tag, sign, yes, verbose)
538
-
539
-
540
- @app.command()
541
- def pull(
542
- target: str = typer.Argument(..., help="Image reference or lock file to pull"),
543
- lock_file: str | None = typer.Option(
544
- None, "--lock", "-l", help="Path to lock file (if target is image ref)"
545
- ),
546
- yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
547
- verify_only: bool = typer.Option(
548
- False, "--verify-only", help="Only verify metadata without pulling"
549
- ),
550
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
551
- ) -> None:
552
- """📥 Pull HUD environment from registry with metadata preview.
553
-
554
- Shows environment details before downloading.
555
-
556
- Examples:
557
- hud pull hud.lock.yaml # Pull from lock file
558
- hud pull myuser/myenv:latest # Pull by image reference
559
- hud pull myuser/myenv --verify-only # Check metadata only
560
- """
561
- pull_command(target, lock_file, yes, verify_only, verbose)
562
-
563
-
564
- @app.command()
565
- def init(
566
- name: str = typer.Argument(None, help="Environment name (default: current directory name)"),
567
- directory: str = typer.Option(".", "--dir", "-d", help="Target directory"),
568
- force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
569
- ) -> None:
570
- """🚀 Initialize a new HUD environment with minimal boilerplate.
571
-
572
- Creates a working MCP environment with:
573
- - Dockerfile for containerization
574
- - pyproject.toml for dependencies
575
- - Minimal MCP server with context
576
- - Required setup/evaluate tools
577
-
578
- Examples:
579
- hud init # Use current directory name
580
- hud init my-env # Create in ./my-env/
581
- hud init my-env --dir /tmp # Create in /tmp/my-env/
582
- """
583
- create_environment(name, directory, force)
584
-
585
-
586
- @app.command()
587
- def quickstart() -> None:
588
- """
589
- Quickstart with evaluating an agent!
590
- """
591
- # Just call the clone command with the quickstart URL
592
- clone("https://github.com/hud-evals/quickstart.git")
593
-
594
-
595
- def main() -> None:
596
- """Main entry point for the CLI."""
597
- # Show header for main help
598
- if len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] in ["--help", "-h"]):
599
- console.print(
600
- Panel.fit(
601
- "[bold cyan]🚀 HUD CLI[/bold cyan]\nMCP Environment Analysis & Debugging",
602
- border_style="cyan",
603
- )
604
- )
605
- console.print("\n[yellow]Quick Start:[/yellow]")
606
- console.print(" 1. Create a new environment: [cyan]hud init my-env && cd my-env[/cyan]")
607
- console.print(" 2. Develop with hot-reload: [cyan]hud dev --interactive[/cyan]")
608
- console.print(" 3. Build for production: [cyan]hud build[/cyan]")
609
- console.print(" 4. Share your environment: [cyan]hud push[/cyan]")
610
- console.print(" 5. Get shared environments: [cyan]hud pull <org/name:tag>[/cyan]")
611
- console.print(" 6. Run and test: [cyan]hud run <image>[/cyan]\n")
612
-
613
- app()
614
-
615
-
616
- if __name__ == "__main__":
617
- main()
1
+ """HUD CLI - Command-line interface for MCP environment analysis and debugging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path # noqa: TC003
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.table import Table
15
+
16
+ from .analyze import (
17
+ analyze_environment,
18
+ analyze_environment_from_config,
19
+ analyze_environment_from_mcp_config,
20
+ )
21
+ from .build import build_command
22
+ from .clone import clone_repository, get_clone_message, print_error, print_tutorial
23
+ from .cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
24
+ from .debug import debug_mcp_stdio
25
+ from .init import create_environment
26
+ from .mcp_server import run_mcp_dev_server
27
+ from .pull import pull_command
28
+ from .push import push_command
29
+ from .utils import CaptureLogger
30
+
31
+ # Create the main Typer app
32
+ app = typer.Typer(
33
+ name="hud",
34
+ help="🚀 HUD CLI for MCP environment analysis and debugging",
35
+ add_completion=False,
36
+ rich_markup_mode="rich",
37
+ )
38
+
39
+ console = Console()
40
+
41
+
42
+ # Capture IMAGE and any following Docker args as a single variadic argument list.
43
+ @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
44
+ def analyze(
45
+ params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
46
+ None, # Optional positional arguments
47
+ help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
48
+ ),
49
+ config: Path = typer.Option( # noqa: B008
50
+ None,
51
+ "--config",
52
+ "-c",
53
+ help="JSON config file with MCP configuration",
54
+ exists=True,
55
+ file_okay=True,
56
+ dir_okay=False,
57
+ ),
58
+ cursor: str | None = typer.Option(
59
+ None,
60
+ "--cursor",
61
+ help="Analyze a server from Cursor config",
62
+ ),
63
+ output_format: str = typer.Option(
64
+ "interactive",
65
+ "--format",
66
+ "-f",
67
+ help="Output format: interactive, json, markdown",
68
+ ),
69
+ verbose: bool = typer.Option(
70
+ False,
71
+ "--verbose",
72
+ "-v",
73
+ help="Enable verbose output (shows tool schemas)",
74
+ ),
75
+ live: bool = typer.Option(
76
+ False,
77
+ "--live",
78
+ help="Run container for live analysis (slower but more accurate)",
79
+ ),
80
+ ) -> None:
81
+ """🔍 Analyze MCP environment - discover tools, resources, and capabilities.
82
+
83
+ By default, uses cached metadata for instant results.
84
+ Use --live to run the container for real-time analysis.
85
+
86
+ Examples:
87
+ hud analyze hudpython/test_init # Fast metadata inspection
88
+ hud analyze my-env --live # Full container analysis
89
+ hud analyze --config mcp-config.json # From MCP config
90
+ hud analyze --cursor text-2048-dev # From Cursor config
91
+ """
92
+ if config:
93
+ # Load config from JSON file (always live for configs)
94
+ asyncio.run(analyze_environment_from_config(config, output_format, verbose))
95
+ elif cursor:
96
+ # Parse cursor config (always live for cursor)
97
+ command, error = parse_cursor_config(cursor)
98
+ if error or command is None:
99
+ console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
100
+ raise typer.Exit(1)
101
+ # Convert to MCP config
102
+ mcp_config = {
103
+ "local": {"command": command[0], "args": command[1:] if len(command) > 1 else []}
104
+ }
105
+ asyncio.run(analyze_environment_from_mcp_config(mcp_config, output_format, verbose))
106
+ elif params:
107
+ image, *docker_args = params
108
+ if live or docker_args: # If docker args provided, assume live mode
109
+ # Build Docker command from image and args
110
+ docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
111
+ asyncio.run(analyze_environment(docker_cmd, output_format, verbose))
112
+ else:
113
+ # Fast mode - analyze from metadata
114
+ from .analyze_metadata import analyze_from_metadata
115
+
116
+ asyncio.run(analyze_from_metadata(image, output_format, verbose))
117
+ else:
118
+ console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
119
+ console.print("\nExamples:")
120
+ console.print(" hud analyze hudpython/test_init # Fast metadata analysis")
121
+ console.print(" hud analyze my-env --live # Live container analysis")
122
+ console.print(" hud analyze --config mcp-config.json # From config file")
123
+ console.print(" hud analyze --cursor my-server # From Cursor")
124
+ raise typer.Exit(1)
125
+
126
+
127
+ # Same variadic approach for debug.
128
+ @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
129
+ def debug(
130
+ params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
131
+ None,
132
+ help="Docker image followed by optional Docker run arguments (e.g., 'hud-image:latest -e KEY=value')", # noqa: E501
133
+ ),
134
+ config: Path = typer.Option( # noqa: B008
135
+ None,
136
+ "--config",
137
+ "-c",
138
+ help="JSON config file with MCP configuration",
139
+ exists=True,
140
+ file_okay=True,
141
+ dir_okay=False,
142
+ ),
143
+ cursor: str | None = typer.Option(
144
+ None,
145
+ "--cursor",
146
+ help="Debug a server from Cursor config",
147
+ ),
148
+ max_phase: int = typer.Option(
149
+ 5,
150
+ "--max-phase",
151
+ "-p",
152
+ min=1,
153
+ max=5,
154
+ help="Maximum debug phase (1-5)",
155
+ ),
156
+ ) -> None:
157
+ """🐛 Debug MCP environment - test initialization, tools, and readiness.
158
+
159
+ Examples:
160
+ hud debug hud-text-2048:latest
161
+ hud debug my-mcp-server:v1 -e API_KEY=xxx -p 8080:8080
162
+ hud debug --config mcp-config.json
163
+ hud debug --cursor text-2048-dev
164
+ hud debug hud-browser:dev --max-phase 3
165
+ """
166
+
167
+ # Determine the command to run
168
+ command = None
169
+
170
+ if config:
171
+ # Load config from JSON file
172
+ with open(config) as f:
173
+ mcp_config = json.load(f)
174
+
175
+ # Extract command from first server in config
176
+ server_name = next(iter(mcp_config.keys()))
177
+ server_config = mcp_config[server_name]
178
+ command = [server_config["command"], *server_config.get("args", [])]
179
+ elif cursor:
180
+ # Parse cursor config
181
+ command, error = parse_cursor_config(cursor)
182
+ if error or command is None:
183
+ console.print(f"[red]❌ {error or 'Failed to parse cursor config'}[/red]")
184
+ raise typer.Exit(1)
185
+ elif params:
186
+ image, *docker_args = params
187
+ # Build Docker command
188
+ command = ["docker", "run", "--rm", "-i", *docker_args, image]
189
+ else:
190
+ console.print("[red]Error: Must specify either a Docker image, --config, or --cursor[/red]")
191
+ console.print("\nExamples:")
192
+ console.print(" hud debug hud-text-2048:latest")
193
+ console.print(" hud debug --config mcp-config.json")
194
+ console.print(" hud debug --cursor my-server")
195
+ raise typer.Exit(1)
196
+
197
+ # Create logger and run debug
198
+ logger = CaptureLogger(print_output=True)
199
+ phases_completed = asyncio.run(debug_mcp_stdio(command, logger, max_phase=max_phase))
200
+
201
+ # Show summary using design system
202
+ from hud.utils.design import HUDDesign
203
+
204
+ design = HUDDesign()
205
+
206
+ design.info("") # Empty line
207
+ design.section_title("Debug Summary")
208
+
209
+ if phases_completed == max_phase:
210
+ design.success(f"All {max_phase} phases completed successfully!")
211
+ if max_phase == 5:
212
+ design.info("Your MCP server is fully functional and ready for production use.")
213
+ else:
214
+ design.warning(f"Completed {phases_completed} out of {max_phase} phases")
215
+ design.info("Check the errors above for troubleshooting.")
216
+
217
+ # Exit with appropriate code
218
+ if phases_completed < max_phase:
219
+ raise typer.Exit(1)
220
+
221
+
222
+ @app.command()
223
+ def cursor_list() -> None:
224
+ """📋 List all MCP servers configured in Cursor."""
225
+ console.print(Panel.fit("📋 [bold cyan]Cursor MCP Servers[/bold cyan]", border_style="cyan"))
226
+
227
+ servers, error = list_cursor_servers()
228
+
229
+ if error:
230
+ console.print(f"[red]❌ {error}[/red]")
231
+ raise typer.Exit(1)
232
+
233
+ if not servers:
234
+ console.print("[yellow]No servers found in Cursor config[/yellow]")
235
+ return
236
+
237
+ # Display servers in a table
238
+ table = Table(title="Available Servers")
239
+ table.add_column("Server Name", style="cyan")
240
+ table.add_column("Command Preview", style="dim")
241
+
242
+ config_path = get_cursor_config_path()
243
+ if config_path.exists():
244
+ with open(config_path) as f:
245
+ config = json.load(f)
246
+ mcp_servers = config.get("mcpServers", {})
247
+
248
+ for server_name in servers:
249
+ server_config = mcp_servers.get(server_name, {})
250
+ command = server_config.get("command", "")
251
+ args = server_config.get("args", [])
252
+
253
+ # Create command preview
254
+ if args:
255
+ preview = f"{command} {' '.join(args[:2])}"
256
+ if len(args) > 2:
257
+ preview += " ..."
258
+ else:
259
+ preview = command
260
+
261
+ table.add_row(server_name, preview)
262
+
263
+ console.print(table)
264
+ console.print(f"\n[dim]Config location: {config_path}[/dim]")
265
+ console.print(
266
+ "\n[green]Tip:[/green] Use [cyan]hud debug --cursor <server-name>[/cyan] to debug a server"
267
+ )
268
+
269
+
270
+ @app.command()
271
+ def version() -> None:
272
+ """Show HUD CLI version."""
273
+ try:
274
+ from hud import __version__
275
+
276
+ console.print(f"HUD CLI version: [cyan]{__version__}[/cyan]")
277
+ except ImportError:
278
+ console.print("HUD CLI version: [cyan]unknown[/cyan]")
279
+
280
+
281
+ @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
282
+ def dev(
283
+ params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
284
+ None,
285
+ help="Environment directory followed by optional Docker arguments (e.g., '. -e KEY=value')",
286
+ ),
287
+ image: str | None = typer.Option(
288
+ None, "--image", "-i", help="Docker image name (overrides auto-detection)"
289
+ ),
290
+ build: bool = typer.Option(False, "--build", "-b", help="Build image before starting"),
291
+ no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild without cache"),
292
+ transport: str = typer.Option(
293
+ "http", "--transport", "-t", help="Transport protocol: http (default) or stdio"
294
+ ),
295
+ port: int = typer.Option(8765, "--port", "-p", help="HTTP server port (ignored for stdio)"),
296
+ no_reload: bool = typer.Option(False, "--no-reload", help="Disable hot-reload"),
297
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show server logs"),
298
+ inspector: bool = typer.Option(
299
+ False, "--inspector", help="Launch MCP Inspector (HTTP mode only)"
300
+ ),
301
+ no_logs: bool = typer.Option(False, "--no-logs", help="Disable streaming Docker logs"),
302
+ interactive: bool = typer.Option(
303
+ False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
304
+ ),
305
+ ) -> None:
306
+ """🔥 Development mode with hot-reload.
307
+
308
+ Runs your MCP environment in Docker with automatic restart on file changes.
309
+
310
+ The container's last command (typically the MCP server) will be wrapped
311
+ with watchfiles for hot-reload functionality.
312
+
313
+ Examples:
314
+ hud dev # Auto-detect in current directory
315
+ hud dev environments/browser # Specific directory
316
+ hud dev . --build # Build image first
317
+ hud dev . --image custom:tag # Use specific image
318
+ hud dev . --no-cache # Force clean rebuild
319
+ hud dev . --verbose # Show detailed logs
320
+ hud dev . --transport stdio # Use stdio proxy for multiple connections
321
+ hud dev . --inspector # Launch MCP Inspector (HTTP mode only)
322
+ hud dev . --interactive # Launch interactive testing mode (HTTP mode only)
323
+ hud dev . --no-logs # Disable Docker log streaming
324
+
325
+ # With Docker arguments (after all options):
326
+ hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
327
+ hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
328
+ hud dev . --build -e DEBUG=true --memory 2g
329
+ """
330
+ # Parse directory and Docker arguments
331
+ if params:
332
+ directory = params[0]
333
+ docker_args = params[1:] if len(params) > 1 else []
334
+ else:
335
+ directory = "."
336
+ docker_args = []
337
+
338
+ run_mcp_dev_server(
339
+ directory,
340
+ image,
341
+ build,
342
+ no_cache,
343
+ transport,
344
+ port,
345
+ no_reload,
346
+ verbose,
347
+ inspector,
348
+ no_logs,
349
+ interactive,
350
+ docker_args,
351
+ )
352
+
353
+
354
+ @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
355
+ def run(
356
+ params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
357
+ None,
358
+ help="Docker image followed by optional arguments (e.g., 'hud-image:latest -e KEY=value')",
359
+ ),
360
+ local: bool = typer.Option(
361
+ False,
362
+ "--local",
363
+ help="Run locally with Docker (default: remote via mcp.hud.so)",
364
+ ),
365
+ remote: bool = typer.Option(
366
+ False,
367
+ "--remote",
368
+ help="Run remotely via mcp.hud.so (default)",
369
+ ),
370
+ transport: str = typer.Option(
371
+ "stdio",
372
+ "--transport",
373
+ "-t",
374
+ help="Transport protocol: stdio (default) or http",
375
+ ),
376
+ port: int = typer.Option(
377
+ 8765,
378
+ "--port",
379
+ "-p",
380
+ help="Port for HTTP transport (ignored for stdio)",
381
+ ),
382
+ url: str = typer.Option(
383
+ None,
384
+ "--url",
385
+ help="Remote MCP server URL (default: HUD_MCP_URL or mcp.hud.so)",
386
+ ),
387
+ api_key: str | None = typer.Option(
388
+ None,
389
+ "--api-key",
390
+ help="API key for remote server (default: HUD_API_KEY env var)",
391
+ ),
392
+ run_id: str | None = typer.Option(
393
+ None,
394
+ "--run-id",
395
+ help="Run ID for tracking (remote only)",
396
+ ),
397
+ verbose: bool = typer.Option(
398
+ False,
399
+ "--verbose",
400
+ "-v",
401
+ help="Show detailed output",
402
+ ),
403
+ ) -> None:
404
+ """🚀 Run MCP server locally or remotely.
405
+
406
+ By default, runs remotely via mcp.hud.so. Use --local for Docker.
407
+
408
+ Remote Examples:
409
+ hud run hud-text-2048:latest
410
+ hud run my-server:v1 -e API_KEY=xxx -h Run-Id:abc123
411
+ hud run my-server:v1 --transport http --port 9000
412
+
413
+ Local Examples:
414
+ hud run --local hud-text-2048:latest
415
+ hud run --local my-server:v1 -e API_KEY=xxx
416
+ hud run --local my-server:v1 --transport http
417
+ """
418
+ if not params:
419
+ typer.echo("❌ Docker image is required")
420
+ raise typer.Exit(1)
421
+
422
+ # Parse image and args
423
+ image = params[0]
424
+ docker_args = params[1:] if len(params) > 1 else []
425
+
426
+ # Handle conflicting flags
427
+ if local and remote:
428
+ typer.echo("❌ Cannot use both --local and --remote")
429
+ raise typer.Exit(1)
430
+
431
+ # Default to remote if not explicitly local
432
+ is_local = local and not remote
433
+
434
+ if is_local:
435
+ # Local Docker execution
436
+ from .runner import run_mcp_server
437
+
438
+ run_mcp_server(image, docker_args, transport, port, verbose)
439
+ else:
440
+ # Remote execution via proxy
441
+ from .remote_runner import run_remote_server
442
+
443
+ # Get URL from options or environment
444
+ if not url:
445
+ url = os.getenv("HUD_MCP_URL", "https://mcp.hud.so/v3/mcp")
446
+
447
+ run_remote_server(image, docker_args, transport, port, url, api_key, run_id, verbose)
448
+
449
+
450
+ @app.command()
451
+ def clone(
452
+ url: str = typer.Argument(
453
+ ...,
454
+ help="Git repository URL to clone",
455
+ ),
456
+ ) -> None:
457
+ """🚀 Clone a git repository quietly with a pretty output.
458
+
459
+ This command wraps 'git clone' with the --quiet flag and displays
460
+ a rich formatted success message. If the repository contains a clone
461
+ message in pyproject.toml, it will be displayed as a tutorial.
462
+
463
+ Configure clone messages in your repository's pyproject.toml:
464
+
465
+ [tool.hud.clone]
466
+ title = "🚀 My Project"
467
+ message = "Thanks for cloning! Run 'pip install -e .' to get started."
468
+
469
+ # Or use markdown format:
470
+ # markdown = "## Welcome!\\n\\nHere's how to get started..."
471
+ # style = "cyan"
472
+
473
+ Examples:
474
+ hud clone https://github.com/user/repo.git
475
+ """
476
+ # Run the clone
477
+ success, result = clone_repository(url)
478
+
479
+ if success:
480
+ # Look for clone message configuration
481
+ clone_config = get_clone_message(result)
482
+ print_tutorial(clone_config)
483
+ else:
484
+ print_error(result)
485
+ raise typer.Exit(1)
486
+
487
+
488
+ @app.command()
489
+ def build(
490
+ directory: str = typer.Argument(".", help="Environment directory to build"),
491
+ tag: str | None = typer.Option(
492
+ None, "--tag", "-t", help="Docker image tag (default: from pyproject.toml)"
493
+ ),
494
+ no_cache: bool = typer.Option(False, "--no-cache", help="Build without Docker cache"),
495
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
496
+ ) -> None:
497
+ """🏗️ Build a HUD environment and generate lock file.
498
+
499
+ This command:
500
+ - Builds a Docker image from your environment
501
+ - Analyzes the MCP server to extract metadata
502
+ - Generates a hud.lock.yaml file for reproducibility
503
+
504
+ Examples:
505
+ hud build # Build current directory
506
+ hud build environments/text_2048
507
+ hud build . --tag my-env:v1.0
508
+ hud build . --no-cache # Force rebuild
509
+ """
510
+ build_command(directory, tag, no_cache, verbose)
511
+
512
+
513
+ @app.command()
514
+ def push(
515
+ directory: str = typer.Argument(".", help="Environment directory containing hud.lock.yaml"),
516
+ image: str | None = typer.Option(None, "--image", "-i", help="Override registry image name"),
517
+ tag: str | None = typer.Option(
518
+ None, "--tag", "-t", help="Override tag (e.g., 'v1.0', 'latest')"
519
+ ),
520
+ sign: bool = typer.Option(
521
+ False, "--sign", help="Sign the image with cosign (not yet implemented)"
522
+ ),
523
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
524
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
525
+ ) -> None:
526
+ """📤 Push HUD environment to registry.
527
+
528
+ Reads hud.lock.yaml from the directory and pushes to registry.
529
+ Auto-detects your Docker username if --image not specified.
530
+
531
+ Examples:
532
+ hud push # Push with auto-detected name
533
+ hud push --tag v1.0 # Push with specific tag
534
+ hud push . --image myuser/myenv:v1.0
535
+ hud push --yes # Skip confirmation
536
+ """
537
+ push_command(directory, image, tag, sign, yes, verbose)
538
+
539
+
540
+ @app.command()
541
+ def pull(
542
+ target: str = typer.Argument(..., help="Image reference or lock file to pull"),
543
+ lock_file: str | None = typer.Option(
544
+ None, "--lock", "-l", help="Path to lock file (if target is image ref)"
545
+ ),
546
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
547
+ verify_only: bool = typer.Option(
548
+ False, "--verify-only", help="Only verify metadata without pulling"
549
+ ),
550
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
551
+ ) -> None:
552
+ """📥 Pull HUD environment from registry with metadata preview.
553
+
554
+ Shows environment details before downloading.
555
+
556
+ Examples:
557
+ hud pull hud.lock.yaml # Pull from lock file
558
+ hud pull myuser/myenv:latest # Pull by image reference
559
+ hud pull myuser/myenv --verify-only # Check metadata only
560
+ """
561
+ pull_command(target, lock_file, yes, verify_only, verbose)
562
+
563
+
564
+ @app.command()
565
+ def init(
566
+ name: str = typer.Argument(None, help="Environment name (default: current directory name)"),
567
+ directory: str = typer.Option(".", "--dir", "-d", help="Target directory"),
568
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
569
+ ) -> None:
570
+ """🚀 Initialize a new HUD environment with minimal boilerplate.
571
+
572
+ Creates a working MCP environment with:
573
+ - Dockerfile for containerization
574
+ - pyproject.toml for dependencies
575
+ - Minimal MCP server with context
576
+ - Required setup/evaluate tools
577
+
578
+ Examples:
579
+ hud init # Use current directory name
580
+ hud init my-env # Create in ./my-env/
581
+ hud init my-env --dir /tmp # Create in /tmp/my-env/
582
+ """
583
+ create_environment(name, directory, force)
584
+
585
+
586
+ @app.command()
587
+ def quickstart() -> None:
588
+ """
589
+ Quickstart with evaluating an agent!
590
+ """
591
+ # Just call the clone command with the quickstart URL
592
+ clone("https://github.com/hud-evals/quickstart.git")
593
+
594
+
595
+ def main() -> None:
596
+ """Main entry point for the CLI."""
597
+ # Show header for main help
598
+ if len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] in ["--help", "-h"]):
599
+ console.print(
600
+ Panel.fit(
601
+ "[bold cyan]🚀 HUD CLI[/bold cyan]\nMCP Environment Analysis & Debugging",
602
+ border_style="cyan",
603
+ )
604
+ )
605
+ console.print("\n[yellow]Quick Start:[/yellow]")
606
+ console.print(" 1. Create a new environment: [cyan]hud init my-env && cd my-env[/cyan]")
607
+ console.print(" 2. Develop with hot-reload: [cyan]hud dev --interactive[/cyan]")
608
+ console.print(" 3. Build for production: [cyan]hud build[/cyan]")
609
+ console.print(" 4. Share your environment: [cyan]hud push[/cyan]")
610
+ console.print(" 5. Get shared environments: [cyan]hud pull <org/name:tag>[/cyan]")
611
+ console.print(" 6. Run and test: [cyan]hud run <image>[/cyan]\n")
612
+
613
+ app()
614
+
615
+
616
+ if __name__ == "__main__":
617
+ main()