drydock-cli 3.0.82__tar.gz → 3.0.84__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 (103) hide show
  1. {drydock_cli-3.0.82/drydock_cli.egg-info → drydock_cli-3.0.84}/PKG-INFO +41 -1
  2. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/README.md +40 -0
  3. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/advisor.py +51 -16
  4. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tui/app.py +12 -2
  5. {drydock_cli-3.0.82 → drydock_cli-3.0.84/drydock_cli.egg-info}/PKG-INFO +41 -1
  6. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/pyproject.toml +1 -1
  7. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_advisor.py +24 -0
  8. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/LICENSE +0 -0
  9. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/NOTICE +0 -0
  10. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/__init__.py +0 -0
  11. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/agent.py +0 -0
  13. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/bash_safety.py +0 -0
  14. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/__init__.py +0 -0
  15. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/rmf-categorize.md +0 -0
  16. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/rmf-control.md +0 -0
  17. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/rmf-poam.md +0 -0
  18. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/rmf-review.md +0 -0
  19. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/stig-assess.md +0 -0
  20. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/builtin_skills/stig-remediate.md +0 -0
  21. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/cci.py +0 -0
  22. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/cli.py +0 -0
  23. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/compaction.py +0 -0
  24. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/config.py +0 -0
  25. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/detect.py +0 -0
  26. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/extract.py +0 -0
  27. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/gittools.py +0 -0
  28. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/graphrag.py +0 -0
  29. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/guards.py +0 -0
  30. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/loop_detect.py +0 -0
  31. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/mcp.py +0 -0
  32. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/poam.py +0 -0
  33. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/providers.py +0 -0
  34. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/rmf.py +0 -0
  35. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/rmf_graph.py +0 -0
  36. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/skills.py +0 -0
  37. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/stig.py +0 -0
  38. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tool_registry.py +0 -0
  39. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tools/__init__.py +0 -0
  40. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tui/__init__.py +0 -0
  41. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tui/approval.py +0 -0
  42. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tui/messages.py +0 -0
  43. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tui/widgets.py +0 -0
  44. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/tuning.py +0 -0
  45. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock/web.py +0 -0
  46. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock_cli.egg-info/SOURCES.txt +0 -0
  47. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock_cli.egg-info/dependency_links.txt +0 -0
  48. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock_cli.egg-info/entry_points.txt +0 -0
  49. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock_cli.egg-info/requires.txt +0 -0
  50. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/drydock_cli.egg-info/top_level.txt +0 -0
  51. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/setup.cfg +0 -0
  52. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_approval.py +0 -0
  53. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_back_command.py +0 -0
  54. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_bash_output_bounding.py +0 -0
  55. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_bash_process_group.py +0 -0
  56. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_bash_safety.py +0 -0
  57. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_bash_timeout_network.py +0 -0
  58. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_cci.py +0 -0
  59. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_cli_agents.py +0 -0
  60. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_compact_command.py +0 -0
  61. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_compaction.py +0 -0
  62. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_config.py +0 -0
  63. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_config_migration.py +0 -0
  64. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_context_limit_config.py +0 -0
  65. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_detect.py +0 -0
  66. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_dispatch.py +0 -0
  67. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_e2e_connected.py +0 -0
  68. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_empty_response.py +0 -0
  69. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_extract.py +0 -0
  70. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_failure_loop.py +0 -0
  71. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_first_run_setup.py +0 -0
  72. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_gittools.py +0 -0
  73. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_graphrag.py +0 -0
  74. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_guards_and_tools.py +0 -0
  75. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_hallucinated_tools.py +0 -0
  76. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_leaked_tool_call.py +0 -0
  77. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_loop_detect.py +0 -0
  78. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_mcp.py +0 -0
  79. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_oneshot_unreachable.py +0 -0
  80. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_plan_autocontinue.py +0 -0
  81. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_poam.py +0 -0
  82. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_providers_unreachable.py +0 -0
  83. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_read_index.py +0 -0
  84. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_rmf.py +0 -0
  85. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_rmf_graph.py +0 -0
  86. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_rmf_stig_graph.py +0 -0
  87. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_runaway_repetition.py +0 -0
  88. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_server_probe.py +0 -0
  89. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_skills.py +0 -0
  90. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_stig.py +0 -0
  91. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_stop.py +0 -0
  92. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_streaming_newlines.py +0 -0
  93. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_subagent.py +0 -0
  94. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_system_prompt_help.py +0 -0
  95. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_todo.py +0 -0
  96. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_tool_arg_parsing.py +0 -0
  97. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_tools_undo.py +0 -0
  98. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_tui.py +0 -0
  99. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_tuning.py +0 -0
  100. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_viewimage.py +0 -0
  101. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_vision_input.py +0 -0
  102. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/tests/test_web_tools.py +0 -0
  103. {drydock_cli-3.0.82 → drydock_cli-3.0.84}/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.82
3
+ Version: 3.0.84
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
@@ -138,6 +138,46 @@ drydock-cli[pdf]` (pypdf). The index is a single JSON at
138
138
  Skills are markdown files in `~/.drydock/skills/` (personal) or
139
139
  `<project>/.drydock/skills/` (project); `/skills new` writes one for you.
140
140
 
141
+ ### Second-model advisor (a stronger model for a second opinion)
142
+
143
+ Drydock's primary model is a small local one. You can wire in a **second,
144
+ stronger model** — e.g. **Gemini** — to consult when the local model is stuck or
145
+ you want to sanity-check a design. It's just another OpenAI-compatible endpoint,
146
+ so there's **no extra dependency**; it's **opt-in** and off until you configure
147
+ it (the only call is the one you point it at — consistent with the no-phone-home
148
+ stance).
149
+
150
+ **Configure it** (persists to `~/.drydock/config.toml`):
151
+ ```
152
+ /advisor url http://<other-box>:4000/v1 # any OpenAI-compatible endpoint
153
+ /advisor model gemini-2.5-pro
154
+ /advisor key <api-key> # if the endpoint needs one
155
+ /advisor test # ping it: reachable? which model? latency?
156
+ /advisor # show current config (key masked)
157
+ ```
158
+
159
+ **Use it** two ways:
160
+ - **You:** `/ask <question>` — consults the advisor and shows its answer.
161
+ - **The agent:** it can call the read-only `Consult` tool on its own when it hits
162
+ something hard, passing the relevant code/error as context. You act on the advice.
163
+
164
+ **Pointing it at Gemini** — two options:
165
+ 1. **Gemini's official OpenAI-compatible endpoint** (if this box has internet):
166
+ ```
167
+ /advisor url https://generativelanguage.googleapis.com/v1beta/openai
168
+ /advisor model gemini-2.5-pro
169
+ /advisor key <your-gemini-api-key>
170
+ ```
171
+ 2. **A proxy on another box** (e.g. where your key lives). LiteLLM is one line:
172
+ ```bash
173
+ pip install 'litellm[proxy]'; export GEMINI_API_KEY=...
174
+ litellm --model gemini/gemini-2.5-pro --host 0.0.0.0 --port 4000
175
+ ```
176
+ then `/advisor url http://<that-box-ip>:4000/v1`.
177
+
178
+ Any OpenAI-compatible model works here (another local server, a hosted model,
179
+ etc.) — Gemini is just the common case.
180
+
141
181
  ### RMF automation (NIST SP 800-53)
142
182
 
143
183
  For Risk Management Framework work, Drydock can ingest the NIST SP 800-53 Rev 5
@@ -114,6 +114,46 @@ drydock-cli[pdf]` (pypdf). The index is a single JSON at
114
114
  Skills are markdown files in `~/.drydock/skills/` (personal) or
115
115
  `<project>/.drydock/skills/` (project); `/skills new` writes one for you.
116
116
 
117
+ ### Second-model advisor (a stronger model for a second opinion)
118
+
119
+ Drydock's primary model is a small local one. You can wire in a **second,
120
+ stronger model** — e.g. **Gemini** — to consult when the local model is stuck or
121
+ you want to sanity-check a design. It's just another OpenAI-compatible endpoint,
122
+ so there's **no extra dependency**; it's **opt-in** and off until you configure
123
+ it (the only call is the one you point it at — consistent with the no-phone-home
124
+ stance).
125
+
126
+ **Configure it** (persists to `~/.drydock/config.toml`):
127
+ ```
128
+ /advisor url http://<other-box>:4000/v1 # any OpenAI-compatible endpoint
129
+ /advisor model gemini-2.5-pro
130
+ /advisor key <api-key> # if the endpoint needs one
131
+ /advisor test # ping it: reachable? which model? latency?
132
+ /advisor # show current config (key masked)
133
+ ```
134
+
135
+ **Use it** two ways:
136
+ - **You:** `/ask <question>` — consults the advisor and shows its answer.
137
+ - **The agent:** it can call the read-only `Consult` tool on its own when it hits
138
+ something hard, passing the relevant code/error as context. You act on the advice.
139
+
140
+ **Pointing it at Gemini** — two options:
141
+ 1. **Gemini's official OpenAI-compatible endpoint** (if this box has internet):
142
+ ```
143
+ /advisor url https://generativelanguage.googleapis.com/v1beta/openai
144
+ /advisor model gemini-2.5-pro
145
+ /advisor key <your-gemini-api-key>
146
+ ```
147
+ 2. **A proxy on another box** (e.g. where your key lives). LiteLLM is one line:
148
+ ```bash
149
+ pip install 'litellm[proxy]'; export GEMINI_API_KEY=...
150
+ litellm --model gemini/gemini-2.5-pro --host 0.0.0.0 --port 4000
151
+ ```
152
+ then `/advisor url http://<that-box-ip>:4000/v1`.
153
+
154
+ Any OpenAI-compatible model works here (another local server, a hosted model,
155
+ etc.) — Gemini is just the common case.
156
+
117
157
  ### RMF automation (NIST SP 800-53)
118
158
 
119
159
  For Risk Management Framework work, Drydock can ingest the NIST SP 800-53 Rev 5
@@ -41,15 +41,10 @@ def not_configured_message() -> str:
41
41
  )
42
42
 
43
43
 
44
- def consult(question: str, config: dict, *, context: str = "", timeout: float = 120.0) -> str:
45
- """Ask the configured advisor model and return its answer (or a clean error).
46
- Never raises a failed consult must not crash the turn."""
47
- question = (question or "").strip()
48
- if not question:
49
- return "Error: nothing to ask the advisor."
50
- if not is_configured(config):
51
- return not_configured_message()
52
-
44
+ def _call(question: str, config: dict, *, context: str = "",
45
+ timeout: float = 120.0, max_tokens: int = 2048) -> str:
46
+ """Make one advisor call. RAISES on transport/model error (callers decide how
47
+ to surface it)."""
53
48
  import httpx
54
49
  from openai import OpenAI
55
50
 
@@ -61,13 +56,53 @@ def consult(question: str, config: dict, *, context: str = "", timeout: float =
61
56
  messages.append({"role": "user",
62
57
  "content": f"Context from the current task:\n{context.strip()}"})
63
58
  messages.append({"role": "user", "content": question})
64
- kwargs = {"model": model, "messages": messages, "temperature": 0.3, "max_tokens": 2048}
59
+ kwargs = {"model": model, "messages": messages, "temperature": 0.3, "max_tokens": max_tokens}
60
+ client = OpenAI(api_key=key, base_url=base, max_retries=1,
61
+ timeout=httpx.Timeout(timeout, connect=10.0))
62
+ resp = client.chat.completions.create(**kwargs)
63
+ return (resp.choices[0].message.content or "").strip()
64
+
65
+
66
+ def consult(question: str, config: dict, *, context: str = "", timeout: float = 120.0) -> str:
67
+ """Ask the configured advisor model and return its answer (or a clean error).
68
+ Never raises — a failed consult must not crash the turn."""
69
+ question = (question or "").strip()
70
+ if not question:
71
+ return "Error: nothing to ask the advisor."
72
+ if not is_configured(config):
73
+ return not_configured_message()
65
74
  try:
66
- client = OpenAI(api_key=key, base_url=base, max_retries=1,
67
- timeout=httpx.Timeout(timeout, connect=10.0))
68
- resp = client.chat.completions.create(**kwargs)
69
- answer = (resp.choices[0].message.content or "").strip()
70
- return answer or "(the advisor model returned an empty response)"
75
+ return _call(question, config, context=context, timeout=timeout) \
76
+ or "(the advisor model returned an empty response)"
71
77
  except Exception as e: # noqa: BLE001 — never crash the caller
72
- return (f"Could not reach the advisor model ({model}) at {base}: {e}\n"
78
+ return (f"Could not reach the advisor model ({config.get('advisor_model')}) at "
79
+ f"{config.get('advisor_base_url')}: {e}\n"
73
80
  "Check /advisor settings and that the endpoint is up.")
81
+
82
+
83
+ def test_connection(config: dict) -> str:
84
+ """Ping the advisor endpoint with a trivial prompt and report reachability +
85
+ latency — one-shot setup confirmation for `/advisor test`."""
86
+ import time
87
+
88
+ if not is_configured(config):
89
+ return not_configured_message()
90
+ model = config.get("advisor_model")
91
+ base = config.get("advisor_base_url")
92
+ t0 = time.monotonic()
93
+ try:
94
+ # Generous read timeout: a slow/cold LOCAL model can take a while to
95
+ # return even a few tokens. A truly-down endpoint fails fast on the 10s
96
+ # connect timeout, so this only waits on a reachable-but-slow server.
97
+ reply = _call("Reply with exactly: OK", config, timeout=90.0, max_tokens=8)
98
+ except Exception as e: # noqa: BLE001
99
+ emsg = str(e).lower()
100
+ if "timeout" in emsg or "timed out" in emsg:
101
+ return (f"✗ advisor timed out (>90s) — {model} at {base} is REACHABLE but slow "
102
+ "to respond (a cold or loaded local model?). `/ask` allows much longer, "
103
+ "so it may still work; or give the server a moment and retry.")
104
+ return (f"✗ advisor unreachable — {model} at {base}\n {e}\n"
105
+ " Check the endpoint is up, the URL ends in /v1, and the key/model are right.")
106
+ dt = time.monotonic() - t0
107
+ return (f"✓ advisor reachable — {model} at {base} responded in {dt:.1f}s"
108
+ + (f' (said: "{reply[:40]}").' if reply else "."))
@@ -864,17 +864,27 @@ class DrydockApp(App):
864
864
  f" endpoint: {u}\n"
865
865
  f" api key: {has_key}\n"
866
866
  "Set up: /advisor url <base_url/v1> · /advisor model <name> · "
867
- "/advisor key <api_key>\n"
867
+ "/advisor key <api_key> · /advisor test\n"
868
868
  "Then: /ask <question> (you) or the agent calls the Consult tool."
869
869
  )
870
870
  return
871
871
  parts = arg.split(maxsplit=1)
872
872
  sub = parts[0].lower()
873
873
  val = parts[1].strip() if len(parts) > 1 else ""
874
+ if sub == "test":
875
+ from drydock import advisor
876
+ if not advisor.is_configured(self.config):
877
+ self._info(advisor.not_configured_message())
878
+ return
879
+ self._info(f"Testing the advisor endpoint ({self.config.get('advisor_base_url')})…")
880
+ self.run_worker(lambda: self.call_from_thread(
881
+ self._info, advisor.test_connection(self.config)), thread=True)
882
+ return
874
883
  keymap = {"url": "advisor_base_url", "endpoint": "advisor_base_url",
875
884
  "model": "advisor_model", "key": "advisor_api_key", "api_key": "advisor_api_key"}
876
885
  if sub not in keymap or not val:
877
- self._mount(ErrorMessage("usage: /advisor [url <base_url> | model <name> | key <api_key>]"))
886
+ self._mount(ErrorMessage(
887
+ "usage: /advisor [url <base_url> | model <name> | key <api_key> | test]"))
878
888
  return
879
889
  self.config[keymap[sub]] = val
880
890
  self._persist_config()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.82
3
+ Version: 3.0.84
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
@@ -138,6 +138,46 @@ drydock-cli[pdf]` (pypdf). The index is a single JSON at
138
138
  Skills are markdown files in `~/.drydock/skills/` (personal) or
139
139
  `<project>/.drydock/skills/` (project); `/skills new` writes one for you.
140
140
 
141
+ ### Second-model advisor (a stronger model for a second opinion)
142
+
143
+ Drydock's primary model is a small local one. You can wire in a **second,
144
+ stronger model** — e.g. **Gemini** — to consult when the local model is stuck or
145
+ you want to sanity-check a design. It's just another OpenAI-compatible endpoint,
146
+ so there's **no extra dependency**; it's **opt-in** and off until you configure
147
+ it (the only call is the one you point it at — consistent with the no-phone-home
148
+ stance).
149
+
150
+ **Configure it** (persists to `~/.drydock/config.toml`):
151
+ ```
152
+ /advisor url http://<other-box>:4000/v1 # any OpenAI-compatible endpoint
153
+ /advisor model gemini-2.5-pro
154
+ /advisor key <api-key> # if the endpoint needs one
155
+ /advisor test # ping it: reachable? which model? latency?
156
+ /advisor # show current config (key masked)
157
+ ```
158
+
159
+ **Use it** two ways:
160
+ - **You:** `/ask <question>` — consults the advisor and shows its answer.
161
+ - **The agent:** it can call the read-only `Consult` tool on its own when it hits
162
+ something hard, passing the relevant code/error as context. You act on the advice.
163
+
164
+ **Pointing it at Gemini** — two options:
165
+ 1. **Gemini's official OpenAI-compatible endpoint** (if this box has internet):
166
+ ```
167
+ /advisor url https://generativelanguage.googleapis.com/v1beta/openai
168
+ /advisor model gemini-2.5-pro
169
+ /advisor key <your-gemini-api-key>
170
+ ```
171
+ 2. **A proxy on another box** (e.g. where your key lives). LiteLLM is one line:
172
+ ```bash
173
+ pip install 'litellm[proxy]'; export GEMINI_API_KEY=...
174
+ litellm --model gemini/gemini-2.5-pro --host 0.0.0.0 --port 4000
175
+ ```
176
+ then `/advisor url http://<that-box-ip>:4000/v1`.
177
+
178
+ Any OpenAI-compatible model works here (another local server, a hosted model,
179
+ etc.) — Gemini is just the common case.
180
+
141
181
  ### RMF automation (NIST SP 800-53)
142
182
 
143
183
  For Risk Management Framework work, Drydock can ingest the NIST SP 800-53 Rev 5
@@ -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.82"
10
+ version = "3.0.84"
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"
@@ -64,3 +64,27 @@ def test_consult_tool_unconfigured():
64
64
 
65
65
  def test_consult_tool_is_read_only():
66
66
  assert reg.get("Consult").read_only is True
67
+
68
+
69
+ def test_test_connection_success(monkeypatch):
70
+ monkeypatch.setattr(advisor, "_call", lambda *a, **k: "OK")
71
+ out = advisor.test_connection({"advisor_base_url": "http://b/v1", "advisor_model": "m"})
72
+ assert out.startswith("✓") and "m" in out and "responded" in out
73
+
74
+
75
+ def test_test_connection_failure(monkeypatch):
76
+ def boom(*a, **k): raise OSError("connection refused")
77
+ monkeypatch.setattr(advisor, "_call", boom)
78
+ out = advisor.test_connection({"advisor_base_url": "http://b/v1", "advisor_model": "m"})
79
+ assert out.startswith("✗") and "unreachable" in out and "connection refused" in out
80
+
81
+
82
+ def test_test_connection_unconfigured():
83
+ assert "No advisor" in advisor.test_connection({})
84
+
85
+
86
+ def test_test_connection_timeout_says_reachable_but_slow(monkeypatch):
87
+ def slow(*a, **k): raise TimeoutError("Request timed out.")
88
+ monkeypatch.setattr(advisor, "_call", slow)
89
+ out = advisor.test_connection({"advisor_base_url": "http://b/v1", "advisor_model": "m"})
90
+ assert out.startswith("✗") and "REACHABLE but slow" in out and "/ask" in out
File without changes
File without changes
File without changes