drydock-cli 3.0.113__tar.gz → 3.0.114__tar.gz

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.
Files changed (123) hide show
  1. {drydock_cli-3.0.113/drydock_cli.egg-info → drydock_cli-3.0.114}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tools/__init__.py +87 -1
  3. {drydock_cli-3.0.113 → drydock_cli-3.0.114/drydock_cli.egg-info}/PKG-INFO +1 -1
  4. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/SOURCES.txt +1 -0
  5. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/pyproject.toml +1 -1
  6. drydock_cli-3.0.114/tests/test_screenshot.py +59 -0
  7. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/LICENSE +0 -0
  8. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/NOTICE +0 -0
  9. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/README.md +0 -0
  10. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/__init__.py +0 -0
  11. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/advisor.py +0 -0
  13. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/agent.py +0 -0
  14. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/bash_safety.py +0 -0
  15. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/__init__.py +0 -0
  16. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-categorize.md +0 -0
  17. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-control.md +0 -0
  18. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-poam.md +0 -0
  19. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-review.md +0 -0
  20. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-assess.md +0 -0
  21. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-remediate.md +0 -0
  22. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/cci.py +0 -0
  23. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/cli.py +0 -0
  24. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/compaction.py +0 -0
  25. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/config.py +0 -0
  26. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/detect.py +0 -0
  27. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/extract.py +0 -0
  28. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/gittools.py +0 -0
  29. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/graphrag.py +0 -0
  30. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/guards.py +0 -0
  31. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/loop_detect.py +0 -0
  32. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/mcp.py +0 -0
  33. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/poam.py +0 -0
  34. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/providers.py +0 -0
  35. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/rmf.py +0 -0
  36. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/rmf_graph.py +0 -0
  37. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/stig.py +0 -0
  39. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tool_registry.py +0 -0
  40. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/__init__.py +0 -0
  41. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/app.py +0 -0
  42. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/approval.py +0 -0
  43. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/messages.py +0 -0
  44. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tui/widgets.py +0 -0
  45. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/tuning.py +0 -0
  46. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock/web.py +0 -0
  47. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/dependency_links.txt +0 -0
  48. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/entry_points.txt +0 -0
  49. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/requires.txt +0 -0
  50. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/drydock_cli.egg-info/top_level.txt +0 -0
  51. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/setup.cfg +0 -0
  52. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_advisor.py +0 -0
  53. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_approval.py +0 -0
  54. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_back_command.py +0 -0
  55. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_background.py +0 -0
  56. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_binary_output.py +0 -0
  57. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_crossplatform.py +0 -0
  58. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_output_bounding.py +0 -0
  59. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_process_group.py +0 -0
  60. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_safety.py +0 -0
  61. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_sanitize.py +0 -0
  62. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_shell.py +0 -0
  63. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_stdin.py +0 -0
  64. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_stop_partial.py +0 -0
  65. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_timeout_network.py +0 -0
  66. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_bash_timeout_param.py +0 -0
  67. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_cci.py +0 -0
  68. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_cli_agents.py +0 -0
  69. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_compact_command.py +0 -0
  70. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_compaction.py +0 -0
  71. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_config.py +0 -0
  72. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_config_migration.py +0 -0
  73. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_context_limit_config.py +0 -0
  74. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_detect.py +0 -0
  75. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_dispatch.py +0 -0
  76. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_e2e_connected.py +0 -0
  77. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_edit_replace_all.py +0 -0
  78. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_empty_response.py +0 -0
  79. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_extract.py +0 -0
  80. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_failure_loop.py +0 -0
  81. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_first_run_setup.py +0 -0
  82. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_gittools.py +0 -0
  83. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_glob_edit_edges.py +0 -0
  84. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_graphify_example.py +0 -0
  85. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_graphrag.py +0 -0
  86. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_graphrag_quoted_path.py +0 -0
  87. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_grep_and_read_robust.py +0 -0
  88. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_guards_and_tools.py +0 -0
  89. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_hallucinated_tools.py +0 -0
  90. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_leaked_tool_call.py +0 -0
  91. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_loop_detect.py +0 -0
  92. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_mcp.py +0 -0
  93. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_oneshot_unreachable.py +0 -0
  94. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_overthink_interrupt.py +0 -0
  95. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_plan_autocontinue.py +0 -0
  96. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_poam.py +0 -0
  97. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_providers_unreachable.py +0 -0
  98. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_read_index.py +0 -0
  99. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_rmf.py +0 -0
  100. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_rmf_graph.py +0 -0
  101. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_rmf_stig_graph.py +0 -0
  102. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_runaway_repetition.py +0 -0
  103. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_server_probe.py +0 -0
  104. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_skills.py +0 -0
  105. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_stall_retry.py +0 -0
  106. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_stig.py +0 -0
  107. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_stop.py +0 -0
  108. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_streaming_newlines.py +0 -0
  109. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_subagent.py +0 -0
  110. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_system_prompt_help.py +0 -0
  111. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_todo.py +0 -0
  112. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion.py +0 -0
  113. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion_more.py +0 -0
  114. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tool_arg_parsing.py +0 -0
  115. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tools_undo.py +0 -0
  116. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tui.py +0 -0
  117. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_tuning.py +0 -0
  118. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_viewimage.py +0 -0
  119. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_vision_input.py +0 -0
  120. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_web_tools.py +0 -0
  121. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_windows_shell.py +0 -0
  122. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_write_content_coerce.py +0 -0
  123. {drydock_cli-3.0.113 → drydock_cli-3.0.114}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.113
3
+ Version: 3.0.114
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -244,6 +244,22 @@ SCHEMAS = [
244
244
  "required": ["path"],
245
245
  },
246
246
  },
247
+ {
248
+ "name": "Screenshot",
249
+ "description": "CAPTURE the current screen and SEE it with your vision — use "
250
+ "when the user asks you to look at their screen, review what's "
251
+ "displayed, debug a GUI, or read something shown on screen. Takes "
252
+ "the screenshot and shows it to you directly (no need to call "
253
+ "ViewImage after). Works on Windows (PowerShell), macOS, and "
254
+ "Linux with a display. Optionally pass `path` to also save the PNG.",
255
+ "input_schema": {
256
+ "type": "object",
257
+ "properties": {
258
+ "path": {"type": "string",
259
+ "description": "Optional file to save the PNG to. Omit to use a temp file."},
260
+ },
261
+ },
262
+ },
247
263
  {
248
264
  "name": "Write",
249
265
  "description": "Write content to a file, creating parent directories as needed.",
@@ -711,6 +727,74 @@ def tool_viewimage(params: dict, config: dict) -> str:
711
727
  "to you. Describe it, read any text in it, or use it to answer the task.")
712
728
 
713
729
 
730
+ def _capture_screen(out: str) -> str | None:
731
+ """Grab the whole (virtual) screen to PNG `out`. Returns an error string, or
732
+ None on success. Per-OS: Windows uses PowerShell + System.Drawing; macOS uses
733
+ screencapture; Linux tries the common grabbers in turn."""
734
+ import shutil
735
+ import subprocess
736
+ try:
737
+ if _IS_WINDOWS:
738
+ ps = (
739
+ "Add-Type -AssemblyName System.Windows.Forms,System.Drawing; "
740
+ "$s=[System.Windows.Forms.SystemInformation]::VirtualScreen; "
741
+ "$b=New-Object System.Drawing.Bitmap $s.Width,$s.Height; "
742
+ "$g=[System.Drawing.Graphics]::FromImage($b); "
743
+ "$g.CopyFromScreen($s.Left,$s.Top,0,0,$b.Size); "
744
+ f"$b.Save('{out}',[System.Drawing.Imaging.ImageFormat]::Png); "
745
+ "$g.Dispose(); $b.Dispose()"
746
+ )
747
+ exe = shutil.which("pwsh") or shutil.which("powershell") or "powershell"
748
+ subprocess.run([exe, "-NoProfile", "-NonInteractive", "-Command", ps],
749
+ capture_output=True, timeout=30)
750
+ elif sys.platform == "darwin":
751
+ subprocess.run(["screencapture", "-x", out], capture_output=True, timeout=30)
752
+ else: # Linux/BSD — try grabbers in order of ubiquity (X11 + wayland)
753
+ grabbers = (["scrot", "-o", out], ["import", "-window", "root", out],
754
+ ["gnome-screenshot", "-f", out], ["grim", out],
755
+ ["spectacle", "-b", "-n", "-o", out], ["maim", out])
756
+ tried = [g[0] for g in grabbers if shutil.which(g[0])]
757
+ if not tried:
758
+ return ("no screenshot tool found. Install one of: scrot, imagemagick "
759
+ "(import), gnome-screenshot, grim (wayland), spectacle, maim.")
760
+ for g in grabbers:
761
+ if shutil.which(g[0]):
762
+ subprocess.run(g, capture_output=True, timeout=30)
763
+ if os.path.isfile(out) and os.path.getsize(out):
764
+ break
765
+ except subprocess.TimeoutExpired:
766
+ return "screen capture timed out (30s)."
767
+ except Exception as e: # noqa: BLE001 — report, never crash the loop
768
+ return f"screen capture failed: {e}"
769
+ return None
770
+
771
+
772
+ def tool_screenshot(params: dict, config: dict) -> str:
773
+ """Capture the screen to a PNG and make it visible to the vision model — the
774
+ returned absolute path is auto-attached by the API boundary, same as ViewImage."""
775
+ import tempfile
776
+
777
+ raw = _as_str_arg(params.get("path")).strip()
778
+ out = os.path.abspath(_resolve_path(raw, config) if raw
779
+ else os.path.join(tempfile.gettempdir(), "drydock_screenshot.png"))
780
+ try:
781
+ if os.path.dirname(out):
782
+ os.makedirs(os.path.dirname(out), exist_ok=True)
783
+ except OSError:
784
+ pass
785
+ err = _capture_screen(out)
786
+ if err:
787
+ return (f"Error: {err} (On a headless server there is no display to capture.)")
788
+ if not os.path.isfile(out) or not os.path.getsize(out):
789
+ return "Error: screen capture produced no image (no display, or the grabber failed)."
790
+ size = os.path.getsize(out)
791
+ if size > _MAX_VIEW_IMAGE_BYTES:
792
+ return (f"Captured the screen to {out} but it is {size // 1_000_000}MB — too "
793
+ "large to view (limit 20MB). The file is saved; open it manually.")
794
+ return (f"Captured the screen to {out} (PNG, {size // 1024 or 1}KB) — it is now "
795
+ "visible to you. Describe what's on screen or use it to answer the task.")
796
+
797
+
714
798
  def tool_read(params: dict, config: dict) -> str:
715
799
  fp = _resolve_path(_as_str_arg(params.get("file_path")), config)
716
800
  # Reading an image as text yields binary garbage — point at the vision tool.
@@ -1801,6 +1885,7 @@ def tool_build_knowledge(params: dict, config: dict) -> str:
1801
1885
 
1802
1886
  _TOOLS = [
1803
1887
  ("Read", tool_read, True),
1888
+ ("Screenshot", tool_screenshot, True),
1804
1889
  ("Write", tool_write, False),
1805
1890
  ("Edit", tool_edit, False),
1806
1891
  ("Bash", tool_bash, False),
@@ -1829,6 +1914,7 @@ def register_all():
1829
1914
  name = schema["name"]
1830
1915
  func = {
1831
1916
  "Read": tool_read, "ViewImage": tool_viewimage,
1917
+ "Screenshot": tool_screenshot,
1832
1918
  "Write": tool_write, "Edit": tool_edit,
1833
1919
  "Bash": tool_bash, "Glob": tool_glob, "Grep": tool_grep,
1834
1920
  "todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
@@ -1843,7 +1929,7 @@ def register_all():
1843
1929
  # Read-only w.r.t. the parent's files (GitStatus/Diff/Log inspect only;
1844
1930
  # GitCommit + GraphAdd write).
1845
1931
  read_only = name in (
1846
- "Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Consult",
1932
+ "Read", "ViewImage", "Screenshot", "Glob", "Grep", "task", "Dispatch", "Consult",
1847
1933
  "Knowledge", "GraphQuery", "StigRules", "StigRule",
1848
1934
  "WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
1849
1935
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.113
3
+ Version: 3.0.114
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -97,6 +97,7 @@ tests/test_rmf.py
97
97
  tests/test_rmf_graph.py
98
98
  tests/test_rmf_stig_graph.py
99
99
  tests/test_runaway_repetition.py
100
+ tests/test_screenshot.py
100
101
  tests/test_server_probe.py
101
102
  tests/test_skills.py
102
103
  tests/test_stall_retry.py
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name is drydock-cli (the established install name, continued
8
8
  # from the retired v2 fork); the import package + CLI command stay `drydock`.
9
9
  name = "drydock-cli"
10
- version = "3.0.113"
10
+ version = "3.0.114"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,59 @@
1
+ """Screenshot tool: capture the screen to PNG and make it visible to the vision
2
+ model (the returned path auto-attaches, same path as ViewImage). Cross-platform:
3
+ PowerShell on Windows, screencapture on macOS, common grabbers on Linux."""
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import tempfile
8
+ from unittest import mock
9
+
10
+ from drydock import tools, tool_registry
11
+ from drydock.providers import detect_image_paths
12
+ from drydock.tuning import filter_tool_schemas
13
+
14
+
15
+ def test_registered_and_available_to_gemma():
16
+ names = [s["name"] for s in tool_registry.schemas()]
17
+ gnames = [s["name"] for s in filter_tool_schemas(tool_registry.schemas(), "gemma4")]
18
+ assert "Screenshot" in names and "Screenshot" in gnames
19
+
20
+
21
+ def test_graceful_when_no_grabber(monkeypatch):
22
+ monkeypatch.setattr(tools, "_IS_WINDOWS", False)
23
+ monkeypatch.setattr("sys.platform", "linux")
24
+ monkeypatch.setattr("shutil.which", lambda _n: None) # no grabbers installed
25
+ out = tools.tool_screenshot({}, {"cwd": os.getcwd()})
26
+ assert out.startswith("Error") and "screenshot tool" in out
27
+
28
+
29
+ def test_success_returns_attachable_path():
30
+ png = tempfile.mktemp(suffix=".png")
31
+
32
+ def fake_cap(o):
33
+ open(o, "wb").write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 200)
34
+ return None
35
+
36
+ with mock.patch.object(tools, "_capture_screen", fake_cap):
37
+ out = tools.tool_screenshot({"path": png}, {"cwd": os.getcwd()})
38
+ assert "visible to you" in out
39
+ assert detect_image_paths(out) == [os.path.abspath(png)] # auto-attaches
40
+
41
+
42
+ def test_windows_builds_powershell_capture():
43
+ calls = {}
44
+ with mock.patch.object(tools, "_IS_WINDOWS", True), \
45
+ mock.patch("shutil.which", side_effect=lambda n: r"C:\pwsh.exe" if n == "pwsh" else None), \
46
+ mock.patch("subprocess.run", side_effect=lambda a, **k: calls.setdefault("argv", a)):
47
+ assert tools._capture_screen(r"C:\shot.png") is None
48
+ argv = calls["argv"]
49
+ assert argv[0].endswith("pwsh.exe") and argv[1] == "-NoProfile"
50
+ assert "CopyFromScreen" in argv[-1] and "ImageFormat]::Png" in argv[-1]
51
+
52
+
53
+ def test_macos_uses_screencapture(monkeypatch):
54
+ calls = {}
55
+ monkeypatch.setattr(tools, "_IS_WINDOWS", False)
56
+ monkeypatch.setattr("sys.platform", "darwin")
57
+ monkeypatch.setattr("subprocess.run", lambda a, **k: calls.setdefault("argv", a))
58
+ tools._capture_screen("/tmp/x.png")
59
+ assert calls["argv"][0] == "screencapture"
File without changes
File without changes
File without changes
File without changes