hud-python 0.3.5__py3-none-any.whl → 0.4.1__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 +15 -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 +370 -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 +379 -0
  45. hud/clients/fastmcp.py +222 -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.1.dist-info/METADATA +476 -0
  126. hud_python-0.4.1.dist-info/RECORD +132 -0
  127. hud_python-0.4.1.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.1.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.1.dist-info}/WHEEL +0 -0
hud/cli/analyze.py ADDED
@@ -0,0 +1,371 @@
1
+ """Analyze command implementation for MCP environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from rich.console import Console
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+ from rich.syntax import Syntax
11
+ from rich.table import Table
12
+ from rich.tree import Tree
13
+
14
+ from hud.clients import MCPClient
15
+ from hud.utils.design import HUDDesign
16
+
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+
20
+ console = Console()
21
+ design = HUDDesign()
22
+
23
+
24
+ def parse_docker_command(docker_cmd: list[str]) -> dict:
25
+ """Convert Docker command to MCP config."""
26
+ return {
27
+ "local": {"command": docker_cmd[0], "args": docker_cmd[1:] if len(docker_cmd) > 1 else []}
28
+ }
29
+
30
+
31
+ async def analyze_environment(docker_cmd: list[str], output_format: str, verbose: bool) -> None:
32
+ """Analyze MCP environment and display results."""
33
+ design.header("MCP Environment Analysis", icon="🔍")
34
+
35
+ # Convert Docker command to MCP config
36
+ mcp_config = parse_docker_command(docker_cmd)
37
+
38
+ # Display command being analyzed
39
+ design.dim_info("Command:", " ".join(docker_cmd))
40
+ design.info("") # Empty line
41
+
42
+ # Create client
43
+ with Progress(
44
+ SpinnerColumn(),
45
+ TextColumn("[progress.description]{task.description}"),
46
+ console=console,
47
+ ) as progress:
48
+ task = progress.add_task("Initializing MCP client...", total=None)
49
+
50
+ client = MCPClient(mcp_config=mcp_config, verbose=verbose, auto_trace=False)
51
+
52
+ try:
53
+ await client.initialize()
54
+ progress.update(task, description="[green]✓ Client initialized[/green]")
55
+
56
+ # Analyze environment
57
+ progress.update(task, description="Analyzing environment...")
58
+ analysis = await client.analyze_environment()
59
+ progress.update(task, description="[green]✓ Analysis complete[/green]")
60
+
61
+ except Exception as e:
62
+ progress.update(task, description=f"[red]✗ Failed: {e}[/red]")
63
+
64
+ # On Windows, Docker stderr might not propagate properly
65
+ import platform
66
+
67
+ if platform.system() == "Windows" and "docker" in docker_cmd[0].lower():
68
+ console.print("\n[yellow]💡 Tip: Docker logs may not show on Windows.[/yellow]")
69
+ console.print(f"[yellow] Try: hud debug {' '.join(docker_cmd[3:])}[/yellow]")
70
+ console.print("[yellow] This will show more detailed error information.[/yellow]")
71
+ elif verbose:
72
+ console.print("\n[dim]For more details, try running with 'hud debug'[/dim]")
73
+
74
+ return
75
+ finally:
76
+ await client.shutdown()
77
+
78
+ # Display results based on format
79
+ if output_format == "json":
80
+ console.print_json(json.dumps(analysis, indent=2))
81
+ elif output_format == "markdown":
82
+ display_markdown(analysis)
83
+ else: # interactive
84
+ display_interactive(analysis)
85
+
86
+
87
+ def display_interactive(analysis: dict) -> None:
88
+ """Display analysis results in interactive format."""
89
+ # Server metadata
90
+ design.section_title("📊 Environment Overview")
91
+ meta_table = Table(show_header=False, box=None)
92
+ meta_table.add_column("Property", style="dim")
93
+ meta_table.add_column("Value")
94
+
95
+ # Check if this is a live analysis (has metadata) or metadata-only analysis
96
+ if "metadata" in analysis:
97
+ # Live analysis format
98
+ for server in analysis["metadata"]["servers"]:
99
+ meta_table.add_row("Server", f"[green]{server}[/green]")
100
+ meta_table.add_row(
101
+ "Initialized",
102
+ "[green]✓[/green]" if analysis["metadata"]["initialized"] else "[red]✗[/red]",
103
+ )
104
+ else:
105
+ # Metadata-only format
106
+ if "image" in analysis:
107
+ # Show simple name in table
108
+ image = analysis["image"]
109
+ display_ref = image.split("@")[0] if ":" in image and "@" in image else image
110
+ meta_table.add_row("Image", f"[green]{display_ref}[/green]")
111
+
112
+ if "status" in analysis:
113
+ meta_table.add_row("Source", analysis.get("source", analysis["status"]).title())
114
+
115
+ if "build_info" in analysis:
116
+ meta_table.add_row("Built", analysis["build_info"].get("generatedAt", "Unknown"))
117
+ meta_table.add_row("HUD Version", analysis["build_info"].get("hudVersion", "Unknown"))
118
+
119
+ if "push_info" in analysis:
120
+ meta_table.add_row("Pushed", analysis["push_info"].get("pushedAt", "Unknown"))
121
+
122
+ if "init_time" in analysis:
123
+ meta_table.add_row("Init Time", f"{analysis['init_time']} ms")
124
+
125
+ if "tool_count" in analysis:
126
+ meta_table.add_row("Tools", str(analysis["tool_count"]))
127
+
128
+ console.print(meta_table)
129
+
130
+ # Tools
131
+ design.section_title("🔧 Available Tools")
132
+ tools_tree = Tree("Tools")
133
+
134
+ # Check if we have hub_tools info (live analysis) or not (metadata-only)
135
+ if "hub_tools" in analysis:
136
+ # Live analysis format - separate regular and hub tools
137
+ # Regular tools
138
+ regular_tools = tools_tree.add("Regular Tools")
139
+ for tool in analysis["tools"]:
140
+ if tool["name"] not in analysis["hub_tools"]:
141
+ tool_node = regular_tools.add(f"[default]{tool['name']}[/default]")
142
+ if tool["description"]:
143
+ tool_node.add(f"[dim]{tool['description']}[/dim]")
144
+
145
+ # Show input schema if verbose
146
+ if analysis.get("verbose") and tool.get("input_schema"):
147
+ schema_str = json.dumps(tool["input_schema"], indent=2)
148
+ syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
149
+ tool_node.add(syntax)
150
+
151
+ # Hub tools
152
+ if analysis["hub_tools"]:
153
+ hub_tools = tools_tree.add("Hub Tools")
154
+ for hub_name, functions in analysis["hub_tools"].items():
155
+ hub_node = hub_tools.add(f"[yellow]{hub_name}[/yellow]")
156
+ for func in functions:
157
+ hub_node.add(f"[default]{func}[/default]")
158
+ else:
159
+ # Metadata-only format - just list all tools
160
+ for tool in analysis["tools"]:
161
+ tool_node = tools_tree.add(f"[default]{tool['name']}[/default]")
162
+ if tool.get("description"):
163
+ tool_node.add(f"[dim]{tool['description']}[/dim]")
164
+
165
+ # Show input schema if verbose
166
+ if tool.get("inputSchema"):
167
+ schema_str = json.dumps(tool["inputSchema"], indent=2)
168
+ syntax = Syntax(schema_str, "json", theme="monokai", line_numbers=False)
169
+ tool_node.add(syntax)
170
+
171
+ console.print(tools_tree)
172
+
173
+ # Resources
174
+ if analysis["resources"]:
175
+ design.section_title("📚 Available Resources")
176
+ resources_table = Table()
177
+ resources_table.add_column("URI", style="default")
178
+ resources_table.add_column("Name", style="white")
179
+ resources_table.add_column("Type", style="dim")
180
+
181
+ for resource in analysis["resources"][:10]:
182
+ resources_table.add_row(
183
+ resource["uri"], resource.get("name", ""), resource.get("mime_type", "")
184
+ )
185
+
186
+ console.print(resources_table)
187
+
188
+ if len(analysis["resources"]) > 10:
189
+ console.print(f"[dim]... and {len(analysis['resources']) - 10} more resources[/dim]")
190
+
191
+ # Telemetry (only for live analysis)
192
+ if analysis.get("telemetry"):
193
+ design.section_title("📡 Telemetry Data")
194
+ telemetry_table = Table(show_header=False, box=None)
195
+ telemetry_table.add_column("Key", style="dim")
196
+ telemetry_table.add_column("Value")
197
+
198
+ if "live_url" in analysis["telemetry"]:
199
+ telemetry_table.add_row("Live URL", f"[link]{analysis['telemetry']['live_url']}[/link]")
200
+ if "status" in analysis["telemetry"]:
201
+ telemetry_table.add_row("Status", f"[green]{analysis['telemetry']['status']}[/green]")
202
+ if "services" in analysis["telemetry"]:
203
+ services = analysis["telemetry"]["services"]
204
+ running = sum(1 for s in services.values() if s == "running")
205
+ telemetry_table.add_row("Services", f"{running}/{len(services)} running")
206
+
207
+ console.print(telemetry_table)
208
+
209
+ # Environment variables (for metadata-only analysis)
210
+ if analysis.get("env_vars"):
211
+ design.section_title("🔑 Environment Variables")
212
+ env_table = Table(show_header=False, box=None)
213
+ env_table.add_column("Type", style="dim")
214
+ env_table.add_column("Variables")
215
+
216
+ if analysis["env_vars"].get("required"):
217
+ env_table.add_row("Required", ", ".join(analysis["env_vars"]["required"]))
218
+ if analysis["env_vars"].get("optional"):
219
+ env_table.add_row("Optional", ", ".join(analysis["env_vars"]["optional"]))
220
+
221
+ console.print(env_table)
222
+
223
+
224
+ def display_markdown(analysis: dict) -> None:
225
+ """Display analysis results in markdown format."""
226
+ md = []
227
+ md.append("# MCP Environment Analysis\n")
228
+
229
+ # Metadata
230
+ md.append("## Environment Overview")
231
+
232
+ # Check if this is live analysis or metadata-only
233
+ if "metadata" in analysis:
234
+ md.append(f"- **Servers**: {', '.join(analysis['metadata']['servers'])}")
235
+ md.append(f"- **Initialized**: {'✓' if analysis['metadata']['initialized'] else '✗'}")
236
+ else:
237
+ # Metadata-only format
238
+ if "image" in analysis:
239
+ md.append(f"- **Image**: {analysis['image']}")
240
+ if "source" in analysis:
241
+ md.append(f"- **Source**: {analysis['source']}")
242
+ if "build_info" in analysis:
243
+ md.append(f"- **Built**: {analysis['build_info'].get('generatedAt', 'Unknown')}")
244
+ if "tool_count" in analysis:
245
+ md.append(f"- **Tools**: {analysis['tool_count']}")
246
+
247
+ md.append("")
248
+
249
+ # Tools
250
+ md.append("## Available Tools\n")
251
+
252
+ # Check if we have hub_tools info (live analysis) or not (metadata-only)
253
+ if "hub_tools" in analysis:
254
+ # Regular tools
255
+ md.append("### Regular Tools")
256
+ for tool in analysis["tools"]:
257
+ if tool["name"] not in analysis["hub_tools"]:
258
+ md.extend([f"- **{tool['name']}**: {tool.get('description', 'No description')}"])
259
+ md.append("")
260
+
261
+ # Hub tools
262
+ if analysis["hub_tools"]:
263
+ md.append("### Hub Tools")
264
+ for hub_name, functions in analysis["hub_tools"].items():
265
+ md.extend([f"- **{hub_name}**"])
266
+ for func in functions:
267
+ md.extend([f" - {func}"])
268
+ md.append("")
269
+ else:
270
+ # Metadata-only format - just list all tools
271
+ for tool in analysis["tools"]:
272
+ md.extend([f"- **{tool['name']}**: {tool.get('description', 'No description')}"])
273
+ md.append("")
274
+
275
+ # Resources
276
+ if analysis["resources"]:
277
+ md.append("## Available Resources\n")
278
+ md.append("| URI | Name | Type |")
279
+ md.append("|-----|------|------|")
280
+ for resource in analysis["resources"]:
281
+ uri = resource["uri"]
282
+ name = resource.get("name", "")
283
+ mime_type = resource.get("mime_type", "")
284
+ md.extend([f"| {uri} | {name} | {mime_type} |"])
285
+ md.append("")
286
+
287
+ # Telemetry (only for live analysis)
288
+ if analysis.get("telemetry"):
289
+ md.append("## Telemetry")
290
+ if "live_url" in analysis["telemetry"]:
291
+ md.extend([f"- **Live URL**: {analysis['telemetry']['live_url']}"])
292
+ if "status" in analysis["telemetry"]:
293
+ md.extend([f"- **Status**: {analysis['telemetry']['status']}"])
294
+ if "services" in analysis["telemetry"]:
295
+ md.extend([f"- **Services**: {analysis['telemetry']['services']}"])
296
+ md.append("")
297
+
298
+ # Environment variables (for metadata-only analysis)
299
+ if analysis.get("env_vars"):
300
+ md.append("## Environment Variables")
301
+ if analysis["env_vars"].get("required"):
302
+ md.extend([f"- **Required**: {', '.join(analysis['env_vars']['required'])}"])
303
+ if analysis["env_vars"].get("optional"):
304
+ md.extend([f"- **Optional**: {', '.join(analysis['env_vars']['optional'])}"])
305
+ md.append("")
306
+
307
+ console.print("\n".join(md))
308
+
309
+
310
+ async def analyze_environment_from_config(
311
+ config_path: Path, output_format: str, verbose: bool
312
+ ) -> None:
313
+ """Analyze MCP environment from a JSON config file."""
314
+ design.header("MCP Environment Analysis", icon="🔍")
315
+
316
+ # Load config from file
317
+ try:
318
+ with open(config_path) as f: # noqa: ASYNC230
319
+ mcp_config = json.load(f)
320
+ console.print(f"[dim]Config: {config_path}[/dim]\n")
321
+ except Exception as e:
322
+ console.print(f"[red]Error loading config: {e}[/red]")
323
+ return
324
+
325
+ await _analyze_with_config(mcp_config, output_format, verbose)
326
+
327
+
328
+ async def analyze_environment_from_mcp_config(
329
+ mcp_config: dict[str, Any], output_format: str, verbose: bool
330
+ ) -> None:
331
+ """Analyze MCP environment from MCP config dict."""
332
+ design.header("MCP Environment Analysis", icon="🔍")
333
+ await _analyze_with_config(mcp_config, output_format, verbose)
334
+
335
+
336
+ async def _analyze_with_config(
337
+ mcp_config: dict[str, Any], output_format: str, verbose: bool
338
+ ) -> None:
339
+ """Internal helper to analyze with MCP config."""
340
+ # Create client
341
+ with Progress(
342
+ SpinnerColumn(),
343
+ TextColumn("[progress.description]{task.description}"),
344
+ console=console,
345
+ ) as progress:
346
+ task = progress.add_task("Initializing MCP client...", total=None)
347
+
348
+ client = MCPClient(mcp_config=mcp_config, verbose=verbose)
349
+
350
+ try:
351
+ await client.initialize()
352
+ progress.update(task, description="[green]✓ Client initialized[/green]")
353
+
354
+ # Analyze environment
355
+ progress.update(task, description="Analyzing environment...")
356
+ analysis = await client.analyze_environment()
357
+ progress.update(task, description="[green]✓ Analysis complete[/green]")
358
+
359
+ except Exception as e:
360
+ progress.update(task, description=f"[red]✗ Failed: {e}[/red]")
361
+ return
362
+ finally:
363
+ await client.shutdown()
364
+
365
+ # Display results based on format
366
+ if output_format == "json":
367
+ console.print_json(json.dumps(analysis, indent=2))
368
+ elif output_format == "markdown":
369
+ display_markdown(analysis)
370
+ else: # interactive
371
+ display_interactive(analysis)
@@ -0,0 +1,230 @@
1
+ """Fast metadata analysis functions for hud analyze."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import requests
8
+ import yaml
9
+ from rich.console import Console
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+
12
+ from hud.settings import settings
13
+ from hud.utils.design import HUDDesign
14
+
15
+ console = Console()
16
+ design = HUDDesign()
17
+
18
+
19
+ def fetch_lock_from_registry(reference: str) -> dict | None:
20
+ """Fetch lock file from HUD registry."""
21
+ try:
22
+ # Reference should be org/name:tag format
23
+ # If no tag specified, append :latest
24
+ if "/" in reference and ":" not in reference:
25
+ reference = f"{reference}:latest"
26
+
27
+ registry_url = f"{settings.hud_telemetry_url.rstrip('/')}/registry/envs/{reference}"
28
+
29
+ headers = {}
30
+ if settings.api_key:
31
+ headers["Authorization"] = f"Bearer {settings.api_key}"
32
+
33
+ response = requests.get(registry_url, headers=headers, timeout=10)
34
+
35
+ if response.status_code == 200:
36
+ data = response.json()
37
+ # Parse the lock YAML from the response
38
+ if "lock" in data:
39
+ return yaml.safe_load(data["lock"])
40
+ elif "lock_data" in data:
41
+ return data["lock_data"]
42
+ else:
43
+ # Try to treat the whole response as lock data
44
+ return data
45
+
46
+ return None
47
+ except Exception:
48
+ return None
49
+
50
+
51
+ def check_local_cache(reference: str) -> dict | None:
52
+ """Check local cache for lock file."""
53
+ # Extract digest if present
54
+ if "@sha256:" in reference:
55
+ digest = reference.split("@sha256:")[-1][:12]
56
+ elif "/" in reference:
57
+ # Try to find by name pattern
58
+ cache_dir = Path.home() / ".hud" / "envs"
59
+ if cache_dir.exists():
60
+ # Look for any cached version of this image
61
+ for env_dir in cache_dir.iterdir():
62
+ if env_dir.is_dir():
63
+ lock_file = env_dir / "hud.lock.yaml"
64
+ if lock_file.exists():
65
+ with open(lock_file) as f:
66
+ lock_data = yaml.safe_load(f)
67
+ # Check if this matches our reference
68
+ if lock_data and "image" in lock_data:
69
+ image = lock_data["image"]
70
+ # Match by name (ignoring tag/digest)
71
+ ref_base = reference.split("@")[0].split(":")[0]
72
+ img_base = image.split("@")[0].split(":")[0]
73
+ if ref_base in img_base or img_base in ref_base:
74
+ return lock_data
75
+ return None
76
+ else:
77
+ digest = "latest"
78
+
79
+ # Check specific digest directory
80
+ lock_file = Path.home() / ".hud" / "envs" / digest / "hud.lock.yaml"
81
+ if lock_file.exists():
82
+ with open(lock_file) as f:
83
+ return yaml.safe_load(f)
84
+
85
+ return None
86
+
87
+
88
+ async def analyze_from_metadata(reference: str, output_format: str, verbose: bool) -> None:
89
+ """Analyze environment from cached or registry metadata."""
90
+ import json
91
+
92
+ from .analyze import display_interactive, display_markdown
93
+
94
+ design.header("MCP Environment Analysis", icon="🔍")
95
+ design.info(f"Looking up: {reference}")
96
+ design.info("")
97
+
98
+ lock_data = None
99
+ source = None
100
+
101
+ # 1. Check local cache first
102
+ with Progress(
103
+ SpinnerColumn(),
104
+ TextColumn("[progress.description]{task.description}"),
105
+ console=console,
106
+ ) as progress:
107
+ task = progress.add_task("Checking local cache...", total=None)
108
+
109
+ lock_data = check_local_cache(reference)
110
+ if lock_data:
111
+ progress.update(task, description="[green]✓ Found in local cache[/green]")
112
+ source = "local"
113
+ else:
114
+ progress.update(
115
+ task, description="[yellow]→ Not in cache, checking registry...[/yellow]"
116
+ )
117
+
118
+ # 2. Try HUD registry
119
+ # Parse reference to get org/name format
120
+ if "/" in reference and "@" not in reference and ":" not in reference:
121
+ # Already in org/name format
122
+ registry_ref = reference
123
+ elif "/" in reference:
124
+ # Extract org/name from full reference
125
+ parts = reference.split("/")
126
+ if len(parts) >= 2:
127
+ # Handle docker.io/org/name or just org/name
128
+ if parts[0] in ["docker.io", "registry-1.docker.io", "index.docker.io"]:
129
+ # Remove registry prefix but keep tag
130
+ registry_ref = "/".join(parts[1:]).split("@")[0]
131
+ else:
132
+ # Keep org/name:tag format
133
+ registry_ref = "/".join(parts[:2]).split("@")[0]
134
+ else:
135
+ registry_ref = reference
136
+ else:
137
+ registry_ref = reference
138
+
139
+ if not settings.api_key:
140
+ progress.update(
141
+ task, description="[yellow]→ No API key (checking public registry)...[/yellow]"
142
+ )
143
+
144
+ lock_data = fetch_lock_from_registry(registry_ref)
145
+ if lock_data:
146
+ progress.update(task, description="[green]✓ Found in HUD registry[/green]")
147
+ source = "registry"
148
+
149
+ # Save to local cache for next time
150
+ if "@sha256:" in lock_data.get("image", ""):
151
+ digest = lock_data["image"].split("@sha256:")[-1][:12]
152
+ else:
153
+ digest = "latest"
154
+
155
+ cache_dir = Path.home() / ".hud" / "envs" / digest
156
+ cache_dir.mkdir(parents=True, exist_ok=True)
157
+ with open(cache_dir / "hud.lock.yaml", "w") as f: # noqa: ASYNC230
158
+ yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
159
+ else:
160
+ progress.update(task, description="[red]✗ Not found[/red]")
161
+
162
+ if not lock_data:
163
+ design.error("Environment metadata not found")
164
+ console.print("\n[yellow]This environment hasn't been analyzed yet.[/yellow]")
165
+ console.print("\nOptions:")
166
+ console.print(f" 1. Pull it first: [cyan]hud pull {reference}[/cyan]")
167
+ console.print(f" 2. Run live analysis: [cyan]hud analyze {reference} --live[/cyan]")
168
+ if not settings.api_key:
169
+ console.print(" 3. Set HUD_API_KEY for private environments")
170
+ return
171
+
172
+ # Convert lock data to analysis format
173
+ analysis = {
174
+ "status": "metadata" if source == "local" else "registry",
175
+ "source": source,
176
+ "tools": [],
177
+ "resources": [],
178
+ "prompts": [],
179
+ }
180
+
181
+ # Add basic info
182
+ if "image" in lock_data:
183
+ analysis["image"] = lock_data["image"]
184
+
185
+ if "build" in lock_data:
186
+ analysis["build_info"] = lock_data["build"]
187
+
188
+ if "push" in lock_data:
189
+ analysis["push_info"] = lock_data["push"]
190
+
191
+ # Extract environment info
192
+ if "environment" in lock_data:
193
+ env = lock_data["environment"]
194
+ if "initializeMs" in env:
195
+ analysis["init_time"] = env["initializeMs"]
196
+ if "toolCount" in env:
197
+ analysis["tool_count"] = env["toolCount"]
198
+ if "variables" in env:
199
+ analysis["env_vars"] = env["variables"]
200
+
201
+ # Extract tools
202
+ if "tools" in lock_data:
203
+ for tool in lock_data["tools"]:
204
+ analysis["tools"].append(
205
+ {
206
+ "name": tool["name"],
207
+ "description": tool.get("description", ""),
208
+ "inputSchema": tool.get("inputSchema", {}) if verbose else None,
209
+ }
210
+ )
211
+
212
+ # Display results
213
+ design.info("")
214
+ if source == "local":
215
+ design.dim_info("Source:", "Local cache")
216
+ else:
217
+ design.dim_info("Source:", "HUD registry")
218
+
219
+ if "image" in analysis:
220
+ design.dim_info("Image:", analysis["image"])
221
+
222
+ design.info("")
223
+
224
+ # Display results based on format
225
+ if output_format == "json":
226
+ console.print_json(json.dumps(analysis, indent=2))
227
+ elif output_format == "markdown":
228
+ display_markdown(analysis)
229
+ else: # interactive
230
+ display_interactive(analysis)