drydock-cli 3.0.81__tar.gz → 3.0.82__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.81/drydock_cli.egg-info → drydock_cli-3.0.82}/PKG-INFO +6 -1
  2. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/README.md +5 -0
  3. drydock_cli-3.0.82/drydock/advisor.py +73 -0
  4. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/config.py +7 -0
  5. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tools/__init__.py +31 -3
  6. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tui/app.py +56 -0
  7. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tui/widgets.py +2 -1
  8. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tuning.py +3 -0
  9. {drydock_cli-3.0.81 → drydock_cli-3.0.82/drydock_cli.egg-info}/PKG-INFO +6 -1
  10. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock_cli.egg-info/SOURCES.txt +2 -0
  11. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/pyproject.toml +1 -1
  12. drydock_cli-3.0.82/tests/test_advisor.py +66 -0
  13. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/LICENSE +0 -0
  14. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/NOTICE +0 -0
  15. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/__init__.py +0 -0
  16. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/__main__.py +0 -0
  17. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/agent.py +0 -0
  18. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/bash_safety.py +0 -0
  19. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/__init__.py +0 -0
  20. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/rmf-categorize.md +0 -0
  21. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/rmf-control.md +0 -0
  22. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/rmf-poam.md +0 -0
  23. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/rmf-review.md +0 -0
  24. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/stig-assess.md +0 -0
  25. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/builtin_skills/stig-remediate.md +0 -0
  26. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/cci.py +0 -0
  27. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/cli.py +0 -0
  28. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/compaction.py +0 -0
  29. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/detect.py +0 -0
  30. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/extract.py +0 -0
  31. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/gittools.py +0 -0
  32. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/graphrag.py +0 -0
  33. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/guards.py +0 -0
  34. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/loop_detect.py +0 -0
  35. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/mcp.py +0 -0
  36. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/poam.py +0 -0
  37. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/providers.py +0 -0
  38. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/rmf.py +0 -0
  39. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/rmf_graph.py +0 -0
  40. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/skills.py +0 -0
  41. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/stig.py +0 -0
  42. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tool_registry.py +0 -0
  43. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tui/__init__.py +0 -0
  44. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tui/approval.py +0 -0
  45. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/tui/messages.py +0 -0
  46. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock/web.py +0 -0
  47. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock_cli.egg-info/dependency_links.txt +0 -0
  48. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock_cli.egg-info/entry_points.txt +0 -0
  49. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock_cli.egg-info/requires.txt +0 -0
  50. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/drydock_cli.egg-info/top_level.txt +0 -0
  51. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/setup.cfg +0 -0
  52. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_approval.py +0 -0
  53. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_back_command.py +0 -0
  54. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_bash_output_bounding.py +0 -0
  55. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_bash_process_group.py +0 -0
  56. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_bash_safety.py +0 -0
  57. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_bash_timeout_network.py +0 -0
  58. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_cci.py +0 -0
  59. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_cli_agents.py +0 -0
  60. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_compact_command.py +0 -0
  61. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_compaction.py +0 -0
  62. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_config.py +0 -0
  63. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_config_migration.py +0 -0
  64. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_context_limit_config.py +0 -0
  65. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_detect.py +0 -0
  66. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_dispatch.py +0 -0
  67. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_e2e_connected.py +0 -0
  68. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_empty_response.py +0 -0
  69. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_extract.py +0 -0
  70. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_failure_loop.py +0 -0
  71. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_first_run_setup.py +0 -0
  72. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_gittools.py +0 -0
  73. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_graphrag.py +0 -0
  74. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_guards_and_tools.py +0 -0
  75. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_hallucinated_tools.py +0 -0
  76. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_leaked_tool_call.py +0 -0
  77. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_loop_detect.py +0 -0
  78. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_mcp.py +0 -0
  79. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_oneshot_unreachable.py +0 -0
  80. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_plan_autocontinue.py +0 -0
  81. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_poam.py +0 -0
  82. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_providers_unreachable.py +0 -0
  83. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_read_index.py +0 -0
  84. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_rmf.py +0 -0
  85. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_rmf_graph.py +0 -0
  86. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_rmf_stig_graph.py +0 -0
  87. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_runaway_repetition.py +0 -0
  88. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_server_probe.py +0 -0
  89. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_skills.py +0 -0
  90. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_stig.py +0 -0
  91. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_stop.py +0 -0
  92. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_streaming_newlines.py +0 -0
  93. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_subagent.py +0 -0
  94. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_system_prompt_help.py +0 -0
  95. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_todo.py +0 -0
  96. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_tool_arg_parsing.py +0 -0
  97. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_tools_undo.py +0 -0
  98. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_tui.py +0 -0
  99. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_tuning.py +0 -0
  100. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_viewimage.py +0 -0
  101. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_vision_input.py +0 -0
  102. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/tests/test_web_tools.py +0 -0
  103. {drydock_cli-3.0.81 → drydock_cli-3.0.82}/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.81
3
+ Version: 3.0.82
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
@@ -72,6 +72,10 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
72
72
  read-only `Knowledge` tool.
73
73
  - **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
74
74
  `task` runs one — each in a fresh context, for focused investigation.
75
+ - **Second-model advisor** — point `/advisor` at a stronger model on any
76
+ OpenAI-compatible endpoint (e.g. Gemini, or a proxy on another box); the agent
77
+ calls the `Consult` tool for a second opinion when stuck, and you can `/ask
78
+ <question>` directly. Opt-in, user-configured — no extra dependency.
75
79
  - **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
76
80
  their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
77
81
  - **Skills** — reusable `/<name>` commands authored as markdown in
@@ -103,6 +107,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
103
107
  | `/model` · `/cwd` | Show/set model & endpoint · working directory |
104
108
  | `/undo` · `/back` | Revert the last write · rewind the last turn |
105
109
  | `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
110
+ | `/advisor` · `/ask <q>` | Set up a 2nd 'advisor' model (Gemini etc.) · consult it |
106
111
  | `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
107
112
 
108
113
  ### Knowledge base (GraphRAG) — ingesting your documents
@@ -48,6 +48,10 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
48
48
  read-only `Knowledge` tool.
49
49
  - **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
50
50
  `task` runs one — each in a fresh context, for focused investigation.
51
+ - **Second-model advisor** — point `/advisor` at a stronger model on any
52
+ OpenAI-compatible endpoint (e.g. Gemini, or a proxy on another box); the agent
53
+ calls the `Consult` tool for a second opinion when stuck, and you can `/ask
54
+ <question>` directly. Opt-in, user-configured — no extra dependency.
51
55
  - **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
52
56
  their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
53
57
  - **Skills** — reusable `/<name>` commands authored as markdown in
@@ -79,6 +83,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
79
83
  | `/model` · `/cwd` | Show/set model & endpoint · working directory |
80
84
  | `/undo` · `/back` | Revert the last write · rewind the last turn |
81
85
  | `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
86
+ | `/advisor` · `/ask <q>` | Set up a 2nd 'advisor' model (Gemini etc.) · consult it |
82
87
  | `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
83
88
 
84
89
  ### Knowledge base (GraphRAG) — ingesting your documents
@@ -0,0 +1,73 @@
1
+ """Second-model "advisor" — consult a stronger model for a second opinion.
2
+
3
+ Drydock's primary model is a small local one. Sometimes you want a heavier model
4
+ (e.g. Gemini) to sanity-check a design, unstick a hard bug, or review an
5
+ approach. The advisor is just ANOTHER OpenAI-compatible endpoint — Gemini exposes
6
+ one (`.../v1beta/openai/`), and any proxy on another box works too — so this
7
+ reuses the existing `openai` client with no new dependency.
8
+
9
+ It's opt-in and user-configured (empty by default): the only network call is the
10
+ one YOU point it at, consistent with drydock's no-phone-home stance. Exposed as
11
+ the `Consult` tool (the agent asks when stuck) and the `/ask` command (you ask).
12
+
13
+ All logic original to Drydock.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ _ADVISOR_SYSTEM = (
18
+ "You are a senior engineering advisor consulted by another AI coding agent "
19
+ "(Drydock) running a smaller local model. Give concise, direct, expert advice "
20
+ "— the specific fix, the design trade-off, or the likely root cause. Prefer "
21
+ "concrete steps and short code over prose. If the question is under-specified, "
22
+ "state the key assumption and answer anyway. You cannot run tools; the calling "
23
+ "agent will act on your answer."
24
+ )
25
+
26
+
27
+ def is_configured(config: dict) -> bool:
28
+ """True when an advisor endpoint + model are set."""
29
+ return bool((config.get("advisor_base_url") or "").strip()
30
+ and (config.get("advisor_model") or "").strip())
31
+
32
+
33
+ def not_configured_message() -> str:
34
+ return (
35
+ "No advisor (second) model is configured. Point it at any OpenAI-compatible "
36
+ "endpoint — e.g. Gemini's, or a proxy on another box:\n"
37
+ " /advisor url http://<other-box>:<port>/v1\n"
38
+ " /advisor model gemini-2.5-pro\n"
39
+ " /advisor key <api-key> (if the endpoint needs one)\n"
40
+ "Then use /ask <question> or let the agent call the Consult tool."
41
+ )
42
+
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
+
53
+ import httpx
54
+ from openai import OpenAI
55
+
56
+ base = config["advisor_base_url"].strip()
57
+ model = config["advisor_model"].strip()
58
+ key = (config.get("advisor_api_key") or "").strip() or "dummy"
59
+ messages: list[dict] = [{"role": "system", "content": _ADVISOR_SYSTEM}]
60
+ if context.strip():
61
+ messages.append({"role": "user",
62
+ "content": f"Context from the current task:\n{context.strip()}"})
63
+ messages.append({"role": "user", "content": question})
64
+ kwargs = {"model": model, "messages": messages, "temperature": 0.3, "max_tokens": 2048}
65
+ 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)"
71
+ except Exception as e: # noqa: BLE001 — never crash the caller
72
+ return (f"Could not reach the advisor model ({model}) at {base}: {e}\n"
73
+ "Check /advisor settings and that the endpoint is up.")
@@ -35,6 +35,13 @@ DEFAULTS: dict[str, object] = {
35
35
  # value. Default 65536 matches the bundled gemma4 server (-c 65536).
36
36
  "context_limit": 65536,
37
37
  "theme": "harbor",
38
+ # Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
39
+ # Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
40
+ # agent can call the `Consult` tool and you can use `/ask`; configure with
41
+ # `/advisor`. It's just another OpenAI-compatible endpoint (no extra deps).
42
+ "advisor_model": "",
43
+ "advisor_base_url": "",
44
+ "advisor_api_key": "",
38
45
  }
39
46
 
40
47
 
@@ -178,6 +178,24 @@ SCHEMAS = [
178
178
  "required": ["tasks"],
179
179
  },
180
180
  },
181
+ {
182
+ "name": "Consult",
183
+ "description": (
184
+ "Ask a SECOND, more powerful advisor model (if the user configured "
185
+ "one, e.g. Gemini) for a second opinion — use when you're stuck, want "
186
+ "to sanity-check a design/approach, or need an expert take on a tricky "
187
+ "bug. It only advises (no tools); YOU act on the answer. Pass the "
188
+ "relevant code/error in `context` so the advisor has what it needs."
189
+ ),
190
+ "input_schema": {
191
+ "type": "object",
192
+ "properties": {
193
+ "question": {"type": "string", "description": "The specific question to ask the advisor."},
194
+ "context": {"type": "string", "description": "Relevant code, error output, or background the advisor needs."},
195
+ },
196
+ "required": ["question"],
197
+ },
198
+ },
181
199
  {
182
200
  "name": "task",
183
201
  "description": (
@@ -1066,6 +1084,16 @@ _SUBAGENT_SYSTEM = (
1066
1084
  )
1067
1085
 
1068
1086
 
1087
+ def tool_consult(params: dict, config: dict) -> str:
1088
+ """Ask the configured second/advisor model (e.g. Gemini) for a second opinion."""
1089
+ from drydock import advisor
1090
+
1091
+ question = (params.get("question") or params.get("prompt") or "").strip()
1092
+ if not question:
1093
+ return "Error: Consult needs a `question`."
1094
+ return advisor.consult(question, config, context=params.get("context", "") or "")
1095
+
1096
+
1069
1097
  # A sub-agent's whole job is to keep its investigation OUT of the main agent's
1070
1098
  # context and hand back only a partition. The system prompt asks for a concise
1071
1099
  # summary, but a runaway model could still return a wall of text — so cap what
@@ -1474,7 +1502,7 @@ def register_all():
1474
1502
  "Write": tool_write, "Edit": tool_edit,
1475
1503
  "Bash": tool_bash, "Glob": tool_glob, "Grep": tool_grep,
1476
1504
  "todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
1477
- "Knowledge": tool_knowledge,
1505
+ "Consult": tool_consult, "Knowledge": tool_knowledge,
1478
1506
  "GraphQuery": tool_graphquery, "GraphAdd": tool_graphadd,
1479
1507
  "StigRules": tool_stigrules, "StigRule": tool_stigrule, "StigSet": tool_stigset,
1480
1508
  "WebSearch": tool_websearch, "WebFetch": tool_webfetch,
@@ -1484,8 +1512,8 @@ def register_all():
1484
1512
  # Read-only w.r.t. the parent's files (GitStatus/Diff/Log inspect only;
1485
1513
  # GitCommit + GraphAdd write).
1486
1514
  read_only = name in (
1487
- "Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Knowledge",
1488
- "GraphQuery", "StigRules", "StigRule",
1515
+ "Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Consult",
1516
+ "Knowledge", "GraphQuery", "StigRules", "StigRule",
1489
1517
  "WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
1490
1518
  )
1491
1519
  register(ToolDef(name=name, schema=schema, func=func, read_only=read_only))
@@ -487,6 +487,10 @@ class DrydockApp(App):
487
487
  self._cmd_compact()
488
488
  elif cmd == "/context":
489
489
  self._cmd_context(arg)
490
+ elif cmd == "/advisor":
491
+ self._cmd_advisor(arg)
492
+ elif cmd == "/ask":
493
+ self._cmd_ask(arg)
490
494
  elif cmd == "/graphrag":
491
495
  self._cmd_graphrag(arg)
492
496
  elif cmd == "/status":
@@ -509,6 +513,7 @@ class DrydockApp(App):
509
513
  " /status session model, cwd, turns, tokens\n"
510
514
  " /compact shrink old context to free up the window\n"
511
515
  " /context view/set the context-window budget (e.g. /context 65536)\n"
516
+ " /advisor set up a 2nd 'advisor' model (e.g. Gemini) · /ask <q> to consult it\n"
512
517
  " /graphrag ingest docs into a knowledge base the agent can use\n"
513
518
  " build <path> · add <path> · query <q> · status · clear\n"
514
519
  " /skills list skills · /skills new <name> <prompt> to create one\n"
@@ -843,6 +848,57 @@ class DrydockApp(App):
843
848
  "so drydock's budget is the effective limit — no server-side cap.")
844
849
  self.call_from_thread(self._info, msg)
845
850
 
851
+ def _cmd_advisor(self, arg: str) -> None:
852
+ """View/set the optional second 'advisor' model (a stronger model, e.g.
853
+ Gemini, on any OpenAI-compatible endpoint). Persists to config.toml."""
854
+ arg = (arg or "").strip()
855
+ if not arg:
856
+ from drydock import advisor
857
+ m = self.config.get("advisor_model") or "(unset)"
858
+ u = self.config.get("advisor_base_url") or "(unset)"
859
+ has_key = "yes" if (self.config.get("advisor_api_key") or "").strip() else "no"
860
+ state = "configured ✓" if advisor.is_configured(self.config) else "not configured"
861
+ self._info(
862
+ f"advisor ({state}):\n"
863
+ f" model: {m}\n"
864
+ f" endpoint: {u}\n"
865
+ f" api key: {has_key}\n"
866
+ "Set up: /advisor url <base_url/v1> · /advisor model <name> · "
867
+ "/advisor key <api_key>\n"
868
+ "Then: /ask <question> (you) or the agent calls the Consult tool."
869
+ )
870
+ return
871
+ parts = arg.split(maxsplit=1)
872
+ sub = parts[0].lower()
873
+ val = parts[1].strip() if len(parts) > 1 else ""
874
+ keymap = {"url": "advisor_base_url", "endpoint": "advisor_base_url",
875
+ "model": "advisor_model", "key": "advisor_api_key", "api_key": "advisor_api_key"}
876
+ if sub not in keymap or not val:
877
+ self._mount(ErrorMessage("usage: /advisor [url <base_url> | model <name> | key <api_key>]"))
878
+ return
879
+ self.config[keymap[sub]] = val
880
+ self._persist_config()
881
+ shown = "•••••" if keymap[sub] == "advisor_api_key" else val
882
+ self._info(f"✓ advisor {sub} set to {shown} (saved to config.toml).")
883
+
884
+ def _cmd_ask(self, arg: str) -> None:
885
+ """Ask the second/advisor model directly and show its answer."""
886
+ from drydock import advisor
887
+ q = (arg or "").strip()
888
+ if not q:
889
+ self._info("usage: /ask <question> — consult your configured advisor (second) model")
890
+ return
891
+ if not advisor.is_configured(self.config):
892
+ self._info(advisor.not_configured_message())
893
+ return
894
+ self._info(f"Asking the advisor ({self.config.get('advisor_model')})…")
895
+ self.run_worker(lambda: self._ask_worker(q), thread=True)
896
+
897
+ def _ask_worker(self, question: str) -> None:
898
+ from drydock import advisor
899
+ answer = advisor.consult(question, self.config)
900
+ self.call_from_thread(self._info, f"💡 advisor:\n{answer}")
901
+
846
902
  def _cmd_compact(self) -> None:
847
903
  """Manually compact the conversation to reclaim context NOW, without
848
904
  waiting for the automatic 60%-of-window threshold (agent.maybe_compact).
@@ -153,7 +153,8 @@ class PromptHistory:
153
153
  # completes the prefix.
154
154
  SLASH_COMMANDS = [
155
155
  "/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
156
- "/compact", "/context", "/graphrag", "/skills", "/loop", "/mcp", "/rmf", "/stig",
156
+ "/compact", "/context", "/advisor", "/ask", "/graphrag", "/skills", "/loop",
157
+ "/mcp", "/rmf", "/stig",
157
158
  "/clear", "/quit",
158
159
  ]
159
160
 
@@ -236,6 +236,9 @@ _DRYDOCK_COMMANDS_HELP = (
236
236
  "- Images / vision: the user just references an image file path in their "
237
237
  "message (a .png/.jpg screenshot, mockup, or diagram on disk) and it is "
238
238
  "attached for you to SEE — no command needed.\n"
239
+ "- Second opinion: if the user set up an advisor model (`/advisor`), you can "
240
+ "call the `Consult` tool to ask a stronger model when you're stuck; the user "
241
+ "can also `/ask <question>` themselves.\n"
239
242
  "- Other: `/model` (model/endpoint), `/cwd`, `/undo` (revert last write), "
240
243
  "`/back` (rewind a turn), `/compact` (shrink context), `/context` (view/set "
241
244
  "the context-window budget), `/loop <n> <prompt>` (repeat a prompt), `/mcp` "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.81
3
+ Version: 3.0.82
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
@@ -72,6 +72,10 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
72
72
  read-only `Knowledge` tool.
73
73
  - **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
74
74
  `task` runs one — each in a fresh context, for focused investigation.
75
+ - **Second-model advisor** — point `/advisor` at a stronger model on any
76
+ OpenAI-compatible endpoint (e.g. Gemini, or a proxy on another box); the agent
77
+ calls the `Consult` tool for a second opinion when stuck, and you can `/ask
78
+ <question>` directly. Opt-in, user-configured — no extra dependency.
75
79
  - **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
76
80
  their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
77
81
  - **Skills** — reusable `/<name>` commands authored as markdown in
@@ -103,6 +107,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
103
107
  | `/model` · `/cwd` | Show/set model & endpoint · working directory |
104
108
  | `/undo` · `/back` | Revert the last write · rewind the last turn |
105
109
  | `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
110
+ | `/advisor` · `/ask <q>` | Set up a 2nd 'advisor' model (Gemini etc.) · consult it |
106
111
  | `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
107
112
 
108
113
  ### Knowledge base (GraphRAG) — ingesting your documents
@@ -4,6 +4,7 @@ README.md
4
4
  pyproject.toml
5
5
  drydock/__init__.py
6
6
  drydock/__main__.py
7
+ drydock/advisor.py
7
8
  drydock/agent.py
8
9
  drydock/bash_safety.py
9
10
  drydock/cci.py
@@ -45,6 +46,7 @@ drydock_cli.egg-info/dependency_links.txt
45
46
  drydock_cli.egg-info/entry_points.txt
46
47
  drydock_cli.egg-info/requires.txt
47
48
  drydock_cli.egg-info/top_level.txt
49
+ tests/test_advisor.py
48
50
  tests/test_approval.py
49
51
  tests/test_back_command.py
50
52
  tests/test_bash_output_bounding.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.81"
10
+ version = "3.0.82"
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,66 @@
1
+ """Second-model advisor: consult() + the Consult tool + config."""
2
+ from __future__ import annotations
3
+
4
+ import drydock.tools # noqa: F401 — registers tools
5
+ from drydock import advisor
6
+ from drydock import tool_registry as reg
7
+
8
+
9
+ def test_is_configured():
10
+ assert not advisor.is_configured({})
11
+ assert not advisor.is_configured({"advisor_base_url": "http://x/v1"}) # no model
12
+ assert advisor.is_configured({"advisor_base_url": "http://x/v1", "advisor_model": "g"})
13
+
14
+
15
+ def test_consult_unconfigured_gives_setup_help():
16
+ out = advisor.consult("anything", {})
17
+ assert "No advisor" in out and "/advisor" in out
18
+
19
+
20
+ def test_consult_empty_question():
21
+ assert "nothing to ask" in advisor.consult(" ", {"advisor_base_url": "http://x/v1", "advisor_model": "g"})
22
+
23
+
24
+ def test_consult_calls_endpoint_and_returns_answer(monkeypatch):
25
+ captured = {}
26
+
27
+ class _Msg: # noqa: D401
28
+ content = "Use a mutex."
29
+
30
+ class _Choice:
31
+ message = _Msg()
32
+
33
+ class _Resp:
34
+ choices = [_Choice()]
35
+
36
+ class _FakeClient:
37
+ def __init__(self, **kw): captured["client"] = kw
38
+ class chat: # noqa: N801
39
+ class completions: # noqa: N801
40
+ @staticmethod
41
+ def create(**kw):
42
+ captured["call"] = kw
43
+ return _Resp()
44
+
45
+ monkeypatch.setattr(advisor, "OpenAI", _FakeClient, raising=False)
46
+ # advisor imports OpenAI lazily; patch the module symbol it will bind
47
+ import openai
48
+ monkeypatch.setattr(openai, "OpenAI", _FakeClient)
49
+ cfg = {"advisor_base_url": "http://box:9/v1", "advisor_model": "gemini-x", "advisor_api_key": "k"}
50
+ out = advisor.consult("How to fix this race?", cfg, context="def f(): ...")
51
+ assert out == "Use a mutex."
52
+ assert captured["client"]["base_url"] == "http://box:9/v1"
53
+ assert captured["call"]["model"] == "gemini-x"
54
+ # system + context + question all present
55
+ roles = [m["role"] for m in captured["call"]["messages"]]
56
+ assert roles == ["system", "user", "user"]
57
+
58
+
59
+ def test_consult_tool_unconfigured():
60
+ out = reg.execute("Consult", {"question": "hi"}, {})
61
+ assert "No advisor" in out
62
+ assert "needs a `question`" in reg.execute("Consult", {}, {})
63
+
64
+
65
+ def test_consult_tool_is_read_only():
66
+ assert reg.get("Consult").read_only is True
File without changes
File without changes
File without changes