drydock-cli 3.0.98__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.98/drydock_cli.egg-info → drydock_cli-3.0.99}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tools/__init__.py +42 -14
  3. {drydock_cli-3.0.98 → drydock_cli-3.0.99/drydock_cli.egg-info}/PKG-INFO +1 -1
  4. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock_cli.egg-info/SOURCES.txt +1 -0
  5. {drydock_cli-3.0.98 → 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.98 → drydock_cli-3.0.99}/LICENSE +0 -0
  8. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/NOTICE +0 -0
  9. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/README.md +0 -0
  10. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/__init__.py +0 -0
  11. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/advisor.py +0 -0
  13. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/agent.py +0 -0
  14. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/bash_safety.py +0 -0
  15. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/__init__.py +0 -0
  16. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-categorize.md +0 -0
  17. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-control.md +0 -0
  18. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-poam.md +0 -0
  19. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/rmf-review.md +0 -0
  20. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/stig-assess.md +0 -0
  21. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/builtin_skills/stig-remediate.md +0 -0
  22. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/cci.py +0 -0
  23. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/cli.py +0 -0
  24. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/compaction.py +0 -0
  25. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/config.py +0 -0
  26. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/detect.py +0 -0
  27. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/extract.py +0 -0
  28. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/gittools.py +0 -0
  29. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/graphrag.py +0 -0
  30. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/guards.py +0 -0
  31. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/loop_detect.py +0 -0
  32. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/mcp.py +0 -0
  33. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/poam.py +0 -0
  34. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/providers.py +0 -0
  35. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/rmf.py +0 -0
  36. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/rmf_graph.py +0 -0
  37. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/stig.py +0 -0
  39. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tool_registry.py +0 -0
  40. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tui/__init__.py +0 -0
  41. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tui/app.py +0 -0
  42. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tui/approval.py +0 -0
  43. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tui/messages.py +0 -0
  44. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tui/widgets.py +0 -0
  45. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/tuning.py +0 -0
  46. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock/web.py +0 -0
  47. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock_cli.egg-info/dependency_links.txt +0 -0
  48. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock_cli.egg-info/entry_points.txt +0 -0
  49. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock_cli.egg-info/requires.txt +0 -0
  50. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/drydock_cli.egg-info/top_level.txt +0 -0
  51. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/setup.cfg +0 -0
  52. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_advisor.py +0 -0
  53. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_approval.py +0 -0
  54. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_back_command.py +0 -0
  55. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_background.py +0 -0
  56. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_binary_output.py +0 -0
  57. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_output_bounding.py +0 -0
  58. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_process_group.py +0 -0
  59. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_safety.py +0 -0
  60. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_sanitize.py +0 -0
  61. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_shell.py +0 -0
  62. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_stdin.py +0 -0
  63. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_timeout_network.py +0 -0
  64. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_bash_timeout_param.py +0 -0
  65. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_cci.py +0 -0
  66. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_cli_agents.py +0 -0
  67. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_compact_command.py +0 -0
  68. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_compaction.py +0 -0
  69. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_config.py +0 -0
  70. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_config_migration.py +0 -0
  71. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_context_limit_config.py +0 -0
  72. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_detect.py +0 -0
  73. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_dispatch.py +0 -0
  74. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_e2e_connected.py +0 -0
  75. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_empty_response.py +0 -0
  76. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_extract.py +0 -0
  77. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_failure_loop.py +0 -0
  78. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_first_run_setup.py +0 -0
  79. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_gittools.py +0 -0
  80. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_graphify_example.py +0 -0
  81. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_graphrag.py +0 -0
  82. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_grep_and_read_robust.py +0 -0
  83. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_guards_and_tools.py +0 -0
  84. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_hallucinated_tools.py +0 -0
  85. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_leaked_tool_call.py +0 -0
  86. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_loop_detect.py +0 -0
  87. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_mcp.py +0 -0
  88. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_oneshot_unreachable.py +0 -0
  89. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_plan_autocontinue.py +0 -0
  90. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_poam.py +0 -0
  91. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_providers_unreachable.py +0 -0
  92. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_read_index.py +0 -0
  93. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_rmf.py +0 -0
  94. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_rmf_graph.py +0 -0
  95. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_rmf_stig_graph.py +0 -0
  96. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_runaway_repetition.py +0 -0
  97. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_server_probe.py +0 -0
  98. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_skills.py +0 -0
  99. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_stig.py +0 -0
  100. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_stop.py +0 -0
  101. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_streaming_newlines.py +0 -0
  102. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_subagent.py +0 -0
  103. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_system_prompt_help.py +0 -0
  104. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_todo.py +0 -0
  105. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_tool_arg_parsing.py +0 -0
  106. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_tools_undo.py +0 -0
  107. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_tui.py +0 -0
  108. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_tuning.py +0 -0
  109. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_viewimage.py +0 -0
  110. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_vision_input.py +0 -0
  111. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_web_tools.py +0 -0
  112. {drydock_cli-3.0.98 → drydock_cli-3.0.99}/tests/test_write_content_coerce.py +0 -0
  113. {drydock_cli-3.0.98 → 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.98
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,14 +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", "")
674
703
  # Local models sometimes send content as a JSON array of lines (or a number),
675
- # which f.write() can't take — coerce instead of crashing with a TypeError
676
- # (tools must return errors, never raise). A list → newline-joined lines.
677
- if isinstance(content, (list, tuple)):
678
- content = "\n".join(str(x) for x in content)
679
- elif not isinstance(content, str):
680
- content = "" if content is None else str(content)
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", ""))
681
707
  if has_conflict_markers(content):
682
708
  return conflict_marker_refusal(fp)
683
709
  prev = _snapshot(fp)
@@ -800,8 +826,10 @@ def tool_edit(params: dict, config: dict) -> str:
800
826
  if not fp or not str(fp).strip():
801
827
  return "Error: Edit needs a real file_path (the path was empty or blank)."
802
828
  fp = _resolve_path(str(fp).strip(), config)
803
- old = params.get("old_string", "")
804
- 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", ""))
805
833
  if has_conflict_markers(new):
806
834
  return conflict_marker_refusal(fp)
807
835
  # If a directory was passed, try to infer the intended file (avoids a
@@ -1105,8 +1133,8 @@ def tool_glob(params: dict, config: dict) -> str:
1105
1133
 
1106
1134
 
1107
1135
  def tool_grep(params: dict, config: dict) -> str:
1108
- pattern = params["pattern"]
1109
- 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 "."
1110
1138
  include = params.get("include", "")
1111
1139
  try:
1112
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.98
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
@@ -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.98"
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"
File without changes
File without changes
File without changes
File without changes