drydock-cli 3.0.112__tar.gz → 3.0.114__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.112/drydock_cli.egg-info → drydock_cli-3.0.114}/PKG-INFO +1 -1
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/graphrag.py +17 -14
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tools/__init__.py +145 -1
- {drydock_cli-3.0.112 → drydock_cli-3.0.114/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/pyproject.toml +1 -1
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_graphrag_quoted_path.py +33 -0
- drydock_cli-3.0.114/tests/test_screenshot.py +59 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/LICENSE +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/NOTICE +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/README.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/agent.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/cci.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/cli.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/config.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/detect.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/extract.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/guards.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/poam.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/providers.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/skills.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/stig.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/web.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/setup.cfg +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_config.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_xccdf.py +0 -0
|
@@ -134,25 +134,28 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
134
134
|
cleaned = [_unquote(p) for p in paths]
|
|
135
135
|
for fp in _iter_text_files([str(Path(cwd) / p) if not os.path.isabs(p) else p
|
|
136
136
|
for p in cleaned]):
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if
|
|
143
|
-
continue # unreadable / no PDF backend — skip cleanly
|
|
144
|
-
else:
|
|
145
|
-
try:
|
|
146
|
-
text = fp.read_text("utf-8", "ignore")
|
|
147
|
-
except OSError:
|
|
137
|
+
# Per-file isolation: ONE unreadable/odd file (a locked Word doc, a weird
|
|
138
|
+
# encoding, a PDF backend quirk) must never crash a whole-folder build —
|
|
139
|
+
# skip it and keep indexing the rest.
|
|
140
|
+
try:
|
|
141
|
+
rel = os.path.relpath(str(fp), cwd)
|
|
142
|
+
if rel in skip_sources:
|
|
148
143
|
continue
|
|
149
|
-
|
|
144
|
+
if fp.suffix.lower() in extract.EXTRACTABLE_EXT:
|
|
145
|
+
text = extract.extract_document(fp) # PDF/Word → text (or None)
|
|
146
|
+
else:
|
|
147
|
+
text = fp.read_text("utf-8", "ignore")
|
|
148
|
+
if not text or not text.strip():
|
|
149
|
+
continue # unreadable / empty — skip cleanly
|
|
150
|
+
# Build this file's chunks locally; commit only if it ALL succeeds so a
|
|
151
|
+
# mid-file failure leaves no partial state and isn't miscounted.
|
|
152
|
+
local = [(body, extract_entities(body)) for body in _chunk_text(text)]
|
|
153
|
+
except Exception: # noqa: BLE001 — isolate a bad file, never abort the build
|
|
150
154
|
continue
|
|
151
155
|
added += 1
|
|
152
156
|
skip_sources.add(rel) # don't double-ingest the same file in one call
|
|
153
|
-
for body in
|
|
157
|
+
for body, ents in local:
|
|
154
158
|
cid = len(chunks)
|
|
155
|
-
ents = extract_entities(body)
|
|
156
159
|
chunks.append({"id": cid, "source": rel, "text": body, "entities": ents})
|
|
157
160
|
for e in ents:
|
|
158
161
|
entity_chunks[e].add(cid)
|
|
@@ -244,6 +244,22 @@ SCHEMAS = [
|
|
|
244
244
|
"required": ["path"],
|
|
245
245
|
},
|
|
246
246
|
},
|
|
247
|
+
{
|
|
248
|
+
"name": "Screenshot",
|
|
249
|
+
"description": "CAPTURE the current screen and SEE it with your vision — use "
|
|
250
|
+
"when the user asks you to look at their screen, review what's "
|
|
251
|
+
"displayed, debug a GUI, or read something shown on screen. Takes "
|
|
252
|
+
"the screenshot and shows it to you directly (no need to call "
|
|
253
|
+
"ViewImage after). Works on Windows (PowerShell), macOS, and "
|
|
254
|
+
"Linux with a display. Optionally pass `path` to also save the PNG.",
|
|
255
|
+
"input_schema": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"path": {"type": "string",
|
|
259
|
+
"description": "Optional file to save the PNG to. Omit to use a temp file."},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
247
263
|
{
|
|
248
264
|
"name": "Write",
|
|
249
265
|
"description": "Write content to a file, creating parent directories as needed.",
|
|
@@ -607,6 +623,34 @@ SCHEMAS = [
|
|
|
607
623
|
"required": ["query"],
|
|
608
624
|
},
|
|
609
625
|
},
|
|
626
|
+
{
|
|
627
|
+
"name": "BuildKnowledge",
|
|
628
|
+
"description": (
|
|
629
|
+
"BUILD or extend the user's GraphRAG knowledge base FROM THEIR documents "
|
|
630
|
+
"or code. Call this YOURSELF when the user asks you to index / ingest / "
|
|
631
|
+
"'make a knowledge base from' a folder or file (e.g. 'create a knowledge "
|
|
632
|
+
"base from my Documents'). Give `path` (a file or directory). mode 'build' "
|
|
633
|
+
"= (re)build from scratch; 'add' = merge into an existing base. When it "
|
|
634
|
+
"succeeds you can search it with the Knowledge tool. Do NOT tell the user "
|
|
635
|
+
"to type a '/graphrag' slash command — actually do it with this tool."
|
|
636
|
+
),
|
|
637
|
+
"input_schema": {
|
|
638
|
+
"type": "object",
|
|
639
|
+
"properties": {
|
|
640
|
+
"path": {
|
|
641
|
+
"type": "string",
|
|
642
|
+
"description": "File or directory to index. A normal path — on "
|
|
643
|
+
"Windows e.g. C:\\Users\\me\\Documents (quotes optional).",
|
|
644
|
+
},
|
|
645
|
+
"mode": {
|
|
646
|
+
"type": "string",
|
|
647
|
+
"description": "'build' (rebuild from scratch, default) or 'add' "
|
|
648
|
+
"(merge into the existing base).",
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
"required": ["path"],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
610
654
|
]
|
|
611
655
|
|
|
612
656
|
# ── Tool implementations ──────────────────────────────────────────────────
|
|
@@ -683,6 +727,74 @@ def tool_viewimage(params: dict, config: dict) -> str:
|
|
|
683
727
|
"to you. Describe it, read any text in it, or use it to answer the task.")
|
|
684
728
|
|
|
685
729
|
|
|
730
|
+
def _capture_screen(out: str) -> str | None:
|
|
731
|
+
"""Grab the whole (virtual) screen to PNG `out`. Returns an error string, or
|
|
732
|
+
None on success. Per-OS: Windows uses PowerShell + System.Drawing; macOS uses
|
|
733
|
+
screencapture; Linux tries the common grabbers in turn."""
|
|
734
|
+
import shutil
|
|
735
|
+
import subprocess
|
|
736
|
+
try:
|
|
737
|
+
if _IS_WINDOWS:
|
|
738
|
+
ps = (
|
|
739
|
+
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; "
|
|
740
|
+
"$s=[System.Windows.Forms.SystemInformation]::VirtualScreen; "
|
|
741
|
+
"$b=New-Object System.Drawing.Bitmap $s.Width,$s.Height; "
|
|
742
|
+
"$g=[System.Drawing.Graphics]::FromImage($b); "
|
|
743
|
+
"$g.CopyFromScreen($s.Left,$s.Top,0,0,$b.Size); "
|
|
744
|
+
f"$b.Save('{out}',[System.Drawing.Imaging.ImageFormat]::Png); "
|
|
745
|
+
"$g.Dispose(); $b.Dispose()"
|
|
746
|
+
)
|
|
747
|
+
exe = shutil.which("pwsh") or shutil.which("powershell") or "powershell"
|
|
748
|
+
subprocess.run([exe, "-NoProfile", "-NonInteractive", "-Command", ps],
|
|
749
|
+
capture_output=True, timeout=30)
|
|
750
|
+
elif sys.platform == "darwin":
|
|
751
|
+
subprocess.run(["screencapture", "-x", out], capture_output=True, timeout=30)
|
|
752
|
+
else: # Linux/BSD — try grabbers in order of ubiquity (X11 + wayland)
|
|
753
|
+
grabbers = (["scrot", "-o", out], ["import", "-window", "root", out],
|
|
754
|
+
["gnome-screenshot", "-f", out], ["grim", out],
|
|
755
|
+
["spectacle", "-b", "-n", "-o", out], ["maim", out])
|
|
756
|
+
tried = [g[0] for g in grabbers if shutil.which(g[0])]
|
|
757
|
+
if not tried:
|
|
758
|
+
return ("no screenshot tool found. Install one of: scrot, imagemagick "
|
|
759
|
+
"(import), gnome-screenshot, grim (wayland), spectacle, maim.")
|
|
760
|
+
for g in grabbers:
|
|
761
|
+
if shutil.which(g[0]):
|
|
762
|
+
subprocess.run(g, capture_output=True, timeout=30)
|
|
763
|
+
if os.path.isfile(out) and os.path.getsize(out):
|
|
764
|
+
break
|
|
765
|
+
except subprocess.TimeoutExpired:
|
|
766
|
+
return "screen capture timed out (30s)."
|
|
767
|
+
except Exception as e: # noqa: BLE001 — report, never crash the loop
|
|
768
|
+
return f"screen capture failed: {e}"
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def tool_screenshot(params: dict, config: dict) -> str:
|
|
773
|
+
"""Capture the screen to a PNG and make it visible to the vision model — the
|
|
774
|
+
returned absolute path is auto-attached by the API boundary, same as ViewImage."""
|
|
775
|
+
import tempfile
|
|
776
|
+
|
|
777
|
+
raw = _as_str_arg(params.get("path")).strip()
|
|
778
|
+
out = os.path.abspath(_resolve_path(raw, config) if raw
|
|
779
|
+
else os.path.join(tempfile.gettempdir(), "drydock_screenshot.png"))
|
|
780
|
+
try:
|
|
781
|
+
if os.path.dirname(out):
|
|
782
|
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
783
|
+
except OSError:
|
|
784
|
+
pass
|
|
785
|
+
err = _capture_screen(out)
|
|
786
|
+
if err:
|
|
787
|
+
return (f"Error: {err} (On a headless server there is no display to capture.)")
|
|
788
|
+
if not os.path.isfile(out) or not os.path.getsize(out):
|
|
789
|
+
return "Error: screen capture produced no image (no display, or the grabber failed)."
|
|
790
|
+
size = os.path.getsize(out)
|
|
791
|
+
if size > _MAX_VIEW_IMAGE_BYTES:
|
|
792
|
+
return (f"Captured the screen to {out} but it is {size // 1_000_000}MB — too "
|
|
793
|
+
"large to view (limit 20MB). The file is saved; open it manually.")
|
|
794
|
+
return (f"Captured the screen to {out} (PNG, {size // 1024 or 1}KB) — it is now "
|
|
795
|
+
"visible to you. Describe what's on screen or use it to answer the task.")
|
|
796
|
+
|
|
797
|
+
|
|
686
798
|
def tool_read(params: dict, config: dict) -> str:
|
|
687
799
|
fp = _resolve_path(_as_str_arg(params.get("file_path")), config)
|
|
688
800
|
# Reading an image as text yields binary garbage — point at the vision tool.
|
|
@@ -1741,10 +1853,39 @@ def tool_knowledge(params: dict, config: dict) -> str:
|
|
|
1741
1853
|
return graphrag.format_results(result, query)
|
|
1742
1854
|
|
|
1743
1855
|
|
|
1856
|
+
def tool_build_knowledge(params: dict, config: dict) -> str:
|
|
1857
|
+
"""Build/extend the GraphRAG knowledge base from a file or directory of docs —
|
|
1858
|
+
the model-callable equivalent of `/graphrag build|add`, so the agent can do it
|
|
1859
|
+
itself when asked instead of telling the user to run a slash command."""
|
|
1860
|
+
from drydock import graphrag
|
|
1861
|
+
|
|
1862
|
+
path = _as_str_arg(params.get("path")).strip()
|
|
1863
|
+
if not path:
|
|
1864
|
+
return ("Error: `BuildKnowledge` needs a `path` — a file or directory of "
|
|
1865
|
+
"documents/code to index.")
|
|
1866
|
+
mode = (_as_str_arg(params.get("mode")) or "build").strip().lower()
|
|
1867
|
+
if mode not in ("build", "add"):
|
|
1868
|
+
mode = "build"
|
|
1869
|
+
cwd = config.get("cwd") or os.getcwd()
|
|
1870
|
+
store = config.get("graphrag_store") or graphrag.default_store_path(cwd)
|
|
1871
|
+
try:
|
|
1872
|
+
fn = graphrag.build_index if mode == "build" else graphrag.add_to_index
|
|
1873
|
+
stats = fn([path], store, cwd=cwd)
|
|
1874
|
+
except Exception as e: # noqa: BLE001 — report, never crash the agent loop
|
|
1875
|
+
return f"Error building the knowledge base from {path}: {e}"
|
|
1876
|
+
if not stats.get("chunks"):
|
|
1877
|
+
return (f"No indexable text found under {path}. Nothing was added. (Supported: "
|
|
1878
|
+
"text/code files, .docx, .pdf, .md, etc. Check the path is correct.)")
|
|
1879
|
+
verb = "Built" if mode == "build" else "Updated"
|
|
1880
|
+
return (f"{verb} the knowledge base: {stats['files']} files, {stats['chunks']} "
|
|
1881
|
+
f"chunks, {stats['entities']} entities. Search it with the Knowledge tool.")
|
|
1882
|
+
|
|
1883
|
+
|
|
1744
1884
|
# ── Register all tools ────────────────────────────────────────────────────
|
|
1745
1885
|
|
|
1746
1886
|
_TOOLS = [
|
|
1747
1887
|
("Read", tool_read, True),
|
|
1888
|
+
("Screenshot", tool_screenshot, True),
|
|
1748
1889
|
("Write", tool_write, False),
|
|
1749
1890
|
("Edit", tool_edit, False),
|
|
1750
1891
|
("Bash", tool_bash, False),
|
|
@@ -1754,6 +1895,7 @@ _TOOLS = [
|
|
|
1754
1895
|
("task", tool_task, True),
|
|
1755
1896
|
("Dispatch", tool_dispatch, True),
|
|
1756
1897
|
("Knowledge", tool_knowledge, True),
|
|
1898
|
+
("BuildKnowledge", tool_build_knowledge, False),
|
|
1757
1899
|
("GraphQuery", tool_graphquery, True),
|
|
1758
1900
|
("GraphAdd", tool_graphadd, False),
|
|
1759
1901
|
("StigRules", tool_stigrules, True),
|
|
@@ -1772,10 +1914,12 @@ def register_all():
|
|
|
1772
1914
|
name = schema["name"]
|
|
1773
1915
|
func = {
|
|
1774
1916
|
"Read": tool_read, "ViewImage": tool_viewimage,
|
|
1917
|
+
"Screenshot": tool_screenshot,
|
|
1775
1918
|
"Write": tool_write, "Edit": tool_edit,
|
|
1776
1919
|
"Bash": tool_bash, "Glob": tool_glob, "Grep": tool_grep,
|
|
1777
1920
|
"todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
|
|
1778
1921
|
"Consult": tool_consult, "Knowledge": tool_knowledge,
|
|
1922
|
+
"BuildKnowledge": tool_build_knowledge,
|
|
1779
1923
|
"GraphQuery": tool_graphquery, "GraphAdd": tool_graphadd,
|
|
1780
1924
|
"StigRules": tool_stigrules, "StigRule": tool_stigrule, "StigSet": tool_stigset,
|
|
1781
1925
|
"WebSearch": tool_websearch, "WebFetch": tool_webfetch,
|
|
@@ -1785,7 +1929,7 @@ def register_all():
|
|
|
1785
1929
|
# Read-only w.r.t. the parent's files (GitStatus/Diff/Log inspect only;
|
|
1786
1930
|
# GitCommit + GraphAdd write).
|
|
1787
1931
|
read_only = name in (
|
|
1788
|
-
"Read", "ViewImage", "Glob", "Grep", "task", "Dispatch", "Consult",
|
|
1932
|
+
"Read", "ViewImage", "Screenshot", "Glob", "Grep", "task", "Dispatch", "Consult",
|
|
1789
1933
|
"Knowledge", "GraphQuery", "StigRules", "StigRule",
|
|
1790
1934
|
"WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
|
|
1791
1935
|
)
|
|
@@ -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.114"
|
|
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"
|
|
@@ -41,3 +41,36 @@ def test_unquote_helper():
|
|
|
41
41
|
assert graphrag._unquote("/plain/path") == "/plain/path"
|
|
42
42
|
assert graphrag._unquote(' "spaced" ') == "spaced"
|
|
43
43
|
assert graphrag._unquote('"') == '"' # lone quote unchanged
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_one_bad_file_does_not_abort_build():
|
|
47
|
+
"""A single file that raises mid-processing must be skipped, not crash the
|
|
48
|
+
whole folder build (the Windows 'NoneType.strip' report)."""
|
|
49
|
+
from unittest import mock
|
|
50
|
+
src, store = _make_docs()
|
|
51
|
+
open(os.path.join(src, "boom.txt"), "w").write("EXPLODE here")
|
|
52
|
+
real = graphrag.extract_entities
|
|
53
|
+
|
|
54
|
+
def boom(text):
|
|
55
|
+
if "EXPLODE" in text:
|
|
56
|
+
raise AttributeError("'NoneType' object has no attribute 'strip'")
|
|
57
|
+
return real(text)
|
|
58
|
+
|
|
59
|
+
with mock.patch.object(graphrag, "extract_entities", boom):
|
|
60
|
+
stats = graphrag.build_index([src], store)
|
|
61
|
+
assert stats["files"] == 2 # the 2 good files indexed, boom.txt skipped (not counted)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_build_knowledge_tool_builds_and_reports():
|
|
65
|
+
from drydock import tools
|
|
66
|
+
src, store = _make_docs()
|
|
67
|
+
out = tools.tool_build_knowledge({"path": f'"{src}"', "mode": "build"},
|
|
68
|
+
{"cwd": os.getcwd(), "graphrag_store": store})
|
|
69
|
+
assert "Built the knowledge base" in out and "2 files" in out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_build_knowledge_available_to_gemma():
|
|
73
|
+
from drydock import tool_registry
|
|
74
|
+
from drydock.tuning import filter_tool_schemas
|
|
75
|
+
names = [s["name"] for s in filter_tool_schemas(tool_registry.schemas(), "gemma4")]
|
|
76
|
+
assert "BuildKnowledge" in names
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Screenshot tool: capture the screen to PNG and make it visible to the vision
|
|
2
|
+
model (the returned path auto-attaches, same path as ViewImage). Cross-platform:
|
|
3
|
+
PowerShell on Windows, screencapture on macOS, common grabbers on Linux."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
from drydock import tools, tool_registry
|
|
11
|
+
from drydock.providers import detect_image_paths
|
|
12
|
+
from drydock.tuning import filter_tool_schemas
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_registered_and_available_to_gemma():
|
|
16
|
+
names = [s["name"] for s in tool_registry.schemas()]
|
|
17
|
+
gnames = [s["name"] for s in filter_tool_schemas(tool_registry.schemas(), "gemma4")]
|
|
18
|
+
assert "Screenshot" in names and "Screenshot" in gnames
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_graceful_when_no_grabber(monkeypatch):
|
|
22
|
+
monkeypatch.setattr(tools, "_IS_WINDOWS", False)
|
|
23
|
+
monkeypatch.setattr("sys.platform", "linux")
|
|
24
|
+
monkeypatch.setattr("shutil.which", lambda _n: None) # no grabbers installed
|
|
25
|
+
out = tools.tool_screenshot({}, {"cwd": os.getcwd()})
|
|
26
|
+
assert out.startswith("Error") and "screenshot tool" in out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_success_returns_attachable_path():
|
|
30
|
+
png = tempfile.mktemp(suffix=".png")
|
|
31
|
+
|
|
32
|
+
def fake_cap(o):
|
|
33
|
+
open(o, "wb").write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 200)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
with mock.patch.object(tools, "_capture_screen", fake_cap):
|
|
37
|
+
out = tools.tool_screenshot({"path": png}, {"cwd": os.getcwd()})
|
|
38
|
+
assert "visible to you" in out
|
|
39
|
+
assert detect_image_paths(out) == [os.path.abspath(png)] # auto-attaches
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_windows_builds_powershell_capture():
|
|
43
|
+
calls = {}
|
|
44
|
+
with mock.patch.object(tools, "_IS_WINDOWS", True), \
|
|
45
|
+
mock.patch("shutil.which", side_effect=lambda n: r"C:\pwsh.exe" if n == "pwsh" else None), \
|
|
46
|
+
mock.patch("subprocess.run", side_effect=lambda a, **k: calls.setdefault("argv", a)):
|
|
47
|
+
assert tools._capture_screen(r"C:\shot.png") is None
|
|
48
|
+
argv = calls["argv"]
|
|
49
|
+
assert argv[0].endswith("pwsh.exe") and argv[1] == "-NoProfile"
|
|
50
|
+
assert "CopyFromScreen" in argv[-1] and "ImageFormat]::Png" in argv[-1]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_macos_uses_screencapture(monkeypatch):
|
|
54
|
+
calls = {}
|
|
55
|
+
monkeypatch.setattr(tools, "_IS_WINDOWS", False)
|
|
56
|
+
monkeypatch.setattr("sys.platform", "darwin")
|
|
57
|
+
monkeypatch.setattr("subprocess.run", lambda a, **k: calls.setdefault("argv", a))
|
|
58
|
+
tools._capture_screen("/tmp/x.png")
|
|
59
|
+
assert calls["argv"][0] == "screencapture"
|
|
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
|