hud-python 0.4.21__py3-none-any.whl → 0.4.23__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 (63) hide show
  1. hud/agents/base.py +37 -37
  2. hud/agents/claude.py +11 -6
  3. hud/agents/grounded_openai.py +282 -0
  4. hud/agents/misc/response_agent.py +3 -2
  5. hud/agents/openai.py +2 -2
  6. hud/agents/openai_chat_generic.py +3 -1
  7. hud/agents/tests/test_client.py +6 -1
  8. hud/agents/tests/test_grounded_openai_agent.py +155 -0
  9. hud/cli/__init__.py +34 -24
  10. hud/cli/analyze.py +27 -26
  11. hud/cli/build.py +50 -46
  12. hud/cli/debug.py +7 -7
  13. hud/cli/dev.py +107 -99
  14. hud/cli/eval.py +33 -31
  15. hud/cli/hf.py +53 -53
  16. hud/cli/init.py +28 -28
  17. hud/cli/list_func.py +22 -22
  18. hud/cli/pull.py +36 -36
  19. hud/cli/push.py +76 -74
  20. hud/cli/remove.py +42 -40
  21. hud/cli/rl/__init__.py +2 -2
  22. hud/cli/rl/init.py +41 -41
  23. hud/cli/rl/pod.py +97 -91
  24. hud/cli/rl/ssh.py +42 -40
  25. hud/cli/rl/train.py +75 -73
  26. hud/cli/rl/utils.py +10 -10
  27. hud/cli/tests/test_analyze.py +1 -1
  28. hud/cli/tests/test_analyze_metadata.py +2 -2
  29. hud/cli/tests/test_pull.py +45 -45
  30. hud/cli/tests/test_push.py +31 -29
  31. hud/cli/tests/test_registry.py +15 -15
  32. hud/cli/utils/environment.py +11 -11
  33. hud/cli/utils/interactive.py +18 -18
  34. hud/cli/utils/logging.py +12 -12
  35. hud/cli/utils/metadata.py +12 -12
  36. hud/cli/utils/registry.py +5 -5
  37. hud/cli/utils/runner.py +23 -23
  38. hud/cli/utils/server.py +16 -16
  39. hud/settings.py +6 -0
  40. hud/shared/hints.py +7 -7
  41. hud/tools/executors/tests/test_base_executor.py +1 -1
  42. hud/tools/executors/xdo.py +1 -1
  43. hud/tools/grounding/__init__.py +13 -0
  44. hud/tools/grounding/config.py +54 -0
  45. hud/tools/grounding/grounded_tool.py +314 -0
  46. hud/tools/grounding/grounder.py +302 -0
  47. hud/tools/grounding/tests/__init__.py +1 -0
  48. hud/tools/grounding/tests/test_grounded_tool.py +196 -0
  49. hud/tools/tests/test_playwright_tool.py +1 -1
  50. hud/tools/tests/test_tools_init.py +1 -1
  51. hud/tools/tests/test_utils.py +2 -2
  52. hud/types.py +4 -4
  53. hud/utils/__init__.py +3 -3
  54. hud/utils/agent_factories.py +86 -0
  55. hud/utils/{design.py → hud_console.py} +39 -33
  56. hud/utils/pretty_errors.py +6 -6
  57. hud/utils/tests/test_version.py +1 -1
  58. hud/version.py +1 -1
  59. {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/METADATA +3 -1
  60. {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/RECORD +63 -54
  61. {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.21.dist-info → hud_python-0.4.23.dist-info}/licenses/LICENSE +0 -0
hud/cli/hf.py CHANGED
@@ -9,9 +9,9 @@ from pathlib import Path
9
9
  import typer
10
10
 
11
11
  from hud.cli.rl.utils import get_mcp_config_from_lock, read_lock_file, write_lock_file
12
- from hud.utils.design import HUDDesign
12
+ from hud.utils.hud_console import HUDConsole
13
13
 
14
- design = HUDDesign()
14
+ hud_console = HUDConsole()
15
15
 
16
16
 
17
17
  def hf_command(
@@ -37,11 +37,11 @@ def hf_command(
37
37
  hud hf tasks.json --name hud-evals/web-tasks --private
38
38
  hud hf tasks.json --name local-dataset --no-push
39
39
  """
40
- design.header("HuggingFace Dataset Converter", icon="📊")
40
+ hud_console.header("HuggingFace Dataset Converter", icon="📊")
41
41
 
42
42
  # Auto-detect task file if not provided
43
43
  if tasks_file is None:
44
- design.info("Looking for task files...")
44
+ hud_console.info("Looking for task files...")
45
45
 
46
46
  # Common task file patterns
47
47
  patterns = [
@@ -60,30 +60,30 @@ def hf_command(
60
60
  json_files = sorted(set(json_files))
61
61
 
62
62
  if not json_files:
63
- design.error("No task files found in current directory")
64
- design.info("Create a task JSON file (e.g., tasks.json) or specify the file path")
63
+ hud_console.error("No task files found in current directory")
64
+ hud_console.info("Create a task JSON file (e.g., tasks.json) or specify the file path")
65
65
  raise typer.Exit(1)
66
66
  elif len(json_files) == 1:
67
67
  tasks_file = json_files[0]
68
- design.info(f"Found task file: {tasks_file}")
68
+ hud_console.info(f"Found task file: {tasks_file}")
69
69
  else:
70
70
  # Multiple files found, let user choose
71
- design.info("Multiple task files found:")
72
- file_choice = design.select(
71
+ hud_console.info("Multiple task files found:")
72
+ file_choice = hud_console.select(
73
73
  "Select a task file to convert:",
74
74
  choices=[str(f) for f in json_files],
75
75
  )
76
76
  tasks_file = Path(file_choice)
77
- design.success(f"Selected: {tasks_file}")
77
+ hud_console.success(f"Selected: {tasks_file}")
78
78
 
79
79
  # Validate inputs
80
80
  if tasks_file and not tasks_file.exists():
81
- design.error(f"Tasks file not found: {tasks_file}")
81
+ hud_console.error(f"Tasks file not found: {tasks_file}")
82
82
  raise typer.Exit(1)
83
83
 
84
84
  # Suggest dataset name if not provided
85
85
  if name is None:
86
- design.info("Generating dataset name suggestion...")
86
+ hud_console.info("Generating dataset name suggestion...")
87
87
 
88
88
  # Try to get HF username from environment or git config
89
89
  hf_username = None
@@ -107,7 +107,7 @@ def hf_command(
107
107
  if result.returncode == 0 and result.stdout.strip():
108
108
  hf_username = result.stdout.strip().lower().replace(" ", "-")
109
109
  except Exception:
110
- design.warning("Failed to get HF username from git config")
110
+ hud_console.warning("Failed to get HF username from git config")
111
111
 
112
112
  # Get environment name from current directory or lock file
113
113
  env_name = Path.cwd().name
@@ -126,7 +126,7 @@ def hf_command(
126
126
  if image_name and image_name != "local":
127
127
  env_name = image_name
128
128
  except Exception as e:
129
- design.warning(f"Failed to get HF username from lock file: {e}")
129
+ hud_console.warning(f"Failed to get HF username from lock file: {e}")
130
130
 
131
131
  # Generate suggestions
132
132
  suggestions = []
@@ -137,60 +137,60 @@ def hf_command(
137
137
  suggestions.append(f"hud-evals/{env_name}-tasks")
138
138
 
139
139
  # Let user choose or enter custom
140
- design.info("Dataset name suggestions:")
140
+ hud_console.info("Dataset name suggestions:")
141
141
  suggestions.append("Enter custom name...")
142
142
 
143
- choice = design.select("Select or enter a dataset name:", choices=suggestions)
143
+ choice = hud_console.select("Select or enter a dataset name:", choices=suggestions)
144
144
 
145
145
  if choice == "Enter custom name...":
146
146
  name = typer.prompt("Enter dataset name (e.g., 'my-org/my-dataset')")
147
147
  else:
148
148
  name = choice
149
149
 
150
- design.success(f"Using dataset name: {name}")
150
+ hud_console.success(f"Using dataset name: {name}")
151
151
 
152
152
  # Validate dataset name format
153
153
  if push and name and "/" not in name:
154
- design.error("Dataset name must include organization (e.g., 'my-org/my-dataset')")
155
- design.info("For local-only datasets, use --no-push")
154
+ hud_console.error("Dataset name must include organization (e.g., 'my-org/my-dataset')")
155
+ hud_console.info("For local-only datasets, use --no-push")
156
156
  raise typer.Exit(1)
157
157
 
158
158
  # Load tasks
159
- design.info(f"Loading tasks from: {tasks_file}")
159
+ hud_console.info(f"Loading tasks from: {tasks_file}")
160
160
  try:
161
161
  if tasks_file is None:
162
162
  raise ValueError("Tasks file is required")
163
163
  with open(tasks_file) as f:
164
164
  tasks_data = json.load(f)
165
165
  except json.JSONDecodeError as e:
166
- design.error(f"Invalid JSON file: {e}")
166
+ hud_console.error(f"Invalid JSON file: {e}")
167
167
  raise typer.Exit(1) from e
168
168
 
169
169
  # Handle both single task and list of tasks
170
170
  if isinstance(tasks_data, dict):
171
171
  tasks = [tasks_data]
172
- design.info("Found 1 task")
172
+ hud_console.info("Found 1 task")
173
173
  elif isinstance(tasks_data, list):
174
174
  tasks = tasks_data
175
- design.info(f"Found {len(tasks)} tasks")
175
+ hud_console.info(f"Found {len(tasks)} tasks")
176
176
  else:
177
- design.error("Tasks file must contain a JSON object or array")
177
+ hud_console.error("Tasks file must contain a JSON object or array")
178
178
  raise typer.Exit(1)
179
179
 
180
180
  # Validate task format
181
181
  valid_tasks = []
182
182
  for i, task in enumerate(tasks):
183
183
  if not isinstance(task, dict):
184
- design.warning(f"Skipping task {i}: not a JSON object")
184
+ hud_console.warning(f"Skipping task {i}: not a JSON object")
185
185
  continue
186
186
 
187
187
  # Required fields
188
188
  if "prompt" not in task:
189
- design.warning(f"Skipping task {i}: missing 'prompt' field")
189
+ hud_console.warning(f"Skipping task {i}: missing 'prompt' field")
190
190
  continue
191
191
 
192
192
  if "evaluate_tool" not in task:
193
- design.warning(f"Skipping task {i}: missing 'evaluate_tool' field")
193
+ hud_console.warning(f"Skipping task {i}: missing 'evaluate_tool' field")
194
194
  continue
195
195
 
196
196
  # Add default values
@@ -203,23 +203,23 @@ def hf_command(
203
203
  if mcp_config:
204
204
  task["mcp_config"] = mcp_config
205
205
  else:
206
- design.warning(f"Task {task['id']}: missing 'mcp_config' field")
206
+ hud_console.warning(f"Task {task['id']}: missing 'mcp_config' field")
207
207
  continue
208
208
 
209
209
  valid_tasks.append(task)
210
210
 
211
211
  if not valid_tasks:
212
- design.error("No valid tasks found")
212
+ hud_console.error("No valid tasks found")
213
213
  raise typer.Exit(1)
214
214
 
215
- design.success(f"Validated {len(valid_tasks)} tasks")
215
+ hud_console.success(f"Validated {len(valid_tasks)} tasks")
216
216
 
217
217
  # Check if dataset is suitable for training
218
218
  if len(valid_tasks) < 4:
219
- design.warning(
219
+ hud_console.warning(
220
220
  f"Dataset has only {len(valid_tasks)} task(s). RL training typically requires at least 4 tasks." # noqa: E501
221
221
  )
222
- use_for_training = design.select(
222
+ use_for_training = hud_console.select(
223
223
  "Will this dataset be used for RL training?",
224
224
  ["Yes, duplicate tasks to reach 4", "No, keep as is"],
225
225
  )
@@ -238,7 +238,7 @@ def hf_command(
238
238
  )
239
239
  valid_tasks.append(duplicated_task)
240
240
 
241
- design.info(f"Duplicated tasks: {original_count} → {len(valid_tasks)}")
241
+ hud_console.info(f"Duplicated tasks: {original_count} → {len(valid_tasks)}")
242
242
 
243
243
  # Check if MCP configs should be converted to remote
244
244
  sample_mcp_config = valid_tasks[0].get("mcp_config", {})
@@ -266,10 +266,10 @@ def hf_command(
266
266
  config_type = "local"
267
267
 
268
268
  if config_type == "remote" and remote_image:
269
- design.info(f"Tasks already use remote MCP configs with image: {remote_image}")
269
+ hud_console.info(f"Tasks already use remote MCP configs with image: {remote_image}")
270
270
 
271
271
  if config_type == "local":
272
- convert_to_remote = design.select(
272
+ convert_to_remote = hud_console.select(
273
273
  "Tasks use local MCP configs. Convert to remote configs for training?",
274
274
  ["Yes, convert to remote (requires public image)", "No, keep local configs"],
275
275
  )
@@ -281,38 +281,38 @@ def hf_command(
281
281
  image = get_image_from_lock()
282
282
 
283
283
  if not image:
284
- design.error("No image found in hud.lock.yaml")
285
- design.hint("Run 'hud build' first")
284
+ hud_console.error("No image found in hud.lock.yaml")
285
+ hud_console.hint("Run 'hud build' first")
286
286
  raise typer.Exit(1)
287
287
 
288
288
  # Check if image contains registry prefix (indicates it's pushed)
289
289
  if "/" not in image or image.startswith("local/"):
290
290
  # Clean up image name for display (remove SHA if present)
291
291
  display_image = image.split("@")[0] if "@" in image else image
292
- design.warning(f"Image '{display_image}' appears to be local only")
293
- push_image = design.select(
292
+ hud_console.warning(f"Image '{display_image}' appears to be local only")
293
+ push_image = hud_console.select(
294
294
  "Would you like to push the image to make it publicly available?",
295
295
  ["Yes, push image", "No, cancel"],
296
296
  )
297
297
 
298
298
  if push_image == "Yes, push image":
299
- design.info("Running 'hud push' to publish image...")
299
+ hud_console.info("Running 'hud push' to publish image...")
300
300
  # Import here to avoid circular imports
301
301
  from hud.cli.push import push_command
302
302
 
303
303
  # Run push command (it's synchronous)
304
304
  push_command(directory=".", yes=True)
305
- design.success("Image pushed successfully")
305
+ hud_console.success("Image pushed successfully")
306
306
 
307
307
  # Re-read the image name as it may have changed
308
308
  image = get_image_from_lock()
309
309
  else:
310
- design.info("Keeping local MCP configs")
310
+ hud_console.info("Keeping local MCP configs")
311
311
  convert_to_remote = None
312
312
 
313
313
  if convert_to_remote and image:
314
314
  # Convert all task configs to remote
315
- design.info(f"Converting MCP configs to use remote image: {image}")
315
+ hud_console.info(f"Converting MCP configs to use remote image: {image}")
316
316
 
317
317
  for task in valid_tasks:
318
318
  # Create remote MCP config
@@ -327,7 +327,7 @@ def hf_command(
327
327
  }
328
328
  task["mcp_config"] = remote_config
329
329
 
330
- design.success("✓ Converted all tasks to use remote MCP configs")
330
+ hud_console.success("✓ Converted all tasks to use remote MCP configs")
331
331
 
332
332
  # Convert to HuggingFace format
333
333
  dataset_dict = {
@@ -352,11 +352,11 @@ def hf_command(
352
352
  try:
353
353
  from datasets import Dataset
354
354
  except ImportError as e:
355
- design.error("datasets library not installed")
356
- design.info("Install with: pip install datasets")
355
+ hud_console.error("datasets library not installed")
356
+ hud_console.info("Install with: pip install datasets")
357
357
  raise typer.Exit(1) from e
358
358
 
359
- design.info(f"Creating HuggingFace dataset: {name}")
359
+ hud_console.info(f"Creating HuggingFace dataset: {name}")
360
360
  dataset = Dataset.from_dict(dataset_dict)
361
361
 
362
362
  # Set up HF token
@@ -365,15 +365,15 @@ def hf_command(
365
365
 
366
366
  os.environ["HF_TOKEN"] = token
367
367
 
368
- design.info(f"Pushing to Hub (private={private})...")
368
+ hud_console.info(f"Pushing to Hub (private={private})...")
369
369
  try:
370
370
  if name is None:
371
371
  raise ValueError("Dataset name is required")
372
372
  dataset.push_to_hub(name, private=private)
373
- design.success(f"Dataset published: https://huggingface.co/datasets/{name}")
373
+ hud_console.success(f"Dataset published: https://huggingface.co/datasets/{name}")
374
374
  except Exception as e:
375
- design.error(f"Failed to push to Hub: {e}")
376
- design.hint("Make sure you're logged in: huggingface-cli login")
375
+ hud_console.error(f"Failed to push to Hub: {e}")
376
+ hud_console.hint("Make sure you're logged in: huggingface-cli login")
377
377
  raise typer.Exit(1) from e
378
378
  else:
379
379
  # Save locally
@@ -382,7 +382,7 @@ def hf_command(
382
382
  output_file = Path(f"{name.replace('/', '_')}_dataset.json")
383
383
  with open(output_file, "w") as f:
384
384
  json.dump(dataset_dict, f, indent=2)
385
- design.success(f"Dataset saved locally: {output_file}")
385
+ hud_console.success(f"Dataset saved locally: {output_file}")
386
386
 
387
387
  # Update hud.lock.yaml if requested
388
388
  if update_lock:
@@ -403,4 +403,4 @@ def update_lock_file(dataset_name: str, task_count: int) -> None:
403
403
 
404
404
  # Write back
405
405
  if write_lock_file(lock_data):
406
- design.success(f"Updated hud.lock.yaml with dataset: {dataset_name}")
406
+ hud_console.success(f"Updated hud.lock.yaml with dataset: {dataset_name}")
hud/cli/init.py CHANGED
@@ -8,7 +8,7 @@ import typer
8
8
  from rich.panel import Panel
9
9
  from rich.syntax import Syntax
10
10
 
11
- from hud.utils.design import HUDDesign
11
+ from hud.utils.hud_console import HUDConsole
12
12
 
13
13
  # Embedded templates
14
14
  DOCKERFILE_TEMPLATE = """FROM python:3.11-slim
@@ -541,7 +541,7 @@ def sanitize_name(name: str) -> str:
541
541
  def create_environment(name: str | None, directory: str, force: bool) -> None:
542
542
  """Create a new HUD environment from templates."""
543
543
 
544
- design = HUDDesign()
544
+ hud_console = HUDConsole()
545
545
 
546
546
  # Determine environment name
547
547
  if name is None:
@@ -549,7 +549,7 @@ def create_environment(name: str | None, directory: str, force: bool) -> None:
549
549
  current_dir = Path.cwd()
550
550
  name = current_dir.name
551
551
  target_dir = current_dir
552
- design.info(f"Using current directory name: {name}")
552
+ hud_console.info(f"Using current directory name: {name}")
553
553
  else:
554
554
  # Create new directory
555
555
  target_dir = Path(directory) / name
@@ -557,16 +557,16 @@ def create_environment(name: str | None, directory: str, force: bool) -> None:
557
557
  # Sanitize name for Python package
558
558
  package_name = sanitize_name(name)
559
559
  if package_name != name:
560
- design.warning(f"Package name adjusted for Python: {name} → {package_name}")
560
+ hud_console.warning(f"Package name adjusted for Python: {name} → {package_name}")
561
561
 
562
562
  # Check if directory exists
563
563
  if target_dir.exists() and any(target_dir.iterdir()):
564
564
  if not force:
565
- design.error(f"Directory {target_dir} already exists and is not empty")
566
- design.info("Use --force to overwrite existing files")
565
+ hud_console.error(f"Directory {target_dir} already exists and is not empty")
566
+ hud_console.info("Use --force to overwrite existing files")
567
567
  raise typer.Exit(1)
568
568
  else:
569
- design.warning(f"Overwriting existing files in {target_dir}")
569
+ hud_console.warning(f"Overwriting existing files in {target_dir}")
570
570
 
571
571
  # Create directory structure
572
572
  src_dir = target_dir / "src" / "controller"
@@ -633,38 +633,38 @@ def create_environment(name: str | None, directory: str, force: bool) -> None:
633
633
  files_created.append(".env")
634
634
 
635
635
  # Success message
636
- design.header(f"Created HUD Environment: {name}")
636
+ hud_console.header(f"Created HUD Environment: {name}")
637
637
 
638
- design.section_title("Files created")
638
+ hud_console.section_title("Files created")
639
639
  for file in files_created:
640
- design.status_item(file, "created")
640
+ hud_console.status_item(file, "created")
641
641
 
642
- design.section_title("Next steps")
642
+ hud_console.section_title("Next steps")
643
643
 
644
644
  # Show commands based on where we created the environment
645
645
  if target_dir == Path.cwd():
646
- design.info("1. Start development server (with MCP inspector):")
647
- design.command_example("hud dev --inspector")
646
+ hud_console.info("1. Start development server (with MCP inspector):")
647
+ hud_console.command_example("hud dev --inspector")
648
648
  else:
649
- design.info("1. Enter the directory:")
650
- design.command_example(f"cd {target_dir}")
651
- design.info("\n2. Start development server (with MCP inspector):")
652
- design.command_example("hud dev --inspector")
649
+ hud_console.info("1. Enter the directory:")
650
+ hud_console.command_example(f"cd {target_dir}")
651
+ hud_console.info("\n2. Start development server (with MCP inspector):")
652
+ hud_console.command_example("hud dev --inspector")
653
653
 
654
- design.info("\n3. Connect from Cursor or test via the MCP inspector:")
655
- design.info(" Follow the instructions shown by hud dev --inspector")
654
+ hud_console.info("\n3. Connect from Cursor or test via the MCP inspector:")
655
+ hud_console.info(" Follow the instructions shown by hud dev --inspector")
656
656
 
657
- design.info("\n4. Test your environment:")
658
- design.command_example("python test_task.py")
657
+ hud_console.info("\n4. Test your environment:")
658
+ hud_console.command_example("python test_task.py")
659
659
 
660
- design.info("\n5. Customize your environment:")
661
- design.info(" - Add tools to src/controller/server.py")
662
- design.info(" - Add state to src/controller/env.py")
663
- design.info(" - Modify tasks in tasks.json")
664
- design.info(" - Experiment in test_env.ipynb")
660
+ hud_console.info("\n5. Customize your environment:")
661
+ hud_console.info(" - Add tools to src/controller/server.py")
662
+ hud_console.info(" - Add state to src/controller/env.py")
663
+ hud_console.info(" - Modify tasks in tasks.json")
664
+ hud_console.info(" - Experiment in test_env.ipynb")
665
665
 
666
666
  # Show a sample of the server code
667
- design.section_title("Your MCP server")
667
+ hud_console.section_title("Your MCP server")
668
668
  sample_code = '''@mcp.tool()
669
669
  async def act() -> str:
670
670
  """Perform an action that changes the environment state."""
@@ -674,4 +674,4 @@ async def act() -> str:
674
674
  return f"Action #{count} performed. Current count: {count}"'''
675
675
 
676
676
  syntax = Syntax(sample_code, "python", theme="monokai", line_numbers=False)
677
- design.console.print(Panel(syntax, border_style="dim"))
677
+ hud_console.console.print(Panel(syntax, border_style="dim"))
hud/cli/list_func.py CHANGED
@@ -8,7 +8,7 @@ import typer
8
8
  import yaml
9
9
  from rich.table import Table
10
10
 
11
- from hud.utils.design import HUDDesign
11
+ from hud.utils.hud_console import HUDConsole
12
12
 
13
13
  from .utils.registry import extract_name_and_tag, get_registry_dir, list_registry_entries
14
14
 
@@ -46,10 +46,10 @@ def list_environments(
46
46
  verbose: bool = False,
47
47
  ) -> None:
48
48
  """List all HUD environments in the local registry."""
49
- design = HUDDesign()
49
+ hud_console = HUDConsole()
50
50
 
51
51
  if not json_output:
52
- design.header("HUD Environment Registry")
52
+ hud_console.header("HUD Environment Registry")
53
53
 
54
54
  # Check for environment directory
55
55
  env_dir = get_registry_dir()
@@ -57,10 +57,10 @@ def list_environments(
57
57
  if json_output:
58
58
  print("[]") # noqa: T201
59
59
  else:
60
- design.info("No environments found in local registry.")
61
- design.info("")
62
- design.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
63
- design.info("Build environments with: [cyan]hud build[/cyan]")
60
+ hud_console.info("No environments found in local registry.")
61
+ hud_console.info("")
62
+ hud_console.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
63
+ hud_console.info("Build environments with: [cyan]hud build[/cyan]")
64
64
  return
65
65
 
66
66
  # Collect all environments using the registry helper
@@ -103,7 +103,7 @@ def list_environments(
103
103
 
104
104
  except Exception as e:
105
105
  if verbose:
106
- design.warning(f"Failed to read {lock_file}: {e}")
106
+ hud_console.warning(f"Failed to read {lock_file}: {e}")
107
107
 
108
108
  # Sort by pulled time (newest first)
109
109
  environments.sort(key=lambda x: x["pulled_time"], reverse=True)
@@ -129,10 +129,10 @@ def list_environments(
129
129
  return
130
130
 
131
131
  if not environments:
132
- design.info("No environments found matching criteria.")
133
- design.info("")
134
- design.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
135
- design.info("Build environments with: [cyan]hud build[/cyan]")
132
+ hud_console.info("No environments found matching criteria.")
133
+ hud_console.info("")
134
+ hud_console.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
135
+ hud_console.info("Build environments with: [cyan]hud build[/cyan]")
136
136
  return
137
137
 
138
138
  # Create table
@@ -169,26 +169,26 @@ def list_environments(
169
169
 
170
170
  table.add_row(*row)
171
171
 
172
- design.print(table) # type: ignore
173
- design.info("")
172
+ hud_console.print(table) # type: ignore
173
+ hud_console.info("")
174
174
 
175
175
  # Show usage hints
176
- design.section_title("Usage")
176
+ hud_console.section_title("Usage")
177
177
  if environments:
178
178
  # Use the most recently pulled environment as example
179
179
  example_env = environments[0]
180
180
  example_ref = f"{example_env['name']}:{example_env['tag']}"
181
181
 
182
- design.info(f"Run an environment: [cyan]hud run {example_ref}[/cyan]")
183
- design.info(f"Analyze tools: [cyan]hud analyze {example_ref}[/cyan]")
184
- design.info(f"Debug server: [cyan]hud debug {example_ref}[/cyan]")
182
+ hud_console.info(f"Run an environment: [cyan]hud run {example_ref}[/cyan]")
183
+ hud_console.info(f"Analyze tools: [cyan]hud analyze {example_ref}[/cyan]")
184
+ hud_console.info(f"Debug server: [cyan]hud debug {example_ref}[/cyan]")
185
185
 
186
- design.info("Pull more environments: [cyan]hud pull <org/name:tag>[/cyan]")
187
- design.info("Build new environments: [cyan]hud build[/cyan]")
186
+ hud_console.info("Pull more environments: [cyan]hud pull <org/name:tag>[/cyan]")
187
+ hud_console.info("Build new environments: [cyan]hud build[/cyan]")
188
188
 
189
189
  if verbose:
190
- design.info("")
191
- design.info(f"[dim]Registry location: {env_dir}[/dim]")
190
+ hud_console.info("")
191
+ hud_console.info(f"[dim]Registry location: {env_dir}[/dim]")
192
192
 
193
193
 
194
194
  def list_command(