drydock-cli 3.0.82__tar.gz → 3.0.83__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.82/drydock_cli.egg-info → drydock_cli-3.0.83}/PKG-INFO +41 -1
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/README.md +40 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/advisor.py +43 -16
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tui/app.py +12 -2
- {drydock_cli-3.0.82 → drydock_cli-3.0.83/drydock_cli.egg-info}/PKG-INFO +41 -1
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/pyproject.toml +1 -1
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_advisor.py +17 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/LICENSE +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/NOTICE +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/agent.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/cci.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/cli.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/config.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/detect.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/extract.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/guards.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/poam.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/providers.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/skills.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/stig.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock/web.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/setup.cfg +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_config.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.82 → drydock_cli-3.0.83}/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.83
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,45 @@ 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":
|
|
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
|
-
|
|
67
|
-
|
|
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 ({
|
|
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
|
+
reply = _call("Reply with exactly: OK", config, timeout=30.0, max_tokens=8)
|
|
95
|
+
except Exception as e: # noqa: BLE001
|
|
96
|
+
return (f"✗ advisor unreachable — {model} at {base}\n {e}\n"
|
|
97
|
+
" Check the endpoint is up, the URL ends in /v1, and the key/model are right.")
|
|
98
|
+
dt = time.monotonic() - t0
|
|
99
|
+
return (f"✓ advisor reachable — {model} at {base} responded in {dt:.1f}s"
|
|
100
|
+
+ (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
|
|
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(
|
|
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.
|
|
3
|
+
Version: 3.0.83
|
|
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.
|
|
10
|
+
version = "3.0.83"
|
|
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,20 @@ 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({})
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|