drydock-cli 3.0.77__tar.gz → 3.0.78__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 (99) hide show
  1. {drydock_cli-3.0.77/drydock_cli.egg-info → drydock_cli-3.0.78}/PKG-INFO +4 -3
  2. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/README.md +3 -2
  3. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/providers.py +41 -19
  4. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tools/__init__.py +43 -4
  5. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tuning.py +8 -4
  6. {drydock_cli-3.0.77 → drydock_cli-3.0.78/drydock_cli.egg-info}/PKG-INFO +4 -3
  7. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock_cli.egg-info/SOURCES.txt +1 -0
  8. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/pyproject.toml +1 -1
  9. drydock_cli-3.0.78/tests/test_viewimage.py +58 -0
  10. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/LICENSE +0 -0
  11. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/NOTICE +0 -0
  12. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/__init__.py +0 -0
  13. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/__main__.py +0 -0
  14. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/agent.py +0 -0
  15. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/bash_safety.py +0 -0
  16. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/__init__.py +0 -0
  17. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-categorize.md +0 -0
  18. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-control.md +0 -0
  19. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-poam.md +0 -0
  20. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-review.md +0 -0
  21. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/stig-assess.md +0 -0
  22. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/builtin_skills/stig-remediate.md +0 -0
  23. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/cci.py +0 -0
  24. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/cli.py +0 -0
  25. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/compaction.py +0 -0
  26. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/config.py +0 -0
  27. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/detect.py +0 -0
  28. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/extract.py +0 -0
  29. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/gittools.py +0 -0
  30. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/graphrag.py +0 -0
  31. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/guards.py +0 -0
  32. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/loop_detect.py +0 -0
  33. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/mcp.py +0 -0
  34. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/rmf.py +0 -0
  35. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/rmf_graph.py +0 -0
  36. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/skills.py +0 -0
  37. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/stig.py +0 -0
  38. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tool_registry.py +0 -0
  39. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tui/__init__.py +0 -0
  40. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tui/app.py +0 -0
  41. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tui/approval.py +0 -0
  42. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tui/messages.py +0 -0
  43. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/tui/widgets.py +0 -0
  44. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock/web.py +0 -0
  45. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock_cli.egg-info/dependency_links.txt +0 -0
  46. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock_cli.egg-info/entry_points.txt +0 -0
  47. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock_cli.egg-info/requires.txt +0 -0
  48. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/drydock_cli.egg-info/top_level.txt +0 -0
  49. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/setup.cfg +0 -0
  50. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_approval.py +0 -0
  51. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_back_command.py +0 -0
  52. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_bash_output_bounding.py +0 -0
  53. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_bash_process_group.py +0 -0
  54. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_bash_safety.py +0 -0
  55. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_bash_timeout_network.py +0 -0
  56. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_cci.py +0 -0
  57. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_cli_agents.py +0 -0
  58. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_compact_command.py +0 -0
  59. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_compaction.py +0 -0
  60. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_config.py +0 -0
  61. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_config_migration.py +0 -0
  62. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_context_limit_config.py +0 -0
  63. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_detect.py +0 -0
  64. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_dispatch.py +0 -0
  65. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_e2e_connected.py +0 -0
  66. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_empty_response.py +0 -0
  67. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_extract.py +0 -0
  68. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_failure_loop.py +0 -0
  69. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_first_run_setup.py +0 -0
  70. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_gittools.py +0 -0
  71. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_graphrag.py +0 -0
  72. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_guards_and_tools.py +0 -0
  73. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_hallucinated_tools.py +0 -0
  74. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_leaked_tool_call.py +0 -0
  75. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_loop_detect.py +0 -0
  76. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_mcp.py +0 -0
  77. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_oneshot_unreachable.py +0 -0
  78. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_plan_autocontinue.py +0 -0
  79. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_providers_unreachable.py +0 -0
  80. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_read_index.py +0 -0
  81. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_rmf.py +0 -0
  82. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_rmf_graph.py +0 -0
  83. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_rmf_stig_graph.py +0 -0
  84. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_runaway_repetition.py +0 -0
  85. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_server_probe.py +0 -0
  86. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_skills.py +0 -0
  87. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_stig.py +0 -0
  88. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_stop.py +0 -0
  89. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_streaming_newlines.py +0 -0
  90. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_subagent.py +0 -0
  91. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_system_prompt_help.py +0 -0
  92. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_todo.py +0 -0
  93. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_tool_arg_parsing.py +0 -0
  94. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_tools_undo.py +0 -0
  95. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_tui.py +0 -0
  96. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_tuning.py +0 -0
  97. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_vision_input.py +0 -0
  98. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/tests/test_web_tools.py +0 -0
  99. {drydock_cli-3.0.77 → drydock_cli-3.0.78}/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.77
3
+ Version: 3.0.78
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
@@ -61,8 +61,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
61
61
  `Edit`, `Bash`, `Glob`, `Grep`.
62
62
  - **Vision (multimodal)** — reference an image path in your message (a `.png`/
63
63
  `.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
64
- model to *see* describe a UI, read text off a screenshot, debug a diagram.
65
- Works with any `--mmproj`-enabled server (e.g. llama.cpp with a vision model).
64
+ model to *see*; the agent can also call the `ViewImage` tool to look at an
65
+ image it discovers on its own describe a UI, read text off a screenshot,
66
+ debug a diagram. Works with any `--mmproj` server (e.g. llama.cpp + a vision model).
66
67
  - **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
67
68
  (structured + truncated; commit is local and reversible).
68
69
  - **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
@@ -37,8 +37,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
37
37
  `Edit`, `Bash`, `Glob`, `Grep`.
38
38
  - **Vision (multimodal)** — reference an image path in your message (a `.png`/
39
39
  `.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
40
- model to *see* describe a UI, read text off a screenshot, debug a diagram.
41
- Works with any `--mmproj`-enabled server (e.g. llama.cpp with a vision model).
40
+ model to *see*; the agent can also call the `ViewImage` tool to look at an
41
+ image it discovers on its own describe a UI, read text off a screenshot,
42
+ debug a diagram. Works with any `--mmproj` server (e.g. llama.cpp + a vision model).
42
43
  - **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
43
44
  (structured + truncated; commit is local and reversible).
44
45
  - **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
@@ -279,32 +279,47 @@ def detect_image_paths(content) -> list[str]:
279
279
  return seen
280
280
 
281
281
 
282
- def _user_content_with_images(content):
283
- """Vision support: if the user's text references image file paths that exist
284
- on disk, attach them as OpenAI multimodal image_url blocks (works with any
285
- --mmproj-enabled server). Text-only prompts pass through unchanged as a plain
286
- string, so display / loop-detection / compaction / token-counting (which all
287
- assume string content) are untouched — the multimodal list is built ONLY here,
288
- at the API boundary."""
289
- seen = detect_image_paths(content)
290
- if not seen:
291
- return content
282
+ _MAX_IMAGE_BYTES = 20_000_000 # don't base64 a >20MB file into a request
283
+
284
+
285
+ def _image_url_block(path: str) -> dict | None:
286
+ """A base64 data-URL image_url block for an on-disk image, or None if it
287
+ can't be read / is too large."""
292
288
  import base64
293
289
  import os
294
290
 
291
+ try:
292
+ if os.path.getsize(path) > _MAX_IMAGE_BYTES:
293
+ return None
294
+ with open(path, "rb") as f:
295
+ b64 = base64.b64encode(f.read()).decode()
296
+ except OSError:
297
+ return None
298
+ mime = _IMAGE_MIME.get(os.path.splitext(path)[1].lower(), "image/png")
299
+ return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
300
+
301
+
302
+ def _content_with_images(content):
303
+ """Turn text that references on-disk image paths into a multimodal content
304
+ list ([text, image_url…]); text without resolvable images passes through
305
+ unchanged. Used for BOTH user messages (the user names an image) and
306
+ ViewImage tool results (the AGENT chose to view one) — built ONLY here at the
307
+ API boundary so display/compaction/token-counting still see plain strings."""
308
+ seen = detect_image_paths(content)
309
+ if not seen:
310
+ return content
295
311
  blocks: list[dict] = [{"type": "text", "text": content}]
296
312
  for p in seen:
297
- mime = _IMAGE_MIME.get(os.path.splitext(p)[1].lower(), "image/png")
298
- try:
299
- with open(p, "rb") as f:
300
- b64 = base64.b64encode(f.read()).decode()
301
- except OSError:
302
- continue
303
- blocks.append({"type": "image_url",
304
- "image_url": {"url": f"data:{mime};base64,{b64}"}})
313
+ block = _image_url_block(p)
314
+ if block:
315
+ blocks.append(block)
305
316
  return blocks if len(blocks) > 1 else content
306
317
 
307
318
 
319
+ # Back-compat alias (user-message vision).
320
+ _user_content_with_images = _content_with_images
321
+
322
+
308
323
  def messages_to_openai(messages: list, system: str) -> list:
309
324
  """Convert neutral messages to OpenAI API format."""
310
325
  # value type is mixed (str content, multimodal list content, tool_calls list)
@@ -331,10 +346,17 @@ def messages_to_openai(messages: list, system: str) -> list:
331
346
  ]
332
347
  result.append(msg)
333
348
  elif role == "tool":
349
+ content = m["content"]
350
+ # Agent-side vision: a ViewImage result names an image the agent chose
351
+ # to look at — attach it so the model SEES it (servers read images
352
+ # from tool messages too, verified). Only ViewImage, so an unrelated
353
+ # tool mentioning a .png path never balloons the request.
354
+ if m.get("name") == "ViewImage" and isinstance(content, str):
355
+ content = _content_with_images(content)
334
356
  result.append({
335
357
  "role": "tool",
336
358
  "tool_call_id": m["tool_call_id"],
337
- "content": m["content"],
359
+ "content": content,
338
360
  })
339
361
  return result
340
362
 
@@ -83,6 +83,17 @@ SCHEMAS = [
83
83
  "required": ["file_path"],
84
84
  },
85
85
  },
86
+ {
87
+ "name": "ViewImage",
88
+ "description": "Look at an image file with your vision — use this when you need to SEE a screenshot, mockup, diagram, photo, or rendered output (.png/.jpg/.jpeg/.gif/.webp/.bmp). The image is shown to you so you can describe it, read text from it, or debug it. (Reading an image with the Read tool gives binary garbage; use ViewImage instead.)",
89
+ "input_schema": {
90
+ "type": "object",
91
+ "properties": {
92
+ "path": {"type": "string", "description": "Path to the image file"},
93
+ },
94
+ "required": ["path"],
95
+ },
96
+ },
86
97
  {
87
98
  "name": "Write",
88
99
  "description": "Write content to a file, creating parent directories as needed.",
@@ -474,6 +485,33 @@ def _file_index(lines: list[str], fp: str) -> str:
474
485
  return f"{head}\nKey locations ({len(anchors)}):\n{body}"
475
486
 
476
487
 
488
+ _IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
489
+ _MAX_VIEW_IMAGE_BYTES = 20_000_000
490
+
491
+
492
+ def tool_viewimage(params: dict, config: dict) -> str:
493
+ """Validate an image path and return a result that names it; the API boundary
494
+ (providers.messages_to_openai) attaches the actual image to THIS tool result
495
+ so the vision model sees it. Returns a plain error string on any problem
496
+ (never an attachable path) so a bad call can't try to attach nothing."""
497
+ raw = (params.get("path") or params.get("file_path") or "").strip()
498
+ if not raw:
499
+ return "Error: ViewImage needs a `path` to an image file."
500
+ fp = _resolve_path(raw, config)
501
+ if os.path.splitext(fp)[1].lower() not in _IMAGE_EXTS:
502
+ return (f"Error: {raw} is not a supported image. ViewImage handles "
503
+ f"{', '.join(_IMAGE_EXTS)}.")
504
+ if not os.path.isfile(fp):
505
+ return f"Error: no image file at {raw} (resolved to {fp})."
506
+ size = os.path.getsize(fp)
507
+ if size > _MAX_VIEW_IMAGE_BYTES:
508
+ return f"Error: {raw} is {size // 1_000_000}MB — too large to view (limit 20MB)."
509
+ kind = os.path.splitext(fp)[1].lstrip(".").upper()
510
+ # The absolute path in this text is what the API boundary attaches.
511
+ return (f"Loaded image {fp} ({kind}, {size // 1024 or 1}KB) — it is now visible "
512
+ "to you. Describe it, read any text in it, or use it to answer the task.")
513
+
514
+
477
515
  def tool_read(params: dict, config: dict) -> str:
478
516
  fp = _resolve_path(params["file_path"], config)
479
517
  limit = params.get("limit") # None = caller didn't specify a window
@@ -1010,7 +1048,7 @@ def tool_todo(params: dict, config: dict) -> str:
1010
1048
  # Tools a sub-agent may use. Excludes Write/Edit/todo (no silent mutations the
1011
1049
  # parent can't see) and `task` itself (no recursion). Bash is included for real
1012
1050
  # exploration (ls/cat/grep/find/git log) and is bounded by the same bash_safety.
1013
- SUBAGENT_TOOLS = ("Read", "Glob", "Grep", "Bash")
1051
+ SUBAGENT_TOOLS = ("Read", "ViewImage", "Glob", "Grep", "Bash")
1014
1052
 
1015
1053
  _SUBAGENT_SYSTEM = (
1016
1054
  "You are a focused exploration sub-agent inside Drydock. You have read-only "
@@ -1428,7 +1466,8 @@ def register_all():
1428
1466
  for schema in SCHEMAS:
1429
1467
  name = schema["name"]
1430
1468
  func = {
1431
- "Read": tool_read, "Write": tool_write, "Edit": tool_edit,
1469
+ "Read": tool_read, "ViewImage": tool_viewimage,
1470
+ "Write": tool_write, "Edit": tool_edit,
1432
1471
  "Bash": tool_bash, "Glob": tool_glob, "Grep": tool_grep,
1433
1472
  "todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
1434
1473
  "Knowledge": tool_knowledge,
@@ -1441,8 +1480,8 @@ def register_all():
1441
1480
  # Read-only w.r.t. the parent's files (GitStatus/Diff/Log inspect only;
1442
1481
  # GitCommit + GraphAdd write).
1443
1482
  read_only = name in (
1444
- "Read", "Glob", "Grep", "task", "Dispatch", "Knowledge", "GraphQuery",
1445
- "StigRules", "StigRule",
1483
+ "Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Knowledge",
1484
+ "GraphQuery", "StigRules", "StigRule",
1446
1485
  "WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
1447
1486
  )
1448
1487
  register(ToolDef(name=name, schema=schema, func=func, read_only=read_only))
@@ -93,8 +93,10 @@ _DEFAULT_SYSTEM_PROMPT = (
93
93
  "every step is done. Don't write a TODO.md file for this.\n"
94
94
  "You have VISION: when the user references an image file path (.png/.jpg/"
95
95
  ".jpeg/.gif/.webp/.bmp on disk — a screenshot, mockup, diagram, or photo), it "
96
- "is attached and you can SEE it; describe it, read text from it, or debug "
97
- "with it never claim you cannot view images."
96
+ "is attached and you can SEE it; you can also call the `ViewImage` tool to "
97
+ "look at any image you find yourself (Read gives binary garbage for images"
98
+ "use ViewImage). Describe it, read text from it, or debug with it — never "
99
+ "claim you cannot view images."
98
100
  )
99
101
 
100
102
  # Short, imperative prompt. Small local models do better with "act now" than
@@ -119,8 +121,10 @@ _GEMMA_SYSTEM_PROMPT = (
119
121
  "write a TODO.md file for this.\n"
120
122
  "You have VISION: when the user's message references an image file path "
121
123
  "(.png/.jpg/.jpeg/.gif/.webp/.bmp that exists on disk — a screenshot, mockup, "
122
- "diagram, or photo), that image is attached and you can SEE it. Describe it, "
123
- "read text from it, or use it to debug never claim you cannot view images."
124
+ "diagram, or photo), that image is attached and you can SEE it. You can also "
125
+ "call the `ViewImage` tool to look at any image you find yourself (Read gives "
126
+ "binary garbage for images — use ViewImage). Describe it, read text from it, "
127
+ "or use it to debug — never claim you cannot view images."
124
128
  )
125
129
 
126
130
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.77
3
+ Version: 3.0.78
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
@@ -61,8 +61,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
61
61
  `Edit`, `Bash`, `Glob`, `Grep`.
62
62
  - **Vision (multimodal)** — reference an image path in your message (a `.png`/
63
63
  `.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
64
- model to *see* describe a UI, read text off a screenshot, debug a diagram.
65
- Works with any `--mmproj`-enabled server (e.g. llama.cpp with a vision model).
64
+ model to *see*; the agent can also call the `ViewImage` tool to look at an
65
+ image it discovers on its own describe a UI, read text off a screenshot,
66
+ debug a diagram. Works with any `--mmproj` server (e.g. llama.cpp + a vision model).
66
67
  - **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
67
68
  (structured + truncated; commit is local and reversible).
68
69
  - **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
@@ -91,6 +91,7 @@ tests/test_tool_arg_parsing.py
91
91
  tests/test_tools_undo.py
92
92
  tests/test_tui.py
93
93
  tests/test_tuning.py
94
+ tests/test_viewimage.py
94
95
  tests/test_vision_input.py
95
96
  tests/test_web_tools.py
96
97
  tests/test_xccdf.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.77"
10
+ version = "3.0.78"
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,58 @@
1
+ """Agent-side vision: the ViewImage tool + its API-boundary attachment.
2
+ The agent can choose to SEE an image it discovers (not just user-referenced
3
+ ones); the image rides back on the ViewImage tool result."""
4
+ from __future__ import annotations
5
+
6
+ import struct
7
+ import zlib
8
+
9
+ import drydock.tools # noqa: F401 — triggers tool registration
10
+ from drydock import tool_registry as reg
11
+ from drydock.providers import messages_to_openai
12
+
13
+
14
+ def _png(path):
15
+ w = h = 8
16
+ raw = b"".join(b"\x00" + bytes([10, 20, 200]) * w for _ in range(h))
17
+ def ch(t, d):
18
+ c = t + d
19
+ return struct.pack(">I", len(d)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
20
+ path.write_bytes(b"\x89PNG\r\n\x1a\n"
21
+ + ch(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
22
+ + ch(b"IDAT", zlib.compress(raw)) + ch(b"IEND", b""))
23
+ return str(path)
24
+
25
+
26
+ def test_viewimage_validates(tmp_path):
27
+ _png(tmp_path / "a.png")
28
+ cfg = {"cwd": str(tmp_path)}
29
+ assert "now visible to you" in reg.execute("ViewImage", {"path": "a.png"}, cfg)
30
+ assert "no image file" in reg.execute("ViewImage", {"path": "missing.png"}, cfg)
31
+ assert "not a supported image" in reg.execute("ViewImage", {"path": str(tmp_path / "x.txt")}, cfg)
32
+ assert "needs a `path`" in reg.execute("ViewImage", {}, cfg)
33
+
34
+
35
+ def test_viewimage_is_read_only_and_subagent_visible():
36
+ td = reg.get("ViewImage")
37
+ assert td.read_only is True
38
+ from drydock.tools import SUBAGENT_TOOLS
39
+ assert "ViewImage" in SUBAGENT_TOOLS
40
+
41
+
42
+ def test_viewimage_result_becomes_multimodal(tmp_path):
43
+ _png(tmp_path / "shot.png")
44
+ cfg = {"cwd": str(tmp_path)}
45
+ res = reg.execute("ViewImage", {"path": "shot.png"}, cfg)
46
+ out = messages_to_openai([{"role": "tool", "tool_call_id": "c1",
47
+ "name": "ViewImage", "content": res}], "sys")
48
+ content = out[-1]["content"]
49
+ assert isinstance(content, list)
50
+ assert any(b.get("type") == "image_url" for b in content)
51
+ assert content[0]["type"] == "text"
52
+
53
+
54
+ def test_non_viewimage_tool_with_png_path_stays_text(tmp_path):
55
+ img = _png(tmp_path / "shot.png")
56
+ out = messages_to_openai([{"role": "tool", "tool_call_id": "c2",
57
+ "name": "Grep", "content": f"found {img} in code"}], "sys")
58
+ assert isinstance(out[-1]["content"], str) # no accidental image ballooning
File without changes
File without changes
File without changes