drydock-cli 3.0.135__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.135/drydock_cli.egg-info → drydock_cli-3.0.136}/PKG-INFO +18 -3
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/README.md +17 -2
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/graphrag.py +227 -19
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tui/app.py +25 -5
- {drydock_cli-3.0.135 → drydock_cli-3.0.136/drydock_cli.egg-info}/PKG-INFO +18 -3
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/pyproject.toml +1 -1
- drydock_cli-3.0.136/tests/test_graphrag_sqlite.py +70 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/LICENSE +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/NOTICE +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/agent.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/cci.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/cli.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/config.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/detect.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/events.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/extract.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/guards.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/poam.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/providers.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/skills.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/stig.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tool_result.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/verification.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock/web.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/setup.cfg +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_config.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_events.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.135 → drydock_cli-3.0.136}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.135 → 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
|
|
|
@@ -260,6 +261,20 @@ 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
|
|
|
@@ -236,6 +237,20 @@ 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
|
|
@@ -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
|
|
|
@@ -260,6 +261,20 @@ 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"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""GraphRAG SQLite+FTS5 backend: scalable store that queries only matching rows
|
|
2
|
+
(no full-file load), with legacy-JSON migration."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
from drydock import graphrag as g
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _docs():
|
|
12
|
+
d = tempfile.mkdtemp()
|
|
13
|
+
docs = os.path.join(d, "docs"); os.makedirs(docs)
|
|
14
|
+
open(os.path.join(docs, "a.md"), "w").write(
|
|
15
|
+
"# Auth\nThe TokenValidator checks JWT expiry. Refunds via the BillingService.")
|
|
16
|
+
open(os.path.join(docs, "b.md"), "w").write(
|
|
17
|
+
"# Billing\nThe BillingService issues refunds within 30 days.")
|
|
18
|
+
return d, docs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_default_store_is_sqlite():
|
|
22
|
+
assert str(g.default_store_path("/x")).endswith(".db")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_build_query_sqlite():
|
|
26
|
+
d, docs = _docs()
|
|
27
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
28
|
+
stats = g.build_index([docs], store, cwd=d)
|
|
29
|
+
assert stats["chunks"] == 2 and os.path.exists(store)
|
|
30
|
+
idx = g.load_index(store)
|
|
31
|
+
assert idx == {"_db": store} # lazy handle, not the data
|
|
32
|
+
r = g.query_index(idx, "how are refunds handled", k=3)
|
|
33
|
+
assert r["chunks"] and any("refund" in c["text"].lower() for c in r["chunks"])
|
|
34
|
+
assert g.sources(idx) == ["docs/a.md", "docs/b.md"]
|
|
35
|
+
assert g.index_stats(idx)["chunks"] == 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_add_to_sqlite_appends():
|
|
39
|
+
d, docs = _docs()
|
|
40
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
41
|
+
g.build_index([docs], store, cwd=d)
|
|
42
|
+
open(os.path.join(docs, "c.md"), "w").write("# Refund policy\nPartial refunds allowed after 30 days.")
|
|
43
|
+
s2 = g.add_to_index([docs], store, cwd=d)
|
|
44
|
+
assert s2["files"] == 1 and s2["chunks"] == 3
|
|
45
|
+
r = g.query_index(g.load_index(store), "partial refund policy", k=3)
|
|
46
|
+
assert any(c["source"] == "docs/c.md" for c in r["chunks"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_migrate_json_to_sqlite():
|
|
50
|
+
d, docs = _docs()
|
|
51
|
+
js = os.path.join(d, "old.json")
|
|
52
|
+
g.build_index([docs], js, cwd=d) # explicit .json → legacy format
|
|
53
|
+
assert os.path.getsize(js) > 0
|
|
54
|
+
db = os.path.join(d, "new.db")
|
|
55
|
+
m = g.migrate_json_to_sqlite(js, db)
|
|
56
|
+
assert m["chunks"] == 2 and os.path.exists(db)
|
|
57
|
+
r = g.query_index(g.load_index(db), "refunds", k=2)
|
|
58
|
+
assert r["chunks"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_resolve_prefers_db_then_json():
|
|
62
|
+
d, docs = _docs()
|
|
63
|
+
base = os.path.join(d, ".drydock", "graphrag.db")
|
|
64
|
+
g.build_index([docs], base, cwd=d)
|
|
65
|
+
# asking for the .json path resolves to the existing .db
|
|
66
|
+
assert str(g._resolve_store(base.replace(".db", ".json"))).endswith(".db")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_missing_store_returns_none():
|
|
70
|
+
assert g.load_index("/no/such/graphrag.db") is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|