drydock-cli 3.0.55__tar.gz → 3.0.56__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 (75) hide show
  1. {drydock_cli-3.0.55/drydock_cli.egg-info → drydock_cli-3.0.56}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tools/__init__.py +54 -5
  3. {drydock_cli-3.0.55 → drydock_cli-3.0.56/drydock_cli.egg-info}/PKG-INFO +1 -1
  4. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock_cli.egg-info/SOURCES.txt +1 -0
  5. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/pyproject.toml +1 -1
  6. drydock_cli-3.0.56/tests/test_read_index.py +52 -0
  7. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/LICENSE +0 -0
  8. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/NOTICE +0 -0
  9. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/README.md +0 -0
  10. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/__init__.py +0 -0
  11. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/agent.py +0 -0
  13. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/bash_safety.py +0 -0
  14. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/cli.py +0 -0
  15. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/compaction.py +0 -0
  16. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/config.py +0 -0
  17. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/detect.py +0 -0
  18. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/gittools.py +0 -0
  19. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/graphrag.py +0 -0
  20. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/guards.py +0 -0
  21. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/loop_detect.py +0 -0
  22. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/mcp.py +0 -0
  23. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/providers.py +0 -0
  24. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/skills.py +0 -0
  25. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tool_registry.py +0 -0
  26. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tui/__init__.py +0 -0
  27. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tui/app.py +0 -0
  28. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tui/approval.py +0 -0
  29. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tui/messages.py +0 -0
  30. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tui/widgets.py +0 -0
  31. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/tuning.py +0 -0
  32. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock/web.py +0 -0
  33. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock_cli.egg-info/dependency_links.txt +0 -0
  34. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock_cli.egg-info/entry_points.txt +0 -0
  35. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock_cli.egg-info/requires.txt +0 -0
  36. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/drydock_cli.egg-info/top_level.txt +0 -0
  37. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/setup.cfg +0 -0
  38. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_approval.py +0 -0
  39. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_back_command.py +0 -0
  40. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_bash_process_group.py +0 -0
  41. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_bash_safety.py +0 -0
  42. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_bash_timeout_network.py +0 -0
  43. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_cli_agents.py +0 -0
  44. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_compact_command.py +0 -0
  45. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_compaction.py +0 -0
  46. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_config.py +0 -0
  47. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_config_migration.py +0 -0
  48. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_context_limit_config.py +0 -0
  49. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_detect.py +0 -0
  50. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_dispatch.py +0 -0
  51. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_empty_response.py +0 -0
  52. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_failure_loop.py +0 -0
  53. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_first_run_setup.py +0 -0
  54. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_gittools.py +0 -0
  55. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_graphrag.py +0 -0
  56. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_guards_and_tools.py +0 -0
  57. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_hallucinated_tools.py +0 -0
  58. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_leaked_tool_call.py +0 -0
  59. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_loop_detect.py +0 -0
  60. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_mcp.py +0 -0
  61. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_oneshot_unreachable.py +0 -0
  62. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_plan_autocontinue.py +0 -0
  63. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_providers_unreachable.py +0 -0
  64. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_runaway_repetition.py +0 -0
  65. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_skills.py +0 -0
  66. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_stop.py +0 -0
  67. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_streaming_newlines.py +0 -0
  68. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_subagent.py +0 -0
  69. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_todo.py +0 -0
  70. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_tool_arg_parsing.py +0 -0
  71. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_tools_undo.py +0 -0
  72. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_tui.py +0 -0
  73. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_tuning.py +0 -0
  74. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_vision_input.py +0 -0
  75. {drydock_cli-3.0.55 → drydock_cli-3.0.56}/tests/test_web_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.55
3
+ Version: 3.0.56
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
@@ -41,7 +41,7 @@ def _resolve_path(path: str, config: dict) -> str:
41
41
  SCHEMAS = [
42
42
  {
43
43
  "name": "Read",
44
- "description": "Read a file. Returns content with line numbers. Use limit/offset for large files.",
44
+ "description": "Read a file. Returns content with line numbers. Use limit/offset for large files. A very large file with no limit returns a STRUCTURE INDEX (key symbols + line numbers) instead of the whole file — then Read the ranges you need with offset/limit.",
45
45
  "input_schema": {
46
46
  "type": "object",
47
47
  "properties": {
@@ -303,18 +303,67 @@ SCHEMAS = [
303
303
 
304
304
  # ── Tool implementations ──────────────────────────────────────────────────
305
305
 
306
+ # Above this many lines, a Read with no explicit window returns a structural
307
+ # INDEX (key symbols + line numbers) instead of dumping the file — semantic
308
+ # chunking so a huge file can't blow the context window. The model then Reads
309
+ # the ranges it needs with offset/limit.
310
+ _BIG_FILE_LINES = 1500
311
+
312
+ # Structural anchors: a definition → a node the model can jump to. Code anchors
313
+ # apply to every file; markdown headers (anchored at column 0 so indented code
314
+ # COMMENTS aren't mistaken for headings) apply only to doc files.
315
+ _CODE_RE = re.compile(
316
+ r"^\s*("
317
+ r"(?:async\s+)?def\s+\w+" # python functions
318
+ r"|class\s+\w+" # python/js/etc classes
319
+ r"|(?:export\s+)?(?:async\s+)?function\s+\w+" # js/ts functions
320
+ r"|(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\(" # js arrow fns
321
+ r"|func\s+\w+" # go
322
+ r"|fn\s+\w+" # rust
323
+ r")"
324
+ )
325
+ _MD_RE = re.compile(r"^#{1,6}\s+\S") # markdown headers, column 0 only
326
+ _DOC_EXT = {".md", ".markdown", ".rst", ".txt", ".org"}
327
+
328
+
329
+ def _file_index(lines: list[str], fp: str) -> str:
330
+ """A compact, line-numbered index of a large file's structure."""
331
+ is_doc = Path(fp).suffix.lower() in _DOC_EXT
332
+ anchors = [
333
+ f" L{i + 1}: {ln.rstrip()[:100]}"
334
+ for i, ln in enumerate(lines)
335
+ if _CODE_RE.match(ln) or (is_doc and _MD_RE.match(ln))
336
+ ]
337
+ head = (
338
+ f"{fp} is large ({len(lines)} lines) — showing a STRUCTURE INDEX instead "
339
+ f"of the whole file (to save context). Read a specific section with "
340
+ f"offset/limit (e.g. {{\"file_path\": ..., \"offset\": <line-1>, "
341
+ f"\"limit\": 120}}).\n"
342
+ )
343
+ if not anchors:
344
+ return head + "(no def/class/header anchors found — read ranges with offset/limit.)"
345
+ body = "\n".join(anchors[:400])
346
+ if len(anchors) > 400:
347
+ body += f"\n [... {len(anchors) - 400} more anchors ...]"
348
+ return f"{head}\nKey locations ({len(anchors)}):\n{body}"
349
+
350
+
306
351
  def tool_read(params: dict, config: dict) -> str:
307
352
  fp = _resolve_path(params["file_path"], config)
308
- limit = params.get("limit", 2000)
353
+ limit = params.get("limit") # None = caller didn't specify a window
309
354
  offset = params.get("offset", 0)
310
355
  try:
311
356
  with open(fp, "r", encoding="utf-8", errors="replace") as f:
312
357
  lines = f.readlines()
313
- selected = lines[offset:offset + limit]
358
+ # Huge file, no explicit window → index it instead of dumping it.
359
+ if limit is None and offset == 0 and len(lines) > _BIG_FILE_LINES:
360
+ return _file_index(lines, fp)
361
+ eff_limit = 2000 if limit is None else limit
362
+ selected = lines[offset:offset + eff_limit]
314
363
  numbered = [f"{i + offset + 1}\t{line.rstrip()}" for i, line in enumerate(selected)]
315
364
  result = "\n".join(numbered)
316
- if len(lines) > offset + limit:
317
- result += f"\n[... {len(lines) - offset - limit} more lines]"
365
+ if len(lines) > offset + eff_limit:
366
+ result += f"\n[... {len(lines) - offset - eff_limit} more lines]"
318
367
  return result or "(empty file)"
319
368
  except FileNotFoundError:
320
369
  return f"Error: file not found: {fp}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.55
3
+ Version: 3.0.56
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
@@ -58,6 +58,7 @@ tests/test_mcp.py
58
58
  tests/test_oneshot_unreachable.py
59
59
  tests/test_plan_autocontinue.py
60
60
  tests/test_providers_unreachable.py
61
+ tests/test_read_index.py
61
62
  tests/test_runaway_repetition.py
62
63
  tests/test_skills.py
63
64
  tests/test_stop.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.55"
10
+ version = "3.0.56"
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,52 @@
1
+ """Semantic chunking: Read returns a STRUCTURE INDEX for a huge file (no
2
+ explicit window) instead of dumping it, so it can't blow the context window."""
3
+ from __future__ import annotations
4
+
5
+ from drydock.tools import tool_read
6
+
7
+
8
+ def _big_py(tmp_path, n=2000):
9
+ lines = []
10
+ for i in range(n):
11
+ if i % 400 == 0:
12
+ lines.append(f"def func_{i}(x):")
13
+ elif i % 400 == 1:
14
+ lines.append(" return x")
15
+ else:
16
+ lines.append(f" # comment {i}")
17
+ (tmp_path / "big.py").write_text("\n".join(lines))
18
+ return {"cwd": str(tmp_path)}
19
+
20
+
21
+ def test_huge_file_no_window_returns_index(tmp_path):
22
+ cfg = _big_py(tmp_path)
23
+ out = tool_read({"file_path": "big.py"}, cfg)
24
+ assert "STRUCTURE INDEX" in out
25
+ assert "def func_0" in out and "def func_400" in out
26
+ assert "# comment" not in out # comments are not anchors
27
+
28
+
29
+ def test_index_is_small_not_a_dump(tmp_path):
30
+ cfg = _big_py(tmp_path)
31
+ out = tool_read({"file_path": "big.py"}, cfg)
32
+ assert len(out.splitlines()) < 50 # an index, not 2000 lines
33
+
34
+
35
+ def test_explicit_window_still_reads_slice(tmp_path):
36
+ cfg = _big_py(tmp_path)
37
+ out = tool_read({"file_path": "big.py", "offset": 0, "limit": 3}, cfg)
38
+ assert "STRUCTURE INDEX" not in out
39
+ assert out.startswith("1\tdef func_0")
40
+
41
+
42
+ def test_small_file_reads_fully(tmp_path):
43
+ (tmp_path / "s.py").write_text("def a():\n return 1\n")
44
+ out = tool_read({"file_path": "s.py"}, {"cwd": str(tmp_path)})
45
+ assert "STRUCTURE INDEX" not in out and "def a()" in out
46
+
47
+
48
+ def test_markdown_headers_indexed(tmp_path):
49
+ body = "\n".join(["# Title"] + [f"para {i}" for i in range(2000)] + ["## Section B"])
50
+ (tmp_path / "doc.md").write_text(body)
51
+ out = tool_read({"file_path": "doc.md"}, {"cwd": str(tmp_path)})
52
+ assert "STRUCTURE INDEX" in out and "# Title" in out and "## Section B" in out
File without changes
File without changes
File without changes
File without changes