drydock-cli 3.0.55__tar.gz → 3.0.57__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.55/drydock_cli.egg-info → drydock_cli-3.0.57}/PKG-INFO +50 -20
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/README.md +49 -19
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tools/__init__.py +54 -5
- {drydock_cli-3.0.55 → drydock_cli-3.0.57/drydock_cli.egg-info}/PKG-INFO +50 -20
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/pyproject.toml +1 -1
- drydock_cli-3.0.57/tests/test_read_index.py +52 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/LICENSE +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/NOTICE +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/agent.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/cli.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/config.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/detect.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/guards.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/providers.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/skills.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock/web.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/setup.cfg +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_config.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.55 → drydock_cli-3.0.57}/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.57
|
|
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
|
|
|
@@ -41,7 +41,7 @@ def _resolve_path(path: str, config: dict) -> str:
|
|
|
41
41
|
SCHEMAS = [
|
|
42
42
|
{
|
|
43
43
|
"name": "Read",
|
|
44
|
-
"description": "Read a file. Returns content with line numbers. Use limit/offset for large files.",
|
|
44
|
+
"description": "Read a file. Returns content with line numbers. Use limit/offset for large files. A very large file with no limit returns a STRUCTURE INDEX (key symbols + line numbers) instead of the whole file — then Read the ranges you need with offset/limit.",
|
|
45
45
|
"input_schema": {
|
|
46
46
|
"type": "object",
|
|
47
47
|
"properties": {
|
|
@@ -303,18 +303,67 @@ SCHEMAS = [
|
|
|
303
303
|
|
|
304
304
|
# ── Tool implementations ──────────────────────────────────────────────────
|
|
305
305
|
|
|
306
|
+
# Above this many lines, a Read with no explicit window returns a structural
|
|
307
|
+
# INDEX (key symbols + line numbers) instead of dumping the file — semantic
|
|
308
|
+
# chunking so a huge file can't blow the context window. The model then Reads
|
|
309
|
+
# the ranges it needs with offset/limit.
|
|
310
|
+
_BIG_FILE_LINES = 1500
|
|
311
|
+
|
|
312
|
+
# Structural anchors: a definition → a node the model can jump to. Code anchors
|
|
313
|
+
# apply to every file; markdown headers (anchored at column 0 so indented code
|
|
314
|
+
# COMMENTS aren't mistaken for headings) apply only to doc files.
|
|
315
|
+
_CODE_RE = re.compile(
|
|
316
|
+
r"^\s*("
|
|
317
|
+
r"(?:async\s+)?def\s+\w+" # python functions
|
|
318
|
+
r"|class\s+\w+" # python/js/etc classes
|
|
319
|
+
r"|(?:export\s+)?(?:async\s+)?function\s+\w+" # js/ts functions
|
|
320
|
+
r"|(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\(" # js arrow fns
|
|
321
|
+
r"|func\s+\w+" # go
|
|
322
|
+
r"|fn\s+\w+" # rust
|
|
323
|
+
r")"
|
|
324
|
+
)
|
|
325
|
+
_MD_RE = re.compile(r"^#{1,6}\s+\S") # markdown headers, column 0 only
|
|
326
|
+
_DOC_EXT = {".md", ".markdown", ".rst", ".txt", ".org"}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _file_index(lines: list[str], fp: str) -> str:
|
|
330
|
+
"""A compact, line-numbered index of a large file's structure."""
|
|
331
|
+
is_doc = Path(fp).suffix.lower() in _DOC_EXT
|
|
332
|
+
anchors = [
|
|
333
|
+
f" L{i + 1}: {ln.rstrip()[:100]}"
|
|
334
|
+
for i, ln in enumerate(lines)
|
|
335
|
+
if _CODE_RE.match(ln) or (is_doc and _MD_RE.match(ln))
|
|
336
|
+
]
|
|
337
|
+
head = (
|
|
338
|
+
f"{fp} is large ({len(lines)} lines) — showing a STRUCTURE INDEX instead "
|
|
339
|
+
f"of the whole file (to save context). Read a specific section with "
|
|
340
|
+
f"offset/limit (e.g. {{\"file_path\": ..., \"offset\": <line-1>, "
|
|
341
|
+
f"\"limit\": 120}}).\n"
|
|
342
|
+
)
|
|
343
|
+
if not anchors:
|
|
344
|
+
return head + "(no def/class/header anchors found — read ranges with offset/limit.)"
|
|
345
|
+
body = "\n".join(anchors[:400])
|
|
346
|
+
if len(anchors) > 400:
|
|
347
|
+
body += f"\n [... {len(anchors) - 400} more anchors ...]"
|
|
348
|
+
return f"{head}\nKey locations ({len(anchors)}):\n{body}"
|
|
349
|
+
|
|
350
|
+
|
|
306
351
|
def tool_read(params: dict, config: dict) -> str:
|
|
307
352
|
fp = _resolve_path(params["file_path"], config)
|
|
308
|
-
limit = params.get("limit"
|
|
353
|
+
limit = params.get("limit") # None = caller didn't specify a window
|
|
309
354
|
offset = params.get("offset", 0)
|
|
310
355
|
try:
|
|
311
356
|
with open(fp, "r", encoding="utf-8", errors="replace") as f:
|
|
312
357
|
lines = f.readlines()
|
|
313
|
-
|
|
358
|
+
# Huge file, no explicit window → index it instead of dumping it.
|
|
359
|
+
if limit is None and offset == 0 and len(lines) > _BIG_FILE_LINES:
|
|
360
|
+
return _file_index(lines, fp)
|
|
361
|
+
eff_limit = 2000 if limit is None else limit
|
|
362
|
+
selected = lines[offset:offset + eff_limit]
|
|
314
363
|
numbered = [f"{i + offset + 1}\t{line.rstrip()}" for i, line in enumerate(selected)]
|
|
315
364
|
result = "\n".join(numbered)
|
|
316
|
-
if len(lines) > offset +
|
|
317
|
-
result += f"\n[... {len(lines) - offset -
|
|
365
|
+
if len(lines) > offset + eff_limit:
|
|
366
|
+
result += f"\n[... {len(lines) - offset - eff_limit} more lines]"
|
|
318
367
|
return result or "(empty file)"
|
|
319
368
|
except FileNotFoundError:
|
|
320
369
|
return f"Error: file not found: {fp}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.57
|
|
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
|
|
|
@@ -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.57"
|
|
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,52 @@
|
|
|
1
|
+
"""Semantic chunking: Read returns a STRUCTURE INDEX for a huge file (no
|
|
2
|
+
explicit window) instead of dumping it, so it can't blow the context window."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from drydock.tools import tool_read
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _big_py(tmp_path, n=2000):
|
|
9
|
+
lines = []
|
|
10
|
+
for i in range(n):
|
|
11
|
+
if i % 400 == 0:
|
|
12
|
+
lines.append(f"def func_{i}(x):")
|
|
13
|
+
elif i % 400 == 1:
|
|
14
|
+
lines.append(" return x")
|
|
15
|
+
else:
|
|
16
|
+
lines.append(f" # comment {i}")
|
|
17
|
+
(tmp_path / "big.py").write_text("\n".join(lines))
|
|
18
|
+
return {"cwd": str(tmp_path)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_huge_file_no_window_returns_index(tmp_path):
|
|
22
|
+
cfg = _big_py(tmp_path)
|
|
23
|
+
out = tool_read({"file_path": "big.py"}, cfg)
|
|
24
|
+
assert "STRUCTURE INDEX" in out
|
|
25
|
+
assert "def func_0" in out and "def func_400" in out
|
|
26
|
+
assert "# comment" not in out # comments are not anchors
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_index_is_small_not_a_dump(tmp_path):
|
|
30
|
+
cfg = _big_py(tmp_path)
|
|
31
|
+
out = tool_read({"file_path": "big.py"}, cfg)
|
|
32
|
+
assert len(out.splitlines()) < 50 # an index, not 2000 lines
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_explicit_window_still_reads_slice(tmp_path):
|
|
36
|
+
cfg = _big_py(tmp_path)
|
|
37
|
+
out = tool_read({"file_path": "big.py", "offset": 0, "limit": 3}, cfg)
|
|
38
|
+
assert "STRUCTURE INDEX" not in out
|
|
39
|
+
assert out.startswith("1\tdef func_0")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_small_file_reads_fully(tmp_path):
|
|
43
|
+
(tmp_path / "s.py").write_text("def a():\n return 1\n")
|
|
44
|
+
out = tool_read({"file_path": "s.py"}, {"cwd": str(tmp_path)})
|
|
45
|
+
assert "STRUCTURE INDEX" not in out and "def a()" in out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_markdown_headers_indexed(tmp_path):
|
|
49
|
+
body = "\n".join(["# Title"] + [f"para {i}" for i in range(2000)] + ["## Section B"])
|
|
50
|
+
(tmp_path / "doc.md").write_text(body)
|
|
51
|
+
out = tool_read({"file_path": "doc.md"}, {"cwd": str(tmp_path)})
|
|
52
|
+
assert "STRUCTURE INDEX" in out and "# Title" in out and "## Section B" in out
|
|
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
|