drydock-cli 3.0.56__tar.gz → 3.0.58__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.56/drydock_cli.egg-info → drydock_cli-3.0.58}/PKG-INFO +50 -20
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/README.md +49 -19
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tools/__init__.py +59 -22
- {drydock_cli-3.0.56 → drydock_cli-3.0.58/drydock_cli.egg-info}/PKG-INFO +50 -20
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/pyproject.toml +1 -1
- drydock_cli-3.0.58/tests/test_bash_output_bounding.py +37 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_failure_loop.py +23 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/LICENSE +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/NOTICE +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/agent.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/cli.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/config.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/detect.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/guards.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/providers.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/skills.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/web.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/setup.cfg +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_config.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_web_tools.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.58
|
|
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
|
|
@@ -23,9 +23,10 @@ Dynamic: license-file
|
|
|
23
23
|
# ⚓ Drydock
|
|
24
24
|
|
|
25
25
|
A local-first, provider-agnostic **terminal coding agent** for your own LLM.
|
|
26
|
-
No accounts, no telemetry, no cloud — the only
|
|
27
|
-
|
|
28
|
-
by llama.cpp on a
|
|
26
|
+
No accounts, no telemetry, no cloud — the only outbound calls are to the model
|
|
27
|
+
endpoint you configure and (optionally) the web-search tools you invoke.
|
|
28
|
+
Primary target: **dense Gemma-4-31B** (QAT, 64K) served by llama.cpp on a
|
|
29
|
+
single workstation.
|
|
29
30
|
|
|
30
31
|
> **v3 — clean-room rebuild.** Drydock is being rebuilt as an original,
|
|
31
32
|
> Apache-2.0 codebase owned end to end (no upstream fork). Every release is
|
|
@@ -44,9 +45,31 @@ your box.
|
|
|
44
45
|
|
|
45
46
|
Shipping. Published on PyPI as **`drydock-cli`** (v3.x). The Textual TUI is the
|
|
46
47
|
default surface: a scrolling transcript with streamed assistant text, collapsible
|
|
47
|
-
tool cards,
|
|
48
|
-
|
|
49
|
-
(
|
|
48
|
+
tool cards, collapsible reasoning ("thinking") cards, a live nautical activity
|
|
49
|
+
line, and a multi-line prompt. The agent loop, OpenAI-compatible provider,
|
|
50
|
+
two-tier compaction, and the full agentic toolset (below) are in, with Gemma
|
|
51
|
+
reliability hardening verified hands-on.
|
|
52
|
+
|
|
53
|
+
## Capabilities
|
|
54
|
+
|
|
55
|
+
A full agentic CLI harness — every tool below is clean-room and dependency-free
|
|
56
|
+
(nothing beyond `openai` + `textual`), and the model calls them autonomously:
|
|
57
|
+
|
|
58
|
+
- **Files & shell** — `Read` (with a structure index for huge files), `Write`,
|
|
59
|
+
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
60
|
+
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
61
|
+
(structured + truncated; commit is local and reversible).
|
|
62
|
+
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
63
|
+
- **Knowledge base (GraphRAG)** — build a local entity-graph index from your
|
|
64
|
+
docs/code with `/graphrag build <path>`; the agent retrieves from it via the
|
|
65
|
+
read-only `Knowledge` tool.
|
|
66
|
+
- **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
|
|
67
|
+
`task` runs one — each in a fresh context, for focused investigation.
|
|
68
|
+
- **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
|
|
69
|
+
their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
|
|
70
|
+
- **Skills** — reusable `/<name>` commands authored as markdown in
|
|
71
|
+
`~/.drydock/skills/` (or `<project>/.drydock/skills/`); `$ARGS` substitution.
|
|
72
|
+
- **Loops** — `/loop <count> <prompt>` runs a prompt iteratively (Esc stops).
|
|
50
73
|
|
|
51
74
|
## Install
|
|
52
75
|
|
|
@@ -55,7 +78,7 @@ pip install drydock-cli
|
|
|
55
78
|
drydock
|
|
56
79
|
```
|
|
57
80
|
|
|
58
|
-
Requires Python 3.
|
|
81
|
+
Requires Python 3.11+. From source instead:
|
|
59
82
|
`git clone https://github.com/fbobe321/drydock-v3.git && cd drydock-v3 && pip install -e .`
|
|
60
83
|
|
|
61
84
|
On first launch with no config, Drydock probes localhost for a running local
|
|
@@ -76,8 +99,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
76
99
|
- A live activity line shows progress while it works:
|
|
77
100
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
78
101
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
79
|
-
- Slash commands: `/model
|
|
80
|
-
|
|
102
|
+
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
103
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
|
|
104
|
+
(build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
|
|
105
|
+
`/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
|
|
106
|
+
`/quit`
|
|
81
107
|
|
|
82
108
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
83
109
|
conventions.
|
|
@@ -96,22 +122,26 @@ blocked:
|
|
|
96
122
|
`raise` outside an except, and refuses to write git conflict-marker content.
|
|
97
123
|
|
|
98
124
|
Point it at a local OpenAI-compatible endpoint (e.g. llama.cpp's `server-cuda`
|
|
99
|
-
serving Gemma
|
|
125
|
+
serving Gemma-4-31B). The web tools (`WebSearch`/`WebFetch`) are read-only and
|
|
126
|
+
degrade cleanly offline; the release scanner allowlists only the search backend.
|
|
100
127
|
|
|
101
128
|
## Model server (reference setup)
|
|
102
129
|
|
|
103
130
|
Drydock is provider-agnostic, but it's tuned and measured against this rig:
|
|
104
131
|
|
|
105
|
-
- **Model:** Gemma-4-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
tensor-split
|
|
132
|
+
- **Model:** dense **Gemma-4-31B** (QAT `Q4_K_XL` GGUF), served by
|
|
133
|
+
`ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`. Swapped from the
|
|
134
|
+
26B-A4B MoE, whose ~4B active params caused fatal agentic tool-loops; the
|
|
135
|
+
dense 31B is loop-free (slower, but it finishes).
|
|
136
|
+
- **GPUs:** 2× NVIDIA RTX 4060 Ti 16GB, **tensor-split** across both cards
|
|
137
|
+
(`--tensor-split 1,1`) so the 31B weights fit.
|
|
111
138
|
- **Context:** 64k (`-c 65536`) with `q8_0` KV-cache quantization
|
|
112
|
-
(`-ctk q8_0 -ctv q8_0`)
|
|
113
|
-
|
|
114
|
-
- **
|
|
139
|
+
(`-ctk q8_0 -ctv q8_0`); set `context_limit` in `~/.drydock/config.toml` to
|
|
140
|
+
match your server's `-c`.
|
|
141
|
+
- **Throughput:** ~15 tok/s decode (tensor-split 31B). Faster single-GPU
|
|
142
|
+
options exist if you drop to a smaller model.
|
|
143
|
+
- **Provider-agnostic:** any OpenAI-compatible endpoint (llama.cpp, vLLM,
|
|
144
|
+
Ollama, LM Studio) works — point `--base-url` at it.
|
|
115
145
|
|
|
116
146
|
## Principles
|
|
117
147
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# ⚓ Drydock
|
|
2
2
|
|
|
3
3
|
A local-first, provider-agnostic **terminal coding agent** for your own LLM.
|
|
4
|
-
No accounts, no telemetry, no cloud — the only
|
|
5
|
-
|
|
6
|
-
by llama.cpp on a
|
|
4
|
+
No accounts, no telemetry, no cloud — the only outbound calls are to the model
|
|
5
|
+
endpoint you configure and (optionally) the web-search tools you invoke.
|
|
6
|
+
Primary target: **dense Gemma-4-31B** (QAT, 64K) served by llama.cpp on a
|
|
7
|
+
single workstation.
|
|
7
8
|
|
|
8
9
|
> **v3 — clean-room rebuild.** Drydock is being rebuilt as an original,
|
|
9
10
|
> Apache-2.0 codebase owned end to end (no upstream fork). Every release is
|
|
@@ -22,9 +23,31 @@ your box.
|
|
|
22
23
|
|
|
23
24
|
Shipping. Published on PyPI as **`drydock-cli`** (v3.x). The Textual TUI is the
|
|
24
25
|
default surface: a scrolling transcript with streamed assistant text, collapsible
|
|
25
|
-
tool cards,
|
|
26
|
-
|
|
27
|
-
(
|
|
26
|
+
tool cards, collapsible reasoning ("thinking") cards, a live nautical activity
|
|
27
|
+
line, and a multi-line prompt. The agent loop, OpenAI-compatible provider,
|
|
28
|
+
two-tier compaction, and the full agentic toolset (below) are in, with Gemma
|
|
29
|
+
reliability hardening verified hands-on.
|
|
30
|
+
|
|
31
|
+
## Capabilities
|
|
32
|
+
|
|
33
|
+
A full agentic CLI harness — every tool below is clean-room and dependency-free
|
|
34
|
+
(nothing beyond `openai` + `textual`), and the model calls them autonomously:
|
|
35
|
+
|
|
36
|
+
- **Files & shell** — `Read` (with a structure index for huge files), `Write`,
|
|
37
|
+
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
38
|
+
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
39
|
+
(structured + truncated; commit is local and reversible).
|
|
40
|
+
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
41
|
+
- **Knowledge base (GraphRAG)** — build a local entity-graph index from your
|
|
42
|
+
docs/code with `/graphrag build <path>`; the agent retrieves from it via the
|
|
43
|
+
read-only `Knowledge` tool.
|
|
44
|
+
- **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
|
|
45
|
+
`task` runs one — each in a fresh context, for focused investigation.
|
|
46
|
+
- **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
|
|
47
|
+
their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
|
|
48
|
+
- **Skills** — reusable `/<name>` commands authored as markdown in
|
|
49
|
+
`~/.drydock/skills/` (or `<project>/.drydock/skills/`); `$ARGS` substitution.
|
|
50
|
+
- **Loops** — `/loop <count> <prompt>` runs a prompt iteratively (Esc stops).
|
|
28
51
|
|
|
29
52
|
## Install
|
|
30
53
|
|
|
@@ -33,7 +56,7 @@ pip install drydock-cli
|
|
|
33
56
|
drydock
|
|
34
57
|
```
|
|
35
58
|
|
|
36
|
-
Requires Python 3.
|
|
59
|
+
Requires Python 3.11+. From source instead:
|
|
37
60
|
`git clone https://github.com/fbobe321/drydock-v3.git && cd drydock-v3 && pip install -e .`
|
|
38
61
|
|
|
39
62
|
On first launch with no config, Drydock probes localhost for a running local
|
|
@@ -54,8 +77,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
54
77
|
- A live activity line shows progress while it works:
|
|
55
78
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
56
79
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
57
|
-
- Slash commands: `/model
|
|
58
|
-
|
|
80
|
+
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
81
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
|
|
82
|
+
(build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
|
|
83
|
+
`/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
|
|
84
|
+
`/quit`
|
|
59
85
|
|
|
60
86
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
61
87
|
conventions.
|
|
@@ -74,22 +100,26 @@ blocked:
|
|
|
74
100
|
`raise` outside an except, and refuses to write git conflict-marker content.
|
|
75
101
|
|
|
76
102
|
Point it at a local OpenAI-compatible endpoint (e.g. llama.cpp's `server-cuda`
|
|
77
|
-
serving Gemma
|
|
103
|
+
serving Gemma-4-31B). The web tools (`WebSearch`/`WebFetch`) are read-only and
|
|
104
|
+
degrade cleanly offline; the release scanner allowlists only the search backend.
|
|
78
105
|
|
|
79
106
|
## Model server (reference setup)
|
|
80
107
|
|
|
81
108
|
Drydock is provider-agnostic, but it's tuned and measured against this rig:
|
|
82
109
|
|
|
83
|
-
- **Model:** Gemma-4-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
tensor-split
|
|
110
|
+
- **Model:** dense **Gemma-4-31B** (QAT `Q4_K_XL` GGUF), served by
|
|
111
|
+
`ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`. Swapped from the
|
|
112
|
+
26B-A4B MoE, whose ~4B active params caused fatal agentic tool-loops; the
|
|
113
|
+
dense 31B is loop-free (slower, but it finishes).
|
|
114
|
+
- **GPUs:** 2× NVIDIA RTX 4060 Ti 16GB, **tensor-split** across both cards
|
|
115
|
+
(`--tensor-split 1,1`) so the 31B weights fit.
|
|
89
116
|
- **Context:** 64k (`-c 65536`) with `q8_0` KV-cache quantization
|
|
90
|
-
(`-ctk q8_0 -ctv q8_0`)
|
|
91
|
-
|
|
92
|
-
- **
|
|
117
|
+
(`-ctk q8_0 -ctv q8_0`); set `context_limit` in `~/.drydock/config.toml` to
|
|
118
|
+
match your server's `-c`.
|
|
119
|
+
- **Throughput:** ~15 tok/s decode (tensor-split 31B). Faster single-GPU
|
|
120
|
+
options exist if you drop to a smaller model.
|
|
121
|
+
- **Provider-agnostic:** any OpenAI-compatible endpoint (llama.cpp, vLLM,
|
|
122
|
+
Ollama, LM Studio) works — point `--base-url` at it.
|
|
93
123
|
|
|
94
124
|
## Principles
|
|
95
125
|
|
|
@@ -11,9 +11,17 @@ import difflib
|
|
|
11
11
|
import glob as _glob
|
|
12
12
|
import signal
|
|
13
13
|
import subprocess
|
|
14
|
+
import threading
|
|
14
15
|
import time
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
18
|
+
# Hard ceiling on bytes read from a command's output. communicate() buffers ALL
|
|
19
|
+
# stdout in RAM before we ever truncate for context, so a runaway/infinite-output
|
|
20
|
+
# command (`yes`, `cat /dev/urandom`, a massive build log) could balloon memory
|
|
21
|
+
# to gigabytes within the timeout. We stream with a byte cap and kill the command
|
|
22
|
+
# once it's hit — bounding RAM regardless of how much it tries to produce.
|
|
23
|
+
_MAX_BASH_OUTPUT_BYTES = 256 * 1024 # 256 KB — plenty of context, safe for RAM
|
|
24
|
+
|
|
17
25
|
from drydock.tool_registry import ToolDef, register
|
|
18
26
|
from drydock.guards import (
|
|
19
27
|
conflict_marker_refusal,
|
|
@@ -728,30 +736,59 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
728
736
|
text=True, cwd=config.get("cwd"), start_new_session=True,
|
|
729
737
|
)
|
|
730
738
|
config.setdefault("_abort", {})["proc"] = proc
|
|
739
|
+
# Read output in a daemon thread with a HARD byte cap, so memory can't
|
|
740
|
+
# balloon on a runaway-output command. The thread stops (and we kill the
|
|
741
|
+
# process) once the cap is hit; the main loop polls cancel + timeout.
|
|
742
|
+
chunks: list[str] = []
|
|
743
|
+
total = [0]
|
|
744
|
+
capped = threading.Event()
|
|
745
|
+
|
|
746
|
+
def _drain():
|
|
747
|
+
assert proc.stdout is not None
|
|
748
|
+
while True:
|
|
749
|
+
block = proc.stdout.read(8192)
|
|
750
|
+
if not block:
|
|
751
|
+
break
|
|
752
|
+
chunks.append(block)
|
|
753
|
+
total[0] += len(block)
|
|
754
|
+
if total[0] >= _MAX_BASH_OUTPUT_BYTES:
|
|
755
|
+
capped.set()
|
|
756
|
+
break
|
|
757
|
+
|
|
758
|
+
reader = threading.Thread(target=_drain, daemon=True)
|
|
759
|
+
reader.start()
|
|
731
760
|
start = time.monotonic()
|
|
732
|
-
while
|
|
733
|
-
|
|
734
|
-
|
|
761
|
+
while reader.is_alive():
|
|
762
|
+
reader.join(0.3)
|
|
763
|
+
if capped.is_set():
|
|
764
|
+
kill_process_group(proc) # stop it producing more
|
|
735
765
|
break
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
output =
|
|
766
|
+
if cancel is not None and cancel.is_set():
|
|
767
|
+
kill_process_group(proc)
|
|
768
|
+
proc.wait()
|
|
769
|
+
return "[stopped by user]"
|
|
770
|
+
if time.monotonic() - start > timeout:
|
|
771
|
+
kill_process_group(proc)
|
|
772
|
+
proc.wait()
|
|
773
|
+
bigger = min(timeout * 4, 1800)
|
|
774
|
+
msg = (
|
|
775
|
+
f"Error: command timed out after {timeout}s. If it is "
|
|
776
|
+
f"legitimately slow (a big query, build, download, or "
|
|
777
|
+
f"test run), retry with a larger timeout — pass "
|
|
778
|
+
f"timeout: {bigger}. Otherwise it may be hung."
|
|
779
|
+
)
|
|
780
|
+
if _is_network_command(cmd):
|
|
781
|
+
msg += _OFFLINE_HINT
|
|
782
|
+
return msg
|
|
783
|
+
proc.wait()
|
|
784
|
+
output = "".join(chunks)
|
|
785
|
+
if capped.is_set():
|
|
786
|
+
return (
|
|
787
|
+
output[:_MAX_BASH_OUTPUT_BYTES]
|
|
788
|
+
+ f"\n[output truncated at {_MAX_BASH_OUTPUT_BYTES // 1024} KB — "
|
|
789
|
+
"the command produced more; redirect to a file and inspect it in "
|
|
790
|
+
"pieces (head/tail/grep) instead of dumping it all]"
|
|
791
|
+
)
|
|
755
792
|
if proc.returncode != 0:
|
|
756
793
|
output += f"\n[exit code: {proc.returncode}]"
|
|
757
794
|
# Offline environments make downloads fail forever; the model tends
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.58
|
|
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
|
|
@@ -23,9 +23,10 @@ Dynamic: license-file
|
|
|
23
23
|
# ⚓ Drydock
|
|
24
24
|
|
|
25
25
|
A local-first, provider-agnostic **terminal coding agent** for your own LLM.
|
|
26
|
-
No accounts, no telemetry, no cloud — the only
|
|
27
|
-
|
|
28
|
-
by llama.cpp on a
|
|
26
|
+
No accounts, no telemetry, no cloud — the only outbound calls are to the model
|
|
27
|
+
endpoint you configure and (optionally) the web-search tools you invoke.
|
|
28
|
+
Primary target: **dense Gemma-4-31B** (QAT, 64K) served by llama.cpp on a
|
|
29
|
+
single workstation.
|
|
29
30
|
|
|
30
31
|
> **v3 — clean-room rebuild.** Drydock is being rebuilt as an original,
|
|
31
32
|
> Apache-2.0 codebase owned end to end (no upstream fork). Every release is
|
|
@@ -44,9 +45,31 @@ your box.
|
|
|
44
45
|
|
|
45
46
|
Shipping. Published on PyPI as **`drydock-cli`** (v3.x). The Textual TUI is the
|
|
46
47
|
default surface: a scrolling transcript with streamed assistant text, collapsible
|
|
47
|
-
tool cards,
|
|
48
|
-
|
|
49
|
-
(
|
|
48
|
+
tool cards, collapsible reasoning ("thinking") cards, a live nautical activity
|
|
49
|
+
line, and a multi-line prompt. The agent loop, OpenAI-compatible provider,
|
|
50
|
+
two-tier compaction, and the full agentic toolset (below) are in, with Gemma
|
|
51
|
+
reliability hardening verified hands-on.
|
|
52
|
+
|
|
53
|
+
## Capabilities
|
|
54
|
+
|
|
55
|
+
A full agentic CLI harness — every tool below is clean-room and dependency-free
|
|
56
|
+
(nothing beyond `openai` + `textual`), and the model calls them autonomously:
|
|
57
|
+
|
|
58
|
+
- **Files & shell** — `Read` (with a structure index for huge files), `Write`,
|
|
59
|
+
`Edit`, `Bash`, `Glob`, `Grep`.
|
|
60
|
+
- **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
|
|
61
|
+
(structured + truncated; commit is local and reversible).
|
|
62
|
+
- **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
|
|
63
|
+
- **Knowledge base (GraphRAG)** — build a local entity-graph index from your
|
|
64
|
+
docs/code with `/graphrag build <path>`; the agent retrieves from it via the
|
|
65
|
+
read-only `Knowledge` tool.
|
|
66
|
+
- **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
|
|
67
|
+
`task` runs one — each in a fresh context, for focused investigation.
|
|
68
|
+
- **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
|
|
69
|
+
their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
|
|
70
|
+
- **Skills** — reusable `/<name>` commands authored as markdown in
|
|
71
|
+
`~/.drydock/skills/` (or `<project>/.drydock/skills/`); `$ARGS` substitution.
|
|
72
|
+
- **Loops** — `/loop <count> <prompt>` runs a prompt iteratively (Esc stops).
|
|
50
73
|
|
|
51
74
|
## Install
|
|
52
75
|
|
|
@@ -55,7 +78,7 @@ pip install drydock-cli
|
|
|
55
78
|
drydock
|
|
56
79
|
```
|
|
57
80
|
|
|
58
|
-
Requires Python 3.
|
|
81
|
+
Requires Python 3.11+. From source instead:
|
|
59
82
|
`git clone https://github.com/fbobe321/drydock-v3.git && cd drydock-v3 && pip install -e .`
|
|
60
83
|
|
|
61
84
|
On first launch with no config, Drydock probes localhost for a running local
|
|
@@ -76,8 +99,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
76
99
|
- A live activity line shows progress while it works:
|
|
77
100
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
78
101
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
79
|
-
- Slash commands: `/model
|
|
80
|
-
|
|
102
|
+
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
103
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
|
|
104
|
+
(build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
|
|
105
|
+
`/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
|
|
106
|
+
`/quit`
|
|
81
107
|
|
|
82
108
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
83
109
|
conventions.
|
|
@@ -96,22 +122,26 @@ blocked:
|
|
|
96
122
|
`raise` outside an except, and refuses to write git conflict-marker content.
|
|
97
123
|
|
|
98
124
|
Point it at a local OpenAI-compatible endpoint (e.g. llama.cpp's `server-cuda`
|
|
99
|
-
serving Gemma
|
|
125
|
+
serving Gemma-4-31B). The web tools (`WebSearch`/`WebFetch`) are read-only and
|
|
126
|
+
degrade cleanly offline; the release scanner allowlists only the search backend.
|
|
100
127
|
|
|
101
128
|
## Model server (reference setup)
|
|
102
129
|
|
|
103
130
|
Drydock is provider-agnostic, but it's tuned and measured against this rig:
|
|
104
131
|
|
|
105
|
-
- **Model:** Gemma-4-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
tensor-split
|
|
132
|
+
- **Model:** dense **Gemma-4-31B** (QAT `Q4_K_XL` GGUF), served by
|
|
133
|
+
`ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`. Swapped from the
|
|
134
|
+
26B-A4B MoE, whose ~4B active params caused fatal agentic tool-loops; the
|
|
135
|
+
dense 31B is loop-free (slower, but it finishes).
|
|
136
|
+
- **GPUs:** 2× NVIDIA RTX 4060 Ti 16GB, **tensor-split** across both cards
|
|
137
|
+
(`--tensor-split 1,1`) so the 31B weights fit.
|
|
111
138
|
- **Context:** 64k (`-c 65536`) with `q8_0` KV-cache quantization
|
|
112
|
-
(`-ctk q8_0 -ctv q8_0`)
|
|
113
|
-
|
|
114
|
-
- **
|
|
139
|
+
(`-ctk q8_0 -ctv q8_0`); set `context_limit` in `~/.drydock/config.toml` to
|
|
140
|
+
match your server's `-c`.
|
|
141
|
+
- **Throughput:** ~15 tok/s decode (tensor-split 31B). Faster single-GPU
|
|
142
|
+
options exist if you drop to a smaller model.
|
|
143
|
+
- **Provider-agnostic:** any OpenAI-compatible endpoint (llama.cpp, vLLM,
|
|
144
|
+
Ollama, LM Studio) works — point `--base-url` at it.
|
|
115
145
|
|
|
116
146
|
## Principles
|
|
117
147
|
|
|
@@ -34,6 +34,7 @@ drydock_cli.egg-info/requires.txt
|
|
|
34
34
|
drydock_cli.egg-info/top_level.txt
|
|
35
35
|
tests/test_approval.py
|
|
36
36
|
tests/test_back_command.py
|
|
37
|
+
tests/test_bash_output_bounding.py
|
|
37
38
|
tests/test_bash_process_group.py
|
|
38
39
|
tests/test_bash_safety.py
|
|
39
40
|
tests/test_bash_timeout_network.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.
|
|
10
|
+
version = "3.0.58"
|
|
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,37 @@
|
|
|
1
|
+
"""Critical VRAM/RAM protection (PRD §3.B): tool_bash must bound a command's
|
|
2
|
+
output DURING capture so a runaway/infinite-output command can't balloon memory
|
|
3
|
+
before the context-truncation cap applies."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from drydock.tools import tool_bash, _MAX_BASH_OUTPUT_BYTES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_infinite_output_is_capped_and_fast(tmp_path):
|
|
12
|
+
cfg = {"cwd": str(tmp_path)}
|
|
13
|
+
t0 = time.monotonic()
|
|
14
|
+
out = tool_bash({"command": "yes"}, cfg) # infinite stream
|
|
15
|
+
elapsed = time.monotonic() - t0
|
|
16
|
+
assert "truncated at" in out # capped, not dumped
|
|
17
|
+
assert len(out) < _MAX_BASH_OUTPUT_BYTES + 500 # bounded memory
|
|
18
|
+
assert elapsed < 20 # killed promptly, no hang
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_large_finite_output_is_capped(tmp_path):
|
|
22
|
+
cfg = {"cwd": str(tmp_path)}
|
|
23
|
+
# ~1 MB of output → must come back truncated near the cap, not 1 MB.
|
|
24
|
+
out = tool_bash({"command": "head -c 1000000 /dev/zero | tr '\\0' 'a'"}, cfg)
|
|
25
|
+
assert len(out) < _MAX_BASH_OUTPUT_BYTES + 500
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_normal_output_untouched(tmp_path):
|
|
29
|
+
cfg = {"cwd": str(tmp_path)}
|
|
30
|
+
out = tool_bash({"command": "echo hello world"}, cfg)
|
|
31
|
+
assert out == "hello world" and "truncated" not in out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_exit_code_still_reported(tmp_path):
|
|
35
|
+
cfg = {"cwd": str(tmp_path)}
|
|
36
|
+
out = tool_bash({"command": "echo oops; exit 3"}, cfg)
|
|
37
|
+
assert "oops" in out and "exit code: 3" in out
|
|
@@ -75,3 +75,26 @@ def test_polling_with_changing_result_not_tripped(monkeypatch):
|
|
|
75
75
|
st = AgentState()
|
|
76
76
|
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
77
77
|
assert any(m.get("content") == "done" for m in st.messages)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_infinite_distinct_tool_calls_terminate_at_max_turns(monkeypatch):
|
|
81
|
+
"""PRD §3.D: a model that keeps calling tools (DIFFERENT, succeeding calls)
|
|
82
|
+
and never yields 'Task Complete' must be forcibly stopped at the hard
|
|
83
|
+
max-iteration ceiling — protecting VRAM/context from an unbounded loop."""
|
|
84
|
+
calls = {"n": 0}
|
|
85
|
+
|
|
86
|
+
def never_done(**kw):
|
|
87
|
+
calls["n"] += 1
|
|
88
|
+
# A unique, succeeding command each turn → the identical-repeat valve
|
|
89
|
+
# never trips, so only the hard max_turns cap can end it.
|
|
90
|
+
return iter([AssistantTurn(
|
|
91
|
+
"", [{"id": str(calls["n"]), "name": "Bash",
|
|
92
|
+
"input": {"command": f"echo step {calls['n']}"}}], 1, 1)])
|
|
93
|
+
|
|
94
|
+
monkeypatch.setattr(agent_mod, "stream", never_done)
|
|
95
|
+
st = AgentState()
|
|
96
|
+
import tempfile
|
|
97
|
+
# Low cap so the test is fast; the real default is 200.
|
|
98
|
+
list(run("loop forever", st, {"model": "m", "max_turns": 12,
|
|
99
|
+
"cwd": tempfile.mkdtemp()}, "sys"))
|
|
100
|
+
assert calls["n"] == 12 # ran exactly to the ceiling, then terminated
|
|
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
|