hud-python 0.4.11__py3-none-any.whl → 0.4.12__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/__main__.py +8 -0
  2. hud/agents/base.py +7 -8
  3. hud/agents/langchain.py +2 -2
  4. hud/agents/tests/test_openai.py +3 -1
  5. hud/cli/__init__.py +106 -51
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +60 -25
  9. hud/cli/eval.py +148 -68
  10. hud/cli/init.py +0 -1
  11. hud/cli/list_func.py +72 -71
  12. hud/cli/pull.py +1 -2
  13. hud/cli/push.py +35 -23
  14. hud/cli/remove.py +35 -41
  15. hud/cli/tests/test_analyze.py +2 -1
  16. hud/cli/tests/test_analyze_metadata.py +42 -49
  17. hud/cli/tests/test_build.py +28 -52
  18. hud/cli/tests/test_cursor.py +1 -1
  19. hud/cli/tests/test_debug.py +1 -1
  20. hud/cli/tests/test_list_func.py +75 -64
  21. hud/cli/tests/test_main_module.py +30 -0
  22. hud/cli/tests/test_mcp_server.py +3 -3
  23. hud/cli/tests/test_pull.py +30 -61
  24. hud/cli/tests/test_push.py +70 -89
  25. hud/cli/tests/test_registry.py +36 -38
  26. hud/cli/tests/test_utils.py +1 -1
  27. hud/cli/utils/__init__.py +1 -0
  28. hud/cli/{docker_utils.py → utils/docker.py} +36 -0
  29. hud/cli/{env_utils.py → utils/environment.py} +7 -7
  30. hud/cli/{interactive.py → utils/interactive.py} +91 -19
  31. hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
  32. hud/cli/{registry.py → utils/registry.py} +28 -30
  33. hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
  34. hud/cli/utils/runner.py +134 -0
  35. hud/cli/utils/server.py +250 -0
  36. hud/clients/base.py +1 -1
  37. hud/clients/fastmcp.py +7 -5
  38. hud/clients/mcp_use.py +8 -6
  39. hud/server/server.py +34 -4
  40. hud/shared/exceptions.py +11 -0
  41. hud/shared/tests/test_exceptions.py +22 -0
  42. hud/telemetry/tests/__init__.py +0 -0
  43. hud/telemetry/tests/test_replay.py +40 -0
  44. hud/telemetry/tests/test_trace.py +63 -0
  45. hud/tools/base.py +20 -3
  46. hud/tools/computer/hud.py +15 -6
  47. hud/tools/executors/tests/test_base_executor.py +27 -0
  48. hud/tools/response.py +12 -8
  49. hud/tools/tests/test_response.py +60 -0
  50. hud/tools/tests/test_tools_init.py +49 -0
  51. hud/utils/design.py +19 -8
  52. hud/utils/mcp.py +17 -5
  53. hud/utils/tests/test_mcp.py +112 -0
  54. hud/utils/tests/test_version.py +1 -1
  55. hud/version.py +1 -1
  56. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
  57. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/RECORD +62 -52
  58. hud/cli/runner.py +0 -160
  59. /hud/cli/{cursor.py → utils/cursor.py} +0 -0
  60. /hud/cli/{utils.py → utils/logging.py} +0 -0
  61. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
hud/cli/eval.py CHANGED
@@ -6,18 +6,13 @@ import asyncio
6
6
  import json
7
7
  import logging
8
8
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, Literal
9
+ from typing import Any, Literal
10
10
 
11
11
  import typer
12
12
 
13
13
  import hud
14
14
  from hud.utils.design import HUDDesign
15
15
 
16
- if TYPE_CHECKING:
17
- from datasets import Dataset
18
- from hud.agents import ClaudeAgent, OperatorAgent
19
- from hud.agents.misc.response_agent import ResponseAgent
20
-
21
16
  logger = logging.getLogger(__name__)
22
17
  design = HUDDesign()
23
18
 
@@ -29,7 +24,7 @@ def build_agent(
29
24
  allowed_tools: list[str] | None = None,
30
25
  ) -> Any:
31
26
  """Create and return the requested agent type."""
32
-
27
+
33
28
  # Import agents lazily to avoid dependency issues
34
29
  try:
35
30
  from hud.agents.misc.response_agent import ResponseAgent
@@ -39,7 +34,7 @@ def build_agent(
39
34
  "Please install with: pip install 'hud-python[agent]'"
40
35
  )
41
36
  raise typer.Exit(1) from e
42
-
37
+
43
38
  if agent_type == "openai":
44
39
  try:
45
40
  from hud.agents import OperatorAgent
@@ -49,14 +44,14 @@ def build_agent(
49
44
  "Please install with: pip install 'hud-python[agent]'"
50
45
  )
51
46
  raise typer.Exit(1) from e
52
-
47
+
53
48
  allowed_tools = allowed_tools or ["openai_computer"]
54
-
49
+
55
50
  return OperatorAgent(
56
51
  allowed_tools=allowed_tools,
57
52
  response_agent=ResponseAgent(),
58
53
  )
59
-
54
+
60
55
  # Fallback Claude agent (Anthropic)
61
56
  try:
62
57
  from hud.agents import ClaudeAgent
@@ -66,10 +61,10 @@ def build_agent(
66
61
  "Please install with: pip install 'hud-python[agent]'"
67
62
  )
68
63
  raise typer.Exit(1) from e
69
-
64
+
70
65
  model = model or "claude-sonnet-4-20250514"
71
66
  allowed_tools = allowed_tools or ["anthropic_computer"]
72
-
67
+
73
68
  return ClaudeAgent(
74
69
  model=model,
75
70
  allowed_tools=allowed_tools,
@@ -85,26 +80,82 @@ async def run_single_task(
85
80
  allowed_tools: list[str] | None = None,
86
81
  max_steps: int = 10,
87
82
  ) -> None:
88
- """Load one task and execute it."""
89
-
83
+ """Load one task and execute it, or detect if JSON contains a list and run as dataset."""
84
+
90
85
  design.info("📊 Loading dataset…")
91
-
92
- # Import Task lazily
86
+
87
+ # Import Task and run_dataset lazily
93
88
  try:
94
- from hud.datasets import Task
89
+ from hud.datasets import Task, run_dataset
95
90
  except ImportError as e:
96
91
  design.error(
97
92
  "Dataset dependencies are not installed. "
98
93
  "Please install with: pip install 'hud-python[agent]'"
99
94
  )
100
95
  raise typer.Exit(1) from e
101
-
102
- # Check if it's a single task JSON file
96
+
97
+ # Check if it's a JSON file
103
98
  path = Path(source)
104
99
  if path.exists() and path.suffix == ".json":
105
- with open(path, "r") as f:
106
- task_data = json.load(f)
107
- task = Task(**task_data)
100
+ with open(path) as f: # noqa: ASYNC230
101
+ json_data = json.load(f)
102
+
103
+ # Check if JSON contains a list of tasks
104
+ if isinstance(json_data, list):
105
+ design.info(f"Found {len(json_data)} tasks in JSON file, running as dataset…")
106
+
107
+ # Build agent class and config for run_dataset
108
+ if agent_type == "openai":
109
+ try:
110
+ from hud.agents import OperatorAgent
111
+
112
+ agent_class = OperatorAgent
113
+ except ImportError as e:
114
+ design.error(
115
+ "OpenAI agent dependencies are not installed. "
116
+ "Please install with: pip install 'hud-python[agent]'"
117
+ )
118
+ raise typer.Exit(1) from e
119
+
120
+ agent_config: dict[str, Any] = {
121
+ "allowed_tools": allowed_tools or ["openai_computer"],
122
+ }
123
+ else:
124
+ try:
125
+ from hud.agents import ClaudeAgent
126
+
127
+ agent_class = ClaudeAgent
128
+ except ImportError as e:
129
+ design.error(
130
+ "Claude agent dependencies are not installed. "
131
+ "Please install with: pip install 'hud-python[agent]'"
132
+ )
133
+ raise typer.Exit(1) from e
134
+
135
+ agent_config = {
136
+ "model": model or "claude-sonnet-4-20250514",
137
+ "allowed_tools": allowed_tools or ["anthropic_computer"],
138
+ }
139
+
140
+ # Run as dataset with single-task concurrency to maintain debug behavior
141
+ results = await run_dataset(
142
+ name=f"JSON Dataset: {path.name}",
143
+ dataset=json_data, # Pass the list directly
144
+ agent_class=agent_class,
145
+ agent_config=agent_config,
146
+ max_concurrent=1, # Run sequentially for debug mode
147
+ metadata={"source": str(path)},
148
+ max_steps=max_steps,
149
+ auto_respond=True,
150
+ )
151
+
152
+ # Display summary
153
+ successful = sum(1 for r in results if getattr(r, "reward", 0) > 0)
154
+ design.success(f"Completed {len(results)} tasks: {successful} successful")
155
+ return
156
+
157
+ # Single task JSON
158
+ task = Task(**json_data)
108
159
  else:
109
160
  # Load from HuggingFace dataset
110
161
  try:
@@ -115,15 +166,15 @@ async def run_single_task(
115
166
  "Please install with: pip install 'hud-python[agent]'"
116
167
  )
117
168
  raise typer.Exit(1) from e
118
-
169
+
119
170
  dataset = load_dataset(source, split="train")
120
-
171
+
121
172
  # Get first task from dataset
122
173
  sample_task = dataset[0] # type: ignore[index]
123
174
  task = Task(**sample_task) # type: ignore[arg-type]
124
-
175
+
125
176
  task_prompt = task.prompt[:50] + "..." if len(task.prompt) > 50 else task.prompt
126
-
177
+
127
178
  with hud.trace(name=task_prompt):
128
179
  agent = build_agent(
129
180
  agent_type,
@@ -145,7 +196,7 @@ async def run_full_dataset(
145
196
  max_steps: int = 50,
146
197
  ) -> list[Any]:
147
198
  """Run evaluation across the entire dataset using hud.datasets.run_dataset."""
148
-
199
+
149
200
  # Import run_dataset lazily
150
201
  try:
151
202
  from hud.datasets import run_dataset
@@ -155,11 +206,29 @@ async def run_full_dataset(
155
206
  "Please install with: pip install 'hud-python[agent]'"
156
207
  )
157
208
  raise typer.Exit(1) from e
158
-
209
+
210
+ # Check if source is a JSON file with list of tasks
211
+ path = Path(source)
212
+ dataset_or_tasks = source
213
+ dataset_name = source.split("/")[-1]
214
+
215
+ if path.exists() and path.suffix == ".json":
216
+ with open(path) as f: # noqa: ASYNC230
217
+ json_data = json.load(f)
218
+
219
+ if isinstance(json_data, list):
220
+ dataset_or_tasks = json_data
221
+ dataset_name = f"JSON Dataset: {path.name}"
222
+ design.info(f"Found {len(json_data)} tasks in JSON file")
223
+ else:
224
+ design.error("JSON file must contain a list of tasks when using --full flag")
225
+ raise typer.Exit(1)
226
+
159
227
  # Build agent class + config for run_dataset
160
228
  if agent_type == "openai":
161
229
  try:
162
230
  from hud.agents import OperatorAgent
231
+
163
232
  agent_class = OperatorAgent
164
233
  except ImportError as e:
165
234
  design.error(
@@ -167,13 +236,14 @@ async def run_full_dataset(
167
236
  "Please install with: pip install 'hud-python[agent]'"
168
237
  )
169
238
  raise typer.Exit(1) from e
170
-
239
+
171
240
  agent_config: dict[str, Any] = {
172
241
  "allowed_tools": allowed_tools or ["openai_computer"],
173
242
  }
174
243
  else:
175
244
  try:
176
245
  from hud.agents import ClaudeAgent
246
+
177
247
  agent_class = ClaudeAgent
178
248
  except ImportError as e:
179
249
  design.error(
@@ -181,16 +251,16 @@ async def run_full_dataset(
181
251
  "Please install with: pip install 'hud-python[agent]'"
182
252
  )
183
253
  raise typer.Exit(1) from e
184
-
254
+
185
255
  agent_config = {
186
256
  "model": model or "claude-sonnet-4-20250514",
187
257
  "allowed_tools": allowed_tools or ["anthropic_computer"],
188
258
  }
189
-
259
+
190
260
  design.info("🚀 Running evaluation…")
191
261
  return await run_dataset(
192
- name=f"Evaluation {source.split('/')[-1]}",
193
- dataset=source,
262
+ name=f"Evaluation {dataset_name}",
263
+ dataset=dataset_or_tasks,
194
264
  agent_class=agent_class,
195
265
  agent_config=agent_config,
196
266
  max_concurrent=max_concurrent,
@@ -203,7 +273,7 @@ async def run_full_dataset(
203
273
  def eval_command(
204
274
  source: str = typer.Argument(
205
275
  ...,
206
- help="HuggingFace dataset identifier (e.g. 'hud-evals/SheetBench-50') or task JSON file",
276
+ help="HuggingFace dataset identifier (e.g. 'hud-evals/SheetBench-50'), single task JSON file, or JSON file with list of tasks", # noqa: E501
207
277
  ),
208
278
  full: bool = typer.Option(
209
279
  False,
@@ -237,66 +307,76 @@ def eval_command(
237
307
  ),
238
308
  ) -> None:
239
309
  """🚀 Run evaluation on datasets or individual tasks with agents.
240
-
310
+
241
311
  Examples:
242
312
  # Evaluate a single task from SheetBench
243
313
  hud eval hud-evals/SheetBench-50
244
-
314
+
245
315
  # Evaluate the FULL SheetBench dataset with Claude
246
316
  hud eval hud-evals/SheetBench-50 --full --agent claude
247
-
317
+
248
318
  # Run a single task from a JSON file
249
319
  hud eval task.json
250
-
320
+
321
+ # Run multiple tasks from a JSON file (auto-detects list)
322
+ hud eval tasks.json # If tasks.json contains a list, runs all tasks
323
+
324
+ # Run JSON list with full dataset mode and concurrency
325
+ hud eval tasks.json --full --max-concurrent 10
326
+
251
327
  # Run with OpenAI Operator agent
252
328
  hud eval hud-evals/OSWorld-Gold-Beta --agent openai
253
329
  """
254
- from hud.settings import settings
255
330
  import os
256
-
331
+
332
+ from hud.settings import settings
333
+
257
334
  # Check for required API keys
258
335
  if agent == "claude":
259
336
  if not settings.anthropic_api_key or not os.environ.get("ANTHROPIC_API_KEY"):
260
337
  design.error("ANTHROPIC_API_KEY is required for Claude agent")
261
338
  design.info("Set it in your environment or .env file: ANTHROPIC_API_KEY=your-key-here")
262
339
  raise typer.Exit(1)
263
- elif agent == "openai":
264
- if not settings.openai_api_key or not os.environ.get("OPENAI_API_KEY"):
265
- design.error("OPENAI_API_KEY is required for OpenAI agent")
266
- design.info("Set it in your environment or .env file: OPENAI_API_KEY=your-key-here")
267
- raise typer.Exit(1)
268
-
340
+ elif agent == "openai" and (
341
+ not settings.openai_api_key or not os.environ.get("OPENAI_API_KEY")
342
+ ):
343
+ design.error("OPENAI_API_KEY is required for OpenAI agent")
344
+ design.info("Set it in your environment or .env file: OPENAI_API_KEY=your-key-here")
345
+ raise typer.Exit(1)
346
+
269
347
  # Check for HUD_API_KEY if using HUD services
270
348
  if not settings.api_key or not os.environ.get("HUD_API_KEY"):
271
349
  design.warning("HUD_API_KEY not set. Some features may be limited.")
272
350
  design.info("Get your API key at: https://app.hud.so")
273
-
351
+
274
352
  # Parse allowed tools
275
353
  allowed_tools_list = (
276
- [t.strip() for t in allowed_tools.split(",") if t.strip()]
277
- if allowed_tools
278
- else None
354
+ [t.strip() for t in allowed_tools.split(",") if t.strip()] if allowed_tools else None
279
355
  )
280
-
356
+
281
357
  # Set default max_steps if not provided
282
358
  if max_steps is None:
283
359
  max_steps = 50 if full else 10
284
-
360
+
285
361
  # Run evaluation
286
362
  if full:
287
- asyncio.run(run_full_dataset(
288
- source,
289
- agent_type=agent,
290
- model=model,
291
- allowed_tools=allowed_tools_list,
292
- max_concurrent=max_concurrent,
293
- max_steps=max_steps,
294
- ))
363
+ asyncio.run(
364
+ run_full_dataset(
365
+ source,
366
+ agent_type=agent,
367
+ model=model,
368
+ allowed_tools=allowed_tools_list,
369
+ max_concurrent=max_concurrent,
370
+ max_steps=max_steps,
371
+ )
372
+ )
295
373
  else:
296
- asyncio.run(run_single_task(
297
- source,
298
- agent_type=agent,
299
- model=model,
300
- allowed_tools=allowed_tools_list,
301
- max_steps=max_steps,
302
- ))
374
+ asyncio.run(
375
+ run_single_task(
376
+ source,
377
+ agent_type=agent,
378
+ model=model,
379
+ allowed_tools=allowed_tools_list,
380
+ max_steps=max_steps,
381
+ )
382
+ )
hud/cli/init.py CHANGED
@@ -173,7 +173,6 @@ def sanitize_name(name: str) -> str:
173
173
 
174
174
  def create_environment(name: str | None, directory: str, force: bool) -> None:
175
175
  """Create a new HUD environment from templates."""
176
- from hud.utils.design import HUDDesign
177
176
 
178
177
  design = HUDDesign()
179
178
 
hud/cli/list_func.py CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import re
6
5
  from datetime import datetime
7
- from pathlib import Path
8
6
 
9
7
  import typer
10
8
  import yaml
@@ -12,30 +10,33 @@ from rich.table import Table
12
10
 
13
11
  from hud.utils.design import HUDDesign
14
12
 
15
- from .registry import get_registry_dir, list_registry_entries, extract_name_and_tag
13
+ from .utils.registry import extract_name_and_tag, get_registry_dir, list_registry_entries
16
14
 
17
15
 
18
16
  def format_timestamp(timestamp: float | None) -> str:
19
17
  """Format timestamp to human-readable relative time."""
20
18
  if not timestamp:
21
19
  return "unknown"
22
-
20
+
23
21
  dt = datetime.fromtimestamp(timestamp)
24
22
  now = datetime.now()
25
23
  delta = now - dt
26
-
27
- if delta.days > 365:
28
- return f"{delta.days // 365}y ago"
29
- elif delta.days > 30:
30
- return f"{delta.days // 30}mo ago"
31
- elif delta.days > 0:
24
+
25
+ # Get total seconds to handle edge cases properly
26
+ total_seconds = delta.total_seconds()
27
+
28
+ if total_seconds < 60:
29
+ return "just now"
30
+ elif total_seconds < 3600:
31
+ return f"{int(total_seconds // 60)}m ago"
32
+ elif total_seconds < 86400: # Less than 24 hours
33
+ return f"{int(total_seconds // 3600)}h ago"
34
+ elif delta.days < 30:
32
35
  return f"{delta.days}d ago"
33
- elif delta.seconds > 3600:
34
- return f"{delta.seconds // 3600}h ago"
35
- elif delta.seconds > 60:
36
- return f"{delta.seconds // 60}m ago"
36
+ elif delta.days < 365:
37
+ return f"{delta.days // 30}mo ago"
37
38
  else:
38
- return "just now"
39
+ return f"{delta.days // 365}y ago"
39
40
 
40
41
 
41
42
  def list_environments(
@@ -46,71 +47,73 @@ def list_environments(
46
47
  ) -> None:
47
48
  """List all HUD environments in the local registry."""
48
49
  design = HUDDesign()
49
-
50
+
50
51
  if not json_output:
51
52
  design.header("HUD Environment Registry")
52
-
53
+
53
54
  # Check for environment directory
54
55
  env_dir = get_registry_dir()
55
56
  if not env_dir.exists():
56
57
  if json_output:
57
- print("[]")
58
+ print("[]") # noqa: T201
58
59
  else:
59
60
  design.info("No environments found in local registry.")
60
61
  design.info("")
61
62
  design.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
62
63
  design.info("Build environments with: [cyan]hud build[/cyan]")
63
64
  return
64
-
65
+
65
66
  # Collect all environments using the registry helper
66
67
  environments = []
67
-
68
+
68
69
  for digest, lock_file in list_registry_entries():
69
70
  try:
70
71
  # Read lock file
71
72
  with open(lock_file) as f:
72
73
  lock_data = yaml.safe_load(f)
73
-
74
+
74
75
  # Extract metadata
75
76
  image = lock_data.get("image", "unknown")
76
77
  name, tag = extract_name_and_tag(image)
77
-
78
+
78
79
  # Apply filter if specified
79
80
  if filter_name and filter_name.lower() not in name.lower():
80
81
  continue
81
-
82
+
82
83
  # Get additional metadata
83
84
  metadata = lock_data.get("metadata", {})
84
85
  description = metadata.get("description", "")
85
86
  tools_count = len(metadata.get("tools", []))
86
-
87
+
87
88
  # Get file modification time as pulled time
88
89
  pulled_time = lock_file.stat().st_mtime
89
-
90
- environments.append({
91
- "name": name,
92
- "tag": tag,
93
- "digest": digest,
94
- "description": description,
95
- "tools_count": tools_count,
96
- "pulled_time": pulled_time,
97
- "image": image,
98
- "path": str(lock_file),
99
- })
100
-
90
+
91
+ environments.append(
92
+ {
93
+ "name": name,
94
+ "tag": tag,
95
+ "digest": digest,
96
+ "description": description,
97
+ "tools_count": tools_count,
98
+ "pulled_time": pulled_time,
99
+ "image": image,
100
+ "path": str(lock_file),
101
+ }
102
+ )
103
+
101
104
  except Exception as e:
102
105
  if verbose:
103
106
  design.warning(f"Failed to read {lock_file}: {e}")
104
-
107
+
105
108
  # Sort by pulled time (newest first)
106
109
  environments.sort(key=lambda x: x["pulled_time"], reverse=True)
107
-
110
+
108
111
  if json_output:
109
112
  # Output as JSON
110
113
  import json
111
- json_data = []
112
- for env in environments:
113
- json_data.append({
114
+
115
+ json_data = [
116
+ {
114
117
  "name": env["name"],
115
118
  "tag": env["tag"],
116
119
  "digest": env["digest"],
@@ -118,67 +121,71 @@ def list_environments(
118
121
  "tools_count": env["tools_count"],
119
122
  "pulled_time": env["pulled_time"],
120
123
  "image": env["image"],
121
- "path": env["path"],
122
- })
123
- print(json.dumps(json_data, indent=2))
124
+ "path": str(env["path"]).replace("\\", "/"), # Normalize path separators for JSON
125
+ }
126
+ for env in environments
127
+ ]
128
+ print(json.dumps(json_data, indent=2)) # noqa: T201
124
129
  return
125
-
130
+
126
131
  if not environments:
127
132
  design.info("No environments found matching criteria.")
128
133
  design.info("")
129
134
  design.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
130
135
  design.info("Build environments with: [cyan]hud build[/cyan]")
131
136
  return
132
-
137
+
133
138
  # Create table
134
- table = Table(title=f"Found {len(environments)} environment{'s' if len(environments) != 1 else ''}")
139
+ table = Table(
140
+ title=f"Found {len(environments)} environment{'s' if len(environments) != 1 else ''}"
141
+ )
135
142
  table.add_column("Environment", style="cyan", no_wrap=True)
136
143
  table.add_column("Description", style="white")
137
144
  table.add_column("Tools", justify="right", style="yellow")
138
145
  table.add_column("Pulled", style="dim")
139
-
146
+
140
147
  if show_all or verbose:
141
148
  table.add_column("Digest", style="dim")
142
-
149
+
143
150
  # Add rows
144
151
  for env in environments:
145
152
  # Truncate description if too long
146
153
  desc = env["description"]
147
154
  if desc and len(desc) > 50 and not verbose:
148
155
  desc = desc[:47] + "..."
149
-
156
+
150
157
  # Combine name and tag for easy copying
151
158
  full_ref = f"{env['name']}:{env['tag']}"
152
-
159
+
153
160
  row = [
154
161
  full_ref,
155
162
  desc or "[dim]No description[/dim]",
156
163
  str(env["tools_count"]),
157
164
  format_timestamp(env["pulled_time"]),
158
165
  ]
159
-
166
+
160
167
  if show_all or verbose:
161
168
  row.append(env["digest"][:12])
162
-
169
+
163
170
  table.add_row(*row)
164
-
165
- design.print(table)
171
+
172
+ design.print(str(table))
166
173
  design.info("")
167
-
174
+
168
175
  # Show usage hints
169
176
  design.section_title("Usage")
170
177
  if environments:
171
178
  # Use the most recently pulled environment as example
172
179
  example_env = environments[0]
173
180
  example_ref = f"{example_env['name']}:{example_env['tag']}"
174
-
181
+
175
182
  design.info(f"Run an environment: [cyan]hud run {example_ref}[/cyan]")
176
183
  design.info(f"Analyze tools: [cyan]hud analyze {example_ref}[/cyan]")
177
184
  design.info(f"Debug server: [cyan]hud debug {example_ref}[/cyan]")
178
-
185
+
179
186
  design.info("Pull more environments: [cyan]hud pull <org/name:tag>[/cyan]")
180
187
  design.info("Build new environments: [cyan]hud build[/cyan]")
181
-
188
+
182
189
  if verbose:
183
190
  design.info("")
184
191
  design.info(f"[dim]Registry location: {env_dir}[/dim]")
@@ -188,21 +195,15 @@ def list_command(
188
195
  filter_name: str | None = typer.Option(
189
196
  None, "--filter", "-f", help="Filter environments by name (case-insensitive)"
190
197
  ),
191
- json_output: bool = typer.Option(
192
- False, "--json", help="Output as JSON"
193
- ),
194
- show_all: bool = typer.Option(
195
- False, "--all", "-a", help="Show all columns including digest"
196
- ),
197
- verbose: bool = typer.Option(
198
- False, "--verbose", "-v", help="Show detailed output"
199
- ),
198
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
199
+ show_all: bool = typer.Option(False, "--all", "-a", help="Show all columns including digest"),
200
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
200
201
  ) -> None:
201
202
  """📋 List all HUD environments in local registry.
202
-
203
+
203
204
  Shows environments pulled with 'hud pull' or built with 'hud build',
204
205
  stored in ~/.hud/envs/
205
-
206
+
206
207
  Examples:
207
208
  hud list # List all environments
208
209
  hud list --filter text # Filter by name
@@ -210,4 +211,4 @@ def list_command(
210
211
  hud list --all # Show digest column
211
212
  hud list --verbose # Show full descriptions
212
213
  """
213
- list_environments(filter_name, json_output, show_all, verbose)
214
+ list_environments(filter_name, json_output, show_all, verbose)
hud/cli/pull.py CHANGED
@@ -6,7 +6,6 @@ import subprocess
6
6
  from pathlib import Path
7
7
  from urllib.parse import quote
8
8
 
9
- import click
10
9
  import requests
11
10
  import typer
12
11
  import yaml
@@ -15,7 +14,7 @@ from rich.table import Table
15
14
  from hud.settings import settings
16
15
  from hud.utils.design import HUDDesign
17
16
 
18
- from .registry import save_to_registry
17
+ from .utils.registry import save_to_registry
19
18
 
20
19
 
21
20
  def get_docker_manifest(image: str) -> dict | None: