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.
Files changed (98) hide show
  1. {drydock_cli-3.0.75/drydock_cli.egg-info → drydock_cli-3.0.77}/PKG-INFO +5 -1
  2. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/README.md +4 -0
  3. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/providers.py +48 -9
  4. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/app.py +36 -4
  5. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tuning.py +18 -6
  6. {drydock_cli-3.0.75 → drydock_cli-3.0.77/drydock_cli.egg-info}/PKG-INFO +5 -1
  7. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/SOURCES.txt +1 -0
  8. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/pyproject.toml +1 -1
  9. drydock_cli-3.0.77/tests/test_server_probe.py +39 -0
  10. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_vision_input.py +20 -0
  11. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/LICENSE +0 -0
  12. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/NOTICE +0 -0
  13. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/__init__.py +0 -0
  14. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/__main__.py +0 -0
  15. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/agent.py +0 -0
  16. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/bash_safety.py +0 -0
  17. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/__init__.py +0 -0
  18. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-categorize.md +0 -0
  19. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-control.md +0 -0
  20. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-poam.md +0 -0
  21. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/rmf-review.md +0 -0
  22. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/stig-assess.md +0 -0
  23. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/builtin_skills/stig-remediate.md +0 -0
  24. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/cci.py +0 -0
  25. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/cli.py +0 -0
  26. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/compaction.py +0 -0
  27. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/config.py +0 -0
  28. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/detect.py +0 -0
  29. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/extract.py +0 -0
  30. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/gittools.py +0 -0
  31. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/graphrag.py +0 -0
  32. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/guards.py +0 -0
  33. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/loop_detect.py +0 -0
  34. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/mcp.py +0 -0
  35. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/rmf.py +0 -0
  36. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/rmf_graph.py +0 -0
  37. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/stig.py +0 -0
  39. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tool_registry.py +0 -0
  40. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tools/__init__.py +0 -0
  41. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/__init__.py +0 -0
  42. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/approval.py +0 -0
  43. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/messages.py +0 -0
  44. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/tui/widgets.py +0 -0
  45. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock/web.py +0 -0
  46. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/dependency_links.txt +0 -0
  47. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/entry_points.txt +0 -0
  48. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/requires.txt +0 -0
  49. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/drydock_cli.egg-info/top_level.txt +0 -0
  50. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/setup.cfg +0 -0
  51. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_approval.py +0 -0
  52. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_back_command.py +0 -0
  53. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_output_bounding.py +0 -0
  54. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_process_group.py +0 -0
  55. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_safety.py +0 -0
  56. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_bash_timeout_network.py +0 -0
  57. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_cci.py +0 -0
  58. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_cli_agents.py +0 -0
  59. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_compact_command.py +0 -0
  60. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_compaction.py +0 -0
  61. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_config.py +0 -0
  62. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_config_migration.py +0 -0
  63. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_context_limit_config.py +0 -0
  64. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_detect.py +0 -0
  65. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_dispatch.py +0 -0
  66. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_e2e_connected.py +0 -0
  67. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_empty_response.py +0 -0
  68. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_extract.py +0 -0
  69. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_failure_loop.py +0 -0
  70. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_first_run_setup.py +0 -0
  71. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_gittools.py +0 -0
  72. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_graphrag.py +0 -0
  73. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_guards_and_tools.py +0 -0
  74. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_hallucinated_tools.py +0 -0
  75. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_leaked_tool_call.py +0 -0
  76. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_loop_detect.py +0 -0
  77. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_mcp.py +0 -0
  78. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_oneshot_unreachable.py +0 -0
  79. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_plan_autocontinue.py +0 -0
  80. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_providers_unreachable.py +0 -0
  81. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_read_index.py +0 -0
  82. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_rmf.py +0 -0
  83. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_rmf_graph.py +0 -0
  84. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_rmf_stig_graph.py +0 -0
  85. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_runaway_repetition.py +0 -0
  86. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_skills.py +0 -0
  87. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_stig.py +0 -0
  88. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_stop.py +0 -0
  89. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_streaming_newlines.py +0 -0
  90. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_subagent.py +0 -0
  91. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_system_prompt_help.py +0 -0
  92. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_todo.py +0 -0
  93. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tool_arg_parsing.py +0 -0
  94. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tools_undo.py +0 -0
  95. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tui.py +0 -0
  96. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_tuning.py +0 -0
  97. {drydock_cli-3.0.75 → drydock_cli-3.0.77}/tests/test_web_tools.py +0 -0
  98. {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.75
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 _user_content_with_images(content):
224
- """Vision support: if the user's text references image file paths that exist
225
- on disk, attach them as OpenAI multimodal image_url blocks (works with any
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 content
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
- "If you're stuck below this, your MODEL SERVER's context is smaller — raise its\n"
764
- "`-c`/`--ctx-size`/`max_model_len` to match. To change drydock's budget:\n"
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), `/loop <n> <prompt>` "
226
- "(repeat a prompt), `/mcp` (connected MCP servers), `/status`, `/clear`, "
227
- "`/help`, `/quit`. Internet + git are tools you call yourself "
228
- "(WebSearch/WebFetch, GitStatus/GitDiff/GitLog/GitCommit)."
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.75
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).
@@ -79,6 +79,7 @@ tests/test_rmf.py
79
79
  tests/test_rmf_graph.py
80
80
  tests/test_rmf_stig_graph.py
81
81
  tests/test_runaway_repetition.py
82
+ tests/test_server_probe.py
82
83
  tests/test_skills.py
83
84
  tests/test_stig.py
84
85
  tests/test_stop.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.75"
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