hud-python 0.3.5__py3-none-any.whl → 0.4.0__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 (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/cli/pull.py ADDED
@@ -0,0 +1,336 @@
1
+ """Pull HUD environments from registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ import click
9
+ import requests
10
+ import typer
11
+ import yaml
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from hud.settings import settings
16
+ from hud.utils.design import HUDDesign
17
+
18
+ console = Console()
19
+
20
+
21
+ def get_docker_manifest(image: str) -> dict | None:
22
+ """Get manifest from Docker registry without pulling the image."""
23
+ try:
24
+ # Try docker manifest inspect (requires experimental features)
25
+ result = subprocess.run( # noqa: S603
26
+ ["docker", "manifest", "inspect", image], # noqa: S607
27
+ capture_output=True,
28
+ text=True,
29
+ )
30
+ if result.returncode == 0:
31
+ import json
32
+
33
+ return json.loads(result.stdout)
34
+ except Exception:
35
+ click.echo("Failed to get Docker manifest", err=True)
36
+ return None
37
+
38
+
39
+ def get_image_size_from_manifest(manifest: dict) -> int | None:
40
+ """Extract total image size from Docker manifest."""
41
+ try:
42
+ total_size = 0
43
+
44
+ # Handle different manifest formats
45
+ if "layers" in manifest:
46
+ # v2 manifest
47
+ for layer in manifest["layers"]:
48
+ total_size += layer.get("size", 0)
49
+ elif "manifests" in manifest:
50
+ first_manifest = manifest["manifests"][0]
51
+ total_size = first_manifest.get("size", 0)
52
+
53
+ return total_size if total_size > 0 else None
54
+ except Exception:
55
+ click.echo("Failed to get image size from manifest", err=True)
56
+ return None
57
+
58
+
59
+ def fetch_lock_from_registry(reference: str) -> dict | None:
60
+ """Fetch lock file from HUD registry."""
61
+ try:
62
+ # Reference should be org/name:tag format
63
+ # If no tag specified, append :latest
64
+ if "/" in reference and ":" not in reference:
65
+ reference = f"{reference}:latest"
66
+
67
+ registry_url = f"{settings.hud_telemetry_url.rstrip('/')}/registry/envs/{reference}"
68
+
69
+ headers = {}
70
+ if settings.api_key:
71
+ headers["Authorization"] = f"Bearer {settings.api_key}"
72
+
73
+ response = requests.get(registry_url, headers=headers, timeout=10)
74
+
75
+ if response.status_code == 200:
76
+ data = response.json()
77
+ # Parse the lock YAML from the response
78
+ if "lock" in data:
79
+ return yaml.safe_load(data["lock"])
80
+ elif "lock_data" in data:
81
+ return data["lock_data"]
82
+ else:
83
+ # Try to treat the whole response as lock data
84
+ return data
85
+
86
+ return None
87
+ except Exception:
88
+ return None
89
+
90
+
91
+ def format_size(size_bytes: int) -> str:
92
+ """Format bytes to human readable size."""
93
+ for unit in ["B", "KB", "MB", "GB"]:
94
+ if size_bytes < 1024:
95
+ return f"{size_bytes:.1f} {unit}"
96
+ size_bytes //= 1024
97
+ return f"{size_bytes:.1f} TB"
98
+
99
+
100
+ def pull_environment(
101
+ target: str,
102
+ lock_file: str | None = None,
103
+ yes: bool = False,
104
+ verify_only: bool = False,
105
+ verbose: bool = False,
106
+ ) -> None:
107
+ """Pull HUD environment from registry."""
108
+ design = HUDDesign()
109
+ design.header("HUD Environment Pull")
110
+
111
+ # Two modes:
112
+ # 1. Pull from lock file (recommended)
113
+ # 2. Pull from image reference directly
114
+
115
+ lock_data = None
116
+ image_ref = target
117
+
118
+ # Mode 1: Lock file provided
119
+ if lock_file or target.endswith((".yaml", ".yml")):
120
+ # If target looks like a lock file, use it
121
+ if target.endswith((".yaml", ".yml")):
122
+ lock_file = target
123
+
124
+ lock_path = Path(lock_file) if lock_file else None
125
+ if lock_path and not lock_path.exists():
126
+ design.error(f"Lock file not found: {lock_file}")
127
+ raise typer.Exit(1)
128
+
129
+ design.info(f"Reading lock file: {lock_file}")
130
+ if lock_path:
131
+ with open(lock_path) as f:
132
+ lock_data = yaml.safe_load(f)
133
+
134
+ image_ref = lock_data.get("image", "") if lock_data else ""
135
+
136
+ # Mode 2: Direct image reference
137
+ else:
138
+ # First, try to parse as org/env reference for HUD registry
139
+ # Check if it's a simple org/name or org/name:tag format (no @sha256)
140
+ if "/" in target and "@" not in target:
141
+ # Looks like org/env reference, possibly with tag
142
+ design.info(f"Checking HUD registry for: {target}")
143
+
144
+ # Check for API key (not required for pulling, but good to inform)
145
+ if not settings.api_key:
146
+ design.info("No HUD API key set (pulling from public registry)")
147
+
148
+ lock_data = fetch_lock_from_registry(target)
149
+
150
+ if lock_data:
151
+ design.success("Found in HUD registry")
152
+ image_ref = lock_data.get("image", "")
153
+ else:
154
+ # Fall back to treating as Docker image
155
+ if not settings.api_key:
156
+ design.info(
157
+ "Not found in HUD registry (try setting HUD_API_KEY for private environments)" # noqa: E501
158
+ )
159
+ else:
160
+ design.info("Not found in HUD registry, treating as Docker image")
161
+
162
+ # Try to get metadata from Docker registry
163
+ if not lock_data:
164
+ design.info(f"Fetching Docker metadata for: {image_ref}")
165
+ manifest = get_docker_manifest(image_ref)
166
+
167
+ if manifest:
168
+ # Create minimal lock data from manifest
169
+ lock_data = {"image": image_ref, "source": "docker-manifest"}
170
+
171
+ # Try to get size
172
+ size = get_image_size_from_manifest(manifest)
173
+ if size:
174
+ lock_data["size"] = format_size(size)
175
+
176
+ if verbose:
177
+ design.info(
178
+ f"Retrieved manifest (type: {manifest.get('mediaType', 'unknown')})"
179
+ )
180
+
181
+ # Display environment summary
182
+ design.section_title("Environment Details")
183
+
184
+ # Create summary table
185
+ table = Table(show_header=False, box=None)
186
+ table.add_column("Property", style="cyan")
187
+ table.add_column("Value")
188
+
189
+ # Image info - show simple name in table
190
+ display_ref = image_ref.split("@")[0] if ":" in image_ref and "@" in image_ref else image_ref
191
+ table.add_row("Image", display_ref)
192
+
193
+ if lock_data:
194
+ # Show size if available
195
+ if "size" in lock_data:
196
+ table.add_row("Size", lock_data["size"])
197
+
198
+ # Check if this is full lock data or minimal manifest data
199
+ if lock_data.get("source") == "docker-manifest":
200
+ # Minimal data from Docker manifest
201
+ table.add_row("Source", "Docker Registry")
202
+ if not yes:
203
+ console.print(
204
+ "\n[yellow]Note:[/yellow] Limited metadata available from Docker registry."
205
+ )
206
+ console.print("For full environment details, use a lock file.\n")
207
+ else:
208
+ # Full lock file data
209
+ if "build" in lock_data:
210
+ table.add_row("Built", lock_data["build"].get("generatedAt", "Unknown"))
211
+ table.add_row("HUD Version", lock_data["build"].get("hudVersion", "Unknown"))
212
+
213
+ if "environment" in lock_data:
214
+ env_data = lock_data["environment"]
215
+ table.add_row("Tools", str(env_data.get("toolCount", "Unknown")))
216
+ table.add_row("Init Time", f"{env_data.get('initializeMs', 'Unknown')} ms")
217
+
218
+ if "push" in lock_data:
219
+ push_data = lock_data["push"]
220
+ table.add_row("Registry", push_data.get("registry", "Unknown"))
221
+ table.add_row("Pushed", push_data.get("pushedAt", "Unknown"))
222
+
223
+ # Environment variables
224
+ env_section = lock_data.get("environment", {})
225
+ if "variables" in env_section:
226
+ vars_data = env_section["variables"]
227
+ if vars_data.get("required"):
228
+ table.add_row("Required Env", ", ".join(vars_data["required"]))
229
+ if vars_data.get("optional"):
230
+ table.add_row("Optional Env", ", ".join(vars_data["optional"]))
231
+
232
+ else:
233
+ # No metadata available
234
+ table.add_row("Source", "Unknown")
235
+
236
+ console.print(table)
237
+
238
+ # Tool summary (show after table)
239
+ if lock_data and "tools" in lock_data:
240
+ console.print("\n[bold]Available Tools:[/bold]")
241
+ for tool in lock_data["tools"]:
242
+ console.print(f" • {tool['name']}: {tool['description']}")
243
+
244
+ # Show warnings if no metadata
245
+ if not lock_data and not yes:
246
+ console.print("\n[yellow]Warning:[/yellow] No metadata available for this image.")
247
+ console.print("The image will be pulled without verification.")
248
+
249
+ # If verify only, stop here
250
+ if verify_only:
251
+ design.success("Verification complete")
252
+ return
253
+
254
+ # Ask for confirmation unless --yes
255
+ if not yes:
256
+ console.print()
257
+ # Show simple name for confirmation, not the full digest
258
+ if ":" in image_ref and "@" in image_ref:
259
+ simple_name = image_ref.split("@")[0]
260
+ else:
261
+ simple_name = image_ref
262
+ if not typer.confirm(f"Pull {simple_name}?"):
263
+ design.info("Aborted")
264
+ raise typer.Exit(0)
265
+
266
+ # Pull the image
267
+ design.progress_message(f"Pulling {image_ref}...")
268
+
269
+ # Run docker pull with progress
270
+ process = subprocess.Popen( # noqa: S603
271
+ ["docker", "pull", image_ref], # noqa: S607
272
+ stdout=subprocess.PIPE,
273
+ stderr=subprocess.STDOUT,
274
+ text=True,
275
+ encoding="utf-8",
276
+ errors="replace",
277
+ )
278
+
279
+ for line in process.stdout or []:
280
+ if verbose or "Downloading" in line or "Extracting" in line or "Pull complete" in line:
281
+ click.echo(line.rstrip(), err=True)
282
+
283
+ process.wait()
284
+
285
+ if process.returncode != 0:
286
+ design.error("Pull failed")
287
+ raise typer.Exit(1)
288
+
289
+ # Store lock file locally if we have full lock data (not minimal manifest data)
290
+ if lock_data and lock_data.get("source") != "docker-manifest":
291
+ # Extract digest from image ref
292
+ digest = image_ref.split("@sha256:")[-1][:12] if "@sha256:" in image_ref else "latest"
293
+
294
+ # Store under ~/.hud/envs/<digest>/
295
+ local_env_dir = Path.home() / ".hud" / "envs" / digest
296
+ local_env_dir.mkdir(parents=True, exist_ok=True)
297
+
298
+ local_lock_path = local_env_dir / "hud.lock.yaml"
299
+ with open(local_lock_path, "w") as f:
300
+ yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
301
+
302
+ if verbose:
303
+ design.info(f"Stored lock file: {local_lock_path}")
304
+
305
+ # Success!
306
+ design.success("Pull complete!")
307
+
308
+ # Show usage
309
+ design.section_title("Next Steps")
310
+
311
+ # Extract simple name for examples
312
+ simple_ref = image_ref.split("@")[0] if ":" in image_ref and "@" in image_ref else image_ref
313
+
314
+ console.print("1. Quick analysis (from metadata):")
315
+ console.print(f" [cyan]hud analyze {simple_ref}[/cyan]\n")
316
+
317
+ console.print("2. Live analysis (runs container):")
318
+ console.print(f" [cyan]hud analyze {simple_ref} --live[/cyan]\n")
319
+
320
+ console.print("3. Run the environment:")
321
+ console.print(f" [cyan]hud run {simple_ref}[/cyan]")
322
+
323
+
324
+ def pull_command(
325
+ target: str = typer.Argument(..., help="Image reference or lock file to pull"),
326
+ lock_file: str | None = typer.Option(
327
+ None, "--lock", "-l", help="Path to lock file (if target is image ref)"
328
+ ),
329
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
330
+ verify_only: bool = typer.Option(
331
+ False, "--verify-only", help="Only verify metadata without pulling"
332
+ ),
333
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
334
+ ) -> None:
335
+ """Pull HUD environment from registry with metadata preview."""
336
+ pull_environment(target, lock_file, yes, verify_only, verbose)