drydock-cli 3.0.97__tar.gz → 3.0.99__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 (113) hide show
  1. {drydock_cli-3.0.97/drydock_cli.egg-info → drydock_cli-3.0.99}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tools/__init__.py +43 -8
  3. {drydock_cli-3.0.97 → drydock_cli-3.0.99/drydock_cli.egg-info}/PKG-INFO +1 -1
  4. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock_cli.egg-info/SOURCES.txt +2 -0
  5. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/pyproject.toml +1 -1
  6. drydock_cli-3.0.99/tests/test_tool_arg_coercion.py +49 -0
  7. drydock_cli-3.0.99/tests/test_write_content_coerce.py +37 -0
  8. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/LICENSE +0 -0
  9. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/NOTICE +0 -0
  10. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/README.md +0 -0
  11. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/__init__.py +0 -0
  12. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/__main__.py +0 -0
  13. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/advisor.py +0 -0
  14. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/agent.py +0 -0
  15. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/bash_safety.py +0 -0
  16. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/__init__.py +0 -0
  17. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-categorize.md +0 -0
  18. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-control.md +0 -0
  19. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-poam.md +0 -0
  20. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-review.md +0 -0
  21. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/stig-assess.md +0 -0
  22. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/builtin_skills/stig-remediate.md +0 -0
  23. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/cci.py +0 -0
  24. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/cli.py +0 -0
  25. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/compaction.py +0 -0
  26. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/config.py +0 -0
  27. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/detect.py +0 -0
  28. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/extract.py +0 -0
  29. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/gittools.py +0 -0
  30. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/graphrag.py +0 -0
  31. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/guards.py +0 -0
  32. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/loop_detect.py +0 -0
  33. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/mcp.py +0 -0
  34. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/poam.py +0 -0
  35. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/providers.py +0 -0
  36. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/rmf.py +0 -0
  37. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/rmf_graph.py +0 -0
  38. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/skills.py +0 -0
  39. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/stig.py +0 -0
  40. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tool_registry.py +0 -0
  41. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tui/__init__.py +0 -0
  42. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tui/app.py +0 -0
  43. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tui/approval.py +0 -0
  44. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tui/messages.py +0 -0
  45. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tui/widgets.py +0 -0
  46. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/tuning.py +0 -0
  47. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock/web.py +0 -0
  48. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock_cli.egg-info/dependency_links.txt +0 -0
  49. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock_cli.egg-info/entry_points.txt +0 -0
  50. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock_cli.egg-info/requires.txt +0 -0
  51. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/drydock_cli.egg-info/top_level.txt +0 -0
  52. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/setup.cfg +0 -0
  53. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_advisor.py +0 -0
  54. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_approval.py +0 -0
  55. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_back_command.py +0 -0
  56. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_background.py +0 -0
  57. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_binary_output.py +0 -0
  58. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_output_bounding.py +0 -0
  59. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_process_group.py +0 -0
  60. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_safety.py +0 -0
  61. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_sanitize.py +0 -0
  62. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_shell.py +0 -0
  63. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_stdin.py +0 -0
  64. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_timeout_network.py +0 -0
  65. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_bash_timeout_param.py +0 -0
  66. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_cci.py +0 -0
  67. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_cli_agents.py +0 -0
  68. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_compact_command.py +0 -0
  69. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_compaction.py +0 -0
  70. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_config.py +0 -0
  71. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_config_migration.py +0 -0
  72. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_context_limit_config.py +0 -0
  73. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_detect.py +0 -0
  74. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_dispatch.py +0 -0
  75. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_e2e_connected.py +0 -0
  76. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_empty_response.py +0 -0
  77. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_extract.py +0 -0
  78. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_failure_loop.py +0 -0
  79. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_first_run_setup.py +0 -0
  80. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_gittools.py +0 -0
  81. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_graphify_example.py +0 -0
  82. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_graphrag.py +0 -0
  83. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_grep_and_read_robust.py +0 -0
  84. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_guards_and_tools.py +0 -0
  85. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_hallucinated_tools.py +0 -0
  86. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_leaked_tool_call.py +0 -0
  87. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_loop_detect.py +0 -0
  88. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_mcp.py +0 -0
  89. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_oneshot_unreachable.py +0 -0
  90. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_plan_autocontinue.py +0 -0
  91. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_poam.py +0 -0
  92. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_providers_unreachable.py +0 -0
  93. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_read_index.py +0 -0
  94. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_rmf.py +0 -0
  95. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_rmf_graph.py +0 -0
  96. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_rmf_stig_graph.py +0 -0
  97. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_runaway_repetition.py +0 -0
  98. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_server_probe.py +0 -0
  99. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_skills.py +0 -0
  100. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_stig.py +0 -0
  101. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_stop.py +0 -0
  102. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_streaming_newlines.py +0 -0
  103. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_subagent.py +0 -0
  104. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_system_prompt_help.py +0 -0
  105. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_todo.py +0 -0
  106. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_tool_arg_parsing.py +0 -0
  107. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_tools_undo.py +0 -0
  108. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_tui.py +0 -0
  109. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_tuning.py +0 -0
  110. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_viewimage.py +0 -0
  111. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_vision_input.py +0 -0
  112. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/tests/test_web_tools.py +0 -0
  113. {drydock_cli-3.0.97 → drydock_cli-3.0.99}/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.97
3
+ Version: 3.0.99
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
@@ -26,6 +26,36 @@ _DEFAULT_TIMEOUT = 120
26
26
  _MAX_TIMEOUT = 1800 # 30 min hard ceiling — a single command shouldn't hang longer
27
27
 
28
28
 
29
+ def _as_text(v, default: str = "") -> str:
30
+ """Coerce a text-body arg (file content, replacement text) to a string. Local
31
+ models sometimes send it as a JSON array of lines → newline-join; None →
32
+ default; other scalars → str()."""
33
+ if isinstance(v, (list, tuple)):
34
+ return "\n".join(str(x) for x in v)
35
+ if v is None:
36
+ return default
37
+ return v if isinstance(v, str) else str(v)
38
+
39
+
40
+ def _as_str_arg(v, default: str = "") -> str:
41
+ """Coerce a scalar string arg (a path, a pattern) to a string. A single-value
42
+ list is unwrapped (models sometimes wrap a lone path/pattern in an array)."""
43
+ if isinstance(v, (list, tuple)):
44
+ return str(v[0]) if v else default
45
+ if v is None:
46
+ return default
47
+ return v if isinstance(v, str) else str(v)
48
+
49
+
50
+ def _coerce_int(value, default: int):
51
+ """Coerce a model-supplied int arg (offset/limit) that may arrive as a string
52
+ ("5") or junk. Falls back to default on anything non-numeric."""
53
+ try:
54
+ return int(float(value))
55
+ except (TypeError, ValueError):
56
+ return default
57
+
58
+
29
59
  def _coerce_timeout(value) -> int:
30
60
  """Make the model-supplied timeout robust: local models often send it as a
31
61
  string ("10"), and a 0/negative/absurd value would make EVERY command time
@@ -585,13 +615,13 @@ def tool_viewimage(params: dict, config: dict) -> str:
585
615
 
586
616
 
587
617
  def tool_read(params: dict, config: dict) -> str:
588
- fp = _resolve_path(params["file_path"], config)
618
+ fp = _resolve_path(_as_str_arg(params.get("file_path")), config)
589
619
  # Reading an image as text yields binary garbage — point at the vision tool.
590
620
  if os.path.splitext(fp)[1].lower() in _IMAGE_EXTS and os.path.isfile(fp):
591
621
  return (f"{params['file_path']} is an image — Read would return binary "
592
622
  "garbage. Use the ViewImage tool to actually SEE it.")
593
- limit = params.get("limit") # None = caller didn't specify a window
594
- offset = params.get("offset", 0)
623
+ limit = None if params.get("limit") is None else _coerce_int(params.get("limit"), 2000)
624
+ offset = _coerce_int(params.get("offset", 0), 0)
595
625
  try:
596
626
  with open(fp, "r", encoding="utf-8", errors="replace") as f:
597
627
  lines = f.readlines()
@@ -670,7 +700,10 @@ def tool_write(params: dict, config: dict) -> str:
670
700
  fp = _resolve_path(str(fp).strip(), config)
671
701
  if Path(fp).is_dir():
672
702
  return f"Error: {fp} is a directory, not a file. Pass a file path."
673
- content = params.get("content", "")
703
+ # Local models sometimes send content as a JSON array of lines (or a number),
704
+ # which f.write() can't take — coerce instead of crashing (tools must return
705
+ # errors, never raise). A list → newline-joined lines.
706
+ content = _as_text(params.get("content", ""))
674
707
  if has_conflict_markers(content):
675
708
  return conflict_marker_refusal(fp)
676
709
  prev = _snapshot(fp)
@@ -793,8 +826,10 @@ def tool_edit(params: dict, config: dict) -> str:
793
826
  if not fp or not str(fp).strip():
794
827
  return "Error: Edit needs a real file_path (the path was empty or blank)."
795
828
  fp = _resolve_path(str(fp).strip(), config)
796
- old = params.get("old_string", "")
797
- new = params.get("new_string", "")
829
+ # Coerce non-string old/new (a JSON array of lines, a number) instead of
830
+ # crashing with a TypeError in the substring search / replace.
831
+ old = _as_text(params.get("old_string", ""))
832
+ new = _as_text(params.get("new_string", ""))
798
833
  if has_conflict_markers(new):
799
834
  return conflict_marker_refusal(fp)
800
835
  # If a directory was passed, try to infer the intended file (avoids a
@@ -1098,8 +1133,8 @@ def tool_glob(params: dict, config: dict) -> str:
1098
1133
 
1099
1134
 
1100
1135
  def tool_grep(params: dict, config: dict) -> str:
1101
- pattern = params["pattern"]
1102
- path = params.get("path") or config.get("cwd") or "."
1136
+ pattern = _as_str_arg(params.get("pattern"))
1137
+ path = _as_str_arg(params.get("path")) or config.get("cwd") or "."
1103
1138
  include = params.get("include", "")
1104
1139
  try:
1105
1140
  cmd = ["grep", "-rn", "--color=never"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.97
3
+ Version: 3.0.99
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
@@ -99,6 +99,7 @@ tests/test_streaming_newlines.py
99
99
  tests/test_subagent.py
100
100
  tests/test_system_prompt_help.py
101
101
  tests/test_todo.py
102
+ tests/test_tool_arg_coercion.py
102
103
  tests/test_tool_arg_parsing.py
103
104
  tests/test_tools_undo.py
104
105
  tests/test_tui.py
@@ -106,4 +107,5 @@ tests/test_tuning.py
106
107
  tests/test_viewimage.py
107
108
  tests/test_vision_input.py
108
109
  tests/test_web_tools.py
110
+ tests/test_write_content_coerce.py
109
111
  tests/test_xccdf.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.97"
10
+ version = "3.0.99"
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,49 @@
1
+ """Model-supplied tool args that arrive as the wrong type (a JSON array of lines,
2
+ a number, a stringified int, a single-element-list-wrapped path) must be coerced,
3
+ never crash — tools return results, never raise. Systematic across the file tools."""
4
+ from __future__ import annotations
5
+
6
+ from drydock.tools import (
7
+ _as_text, _as_str_arg, _coerce_int,
8
+ tool_edit, tool_read, tool_grep, tool_write,
9
+ )
10
+
11
+
12
+ def test_helpers():
13
+ assert _as_text(["a", "b"]) == "a\nb"
14
+ assert _as_text(None) == "" and _as_text(42) == "42" and _as_text("x") == "x"
15
+ assert _as_str_arg(["/p"]) == "/p" and _as_str_arg(None) == "" and _as_str_arg(5) == "5"
16
+ assert _coerce_int("5", 0) == 5 and _coerce_int("junk", 7) == 7 and _coerce_int(None, 3) == 3
17
+
18
+
19
+ def _cfg(tmp): return {"cwd": str(tmp)}
20
+
21
+
22
+ def test_edit_non_string_new(tmp_path):
23
+ f = tmp_path / "e.txt"; f.write_text("alpha beta\n")
24
+ out = tool_edit({"file_path": str(f), "old_string": "alpha", "new_string": ["x", "y"]}, _cfg(tmp_path))
25
+ assert "Error" not in out and f.read_text().startswith("x\ny")
26
+
27
+
28
+ def test_edit_int_new(tmp_path):
29
+ f = tmp_path / "e.txt"; f.write_text("beta\n")
30
+ out = tool_edit({"file_path": str(f), "old_string": "beta", "new_string": 99}, _cfg(tmp_path))
31
+ assert "99" in f.read_text() and "Error" not in out
32
+
33
+
34
+ def test_read_list_path_and_str_offsets(tmp_path):
35
+ f = tmp_path / "r.txt"; f.write_text("line1\nline2\n")
36
+ out = tool_read({"file_path": [str(f)], "offset": "0", "limit": "5"}, _cfg(tmp_path))
37
+ assert "line1" in out
38
+
39
+
40
+ def test_grep_list_pattern(tmp_path):
41
+ f = tmp_path / "g.txt"; f.write_text("needle here\n")
42
+ out = tool_grep({"pattern": ["needle"], "path": str(f)}, _cfg(tmp_path))
43
+ assert "needle" in out
44
+
45
+
46
+ def test_write_list_content(tmp_path):
47
+ f = tmp_path / "w.txt"
48
+ tool_write({"file_path": str(f), "content": ["a", "b"]}, _cfg(tmp_path))
49
+ assert f.read_text() == "a\nb"
@@ -0,0 +1,37 @@
1
+ """tool_write must not raise on non-string content. Local models sometimes send
2
+ `content` as a JSON array of lines (or a number); f.write() would TypeError, and
3
+ tools must return a result, never raise. A list → newline-joined lines."""
4
+ from __future__ import annotations
5
+
6
+ from drydock.tools import tool_write
7
+
8
+
9
+ def _write(tmp_path, content):
10
+ fp = tmp_path / "out.txt"
11
+ out = tool_write({"file_path": str(fp), "content": content}, {"cwd": str(tmp_path)})
12
+ return out, fp.read_text()
13
+
14
+
15
+ def test_list_content_joined(tmp_path):
16
+ out, text = _write(tmp_path, ["line1", "line2", "line3"])
17
+ assert "Error" not in out and text == "line1\nline2\nline3"
18
+
19
+
20
+ def test_int_content_stringified(tmp_path):
21
+ out, text = _write(tmp_path, 42)
22
+ assert "Error" not in out and text == "42"
23
+
24
+
25
+ def test_none_content_empty(tmp_path):
26
+ out, text = _write(tmp_path, None)
27
+ assert "Error" not in out and text == ""
28
+
29
+
30
+ def test_normal_string_unchanged(tmp_path):
31
+ out, text = _write(tmp_path, "hello\nworld")
32
+ assert text == "hello\nworld"
33
+
34
+
35
+ def test_list_of_numbers(tmp_path):
36
+ out, text = _write(tmp_path, [1, 2, 3])
37
+ assert text == "1\n2\n3"
File without changes
File without changes
File without changes
File without changes