hud-python 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/analyze.py CHANGED
@@ -1,371 +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)
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)