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.
Files changed (123) hide show
  1. {drydock_cli-3.0.112/drydock_cli.egg-info → drydock_cli-3.0.114}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/graphrag.py +17 -14
  3. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tools/__init__.py +145 -1
  4. {drydock_cli-3.0.112 → drydock_cli-3.0.114/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/SOURCES.txt +1 -0
  6. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/pyproject.toml +1 -1
  7. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_graphrag_quoted_path.py +33 -0
  8. drydock_cli-3.0.114/tests/test_screenshot.py +59 -0
  9. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/LICENSE +0 -0
  10. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/NOTICE +0 -0
  11. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/README.md +0 -0
  12. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/__init__.py +0 -0
  13. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/__main__.py +0 -0
  14. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/advisor.py +0 -0
  15. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/agent.py +0 -0
  16. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/bash_safety.py +0 -0
  17. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/__init__.py +0 -0
  18. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-categorize.md +0 -0
  19. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-control.md +0 -0
  20. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-poam.md +0 -0
  21. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/rmf-review.md +0 -0
  22. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-assess.md +0 -0
  23. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/builtin_skills/stig-remediate.md +0 -0
  24. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/cci.py +0 -0
  25. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/cli.py +0 -0
  26. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/compaction.py +0 -0
  27. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/config.py +0 -0
  28. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/detect.py +0 -0
  29. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/extract.py +0 -0
  30. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/gittools.py +0 -0
  31. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/guards.py +0 -0
  32. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/loop_detect.py +0 -0
  33. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/mcp.py +0 -0
  34. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/poam.py +0 -0
  35. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/providers.py +0 -0
  36. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/rmf.py +0 -0
  37. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/rmf_graph.py +0 -0
  38. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/skills.py +0 -0
  39. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/stig.py +0 -0
  40. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tool_registry.py +0 -0
  41. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/__init__.py +0 -0
  42. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/app.py +0 -0
  43. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/approval.py +0 -0
  44. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/messages.py +0 -0
  45. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tui/widgets.py +0 -0
  46. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/tuning.py +0 -0
  47. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock/web.py +0 -0
  48. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/dependency_links.txt +0 -0
  49. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/entry_points.txt +0 -0
  50. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/requires.txt +0 -0
  51. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/drydock_cli.egg-info/top_level.txt +0 -0
  52. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/setup.cfg +0 -0
  53. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_advisor.py +0 -0
  54. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_approval.py +0 -0
  55. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_back_command.py +0 -0
  56. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_background.py +0 -0
  57. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_binary_output.py +0 -0
  58. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_crossplatform.py +0 -0
  59. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_output_bounding.py +0 -0
  60. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_process_group.py +0 -0
  61. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_safety.py +0 -0
  62. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_sanitize.py +0 -0
  63. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_shell.py +0 -0
  64. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_stdin.py +0 -0
  65. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_stop_partial.py +0 -0
  66. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_timeout_network.py +0 -0
  67. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_bash_timeout_param.py +0 -0
  68. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_cci.py +0 -0
  69. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_cli_agents.py +0 -0
  70. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_compact_command.py +0 -0
  71. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_compaction.py +0 -0
  72. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_config.py +0 -0
  73. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_config_migration.py +0 -0
  74. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_context_limit_config.py +0 -0
  75. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_detect.py +0 -0
  76. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_dispatch.py +0 -0
  77. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_e2e_connected.py +0 -0
  78. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_edit_replace_all.py +0 -0
  79. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_empty_response.py +0 -0
  80. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_extract.py +0 -0
  81. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_failure_loop.py +0 -0
  82. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_first_run_setup.py +0 -0
  83. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_gittools.py +0 -0
  84. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_glob_edit_edges.py +0 -0
  85. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_graphify_example.py +0 -0
  86. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_graphrag.py +0 -0
  87. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_grep_and_read_robust.py +0 -0
  88. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_guards_and_tools.py +0 -0
  89. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_hallucinated_tools.py +0 -0
  90. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_leaked_tool_call.py +0 -0
  91. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_loop_detect.py +0 -0
  92. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_mcp.py +0 -0
  93. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_oneshot_unreachable.py +0 -0
  94. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_overthink_interrupt.py +0 -0
  95. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_plan_autocontinue.py +0 -0
  96. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_poam.py +0 -0
  97. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_providers_unreachable.py +0 -0
  98. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_read_index.py +0 -0
  99. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_rmf.py +0 -0
  100. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_rmf_graph.py +0 -0
  101. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_rmf_stig_graph.py +0 -0
  102. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_runaway_repetition.py +0 -0
  103. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_server_probe.py +0 -0
  104. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_skills.py +0 -0
  105. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_stall_retry.py +0 -0
  106. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_stig.py +0 -0
  107. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_stop.py +0 -0
  108. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_streaming_newlines.py +0 -0
  109. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_subagent.py +0 -0
  110. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_system_prompt_help.py +0 -0
  111. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_todo.py +0 -0
  112. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion.py +0 -0
  113. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tool_arg_coercion_more.py +0 -0
  114. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tool_arg_parsing.py +0 -0
  115. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tools_undo.py +0 -0
  116. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tui.py +0 -0
  117. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_tuning.py +0 -0
  118. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_viewimage.py +0 -0
  119. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_vision_input.py +0 -0
  120. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_web_tools.py +0 -0
  121. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_windows_shell.py +0 -0
  122. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/tests/test_write_content_coerce.py +0 -0
  123. {drydock_cli-3.0.112 → drydock_cli-3.0.114}/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.112
3
+ Version: 3.0.114
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
@@ -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
- rel = os.path.relpath(str(fp), cwd)
138
- if rel in skip_sources:
139
- continue
140
- if fp.suffix.lower() in extract.EXTRACTABLE_EXT:
141
- text = extract.extract_document(fp) # PDF/Word → text (or None)
142
- if not text:
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
- if not text.strip():
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 _chunk_text(text):
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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.112
3
+ Version: 3.0.114
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
@@ -97,6 +97,7 @@ tests/test_rmf.py
97
97
  tests/test_rmf_graph.py
98
98
  tests/test_rmf_stig_graph.py
99
99
  tests/test_runaway_repetition.py
100
+ tests/test_screenshot.py
100
101
  tests/test_server_probe.py
101
102
  tests/test_skills.py
102
103
  tests/test_stall_retry.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.112"
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