drydock-cli 3.0.75__tar.gz → 3.0.77__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.75/drydock_cli.egg-info → drydock_cli-3.0.77}/PKG-INFO +5 -1
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/README.md +4 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/providers.py +48 -9
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/app.py +36 -4
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tuning.py +18 -6
- {drydock_cli-3.0.75 → drydock_cli-3.0.77/drydock_cli.egg-info}/PKG-INFO +5 -1
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/pyproject.toml +1 -1
- drydock_cli-3.0.77/tests/test_server_probe.py +39 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_vision_input.py +20 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/LICENSE +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/NOTICE +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/agent.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/cci.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/cli.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/config.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/detect.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/extract.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/guards.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/skills.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/stig.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/web.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/setup.cfg +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_config.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.75 → drydock_cli-3.0.77}/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.77
|
|
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,10 @@ 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* — 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).
|
|
62
66
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
63
67
|
(structured + truncated; commit is local and reversible).
|
|
64
68
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -35,6 +35,10 @@ 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* — 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).
|
|
38
42
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
39
43
|
(structured + truncated; commit is local and reversible).
|
|
40
44
|
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
@@ -56,6 +56,36 @@ PROVIDERS: dict[str, dict] = {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def probe_server_context(base_url: str, *, timeout: float = 4.0) -> int | None:
|
|
60
|
+
"""Best-effort: ask the model server its REAL context window (tokens).
|
|
61
|
+
llama.cpp exposes it on `/props` (n_ctx); vLLM puts `max_model_len` on
|
|
62
|
+
`/v1/models`. Returns None if unknown/unreachable — never raises. This is the
|
|
63
|
+
definitive diagnostic for a "stuck at N tokens" cap that isn't drydock's
|
|
64
|
+
config (it's the server's own `-c` / `--ctx-size` / `max_model_len`)."""
|
|
65
|
+
import json
|
|
66
|
+
import urllib.request
|
|
67
|
+
|
|
68
|
+
root = base_url.rstrip("/")
|
|
69
|
+
base_no_v1 = root[:-3].rstrip("/") if root.endswith("/v1") else root
|
|
70
|
+
|
|
71
|
+
def _n_ctx(d):
|
|
72
|
+
return (d.get("default_generation_settings") or {}).get("n_ctx") or d.get("n_ctx")
|
|
73
|
+
|
|
74
|
+
def _max_model_len(d):
|
|
75
|
+
return next((m.get("max_model_len") for m in d.get("data", []) if m.get("max_model_len")), None)
|
|
76
|
+
|
|
77
|
+
for url, pick in ((base_no_v1 + "/props", _n_ctx), (root + "/models", _max_model_len)):
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 (user's own server)
|
|
80
|
+
data = json.loads(r.read().decode("utf-8", "ignore"))
|
|
81
|
+
val = pick(data)
|
|
82
|
+
if isinstance(val, int) and val > 0:
|
|
83
|
+
return val
|
|
84
|
+
except Exception: # noqa: BLE001 — best-effort probe
|
|
85
|
+
continue
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
59
89
|
class LLMUnreachable(RuntimeError):
|
|
60
90
|
"""The configured LLM endpoint could not be reached. Carries a
|
|
61
91
|
user-facing message with remediation steps (shown verbatim in the TUI)."""
|
|
@@ -220,16 +250,11 @@ _IMAGE_MIME = {
|
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
|
|
223
|
-
def
|
|
224
|
-
"""
|
|
225
|
-
|
|
226
|
-
--mmproj-enabled server). Text-only prompts pass through unchanged as a plain
|
|
227
|
-
string, so display / loop-detection / compaction / token-counting (which all
|
|
228
|
-
assume string content) are untouched — the multimodal list is built ONLY here,
|
|
229
|
-
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')."""
|
|
230
256
|
if not isinstance(content, str) or "." not in content:
|
|
231
|
-
return
|
|
232
|
-
import base64
|
|
257
|
+
return []
|
|
233
258
|
import os
|
|
234
259
|
import re
|
|
235
260
|
|
|
@@ -251,8 +276,22 @@ def _user_content_with_images(content):
|
|
|
251
276
|
p = os.path.expanduser(raw)
|
|
252
277
|
if os.path.isfile(p) and p not in seen:
|
|
253
278
|
seen.append(p)
|
|
279
|
+
return seen
|
|
280
|
+
|
|
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)
|
|
254
290
|
if not seen:
|
|
255
291
|
return content
|
|
292
|
+
import base64
|
|
293
|
+
import os
|
|
294
|
+
|
|
256
295
|
blocks: list[dict] = [{"type": "text", "text": content}]
|
|
257
296
|
for p in seen:
|
|
258
297
|
mime = _IMAGE_MIME.get(os.path.splitext(p)[1].lower(), "image/png")
|
|
@@ -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()
|
|
@@ -758,12 +766,12 @@ class DrydockApp(App):
|
|
|
758
766
|
arg = (arg or "").strip()
|
|
759
767
|
if not arg:
|
|
760
768
|
self._info(
|
|
761
|
-
f"context_limit: {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
|
|
769
|
+
f"context_limit (drydock): {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
|
|
762
770
|
"Source order: built-in default 65536 < ~/.drydock/config.toml < --context-limit.\n"
|
|
763
|
-
"
|
|
764
|
-
"
|
|
765
|
-
" /context <tokens> e.g. /context 65536 (saved to config.toml)"
|
|
771
|
+
"Probing your model server for its REAL context window…\n"
|
|
772
|
+
" To change drydock's budget: /context <tokens> (saved to config.toml)"
|
|
766
773
|
)
|
|
774
|
+
self.run_worker(lambda: self._probe_server_context(limit), thread=True)
|
|
767
775
|
return
|
|
768
776
|
try:
|
|
769
777
|
n = int(arg.replace(",", "").replace("k", "000").replace("K", "000"))
|
|
@@ -781,6 +789,30 @@ class DrydockApp(App):
|
|
|
781
789
|
"least this, or the server will still cap you below it."
|
|
782
790
|
)
|
|
783
791
|
|
|
792
|
+
def _probe_server_context(self, limit: int) -> None:
|
|
793
|
+
"""Worker: ask the model server its real context window and report whether
|
|
794
|
+
IT (not drydock's config) is the thing capping you — the definitive answer
|
|
795
|
+
to 'stuck at N tokens'."""
|
|
796
|
+
from drydock import providers
|
|
797
|
+
|
|
798
|
+
base_url = self.config.get("base_url") or providers.PROVIDERS.get(
|
|
799
|
+
self.config.get("provider") or "vllm", {}).get("base_url", "http://localhost:8000/v1")
|
|
800
|
+
n_ctx = providers.probe_server_context(base_url)
|
|
801
|
+
if n_ctx is None:
|
|
802
|
+
msg = (f"Model server ({base_url}) didn't report its context size "
|
|
803
|
+
"(not llama.cpp /props or vLLM max_model_len, or unreachable). "
|
|
804
|
+
f"Your effective cap is the smaller of drydock's {limit:,} and the "
|
|
805
|
+
"server's own -c/--ctx-size.")
|
|
806
|
+
elif n_ctx < limit:
|
|
807
|
+
msg = (f"⚠ Model server reports n_ctx = {n_ctx:,} tokens — SMALLER than "
|
|
808
|
+
f"drydock's {limit:,}. The SERVER is your real cap (you'll stick near "
|
|
809
|
+
f"{n_ctx // 1024}k). Restart it with a larger -c/--ctx-size/max_model_len, "
|
|
810
|
+
"or lower /context to match.")
|
|
811
|
+
else:
|
|
812
|
+
msg = (f"✓ Model server reports n_ctx = {n_ctx:,} tokens (≥ drydock's {limit:,}), "
|
|
813
|
+
"so drydock's budget is the effective limit — no server-side cap.")
|
|
814
|
+
self.call_from_thread(self._info, msg)
|
|
815
|
+
|
|
784
816
|
def _cmd_compact(self) -> None:
|
|
785
817
|
"""Manually compact the conversation to reclaim context NOW, without
|
|
786
818
|
waiting for the automatic 60%-of-window threshold (agent.maybe_compact).
|
|
@@ -90,7 +90,11 @@ _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; describe it, read text from it, or debug "
|
|
97
|
+
"with it — never claim you cannot view images."
|
|
94
98
|
)
|
|
95
99
|
|
|
96
100
|
# Short, imperative prompt. Small local models do better with "act now" than
|
|
@@ -112,7 +116,11 @@ _GEMMA_SYSTEM_PROMPT = (
|
|
|
112
116
|
"(one task per line, '[ ]'/'[~]'/'[x]'), then IMMEDIATELY start doing the "
|
|
113
117
|
"steps — do NOT stop to ask whether to proceed. Call `todo` again to update "
|
|
114
118
|
"it as you finish each step, and only stop when every step is done. Do not "
|
|
115
|
-
"write a TODO.md file for this
|
|
119
|
+
"write a TODO.md file for this.\n"
|
|
120
|
+
"You have VISION: when the user's message references an image file path "
|
|
121
|
+
"(.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."
|
|
116
124
|
)
|
|
117
125
|
|
|
118
126
|
|
|
@@ -221,11 +229,15 @@ _DRYDOCK_COMMANDS_HELP = (
|
|
|
221
229
|
"- Custom skills (reusable `/<name>` prompts): `/skills new <name> <prompt "
|
|
222
230
|
"text>` creates one (use $ARGS in the prompt for trailing input); `/skills` "
|
|
223
231
|
"lists them; then they run it as `/<name>`.\n"
|
|
232
|
+
"- Images / vision: the user just references an image file path in their "
|
|
233
|
+
"message (a .png/.jpg screenshot, mockup, or diagram on disk) and it is "
|
|
234
|
+
"attached for you to SEE — no command needed.\n"
|
|
224
235
|
"- Other: `/model` (model/endpoint), `/cwd`, `/undo` (revert last write), "
|
|
225
|
-
"`/back` (rewind a turn), `/compact` (shrink context), `/
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
236
|
+
"`/back` (rewind a turn), `/compact` (shrink context), `/context` (view/set "
|
|
237
|
+
"the context-window budget), `/loop <n> <prompt>` (repeat a prompt), `/mcp` "
|
|
238
|
+
"(connected MCP servers), `/rmf` & `/stig` (NIST 800-53 / DISA STIG), "
|
|
239
|
+
"`/status`, `/clear`, `/help`, `/quit`. Internet + git are tools you call "
|
|
240
|
+
"yourself (WebSearch/WebFetch, GitStatus/GitDiff/GitLog/GitCommit)."
|
|
229
241
|
)
|
|
230
242
|
|
|
231
243
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.77
|
|
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,10 @@ 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* — 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).
|
|
62
66
|
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
63
67
|
(structured + truncated; commit is local and reversible).
|
|
64
68
|
- **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.77"
|
|
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,39 @@
|
|
|
1
|
+
"""Server context-window probe — the definitive 'what's capping me' diagnostic."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from drydock import providers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Resp:
|
|
10
|
+
def __init__(self, body): self._b = json.dumps(body).encode()
|
|
11
|
+
def read(self): return self._b
|
|
12
|
+
def __enter__(self): return self
|
|
13
|
+
def __exit__(self, *a): return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_probe_llamacpp_props_n_ctx(monkeypatch):
|
|
17
|
+
import urllib.request
|
|
18
|
+
def fake(url, timeout=0):
|
|
19
|
+
assert url.endswith("/props") # llama.cpp endpoint, /v1 stripped
|
|
20
|
+
return _Resp({"default_generation_settings": {"n_ctx": 32768}})
|
|
21
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake)
|
|
22
|
+
assert providers.probe_server_context("http://localhost:8000/v1") == 32768
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_probe_vllm_max_model_len(monkeypatch):
|
|
26
|
+
import urllib.request
|
|
27
|
+
def fake(url, timeout=0):
|
|
28
|
+
if url.endswith("/props"):
|
|
29
|
+
raise OSError("no props on vLLM")
|
|
30
|
+
return _Resp({"data": [{"id": "m", "max_model_len": 65536}]})
|
|
31
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake)
|
|
32
|
+
assert providers.probe_server_context("http://localhost:8000/v1") == 65536
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_probe_unreachable_is_none(monkeypatch):
|
|
36
|
+
import urllib.request
|
|
37
|
+
def boom(url, timeout=0): raise OSError("offline")
|
|
38
|
+
monkeypatch.setattr(urllib.request, "urlopen", boom)
|
|
39
|
+
assert providers.probe_server_context("http://localhost:9999/v1") is None
|
|
@@ -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
|