hud-python 0.2.10__py3-none-any.whl → 0.3.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 (64) hide show
  1. hud/__init__.py +14 -5
  2. hud/env/docker_client.py +1 -1
  3. hud/env/environment.py +10 -7
  4. hud/env/local_docker_client.py +1 -1
  5. hud/env/remote_client.py +1 -1
  6. hud/env/remote_docker_client.py +2 -2
  7. hud/exceptions.py +2 -1
  8. hud/mcp_agent/__init__.py +15 -0
  9. hud/mcp_agent/base.py +723 -0
  10. hud/mcp_agent/claude.py +316 -0
  11. hud/mcp_agent/langchain.py +231 -0
  12. hud/mcp_agent/openai.py +318 -0
  13. hud/mcp_agent/tests/__init__.py +1 -0
  14. hud/mcp_agent/tests/test_base.py +437 -0
  15. hud/settings.py +14 -2
  16. hud/task.py +4 -0
  17. hud/telemetry/__init__.py +11 -7
  18. hud/telemetry/_trace.py +82 -71
  19. hud/telemetry/context.py +9 -27
  20. hud/telemetry/exporter.py +6 -5
  21. hud/telemetry/instrumentation/mcp.py +174 -410
  22. hud/telemetry/mcp_models.py +13 -74
  23. hud/telemetry/tests/test_context.py +9 -6
  24. hud/telemetry/tests/test_trace.py +92 -61
  25. hud/tools/__init__.py +21 -0
  26. hud/tools/base.py +65 -0
  27. hud/tools/bash.py +137 -0
  28. hud/tools/computer/__init__.py +13 -0
  29. hud/tools/computer/anthropic.py +411 -0
  30. hud/tools/computer/hud.py +315 -0
  31. hud/tools/computer/openai.py +283 -0
  32. hud/tools/edit.py +290 -0
  33. hud/tools/executors/__init__.py +13 -0
  34. hud/tools/executors/base.py +331 -0
  35. hud/tools/executors/pyautogui.py +585 -0
  36. hud/tools/executors/tests/__init__.py +1 -0
  37. hud/tools/executors/tests/test_base_executor.py +338 -0
  38. hud/tools/executors/tests/test_pyautogui_executor.py +162 -0
  39. hud/tools/executors/xdo.py +503 -0
  40. hud/tools/helper/README.md +56 -0
  41. hud/tools/helper/__init__.py +9 -0
  42. hud/tools/helper/mcp_server.py +78 -0
  43. hud/tools/helper/server_initialization.py +115 -0
  44. hud/tools/helper/utils.py +58 -0
  45. hud/tools/playwright_tool.py +373 -0
  46. hud/tools/tests/__init__.py +3 -0
  47. hud/tools/tests/test_bash.py +152 -0
  48. hud/tools/tests/test_computer.py +52 -0
  49. hud/tools/tests/test_computer_actions.py +34 -0
  50. hud/tools/tests/test_edit.py +233 -0
  51. hud/tools/tests/test_init.py +27 -0
  52. hud/tools/tests/test_playwright_tool.py +183 -0
  53. hud/tools/tests/test_tools.py +154 -0
  54. hud/tools/tests/test_utils.py +156 -0
  55. hud/tools/utils.py +50 -0
  56. hud/types.py +10 -1
  57. hud/utils/tests/test_init.py +21 -0
  58. hud/utils/tests/test_version.py +1 -1
  59. hud/version.py +1 -1
  60. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/METADATA +9 -6
  61. hud_python-0.3.0.dist-info/RECORD +124 -0
  62. hud_python-0.2.10.dist-info/RECORD +0 -85
  63. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/WHEEL +0 -0
  64. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,585 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import logging
6
+ import os
7
+ from io import BytesIO
8
+ from typing import Literal
9
+
10
+ if "DISPLAY" not in os.environ:
11
+ try:
12
+ from hud.settings import settings
13
+
14
+ os.environ["DISPLAY"] = settings.display
15
+ except (ImportError, AttributeError):
16
+ os.environ["DISPLAY"] = ":0"
17
+
18
+ try:
19
+ import pyautogui
20
+
21
+ PYAUTOGUI_AVAILABLE = True
22
+ except ImportError:
23
+ PYAUTOGUI_AVAILABLE = False
24
+
25
+ from hud.tools.base import ToolResult
26
+
27
+ from .base import BaseExecutor
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Map CLA standard keys to PyAutoGUI keys (only where they differ)
32
+ CLA_TO_PYAUTOGUI = {
33
+ # Most keys are the same in PyAutoGUI, only map the differences
34
+ "escape": "esc",
35
+ "enter": "return",
36
+ "pageup": "pgup",
37
+ "pagedown": "pgdn",
38
+ "printscreen": "prtscr",
39
+ "prtsc": "prtscr",
40
+ "super": "win",
41
+ "command": "cmd",
42
+ }
43
+
44
+
45
+ class PyAutoGUIExecutor(BaseExecutor):
46
+ """
47
+ Cross-platform executor using PyAutoGUI.
48
+ Works on Windows, macOS, and Linux.
49
+
50
+ This executor should only be instantiated when PyAutoGUI is available and functional.
51
+ """
52
+
53
+ def __init__(self, display_num: int | None = None) -> None:
54
+ """
55
+ Initialize the executor.
56
+
57
+ Args:
58
+ display_num: X display number (used only on Linux, ignored on Windows/macOS)
59
+ """
60
+ super().__init__(display_num)
61
+
62
+ logger.info("PyAutoGUIExecutor initialized")
63
+
64
+ # Configure PyAutoGUI settings
65
+ pyautogui.FAILSAFE = False # Disable fail-safe feature
66
+ pyautogui.PAUSE = 0.1 # Small pause between actions
67
+
68
+ def _map_key(self, key: str) -> str:
69
+ """Map CLA standard key to PyAutoGUI key."""
70
+ return CLA_TO_PYAUTOGUI.get(key.lower(), key.lower())
71
+
72
+ def _map_keys(self, keys: list[str]) -> list[str]:
73
+ """Map CLA standard keys to PyAutoGUI keys."""
74
+ mapped_keys = []
75
+ for key in keys:
76
+ # Handle key combinations like "ctrl+a"
77
+ if "+" in key:
78
+ parts = key.split("+")
79
+ mapped_parts = [self._map_key(part) for part in parts]
80
+ mapped_keys.append("+".join(mapped_parts))
81
+ else:
82
+ mapped_keys.append(self._map_key(key))
83
+ return mapped_keys
84
+
85
+ @classmethod
86
+ def is_available(cls) -> bool:
87
+ """
88
+ Check if PyAutoGUI is available and functional.
89
+
90
+ Returns:
91
+ True if PyAutoGUI is available and functional, False otherwise
92
+ """
93
+ if not PYAUTOGUI_AVAILABLE:
94
+ return False
95
+
96
+ try:
97
+ # Try to get screen size as a simple test
98
+ pyautogui.size()
99
+ return True
100
+ except Exception:
101
+ return False
102
+
103
+ async def screenshot(self) -> str | None:
104
+ """
105
+ Take a screenshot and return base64 encoded image.
106
+
107
+ Returns:
108
+ Base64 encoded PNG image or None if failed
109
+ """
110
+ try:
111
+ # Take screenshot using PyAutoGUI
112
+ screenshot = pyautogui.screenshot()
113
+
114
+ # Convert to base64
115
+ buffer = BytesIO()
116
+ screenshot.save(buffer, format="PNG")
117
+ image_data = buffer.getvalue()
118
+ return base64.b64encode(image_data).decode()
119
+ except Exception as e:
120
+ logger.error("Failed to take screenshot: %s", e)
121
+ return None
122
+
123
+ # ===== Helper Methods =====
124
+
125
+ def _hold_keys_context(self, keys: list[str] | None) -> None:
126
+ """
127
+ Press and hold keys.
128
+
129
+ Args:
130
+ keys: List of keys to hold
131
+ """
132
+ if keys:
133
+ for key in keys:
134
+ pyautogui.keyDown(key)
135
+
136
+ def _release_keys(self, keys: list[str] | None) -> None:
137
+ """Release held keys."""
138
+ if keys:
139
+ for key in reversed(keys): # Release in reverse order
140
+ pyautogui.keyUp(key)
141
+
142
+ # ===== CLA Action Implementations =====
143
+
144
+ async def click(
145
+ self,
146
+ x: int | None = None,
147
+ y: int | None = None,
148
+ button: Literal["left", "right", "middle", "back", "forward"] = "left",
149
+ pattern: list[int] | None = None,
150
+ hold_keys: list[str] | None = None,
151
+ take_screenshot: bool = True,
152
+ ) -> ToolResult:
153
+ """Click at specified coordinates or current position."""
154
+ try:
155
+ # Map button names (PyAutoGUI doesn't support back/forward)
156
+ button_map = {
157
+ "left": "left",
158
+ "right": "right",
159
+ "middle": "middle",
160
+ "back": "left",
161
+ "forward": "right",
162
+ } # Fallback for unsupported
163
+ button_name = button_map.get(button, "left")
164
+
165
+ # Hold keys if specified
166
+ self._hold_keys_context(hold_keys)
167
+
168
+ try:
169
+ # Handle multi-clicks based on pattern
170
+ if pattern:
171
+ clicks = len(pattern) + 1
172
+ interval = pattern[0] / 1000.0 if pattern else 0.1 # Convert ms to seconds
173
+
174
+ if x is not None and y is not None:
175
+ pyautogui.click(
176
+ x=x, y=y, clicks=clicks, interval=interval, button=button_name
177
+ )
178
+ else:
179
+ pyautogui.click(clicks=clicks, interval=interval, button=button_name)
180
+ else:
181
+ # Single click
182
+ if x is not None and y is not None:
183
+ pyautogui.click(x=x, y=y, button=button_name)
184
+ else:
185
+ pyautogui.click(button=button_name)
186
+ finally:
187
+ # Release held keys
188
+ self._release_keys(hold_keys)
189
+
190
+ result = ToolResult(
191
+ output=f"Clicked {button} button at ({x}, {y})" if x else f"Clicked {button} button"
192
+ )
193
+
194
+ if take_screenshot:
195
+ await asyncio.sleep(self._screenshot_delay)
196
+ screenshot = await self.screenshot()
197
+ if screenshot:
198
+ result = ToolResult(
199
+ output=result.output, error=result.error, base64_image=screenshot
200
+ )
201
+
202
+ return result
203
+ except Exception as e:
204
+ return ToolResult(error=str(e))
205
+
206
+ async def type(
207
+ self, text: str, enter_after: bool = False, delay: int = 12, take_screenshot: bool = True
208
+ ) -> ToolResult:
209
+ """Type text with specified delay between keystrokes."""
210
+ try:
211
+ # Convert delay from milliseconds to seconds for PyAutoGUI
212
+ interval = delay / 1000.0
213
+ pyautogui.typewrite(text, interval=interval)
214
+
215
+ if enter_after:
216
+ pyautogui.press("enter")
217
+
218
+ result = ToolResult(
219
+ output=f"Typed: '{text}'" + (" and pressed Enter" if enter_after else "")
220
+ )
221
+
222
+ if take_screenshot:
223
+ await asyncio.sleep(self._screenshot_delay)
224
+ screenshot = await self.screenshot()
225
+ if screenshot:
226
+ result = ToolResult(
227
+ output=result.output, error=result.error, base64_image=screenshot
228
+ )
229
+
230
+ return result
231
+ except Exception as e:
232
+ return ToolResult(error=str(e))
233
+
234
+ async def key(self, key_sequence: str, take_screenshot: bool = True) -> ToolResult:
235
+ """Press a key or key combination."""
236
+ try:
237
+ # Handle key combinations (e.g., "ctrl+c")
238
+ if "+" in key_sequence:
239
+ keys = key_sequence.split("+")
240
+ pyautogui.hotkey(*keys)
241
+ result = ToolResult(output=f"Pressed hotkey: {key_sequence}")
242
+ else:
243
+ # Map common key names from xdotool to PyAutoGUI
244
+ key = key_sequence.lower()
245
+ pyautogui.press(CLA_TO_PYAUTOGUI.get(key, key))
246
+ result = ToolResult(output=f"Pressed key: {key_sequence}")
247
+
248
+ if take_screenshot:
249
+ await asyncio.sleep(self._screenshot_delay)
250
+ screenshot = await self.screenshot()
251
+ if screenshot:
252
+ result = ToolResult(
253
+ output=result.output, error=result.error, base64_image=screenshot
254
+ )
255
+
256
+ return result
257
+ except Exception as e:
258
+ return ToolResult(error=str(e))
259
+
260
+ async def press(self, keys: list[str], take_screenshot: bool = True) -> ToolResult:
261
+ """Press a key combination (hotkey)."""
262
+ try:
263
+ # Map CLA keys to PyAutoGUI keys
264
+ mapped_keys = self._map_keys(keys)
265
+
266
+ # Handle single key or combination
267
+ if len(mapped_keys) == 1 and "+" not in mapped_keys[0]:
268
+ pyautogui.press(mapped_keys[0])
269
+ result = ToolResult(output=f"Pressed key: {keys[0]}")
270
+ else:
271
+ # For combinations, use hotkey
272
+ hotkey_parts = []
273
+ for key in mapped_keys:
274
+ if "+" in key:
275
+ hotkey_parts.extend(key.split("+"))
276
+ else:
277
+ hotkey_parts.append(key)
278
+ pyautogui.hotkey(*hotkey_parts)
279
+ result = ToolResult(output=f"Pressed hotkey: {'+'.join(keys)}")
280
+
281
+ if take_screenshot:
282
+ await asyncio.sleep(self._screenshot_delay)
283
+ screenshot = await self.screenshot()
284
+ if screenshot:
285
+ result = ToolResult(
286
+ output=result.output, error=result.error, base64_image=screenshot
287
+ )
288
+
289
+ return result
290
+ except Exception as e:
291
+ return ToolResult(error=str(e))
292
+
293
+ async def keydown(self, keys: list[str], take_screenshot: bool = True) -> ToolResult:
294
+ """Press and hold keys."""
295
+ try:
296
+ # Map CLA keys to PyAutoGUI keys
297
+ mapped_keys = self._map_keys(keys)
298
+ for key in mapped_keys:
299
+ pyautogui.keyDown(key)
300
+
301
+ result = ToolResult(output=f"Keys down: {', '.join(keys)}")
302
+
303
+ if take_screenshot:
304
+ await asyncio.sleep(self._screenshot_delay)
305
+ screenshot = await self.screenshot()
306
+ if screenshot:
307
+ result = ToolResult(
308
+ output=result.output, error=result.error, base64_image=screenshot
309
+ )
310
+
311
+ return result
312
+ except Exception as e:
313
+ return ToolResult(error=str(e))
314
+
315
+ async def keyup(self, keys: list[str], take_screenshot: bool = True) -> ToolResult:
316
+ """Release held keys."""
317
+ try:
318
+ # Map CLA keys to PyAutoGUI keys
319
+ mapped_keys = self._map_keys(keys)
320
+ for key in reversed(mapped_keys): # Release in reverse order
321
+ pyautogui.keyUp(key)
322
+
323
+ result = ToolResult(output=f"Keys up: {', '.join(keys)}")
324
+
325
+ if take_screenshot:
326
+ await asyncio.sleep(self._screenshot_delay)
327
+ screenshot = await self.screenshot()
328
+ if screenshot:
329
+ result = ToolResult(
330
+ output=result.output, error=result.error, base64_image=screenshot
331
+ )
332
+
333
+ return result
334
+ except Exception as e:
335
+ return ToolResult(error=str(e))
336
+
337
+ async def scroll(
338
+ self,
339
+ x: int | None = None,
340
+ y: int | None = None,
341
+ scroll_x: int | None = None,
342
+ scroll_y: int | None = None,
343
+ hold_keys: list[str] | None = None,
344
+ take_screenshot: bool = True,
345
+ ) -> ToolResult:
346
+ """Scroll at specified position."""
347
+ try:
348
+ # Move to position if specified
349
+ if x is not None and y is not None:
350
+ pyautogui.moveTo(x, y)
351
+
352
+ # Hold keys if specified
353
+ self._hold_keys_context(hold_keys)
354
+
355
+ try:
356
+ msg_parts = []
357
+
358
+ # Perform vertical scroll
359
+ if scroll_y and scroll_y != 0:
360
+ # PyAutoGUI: positive = up, negative = down (opposite of our convention)
361
+ pyautogui.scroll(-scroll_y)
362
+ msg_parts.append(f"vertically by {scroll_y}")
363
+
364
+ # Perform horizontal scroll (if supported)
365
+ if scroll_x and scroll_x != 0:
366
+ # PyAutoGUI horizontal scroll might not work on all platforms
367
+ try:
368
+ pyautogui.hscroll(scroll_x)
369
+ msg_parts.append(f"horizontally by {scroll_x}")
370
+ except AttributeError:
371
+ # hscroll not available
372
+ msg_parts.append(f"horizontally by {scroll_x} (not supported)")
373
+
374
+ if not msg_parts:
375
+ return ToolResult(output="No scroll amount specified")
376
+
377
+ msg = "Scrolled " + " and ".join(msg_parts)
378
+ if x is not None and y is not None:
379
+ msg += f" at ({x}, {y})"
380
+ if hold_keys:
381
+ msg += f" while holding {hold_keys}"
382
+ finally:
383
+ # Release held keys
384
+ self._release_keys(hold_keys)
385
+
386
+ result = ToolResult(output=msg)
387
+
388
+ if take_screenshot:
389
+ await asyncio.sleep(self._screenshot_delay)
390
+ screenshot = await self.screenshot()
391
+ if screenshot:
392
+ result = ToolResult(
393
+ output=result.output, error=result.error, base64_image=screenshot
394
+ )
395
+
396
+ return result
397
+ except Exception as e:
398
+ return ToolResult(error=str(e))
399
+
400
+ async def move(
401
+ self,
402
+ x: int | None = None,
403
+ y: int | None = None,
404
+ offset_x: int | None = None,
405
+ offset_y: int | None = None,
406
+ take_screenshot: bool = True,
407
+ ) -> ToolResult:
408
+ """Move mouse cursor."""
409
+ try:
410
+ if x is not None and y is not None:
411
+ # Absolute move
412
+ pyautogui.moveTo(x, y, duration=0.1)
413
+ result = ToolResult(output=f"Moved mouse to ({x}, {y})")
414
+ elif offset_x is not None or offset_y is not None:
415
+ # Relative move
416
+ offset_x = offset_x or 0
417
+ offset_y = offset_y or 0
418
+ pyautogui.moveRel(xOffset=offset_x, yOffset=offset_y, duration=0.1)
419
+ result = ToolResult(output=f"Moved mouse by offset ({offset_x}, {offset_y})")
420
+ else:
421
+ return ToolResult(output="No move coordinates specified")
422
+
423
+ if take_screenshot:
424
+ await asyncio.sleep(self._screenshot_delay)
425
+ screenshot = await self.screenshot()
426
+ if screenshot:
427
+ result = ToolResult(
428
+ output=result.output, error=result.error, base64_image=screenshot
429
+ )
430
+
431
+ return result
432
+ except Exception as e:
433
+ return ToolResult(error=str(e))
434
+
435
+ async def drag(
436
+ self,
437
+ path: list[tuple[int, int]],
438
+ pattern: list[int] | None = None,
439
+ hold_keys: list[str] | None = None,
440
+ take_screenshot: bool = True,
441
+ ) -> ToolResult:
442
+ """Drag along a path."""
443
+ if len(path) < 2:
444
+ return ToolResult(error="Drag path must have at least 2 points")
445
+
446
+ try:
447
+ # Hold keys if specified
448
+ self._hold_keys_context(hold_keys)
449
+
450
+ try:
451
+ # Move to start
452
+ start_x, start_y = path[0]
453
+ pyautogui.moveTo(start_x, start_y)
454
+
455
+ # Handle multi-point drag
456
+ if len(path) == 2:
457
+ # Simple drag
458
+ end_x, end_y = path[1]
459
+ pyautogui.dragTo(end_x, end_y, duration=0.5, button="left")
460
+ result = ToolResult(
461
+ output=f"Dragged from ({start_x}, {start_y}) to ({end_x}, {end_y})"
462
+ )
463
+ else:
464
+ # Multi-point drag
465
+ pyautogui.mouseDown(button="left")
466
+ for i, (x, y) in enumerate(path[1:], 1):
467
+ duration = 0.1
468
+ if pattern and i - 1 < len(pattern):
469
+ duration = pattern[i - 1] / 1000.0 # Convert ms to seconds
470
+ pyautogui.moveTo(x, y, duration=duration)
471
+ pyautogui.mouseUp(button="left")
472
+
473
+ result = ToolResult(output=f"Dragged along {len(path)} points")
474
+
475
+ if hold_keys:
476
+ result = ToolResult(output=f"{result.output} while holding {hold_keys}")
477
+ finally:
478
+ # Release held keys
479
+ self._release_keys(hold_keys)
480
+
481
+ if take_screenshot:
482
+ await asyncio.sleep(self._screenshot_delay)
483
+ screenshot = await self.screenshot()
484
+ if screenshot:
485
+ result = ToolResult(
486
+ output=result.output, error=result.error, base64_image=screenshot
487
+ )
488
+
489
+ return result
490
+ except Exception as e:
491
+ return ToolResult(error=str(e))
492
+
493
+ async def mouse_down(
494
+ self,
495
+ button: Literal["left", "right", "middle", "back", "forward"] = "left",
496
+ take_screenshot: bool = True,
497
+ ) -> ToolResult:
498
+ """Press and hold a mouse button."""
499
+ try:
500
+ # Map button names (PyAutoGUI doesn't support back/forward)
501
+ button_map = {
502
+ "left": "left",
503
+ "right": "right",
504
+ "middle": "middle",
505
+ "back": "left",
506
+ "forward": "right",
507
+ } # Fallback for unsupported
508
+ button_name = button_map.get(button, "left")
509
+
510
+ pyautogui.mouseDown(button=button_name)
511
+ result = ToolResult(output=f"Mouse down: {button} button")
512
+
513
+ if take_screenshot:
514
+ await asyncio.sleep(self._screenshot_delay)
515
+ screenshot = await self.screenshot()
516
+ if screenshot:
517
+ result = ToolResult(
518
+ output=result.output, error=result.error, base64_image=screenshot
519
+ )
520
+
521
+ return result
522
+ except Exception as e:
523
+ return ToolResult(error=str(e))
524
+
525
+ async def mouse_up(
526
+ self,
527
+ button: Literal["left", "right", "middle", "back", "forward"] = "left",
528
+ take_screenshot: bool = True,
529
+ ) -> ToolResult:
530
+ """Release a mouse button."""
531
+ try:
532
+ # Map button names (PyAutoGUI doesn't support back/forward)
533
+ button_map = {
534
+ "left": "left",
535
+ "right": "right",
536
+ "middle": "middle",
537
+ "back": "left",
538
+ "forward": "right",
539
+ } # Fallback for unsupported
540
+ button_name = button_map.get(button, "left")
541
+
542
+ pyautogui.mouseUp(button=button_name)
543
+ result = ToolResult(output=f"Mouse up: {button} button")
544
+
545
+ if take_screenshot:
546
+ await asyncio.sleep(self._screenshot_delay)
547
+ screenshot = await self.screenshot()
548
+ if screenshot:
549
+ result = ToolResult(
550
+ output=result.output, error=result.error, base64_image=screenshot
551
+ )
552
+
553
+ return result
554
+ except Exception as e:
555
+ return ToolResult(error=str(e))
556
+
557
+ async def hold_key(self, key: str, duration: float, take_screenshot: bool = True) -> ToolResult:
558
+ """Hold a key for a specified duration."""
559
+ try:
560
+ # Map CLA key to PyAutoGUI key
561
+ mapped_key = self._map_key(key)
562
+ pyautogui.keyDown(mapped_key)
563
+ await asyncio.sleep(duration)
564
+ pyautogui.keyUp(mapped_key)
565
+
566
+ result = ToolResult(output=f"Held key '{key}' for {duration} seconds")
567
+
568
+ if take_screenshot:
569
+ screenshot = await self.screenshot()
570
+ if screenshot:
571
+ result = ToolResult(
572
+ output=result.output, error=result.error, base64_image=screenshot
573
+ )
574
+
575
+ return result
576
+ except Exception as e:
577
+ return ToolResult(error=str(e))
578
+
579
+ async def position(self) -> ToolResult:
580
+ """Get current cursor position."""
581
+ try:
582
+ x, y = pyautogui.position()
583
+ return ToolResult(output=f"Mouse position: ({x}, {y})")
584
+ except Exception as e:
585
+ return ToolResult(error=str(e))
@@ -0,0 +1 @@
1
+ """Tests for tool executors."""