drydock-cli 3.0.134__tar.gz → 3.0.136__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.134/drydock_cli.egg-info → drydock_cli-3.0.136}/PKG-INFO +19 -4
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/README.md +18 -3
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/cli.py +24 -14
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/graphrag.py +227 -19
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tui/app.py +25 -5
- {drydock_cli-3.0.134 → drydock_cli-3.0.136/drydock_cli.egg-info}/PKG-INFO +19 -4
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/pyproject.toml +1 -1
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_config_migration.py +3 -3
- drydock_cli-3.0.136/tests/test_first_run_setup.py +81 -0
- drydock_cli-3.0.136/tests/test_graphrag_sqlite.py +70 -0
- drydock_cli-3.0.134/tests/test_first_run_setup.py +0 -34
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/LICENSE +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/NOTICE +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/agent.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/cci.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/config.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/detect.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/events.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/extract.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/guards.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/poam.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/providers.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/skills.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/stig.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tool_result.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/verification.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock/web.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/setup.cfg +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_config.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_events.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.134 → drydock_cli-3.0.136}/tests/test_xccdf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.136
|
|
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
|
|
@@ -107,6 +107,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
107
107
|
| `/graphrag add <path>` | Incrementally add more documents to the base |
|
|
108
108
|
| `/graphrag query <q>` | Test what the base returns (no model) |
|
|
109
109
|
| `/graphrag status` · `clear` | List indexed sources · wipe the base |
|
|
110
|
+
| `/graphrag migrate` | Convert a legacy JSON index to the fast SQLite store |
|
|
110
111
|
| `/skills` | List your skills |
|
|
111
112
|
| `/skills new <name> <prompt>` | Create a reusable `/<name>` skill (use `$ARGS` for input) |
|
|
112
113
|
| `/<name>` | Run a skill |
|
|
@@ -142,8 +143,8 @@ tool) when a question touches your material. Ingests text formats
|
|
|
142
143
|
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
143
144
|
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
144
145
|
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
145
|
-
drydock-cli[pdf]` (pypdf). The index is a
|
|
146
|
-
`<project>/.drydock/graphrag.
|
|
146
|
+
drydock-cli[pdf]` (pypdf). The index is a SQLite database at
|
|
147
|
+
`<project>/.drydock/graphrag.db` (FTS5-indexed, so queries stay fast at multi-GB scale) — clean-room, no embeddings.
|
|
147
148
|
|
|
148
149
|
### Custom skills
|
|
149
150
|
|
|
@@ -253,13 +254,27 @@ drydock
|
|
|
253
254
|
```
|
|
254
255
|
|
|
255
256
|
Requires Python 3.11+. From source instead:
|
|
256
|
-
`git clone https://github.com/fbobe321/drydock
|
|
257
|
+
`git clone https://github.com/fbobe321/drydock.git && cd drydock && pip install -e .`
|
|
257
258
|
|
|
258
259
|
On first launch with no config, Drydock probes localhost for a running local
|
|
259
260
|
LLM (llama.cpp/vLLM `:8000`, Ollama `:11434`, LM Studio `:1234`) and wires up
|
|
260
261
|
the first one it finds — no account or API-key prompt. Override anytime with
|
|
261
262
|
`--model` / `--provider` / `--base-url` or `~/.drydock/config.toml`.
|
|
262
263
|
|
|
264
|
+
### Docker
|
|
265
|
+
|
|
266
|
+
A prebuilt image is on Docker Hub. Drydock is just the agent — point it at your
|
|
267
|
+
own OpenAI-compatible model server (e.g. one running on the host):
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
docker run -it --add-host=host.docker.internal:host-gateway \
|
|
271
|
+
-v "$PWD:/work" fbobe3/drydock \
|
|
272
|
+
--base-url http://host.docker.internal:8000/v1 --model gemma4
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`-v "$PWD:/work"` mounts your project so the agent can read/edit it. Tags:
|
|
276
|
+
`fbobe3/drydock:latest` and `fbobe3/drydock:<version>` (e.g. `:3.0.135`).
|
|
277
|
+
|
|
263
278
|
## Using it
|
|
264
279
|
|
|
265
280
|
Type a task and press **Enter**. Drydock reads/writes/edits files and runs
|
|
@@ -83,6 +83,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
83
83
|
| `/graphrag add <path>` | Incrementally add more documents to the base |
|
|
84
84
|
| `/graphrag query <q>` | Test what the base returns (no model) |
|
|
85
85
|
| `/graphrag status` · `clear` | List indexed sources · wipe the base |
|
|
86
|
+
| `/graphrag migrate` | Convert a legacy JSON index to the fast SQLite store |
|
|
86
87
|
| `/skills` | List your skills |
|
|
87
88
|
| `/skills new <name> <prompt>` | Create a reusable `/<name>` skill (use `$ARGS` for input) |
|
|
88
89
|
| `/<name>` | Run a skill |
|
|
@@ -118,8 +119,8 @@ tool) when a question touches your material. Ingests text formats
|
|
|
118
119
|
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
119
120
|
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
120
121
|
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
121
|
-
drydock-cli[pdf]` (pypdf). The index is a
|
|
122
|
-
`<project>/.drydock/graphrag.
|
|
122
|
+
drydock-cli[pdf]` (pypdf). The index is a SQLite database at
|
|
123
|
+
`<project>/.drydock/graphrag.db` (FTS5-indexed, so queries stay fast at multi-GB scale) — clean-room, no embeddings.
|
|
123
124
|
|
|
124
125
|
### Custom skills
|
|
125
126
|
|
|
@@ -229,13 +230,27 @@ drydock
|
|
|
229
230
|
```
|
|
230
231
|
|
|
231
232
|
Requires Python 3.11+. From source instead:
|
|
232
|
-
`git clone https://github.com/fbobe321/drydock
|
|
233
|
+
`git clone https://github.com/fbobe321/drydock.git && cd drydock && pip install -e .`
|
|
233
234
|
|
|
234
235
|
On first launch with no config, Drydock probes localhost for a running local
|
|
235
236
|
LLM (llama.cpp/vLLM `:8000`, Ollama `:11434`, LM Studio `:1234`) and wires up
|
|
236
237
|
the first one it finds — no account or API-key prompt. Override anytime with
|
|
237
238
|
`--model` / `--provider` / `--base-url` or `~/.drydock/config.toml`.
|
|
238
239
|
|
|
240
|
+
### Docker
|
|
241
|
+
|
|
242
|
+
A prebuilt image is on Docker Hub. Drydock is just the agent — point it at your
|
|
243
|
+
own OpenAI-compatible model server (e.g. one running on the host):
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
docker run -it --add-host=host.docker.internal:host-gateway \
|
|
247
|
+
-v "$PWD:/work" fbobe3/drydock \
|
|
248
|
+
--base-url http://host.docker.internal:8000/v1 --model gemma4
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`-v "$PWD:/work"` mounts your project so the agent can read/edit it. Tags:
|
|
252
|
+
`fbobe3/drydock:latest` and `fbobe3/drydock:<version>` (e.g. `:3.0.135`).
|
|
253
|
+
|
|
239
254
|
## Using it
|
|
240
255
|
|
|
241
256
|
Type a task and press **Enter**. Drydock reads/writes/edits files and runs
|
|
@@ -248,6 +248,27 @@ def _summarize(inputs: dict) -> str:
|
|
|
248
248
|
|
|
249
249
|
# ── Entry point ───────────────────────────────────────────────────────────
|
|
250
250
|
|
|
251
|
+
def _resolve_first_run(cfg: dict, args, cfg_path, cfgmod, interactive: bool) -> tuple[dict, str]:
|
|
252
|
+
"""First-launch model-server resolution. AUTODETECT a live local server and
|
|
253
|
+
wire it up with ZERO prompts (the frictionless path). Only when nothing is
|
|
254
|
+
detected AND we're interactive do we ask the user to enter their own URL — so
|
|
255
|
+
the easy install (server already running) needs no input, while a custom
|
|
256
|
+
endpoint is still one prompt (or `--base-url`, or `/model url` later)."""
|
|
257
|
+
from drydock import detect
|
|
258
|
+
|
|
259
|
+
found = detect.detect_local_llms()
|
|
260
|
+
if found:
|
|
261
|
+
cfg["provider"] = found[0]["provider"]
|
|
262
|
+
cfg["base_url"] = found[0]["base_url"]
|
|
263
|
+
if found[0].get("models") and not getattr(args, "model", None):
|
|
264
|
+
cfg["model"] = found[0]["models"][0]
|
|
265
|
+
cfgmod.save_file(cfg, cfg_path)
|
|
266
|
+
return cfg, detect.onboarding_message(found)
|
|
267
|
+
if interactive:
|
|
268
|
+
return _first_run_setup(cfg, cfg_path, cfgmod)
|
|
269
|
+
return cfg, detect.onboarding_message(found)
|
|
270
|
+
|
|
271
|
+
|
|
251
272
|
def _first_run_setup(cfg: dict, cfg_path, cfgmod) -> tuple[dict, str]:
|
|
252
273
|
"""Interactive first-launch: ask for the model server URL + model name and
|
|
253
274
|
persist them to config.toml. Autodetect (if anything is listening) supplies
|
|
@@ -346,22 +367,11 @@ def main():
|
|
|
346
367
|
"context_limit": args.context_limit,
|
|
347
368
|
}, cfg_path)
|
|
348
369
|
|
|
349
|
-
# First launch:
|
|
350
|
-
# persist them to config.toml. Interactive only — when stdin isn't a TTY
|
|
351
|
-
# (piped / -p / cron) we fall back to silent autodetect so nothing blocks.
|
|
370
|
+
# First launch: autodetect a local server and wire it up (see _resolve_first_run).
|
|
352
371
|
onboarding = ""
|
|
353
372
|
if first_run and not args.provider and not args.base_url:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
else:
|
|
357
|
-
from drydock import detect
|
|
358
|
-
|
|
359
|
-
found = detect.detect_local_llms()
|
|
360
|
-
onboarding = detect.onboarding_message(found)
|
|
361
|
-
if found:
|
|
362
|
-
cfg["provider"] = found[0]["provider"]
|
|
363
|
-
cfg["base_url"] = found[0]["base_url"]
|
|
364
|
-
cfgmod.save_file(cfg, cfg_path)
|
|
373
|
+
interactive = sys.stdin.isatty() and not args.prompt
|
|
374
|
+
cfg, onboarding = _resolve_first_run(cfg, args, cfg_path, cfgmod, interactive)
|
|
365
375
|
|
|
366
376
|
config = {
|
|
367
377
|
# context_limit now comes from cfg (DEFAULTS < config.toml < --context-limit)
|
|
@@ -25,10 +25,11 @@ from __future__ import annotations
|
|
|
25
25
|
import json
|
|
26
26
|
import os
|
|
27
27
|
import re
|
|
28
|
+
import sqlite3
|
|
28
29
|
from collections import Counter, defaultdict
|
|
30
|
+
from pathlib import Path
|
|
29
31
|
|
|
30
32
|
from drydock import extract
|
|
31
|
-
from pathlib import Path
|
|
32
33
|
|
|
33
34
|
# Files we ingest as text. Everything else (binaries, images) is skipped.
|
|
34
35
|
_TEXT_EXT = {
|
|
@@ -59,8 +60,150 @@ _RE_WORD = re.compile(r"[a-zA-Z0-9_]+")
|
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def default_store_path(cwd: str) -> Path:
|
|
62
|
-
"""Project-local index
|
|
63
|
-
|
|
63
|
+
"""Project-local index. SQLite so queries touch only matching rows (fast even
|
|
64
|
+
at multi-GB scale) instead of parsing a giant JSON on every query."""
|
|
65
|
+
return Path(cwd) / ".drydock" / "graphrag.db"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_store(store_path) -> Path:
|
|
69
|
+
"""Given a requested store path, return the one that actually exists — a
|
|
70
|
+
``.db`` (SQLite) if present, else a legacy ``.json`` sibling, else the path
|
|
71
|
+
as given (a fresh build will create it)."""
|
|
72
|
+
p = Path(store_path)
|
|
73
|
+
if p.exists():
|
|
74
|
+
return p
|
|
75
|
+
db = p.with_suffix(".db")
|
|
76
|
+
if db.exists():
|
|
77
|
+
return db
|
|
78
|
+
js = p.with_suffix(".json")
|
|
79
|
+
if js.exists():
|
|
80
|
+
return js
|
|
81
|
+
return p
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── SQLite + FTS5 backend (the scalable store) ────────────────────────────
|
|
85
|
+
|
|
86
|
+
_SCHEMA = """
|
|
87
|
+
CREATE TABLE IF NOT EXISTS chunks(id INTEGER PRIMARY KEY, source TEXT, text TEXT);
|
|
88
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(text, content='chunks', content_rowid='id');
|
|
89
|
+
CREATE TABLE IF NOT EXISTS entities(entity TEXT, chunk_id INTEGER);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_entity ON entities(entity);
|
|
91
|
+
CREATE TABLE IF NOT EXISTS edges(a TEXT, b TEXT, weight INTEGER);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_edge_a ON edges(a);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT);
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
_FTS_STRIP = re.compile(r'[^A-Za-z0-9_ ]+')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_sqlite(path) -> bool:
|
|
100
|
+
return str(path).endswith(".db")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _write_sqlite(store_path, chunks, entity_chunks, edges) -> None:
|
|
104
|
+
"""(Re)build the SQLite store from the accumulated chunks/entities/edges."""
|
|
105
|
+
sp = Path(store_path)
|
|
106
|
+
sp.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
if sp.exists():
|
|
108
|
+
sp.unlink()
|
|
109
|
+
con = sqlite3.connect(str(sp))
|
|
110
|
+
try:
|
|
111
|
+
con.executescript(_SCHEMA)
|
|
112
|
+
con.executemany("INSERT INTO chunks(id,source,text) VALUES(?,?,?)",
|
|
113
|
+
((c["id"], c["source"], c["text"]) for c in chunks))
|
|
114
|
+
con.executemany("INSERT INTO chunks_fts(rowid,text) VALUES(?,?)",
|
|
115
|
+
((c["id"], c["text"]) for c in chunks))
|
|
116
|
+
con.executemany("INSERT INTO entities(entity,chunk_id) VALUES(?,?)",
|
|
117
|
+
((e, cid) for e, cids in entity_chunks.items() for cid in cids))
|
|
118
|
+
con.executemany("INSERT INTO edges(a,b,weight) VALUES(?,?,?)",
|
|
119
|
+
((a, b, w) for a, nbrs in edges.items() for b, w in nbrs.items()))
|
|
120
|
+
con.execute("INSERT OR REPLACE INTO meta(key,value) VALUES('version','2')")
|
|
121
|
+
con.commit()
|
|
122
|
+
finally:
|
|
123
|
+
con.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _fts_query(words) -> str:
|
|
127
|
+
"""Build a safe FTS5 MATCH expression (OR of quoted terms)."""
|
|
128
|
+
terms = []
|
|
129
|
+
for w in words:
|
|
130
|
+
w = _FTS_STRIP.sub(" ", w).strip()
|
|
131
|
+
if w:
|
|
132
|
+
terms.append('"' + w + '"')
|
|
133
|
+
return " OR ".join(terms)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _query_sqlite(store_path, query: str, k: int, hops: bool) -> dict:
|
|
137
|
+
"""Fast query: FTS5 full-text over chunk text + indexed entity/graph lookups.
|
|
138
|
+
Loads only the matching rows — no full-file parse."""
|
|
139
|
+
con = sqlite3.connect(str(store_path))
|
|
140
|
+
con.row_factory = sqlite3.Row
|
|
141
|
+
cur = con.cursor()
|
|
142
|
+
try:
|
|
143
|
+
q_entities = set(extract_entities(query))
|
|
144
|
+
q_words = {w for w in (w.lower() for w in _RE_WORD.findall(query))
|
|
145
|
+
if len(w) > 2 and w not in _STOPWORDS}
|
|
146
|
+
scores: dict[int, float] = defaultdict(float)
|
|
147
|
+
|
|
148
|
+
# 1) FTS5 keyword retrieval (indexed, bm25-ranked) — the main recall path.
|
|
149
|
+
expr = _fts_query(q_words)
|
|
150
|
+
if expr:
|
|
151
|
+
try:
|
|
152
|
+
rows = cur.execute(
|
|
153
|
+
"SELECT rowid, rank FROM chunks_fts WHERE chunks_fts MATCH ? "
|
|
154
|
+
"ORDER BY rank LIMIT 400", (expr,)).fetchall()
|
|
155
|
+
for r in rows:
|
|
156
|
+
scores[r["rowid"]] += 0.5 + min(2.0, -float(r["rank"]) / 4.0)
|
|
157
|
+
except sqlite3.OperationalError:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# 2) Exact entity hits (indexed) weigh most; collect matched entities.
|
|
161
|
+
matched: set[str] = set()
|
|
162
|
+
cand = {e.lower() for e in q_entities} | q_words
|
|
163
|
+
for qe in cand:
|
|
164
|
+
for r in cur.execute("SELECT chunk_id, entity FROM entities WHERE entity=?", (qe,)):
|
|
165
|
+
scores[r["chunk_id"]] += 3.0
|
|
166
|
+
matched.add(r["entity"])
|
|
167
|
+
|
|
168
|
+
# 3) 1-hop graph expansion over the strongest neighbors of matched entities.
|
|
169
|
+
related: list[str] = []
|
|
170
|
+
if hops and matched:
|
|
171
|
+
for e in list(matched):
|
|
172
|
+
nbrs = cur.execute("SELECT b, weight FROM edges WHERE a=? ORDER BY weight DESC LIMIT 5",
|
|
173
|
+
(e,)).fetchall()
|
|
174
|
+
for nb in nbrs:
|
|
175
|
+
if nb["b"] not in matched:
|
|
176
|
+
related.append(nb["b"])
|
|
177
|
+
for r in cur.execute("SELECT chunk_id FROM entities WHERE entity=?", (nb["b"],)):
|
|
178
|
+
scores[r["chunk_id"]] += 1.0
|
|
179
|
+
|
|
180
|
+
if not scores:
|
|
181
|
+
return {"chunks": [], "related": []}
|
|
182
|
+
ranked = sorted(scores.items(), key=lambda kv: -kv[1])[:k]
|
|
183
|
+
out = []
|
|
184
|
+
for cid, sc in ranked:
|
|
185
|
+
if sc <= 0:
|
|
186
|
+
continue
|
|
187
|
+
row = cur.execute("SELECT source, text FROM chunks WHERE id=?", (cid,)).fetchone()
|
|
188
|
+
if row:
|
|
189
|
+
out.append({"source": row["source"], "text": row["text"], "score": round(sc, 1)})
|
|
190
|
+
seen: set[str] = set()
|
|
191
|
+
rel_unique = [r for r in related if not (r in seen or seen.add(r))][:12]
|
|
192
|
+
return {"chunks": out, "related": rel_unique}
|
|
193
|
+
finally:
|
|
194
|
+
con.close()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def migrate_json_to_sqlite(json_path, db_path) -> dict:
|
|
198
|
+
"""One-time conversion of a legacy JSON index to the SQLite store. Loads the
|
|
199
|
+
JSON once (slow for a huge file, but one-time), then queries are fast."""
|
|
200
|
+
data = json.loads(Path(json_path).read_text("utf-8"))
|
|
201
|
+
chunks = data.get("chunks", [])
|
|
202
|
+
entity_chunks = {e: list(cids) for e, cids in data.get("entities", {}).items()}
|
|
203
|
+
edges = {a: dict(nbrs) for a, nbrs in data.get("edges", {}).items()}
|
|
204
|
+
_write_sqlite(db_path, chunks, entity_chunks, edges)
|
|
205
|
+
return {"chunks": len(chunks), "entities": len(entity_chunks),
|
|
206
|
+
"edges": sum(len(n) for n in edges.values())}
|
|
64
207
|
|
|
65
208
|
|
|
66
209
|
def extract_entities(text: str) -> list[str]:
|
|
@@ -167,15 +310,22 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
167
310
|
|
|
168
311
|
|
|
169
312
|
def _save(store_path, chunks, entity_chunks, edges, files_added):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
313
|
+
# SQLite by default (scalable). A caller can still target a .json path
|
|
314
|
+
# explicitly (small/portable indexes, tests) and get the legacy format.
|
|
315
|
+
if _is_sqlite(store_path):
|
|
316
|
+
_write_sqlite(store_path, chunks,
|
|
317
|
+
{e: sorted(cids) for e, cids in entity_chunks.items()},
|
|
318
|
+
{a: dict(nbrs) for a, nbrs in edges.items()})
|
|
319
|
+
else:
|
|
320
|
+
index = {
|
|
321
|
+
"version": 1,
|
|
322
|
+
"chunks": chunks,
|
|
323
|
+
"entities": {e: sorted(cids) for e, cids in entity_chunks.items()},
|
|
324
|
+
"edges": {a: dict(nbrs) for a, nbrs in edges.items()},
|
|
325
|
+
}
|
|
326
|
+
sp = Path(store_path)
|
|
327
|
+
sp.parent.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
sp.write_text(json.dumps(index), encoding="utf-8")
|
|
179
329
|
return {
|
|
180
330
|
"files": files_added,
|
|
181
331
|
"chunks": len(chunks),
|
|
@@ -194,13 +344,36 @@ def build_index(paths: list[str], store_path: str | Path, *, cwd: str = ".") ->
|
|
|
194
344
|
return _save(store_path, chunks, entity_chunks, edges, added)
|
|
195
345
|
|
|
196
346
|
|
|
347
|
+
def _read_all(store_path) -> dict:
|
|
348
|
+
"""Read a whole index into a dict (chunks/entities/edges) — used by add_to_index
|
|
349
|
+
to append. Handles both the SQLite store and a legacy JSON file."""
|
|
350
|
+
sp = _resolve_store(store_path)
|
|
351
|
+
if _is_sqlite(sp):
|
|
352
|
+
con = sqlite3.connect(str(sp))
|
|
353
|
+
con.row_factory = sqlite3.Row
|
|
354
|
+
try:
|
|
355
|
+
chunks = [{"id": r["id"], "source": r["source"], "text": r["text"]}
|
|
356
|
+
for r in con.execute("SELECT id,source,text FROM chunks")]
|
|
357
|
+
entities: dict[str, list[int]] = defaultdict(list)
|
|
358
|
+
for r in con.execute("SELECT entity,chunk_id FROM entities"):
|
|
359
|
+
entities[r["entity"]].append(r["chunk_id"])
|
|
360
|
+
edges: dict[str, dict] = defaultdict(dict)
|
|
361
|
+
for r in con.execute("SELECT a,b,weight FROM edges"):
|
|
362
|
+
edges[r["a"]][r["b"]] = r["weight"]
|
|
363
|
+
return {"chunks": chunks, "entities": entities, "edges": edges}
|
|
364
|
+
finally:
|
|
365
|
+
con.close()
|
|
366
|
+
return json.loads(Path(sp).read_text("utf-8"))
|
|
367
|
+
|
|
368
|
+
|
|
197
369
|
def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = ".") -> dict:
|
|
198
370
|
"""Incrementally ADD documents to an existing index (build it if none yet).
|
|
199
371
|
Files already indexed (by relative path) are skipped — clear+build to refresh
|
|
200
372
|
changed files. Returns {files (added), chunks, entities, edges} totals."""
|
|
201
|
-
|
|
202
|
-
if
|
|
373
|
+
sp = _resolve_store(store_path)
|
|
374
|
+
if not sp.exists():
|
|
203
375
|
return build_index(paths, store_path, cwd=cwd)
|
|
376
|
+
existing = _read_all(sp)
|
|
204
377
|
chunks: list[dict] = list(existing.get("chunks", []))
|
|
205
378
|
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
206
379
|
for e, cids in existing.get("entities", {}).items():
|
|
@@ -210,20 +383,50 @@ def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = ".") ->
|
|
|
210
383
|
edges[a] = Counter(nbrs)
|
|
211
384
|
skip = {c["source"] for c in chunks}
|
|
212
385
|
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip)
|
|
213
|
-
return _save(
|
|
386
|
+
return _save(sp, chunks, entity_chunks, edges, added)
|
|
214
387
|
|
|
215
388
|
|
|
216
389
|
def sources(index: dict) -> list[str]:
|
|
217
|
-
"""The distinct source files in an index, sorted.
|
|
390
|
+
"""The distinct source files in an index, sorted. Accepts a loaded JSON dict
|
|
391
|
+
OR a SQLite handle ({'_db': path})."""
|
|
392
|
+
db = index.get("_db")
|
|
393
|
+
if db:
|
|
394
|
+
con = sqlite3.connect(str(db))
|
|
395
|
+
try:
|
|
396
|
+
return sorted(r[0] for r in con.execute("SELECT DISTINCT source FROM chunks"))
|
|
397
|
+
finally:
|
|
398
|
+
con.close()
|
|
218
399
|
return sorted({c["source"] for c in index.get("chunks", [])})
|
|
219
400
|
|
|
220
401
|
|
|
221
|
-
def
|
|
222
|
-
|
|
402
|
+
def index_stats(index: dict) -> dict:
|
|
403
|
+
"""{chunks, entities} counts for a loaded dict OR a SQLite handle — without
|
|
404
|
+
reading a large index into memory."""
|
|
405
|
+
db = index.get("_db")
|
|
406
|
+
if db:
|
|
407
|
+
con = sqlite3.connect(str(db))
|
|
408
|
+
try:
|
|
409
|
+
c = con.execute("SELECT COUNT(*) FROM chunks").fetchone()[0]
|
|
410
|
+
e = con.execute("SELECT COUNT(DISTINCT entity) FROM entities").fetchone()[0]
|
|
411
|
+
return {"chunks": c, "entities": e}
|
|
412
|
+
finally:
|
|
413
|
+
con.close()
|
|
414
|
+
return {"chunks": len(index.get("chunks", [])), "entities": len(index.get("entities", {}))}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def load_index(store_path: str | Path):
|
|
418
|
+
"""Return a query handle for the index at store_path, or None if none exists.
|
|
419
|
+
|
|
420
|
+
For the SQLite store this is a tiny handle ({'_db': path}) — it does NOT read
|
|
421
|
+
the data (queries hit the DB directly, so a multi-GB index stays instant). For
|
|
422
|
+
a legacy JSON file it loads the dict (small/portable indexes)."""
|
|
423
|
+
sp = _resolve_store(store_path)
|
|
223
424
|
if not sp.exists():
|
|
224
425
|
return None
|
|
426
|
+
if _is_sqlite(sp):
|
|
427
|
+
return {"_db": str(sp)}
|
|
225
428
|
try:
|
|
226
|
-
return json.loads(sp.read_text("utf-8"))
|
|
429
|
+
return json.loads(Path(sp).read_text("utf-8"))
|
|
227
430
|
except (OSError, ValueError):
|
|
228
431
|
return None
|
|
229
432
|
|
|
@@ -233,6 +436,11 @@ def query_index(index: dict, query: str, *, k: int = 5, hops: bool = True) -> di
|
|
|
233
436
|
|
|
234
437
|
Returns {chunks: [{source, text, score}], related: [entities]}.
|
|
235
438
|
"""
|
|
439
|
+
# SQLite handle → the fast, indexed path (no full-file load).
|
|
440
|
+
db = index.get("_db")
|
|
441
|
+
if db:
|
|
442
|
+
return _query_sqlite(db, query, k, hops)
|
|
443
|
+
|
|
236
444
|
chunks = index.get("chunks", [])
|
|
237
445
|
entities = index.get("entities", {})
|
|
238
446
|
edges = index.get("edges", {})
|
|
@@ -1125,23 +1125,43 @@ class DrydockApp(App):
|
|
|
1125
1125
|
self._info("No knowledge base yet. Build one: /graphrag build <path>")
|
|
1126
1126
|
else:
|
|
1127
1127
|
srcs = graphrag.sources(index)
|
|
1128
|
+
st = graphrag.index_stats(index)
|
|
1128
1129
|
shown = "\n".join(f" · {s}" for s in srcs[:20])
|
|
1129
1130
|
more = f"\n … +{len(srcs) - 20} more" if len(srcs) > 20 else ""
|
|
1131
|
+
resolved = graphrag._resolve_store(store)
|
|
1132
|
+
legacy = " ⚠ legacy JSON — run /graphrag migrate for fast queries" \
|
|
1133
|
+
if str(resolved).endswith(".json") else ""
|
|
1130
1134
|
self._info(
|
|
1131
|
-
f"Knowledge base: {
|
|
1132
|
-
f"{len(
|
|
1133
|
-
f"({store}).\n{shown}{more}"
|
|
1135
|
+
f"Knowledge base: {st['chunks']} chunks · {st['entities']} entities · "
|
|
1136
|
+
f"{len(srcs)} sources ({resolved}).{legacy}\n{shown}{more}"
|
|
1134
1137
|
)
|
|
1138
|
+
elif sub == "migrate":
|
|
1139
|
+
resolved = graphrag._resolve_store(store)
|
|
1140
|
+
if not str(resolved).endswith(".json"):
|
|
1141
|
+
self._info("Already using the fast SQLite store — nothing to migrate.")
|
|
1142
|
+
return
|
|
1143
|
+
db = resolved.with_suffix(".db")
|
|
1144
|
+
self._info(f"Migrating legacy index → {db} (one-time; loads the JSON once) …")
|
|
1145
|
+
try:
|
|
1146
|
+
m = graphrag.migrate_json_to_sqlite(resolved, db)
|
|
1147
|
+
except Exception as e: # noqa: BLE001
|
|
1148
|
+
self._mount(ErrorMessage(f"migrate failed: {e}"))
|
|
1149
|
+
return
|
|
1150
|
+
self._info(
|
|
1151
|
+
f"✓ Migrated: {m['chunks']} chunks · {m['entities']} entities → {db}. "
|
|
1152
|
+
f"Queries now hit the index directly (no full-file load). You can delete "
|
|
1153
|
+
f"the old {resolved.name} once you've confirmed queries work."
|
|
1154
|
+
)
|
|
1135
1155
|
elif sub == "clear":
|
|
1136
1156
|
try:
|
|
1137
|
-
store.unlink(missing_ok=True)
|
|
1157
|
+
graphrag._resolve_store(store).unlink(missing_ok=True)
|
|
1138
1158
|
self._info("Knowledge base cleared.")
|
|
1139
1159
|
except OSError as e:
|
|
1140
1160
|
self._mount(ErrorMessage(f"could not clear: {e}"))
|
|
1141
1161
|
else:
|
|
1142
1162
|
self._info(
|
|
1143
1163
|
"usage: /graphrag build <path> · add <path> · query <question> "
|
|
1144
|
-
"· status · clear"
|
|
1164
|
+
"· status · migrate · clear"
|
|
1145
1165
|
)
|
|
1146
1166
|
|
|
1147
1167
|
def _persist_config(self) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.136
|
|
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
|
|
@@ -107,6 +107,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
107
107
|
| `/graphrag add <path>` | Incrementally add more documents to the base |
|
|
108
108
|
| `/graphrag query <q>` | Test what the base returns (no model) |
|
|
109
109
|
| `/graphrag status` · `clear` | List indexed sources · wipe the base |
|
|
110
|
+
| `/graphrag migrate` | Convert a legacy JSON index to the fast SQLite store |
|
|
110
111
|
| `/skills` | List your skills |
|
|
111
112
|
| `/skills new <name> <prompt>` | Create a reusable `/<name>` skill (use `$ARGS` for input) |
|
|
112
113
|
| `/<name>` | Run a skill |
|
|
@@ -142,8 +143,8 @@ tool) when a question touches your material. Ingests text formats
|
|
|
142
143
|
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
143
144
|
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
144
145
|
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
145
|
-
drydock-cli[pdf]` (pypdf). The index is a
|
|
146
|
-
`<project>/.drydock/graphrag.
|
|
146
|
+
drydock-cli[pdf]` (pypdf). The index is a SQLite database at
|
|
147
|
+
`<project>/.drydock/graphrag.db` (FTS5-indexed, so queries stay fast at multi-GB scale) — clean-room, no embeddings.
|
|
147
148
|
|
|
148
149
|
### Custom skills
|
|
149
150
|
|
|
@@ -253,13 +254,27 @@ drydock
|
|
|
253
254
|
```
|
|
254
255
|
|
|
255
256
|
Requires Python 3.11+. From source instead:
|
|
256
|
-
`git clone https://github.com/fbobe321/drydock
|
|
257
|
+
`git clone https://github.com/fbobe321/drydock.git && cd drydock && pip install -e .`
|
|
257
258
|
|
|
258
259
|
On first launch with no config, Drydock probes localhost for a running local
|
|
259
260
|
LLM (llama.cpp/vLLM `:8000`, Ollama `:11434`, LM Studio `:1234`) and wires up
|
|
260
261
|
the first one it finds — no account or API-key prompt. Override anytime with
|
|
261
262
|
`--model` / `--provider` / `--base-url` or `~/.drydock/config.toml`.
|
|
262
263
|
|
|
264
|
+
### Docker
|
|
265
|
+
|
|
266
|
+
A prebuilt image is on Docker Hub. Drydock is just the agent — point it at your
|
|
267
|
+
own OpenAI-compatible model server (e.g. one running on the host):
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
docker run -it --add-host=host.docker.internal:host-gateway \
|
|
271
|
+
-v "$PWD:/work" fbobe3/drydock \
|
|
272
|
+
--base-url http://host.docker.internal:8000/v1 --model gemma4
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`-v "$PWD:/work"` mounts your project so the agent can read/edit it. Tags:
|
|
276
|
+
`fbobe3/drydock:latest` and `fbobe3/drydock:<version>` (e.g. `:3.0.135`).
|
|
277
|
+
|
|
263
278
|
## Using it
|
|
264
279
|
|
|
265
280
|
Type a task and press **Enter**. Drydock reads/writes/edits files and runs
|
|
@@ -96,6 +96,7 @@ tests/test_glob_edit_edges.py
|
|
|
96
96
|
tests/test_graphify_example.py
|
|
97
97
|
tests/test_graphrag.py
|
|
98
98
|
tests/test_graphrag_quoted_path.py
|
|
99
|
+
tests/test_graphrag_sqlite.py
|
|
99
100
|
tests/test_grep_and_read_robust.py
|
|
100
101
|
tests/test_guards_and_tools.py
|
|
101
102
|
tests/test_hallucinated_tools.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.136"
|
|
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"
|
|
@@ -77,8 +77,8 @@ def test_partial_config_backfills_missing_keys(tmp_path):
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def test_explicit_user_values_are_respected(tmp_path):
|
|
80
|
-
m = _load(tmp_path, 'base_url = "http://192.
|
|
81
|
-
assert m["base_url"] == "http://192.
|
|
80
|
+
m = _load(tmp_path, 'base_url = "http://192.0.2.10:8000/v1"\nmodel = "gemma4"\n')
|
|
81
|
+
assert m["base_url"] == "http://192.0.2.10:8000/v1"
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def test_default_base_url_is_concrete_not_empty():
|
|
@@ -108,7 +108,7 @@ def test_v2_file_is_migrated_and_backed_up(tmp_path):
|
|
|
108
108
|
def test_real_v3_file_is_left_untouched(tmp_path):
|
|
109
109
|
"""A genuine v3 file (has a known key) must NOT be migrated/rewritten."""
|
|
110
110
|
p = tmp_path / "config.toml"
|
|
111
|
-
p.write_text('base_url = "http://192.
|
|
111
|
+
p.write_text('base_url = "http://192.0.2.20:8000/v1"\n# my hand edit\n')
|
|
112
112
|
cfg.resolve({}, path=p)
|
|
113
113
|
assert not p.with_name("config.toml.bak").exists()
|
|
114
114
|
assert "# my hand edit" in p.read_text() # untouched
|