drydock-cli 3.0.76__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.
- {drydock_cli-3.0.76/drydock_cli.egg-info → drydock_cli-3.0.78}/PKG-INFO +6 -1
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/README.md +5 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/providers.py +49 -18
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tools/__init__.py +43 -4
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tui/app.py +8 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tuning.py +22 -6
- {drydock_cli-3.0.76 → drydock_cli-3.0.78/drydock_cli.egg-info}/PKG-INFO +6 -1
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/pyproject.toml +1 -1
- drydock_cli-3.0.78/tests/test_viewimage.py +58 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_vision_input.py +20 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/LICENSE +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/NOTICE +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/agent.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/cci.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/cli.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/config.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/detect.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/extract.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/guards.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/skills.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/stig.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock/web.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/setup.cfg +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_config.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.76 → drydock_cli-3.0.78}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.76 → 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.
|
|
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
|
|
@@ -59,6 +59,11 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
|
|
|
59
59
|
|
|
60
60
|
- **Files & shell** — `Read` (with a structure index for huge files), `Write`,
|
|
61
61
|
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
62
|
+
- **Vision (multimodal)** — reference an image path in your message (a `.png`/
|
|
63
|
+
`.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
|
|
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).
|
|
62
67
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
63
68
|
(structured + truncated; commit is local and reversible).
|
|
64
69
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -35,6 +35,11 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
|
|
|
35
35
|
|
|
36
36
|
- **Files & shell** — `Read` (with a structure index for huge files), `Write`,
|
|
37
37
|
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
38
|
+
- **Vision (multimodal)** — reference an image path in your message (a `.png`/
|
|
39
|
+
`.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
|
|
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).
|
|
38
43
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
39
44
|
(structured + truncated; commit is local and reversible).
|
|
40
45
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -250,16 +250,11 @@ _IMAGE_MIME = {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
|
|
253
|
-
def
|
|
254
|
-
"""
|
|
255
|
-
|
|
256
|
-
--mmproj-enabled server). Text-only prompts pass through unchanged as a plain
|
|
257
|
-
string, so display / loop-detection / compaction / token-counting (which all
|
|
258
|
-
assume string content) are untouched — the multimodal list is built ONLY here,
|
|
259
|
-
at the API boundary."""
|
|
253
|
+
def detect_image_paths(content) -> list[str]:
|
|
254
|
+
"""Image file paths referenced in a user message that exist on disk. Shared
|
|
255
|
+
by the multimodal attach (below) and the TUI (to confirm '📎 attached')."""
|
|
260
256
|
if not isinstance(content, str) or "." not in content:
|
|
261
|
-
return
|
|
262
|
-
import base64
|
|
257
|
+
return []
|
|
263
258
|
import os
|
|
264
259
|
import re
|
|
265
260
|
|
|
@@ -281,21 +276,50 @@ def _user_content_with_images(content):
|
|
|
281
276
|
p = os.path.expanduser(raw)
|
|
282
277
|
if os.path.isfile(p) and p not in seen:
|
|
283
278
|
seen.append(p)
|
|
279
|
+
return seen
|
|
280
|
+
|
|
281
|
+
|
|
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."""
|
|
288
|
+
import base64
|
|
289
|
+
import os
|
|
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)
|
|
284
309
|
if not seen:
|
|
285
310
|
return content
|
|
286
311
|
blocks: list[dict] = [{"type": "text", "text": content}]
|
|
287
312
|
for p in seen:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
b64 = base64.b64encode(f.read()).decode()
|
|
292
|
-
except OSError:
|
|
293
|
-
continue
|
|
294
|
-
blocks.append({"type": "image_url",
|
|
295
|
-
"image_url": {"url": f"data:{mime};base64,{b64}"}})
|
|
313
|
+
block = _image_url_block(p)
|
|
314
|
+
if block:
|
|
315
|
+
blocks.append(block)
|
|
296
316
|
return blocks if len(blocks) > 1 else content
|
|
297
317
|
|
|
298
318
|
|
|
319
|
+
# Back-compat alias (user-message vision).
|
|
320
|
+
_user_content_with_images = _content_with_images
|
|
321
|
+
|
|
322
|
+
|
|
299
323
|
def messages_to_openai(messages: list, system: str) -> list:
|
|
300
324
|
"""Convert neutral messages to OpenAI API format."""
|
|
301
325
|
# value type is mixed (str content, multimodal list content, tool_calls list)
|
|
@@ -322,10 +346,17 @@ def messages_to_openai(messages: list, system: str) -> list:
|
|
|
322
346
|
]
|
|
323
347
|
result.append(msg)
|
|
324
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)
|
|
325
356
|
result.append({
|
|
326
357
|
"role": "tool",
|
|
327
358
|
"tool_call_id": m["tool_call_id"],
|
|
328
|
-
"content":
|
|
359
|
+
"content": content,
|
|
329
360
|
})
|
|
330
361
|
return result
|
|
331
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, "
|
|
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",
|
|
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))
|
|
@@ -409,6 +409,14 @@ class DrydockApp(App):
|
|
|
409
409
|
# running, queue this one and drain it when the current turn finishes
|
|
410
410
|
# instead of dropping it on the floor.
|
|
411
411
|
self._mount(UserMessage(text))
|
|
412
|
+
# Confirm any image attachments so the user SEES vision is active (the
|
|
413
|
+
# actual attach happens at the API boundary in providers).
|
|
414
|
+
from drydock import providers
|
|
415
|
+
imgs = providers.detect_image_paths(text)
|
|
416
|
+
if imgs:
|
|
417
|
+
import os as _os
|
|
418
|
+
names = ", ".join(_os.path.basename(p) for p in imgs)
|
|
419
|
+
self._info(f"📎 attached {len(imgs)} image(s) for the model to see: {names}")
|
|
412
420
|
if self._busy:
|
|
413
421
|
self._queue.append(text)
|
|
414
422
|
self._refresh_status()
|
|
@@ -90,7 +90,13 @@ _DEFAULT_SYSTEM_PROMPT = (
|
|
|
90
90
|
"start to lay out the plan, then START WORKING THROUGH IT IMMEDIATELY — do "
|
|
91
91
|
"NOT stop to ask whether to proceed. Call `todo` again to flip a step to "
|
|
92
92
|
"done and the next to in-progress as you finish each, and only stop when "
|
|
93
|
-
"every step is done. Don't write a TODO.md file for this
|
|
93
|
+
"every step is done. Don't write a TODO.md file for this.\n"
|
|
94
|
+
"You have VISION: when the user references an image file path (.png/.jpg/"
|
|
95
|
+
".jpeg/.gif/.webp/.bmp on disk — a screenshot, mockup, diagram, or photo), it "
|
|
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."
|
|
94
100
|
)
|
|
95
101
|
|
|
96
102
|
# Short, imperative prompt. Small local models do better with "act now" than
|
|
@@ -112,7 +118,13 @@ _GEMMA_SYSTEM_PROMPT = (
|
|
|
112
118
|
"(one task per line, '[ ]'/'[~]'/'[x]'), then IMMEDIATELY start doing the "
|
|
113
119
|
"steps — do NOT stop to ask whether to proceed. Call `todo` again to update "
|
|
114
120
|
"it as you finish each step, and only stop when every step is done. Do not "
|
|
115
|
-
"write a TODO.md file for this
|
|
121
|
+
"write a TODO.md file for this.\n"
|
|
122
|
+
"You have VISION: when the user's message references an image file path "
|
|
123
|
+
"(.png/.jpg/.jpeg/.gif/.webp/.bmp that exists on disk — a screenshot, mockup, "
|
|
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."
|
|
116
128
|
)
|
|
117
129
|
|
|
118
130
|
|
|
@@ -221,11 +233,15 @@ _DRYDOCK_COMMANDS_HELP = (
|
|
|
221
233
|
"- Custom skills (reusable `/<name>` prompts): `/skills new <name> <prompt "
|
|
222
234
|
"text>` creates one (use $ARGS in the prompt for trailing input); `/skills` "
|
|
223
235
|
"lists them; then they run it as `/<name>`.\n"
|
|
236
|
+
"- Images / vision: the user just references an image file path in their "
|
|
237
|
+
"message (a .png/.jpg screenshot, mockup, or diagram on disk) and it is "
|
|
238
|
+
"attached for you to SEE — no command needed.\n"
|
|
224
239
|
"- Other: `/model` (model/endpoint), `/cwd`, `/undo` (revert last write), "
|
|
225
|
-
"`/back` (rewind a turn), `/compact` (shrink context), `/
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
240
|
+
"`/back` (rewind a turn), `/compact` (shrink context), `/context` (view/set "
|
|
241
|
+
"the context-window budget), `/loop <n> <prompt>` (repeat a prompt), `/mcp` "
|
|
242
|
+
"(connected MCP servers), `/rmf` & `/stig` (NIST 800-53 / DISA STIG), "
|
|
243
|
+
"`/status`, `/clear`, `/help`, `/quit`. Internet + git are tools you call "
|
|
244
|
+
"yourself (WebSearch/WebFetch, GitStatus/GitDiff/GitLog/GitCommit)."
|
|
229
245
|
)
|
|
230
246
|
|
|
231
247
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
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
|
|
@@ -59,6 +59,11 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
|
|
|
59
59
|
|
|
60
60
|
- **Files & shell** — `Read` (with a structure index for huge files), `Write`,
|
|
61
61
|
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
62
|
+
- **Vision (multimodal)** — reference an image path in your message (a `.png`/
|
|
63
|
+
`.jpg` screenshot, mockup, or diagram) and it's attached for a vision-capable
|
|
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).
|
|
62
67
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
63
68
|
(structured + truncated; commit is local and reversible).
|
|
64
69
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -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.
|
|
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
|
|
@@ -82,3 +82,23 @@ def test_image_path_in_backticks_and_punctuation(tmp_path):
|
|
|
82
82
|
out = _user_content_with_images(prompt)
|
|
83
83
|
assert isinstance(out, list), f"not attached: {prompt!r}"
|
|
84
84
|
assert any(b.get("type") == "image_url" for b in out), prompt
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_detect_image_paths_shared_helper(tmp_path):
|
|
88
|
+
from drydock.providers import detect_image_paths
|
|
89
|
+
img = _write_png(tmp_path / "shot.png")
|
|
90
|
+
# plain, quoted, backticked, and trailing-punctuation forms all resolve
|
|
91
|
+
assert detect_image_paths(f"look at {img}") == [img]
|
|
92
|
+
assert detect_image_paths(f'see "{img}".') == [img]
|
|
93
|
+
assert detect_image_paths(f"debug `{img}`,") == [img]
|
|
94
|
+
# nonexistent path or text-only → nothing attached
|
|
95
|
+
assert detect_image_paths(f"{tmp_path}/nope.png") == []
|
|
96
|
+
assert detect_image_paths("no images here, just prose") == []
|
|
97
|
+
# de-dupes repeats
|
|
98
|
+
assert detect_image_paths(f"{img} and again {img}") == [img]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_vision_in_system_prompt():
|
|
102
|
+
from drydock.tuning import system_prompt_for_model
|
|
103
|
+
assert "VISION" in system_prompt_for_model("gemma4")
|
|
104
|
+
assert "VISION" in system_prompt_for_model("some-other-model")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|