codex-agent-framework 0.1.26__tar.gz → 0.1.27__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 (132) hide show
  1. {codex_agent_framework-0.1.26/codex_agent_framework.egg-info → codex_agent_framework-0.1.27}/PKG-INFO +2 -1
  2. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/README.md +1 -0
  3. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/context/__init__.py +12 -0
  4. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/files/__init__.py +3 -3
  5. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/files/tools.py +14 -6
  6. codex_agent_framework-0.1.27/codex_agent/builtin_plugins/interface/__init__.py +53 -0
  7. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/status.py +83 -1
  8. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tui/chat.py +43 -6
  9. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/utils.py +151 -19
  10. codex_agent_framework-0.1.27/codex_agent/version.py +1 -0
  11. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27/codex_agent_framework.egg-info}/PKG-INFO +2 -1
  12. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/pyproject.toml +1 -1
  13. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_agent.py +18 -0
  14. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_chat.py +89 -1
  15. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_utils.py +64 -3
  16. codex_agent_framework-0.1.26/codex_agent/builtin_plugins/interface/__init__.py +0 -38
  17. codex_agent_framework-0.1.26/codex_agent/version.py +0 -1
  18. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/CHANGELOG.md +0 -0
  19. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/LICENSE +0 -0
  20. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/MANIFEST.in +0 -0
  21. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/__init__.py +0 -0
  22. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/__main__.py +0 -0
  23. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/agent.py +0 -0
  24. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/agent_runtime.py +0 -0
  25. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/ai.py +0 -0
  26. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/browser.py +0 -0
  27. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_commands.py +0 -0
  28. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/__init__.py +0 -0
  29. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  30. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  31. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  32. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  33. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  34. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  35. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/system.py +0 -0
  36. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/vision.py +0 -0
  37. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  38. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  39. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  40. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  41. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  42. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  43. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  44. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  45. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  46. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  47. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  48. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  49. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  50. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  51. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  52. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  53. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  54. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  55. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  56. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  57. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  58. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  59. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  60. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  61. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  62. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  63. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  64. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  65. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/__init__.py +0 -0
  66. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/headless.py +0 -0
  67. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/main.py +0 -0
  68. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/output.py +0 -0
  69. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/root.py +0 -0
  70. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/runner.py +0 -0
  71. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/client.py +0 -0
  72. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/command.py +0 -0
  73. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/config.py +0 -0
  74. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/context.py +0 -0
  75. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/desktop.py +0 -0
  76. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/event.py +0 -0
  77. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/__init__.py +0 -0
  78. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/default_gitignore +0 -0
  79. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/get_text.py +0 -0
  80. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/simpler_get_text.py +0 -0
  81. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/hooks.py +0 -0
  82. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/image.py +0 -0
  83. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/latex.py +0 -0
  84. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/mainloop.py +0 -0
  85. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/message.py +0 -0
  86. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/plugin.py +0 -0
  87. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/prompts/system_prompt.txt +0 -0
  88. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/provider.py +0 -0
  89. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/runtime.py +0 -0
  90. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/scheduler.py +0 -0
  91. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  92. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/server/__init__.py +0 -0
  93. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/server/app.py +0 -0
  94. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/server/core.py +0 -0
  95. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/service.py +0 -0
  96. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/sessions.py +0 -0
  97. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/stream.py +0 -0
  98. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/stream_utils.py +0 -0
  99. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tool.py +0 -0
  100. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tray.py +0 -0
  101. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tui/__init__.py +0 -0
  102. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tui/lifecycle.py +0 -0
  103. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/worker.py +0 -0
  104. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
  105. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  106. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  107. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/requires.txt +0 -0
  108. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/top_level.txt +0 -0
  109. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/dependencies.txt +0 -0
  110. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/setup.cfg +0 -0
  111. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_agent_runtime.py +0 -0
  112. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_ai.py +0 -0
  113. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_browser.py +0 -0
  114. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_builtin_config.py +0 -0
  115. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_cli.py +0 -0
  116. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_cli_headless.py +0 -0
  117. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_cli_root.py +0 -0
  118. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_client.py +0 -0
  119. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_events.py +0 -0
  120. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_get_text_browser.py +0 -0
  121. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_hooks.py +0 -0
  122. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_image_message.py +0 -0
  123. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_local_desktop.py +0 -0
  124. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_memory.py +0 -0
  125. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_messages.py +0 -0
  126. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_planner.py +0 -0
  127. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_scheduler.py +0 -0
  128. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_server.py +0 -0
  129. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_service.py +0 -0
  130. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_tray.py +0 -0
  131. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_tui.py +0 -0
  132. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.26
3
+ Version: 0.1.27
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -626,6 +626,7 @@ Core LLM calls, memory embeddings, non-streaming STT transcription, and realtime
626
626
 
627
627
  ## Recent releases
628
628
 
629
+ - `0.1.27`: upgrade the agentic `read` tool with folder trees, recursive folder pattern search, and precise `start_at_line`/`end_at_line` ranges; add visible wrapper/token split stats, compactable turn counts, and refreshed TUI status-bar context telemetry.
629
630
  - `0.1.26`: align SDK-facing config with Responses API shapes, add the `context` management plugin with selective non-destructive wrapper pruning, keep wrappers out of memory retrieval and compaction payloads, move embeddings/STT/realtime onto Codex backend auth, streamline voice/TTS plugin config, and improve TUI streaming/windowing.
630
631
  - `0.1.25`: move built-in capabilities further into plugin-scoped modules, add bundled plugin requirement metadata, and replace the TUI conversation pane with a selectable rich chat area that preserves mouse scrolling, copy selection, lazy transcript loading, and bottom-aware auto-scroll.
631
632
  - `0.1.24`: add slash-command lifecycle events and TUI progress/result rendering so commands such as `/compact` and `/help` show running, done, failed, result, and error feedback instead of going silent.
@@ -577,6 +577,7 @@ Core LLM calls, memory embeddings, non-streaming STT transcription, and realtime
577
577
 
578
578
  ## Recent releases
579
579
 
580
+ - `0.1.27`: upgrade the agentic `read` tool with folder trees, recursive folder pattern search, and precise `start_at_line`/`end_at_line` ranges; add visible wrapper/token split stats, compactable turn counts, and refreshed TUI status-bar context telemetry.
580
581
  - `0.1.26`: align SDK-facing config with Responses API shapes, add the `context` management plugin with selective non-destructive wrapper pruning, keep wrappers out of memory retrieval and compaction payloads, move embeddings/STT/realtime onto Codex backend auth, streamline voice/TTS plugin config, and improve TUI streaming/windowing.
581
582
  - `0.1.25`: move built-in capabilities further into plugin-scoped modules, add bundled plugin requirement metadata, and replace the TUI conversation pane with a selectable rich chat area that preserves mouse scrolling, copy selection, lazy transcript loading, and bottom-aware auto-scroll.
582
583
  - `0.1.24`: add slash-command lifecycle events and TUI progress/result rendering so commands such as `/compact` and `/help` show running, done, failed, result, and error feedback instead of going silent.
@@ -33,6 +33,15 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
33
33
  total_history_messages=status.get("total_history_messages", 0),
34
34
  persistent_session_messages=status.get("persistent_session_messages", 0),
35
35
  temporary_session_messages=status.get("temporary_session_messages", 0),
36
+ active_wrapper_messages=status.get("active_wrapper_messages", 0),
37
+ vanished_wrapper_messages=status.get("vanished_wrapper_messages", 0),
38
+ total_wrapper_messages=status.get("total_wrapper_messages", 0),
39
+ base_context_tokens=status.get("base_context_tokens", status.used_tokens),
40
+ active_wrapper_tokens=status.get("active_wrapper_tokens", 0),
41
+ vanished_wrapper_tokens=status.get("vanished_wrapper_tokens", 0),
42
+ compactable_turns=status.get("compactable_turns", 0),
43
+ compactable_messages=status.get("compactable_messages", 0),
44
+ compactable_tokens=status.get("compactable_tokens", 0),
36
45
  context_session_messages=status.get("context_session_messages", 0),
37
46
  image_messages=status.get("image_messages", 0),
38
47
  pending_messages=status.get("pending_messages", 0),
@@ -48,8 +57,11 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
48
57
  return "\n".join([
49
58
  "Context status:",
50
59
  f"- tokens: {status.used_tokens}/{status.input_token_limit} ({status.percent:.2f}%)",
60
+ f"- token split base/active_wrappers/vanished_wrappers: {status.base_context_tokens}/{status.active_wrapper_tokens}/{status.vanished_wrapper_tokens}",
51
61
  f"- messages sent/total: {status.sent_session_messages}/{status.total_session_messages}",
52
62
  f"- persistent/temporary context messages: {status.persistent_session_messages}/{status.temporary_session_messages}",
63
+ f"- wrappers active/vanished/total: {status.active_wrapper_messages}/{status.vanished_wrapper_messages}/{status.total_wrapper_messages}",
64
+ f"- compactable turns/messages/tokens: {status.compactable_turns}/{status.compactable_messages}/{status.compactable_tokens}",
53
65
  f"- visible/total history messages: {status.visible_history_messages}/{status.total_history_messages}",
54
66
  f"- tool spec tokens: {status.tool_spec_tokens}",
55
67
  f"- auto_compact: {status.auto_compact} threshold={status.auto_compact_threshold}",
@@ -7,13 +7,13 @@ from . import tools as file_tools
7
7
 
8
8
  class FilesPlugin(Plugin):
9
9
  name = "files"
10
- description = "Strict local UTF-8 file read/write/edit operations."
10
+ description = "Strict local UTF-8 file/folder read and search plus write/edit operations."
11
11
  prefix_tools = False
12
12
  system_prompt = """
13
13
  # File Tools
14
14
 
15
- Use the specialized file tools for local UTF-8 text file operations:
16
- - `read`: read line-numbered local UTF-8 files precisely, with optional pattern search.
15
+ Use the specialized file tools for local UTF-8 text operations:
16
+ - `read`: read line-numbered local UTF-8 files precisely, list local folders as a compact one-level tree, or search folders recursively with a pattern.
17
17
  - `write`: create or overwrite complete UTF-8 text files; use with caution.
18
18
  - `edit`: perform targeted exact-string replacements in local UTF-8 text files.
19
19
 
@@ -8,10 +8,11 @@ from ...utils import read_document_content, view_document_content
8
8
  def read(reads):
9
9
  """
10
10
  description: |
11
- Strictly read one or more local UTF-8 text files as source-of-truth content.
12
- This tool always prefixes returned content with reliable source line numbers.
13
- It does not extract document formats, fetch URLs, inspect Python objects, or render directory trees.
14
- Use `view` for folders, URLs, PDFs, DOCX, ODT, HTML, XLSX, and other extracted or transformed content.
11
+ Read one or more local UTF-8 text files or folders as source-of-truth content.
12
+ File reads always prefix returned content with reliable source line numbers.
13
+ Folder reads without `pattern` render a compact tree with one level of recursion.
14
+ Folder reads with `pattern` recursively search UTF-8 files under the folder and return numbered snippets per matching file.
15
+ This tool does not extract document formats, fetch URLs, or inspect Python objects; use `view` for URLs, PDFs, DOCX, ODT, HTML, XLSX, archives, and other extracted or transformed content.
15
16
 
16
17
  For large outputs: the output is automatically truncated. When truncated, a message indicates the omitted line range.
17
18
  When `pattern` is provided, it is treated as a Python regex by default and all matches are returned as separated numbered snippets with up to 10 lines before and after each match.
@@ -20,16 +21,19 @@ def read(reads):
20
21
  reads:
21
22
  description: |
22
23
  A single read object or a list of read objects.
23
- Each object must contain `source` and may contain `start_at_line`, `pattern`, and `regex`.
24
+ Each object must contain `source` and may contain `start_at_line`, `end_at_line`, `pattern`, and `regex`.
24
25
  anyOf:
25
26
  - type: object
26
27
  properties:
27
28
  source:
28
- description: Local UTF-8 text file path only. For directories, URLs, rich documents, or Python objects, use `view` instead.
29
+ description: Local UTF-8 text file or folder path. Folders are listed as a compact tree, or searched recursively when pattern is provided. For URLs, rich documents, archives, or Python objects, use `view` instead.
29
30
  type: string
30
31
  start_at_line:
31
32
  description: Line number to start reading from (1-indexed). Default is 1 (start from beginning).
32
33
  type: integer
34
+ end_at_line:
35
+ description: Optional inclusive line number to stop reading at for direct file reads.
36
+ type: integer
33
37
  pattern:
34
38
  description: Optional search pattern. Treated as a Python regular expression by default.
35
39
  type: string
@@ -43,9 +47,12 @@ def read(reads):
43
47
  type: object
44
48
  properties:
45
49
  source:
50
+ description: Local UTF-8 text file or folder path. Folders are listed as a compact tree, or searched recursively when pattern is provided.
46
51
  type: string
47
52
  start_at_line:
48
53
  type: integer
54
+ end_at_line:
55
+ type: integer
49
56
  pattern:
50
57
  type: string
51
58
  regex:
@@ -79,6 +86,7 @@ def apply_file_read(item):
79
86
  return read_document_content(
80
87
  source,
81
88
  start_at_line=item.get("start_at_line", 1),
89
+ end_at_line=item.get("end_at_line"),
82
90
  max_tokens=max_tokens,
83
91
  pattern=item.get("pattern"),
84
92
  regex=item.get("regex", True),
@@ -0,0 +1,53 @@
1
+ """Built-in local interface plugin."""
2
+
3
+ from ...plugin import Plugin
4
+ from ...provider import provider
5
+ from ...tool import tool
6
+ from ...tui import current_tui, open_tui_terminal, terminate_tui
7
+
8
+
9
+ class InterfacePlugin(Plugin):
10
+ name = "interface"
11
+ description = "Open and close the local TUI interface."
12
+ prefix_tools = False
13
+
14
+ @provider
15
+ def tui_status(self):
16
+ """Expose whether the local TUI client is currently open."""
17
+ tui = current_tui()
18
+ if not tui:
19
+ return "TUI status: closed."
20
+ return (
21
+ "TUI status: open. "
22
+ f"pid={tui.get('pid')} "
23
+ f"base_url={tui.get('base_url')} "
24
+ f"timestamp={tui.get('timestamp')}"
25
+ )
26
+
27
+ @tool(name="open_tui")
28
+ def open_tui(self):
29
+ """
30
+ description: Open the Textual chat TUI client in a separate terminal connected to the local agent server.
31
+ parameters: {}
32
+ required: []
33
+ """
34
+ tui = current_tui()
35
+ if tui:
36
+ return {"status": "already_open", "tui": tui}
37
+ process = open_tui_terminal(base_url=self.agent.config.get("server_url", "http://127.0.0.1:8765"))
38
+ if process is None:
39
+ raise RuntimeError("No supported terminal emulator found to open the TUI")
40
+ return {"status": "opened", "pid": process.pid}
41
+
42
+ @tool(name="close_tui")
43
+ def close_tui(self):
44
+ """
45
+ description: Close the currently registered Textual chat TUI client process.
46
+ parameters: {}
47
+ required: []
48
+ """
49
+ tui = current_tui()
50
+ if not tui:
51
+ return {"status": "not_open"}
52
+ terminate_tui(tui.pid)
53
+ return {"status": "closed", "tui": tui}
@@ -1,6 +1,7 @@
1
1
  from modict import modict
2
2
 
3
3
  from .image import ImageMessage
4
+ from .message import ToolResultWrapper
4
5
  from .utils import sort, truncate
5
6
 
6
7
 
@@ -32,6 +33,7 @@ class StatusManager:
32
33
  max_images = self.agent.config.get("max_images", 10)
33
34
  context_manager = self.agent.context_manager
34
35
  context_manager.prune_orphaned_session_tool_outputs()
36
+ all_session_messages = list(self.agent.session.messages) if self.agent.session else []
35
37
  session_messages = sort(
36
38
  msg for msg in self.agent.session.context_messages()
37
39
  if context_manager.is_message_visible_in_context(msg)
@@ -58,7 +60,9 @@ class StatusManager:
58
60
  history_start = len(truncated_others) - i - 1
59
61
  else:
60
62
  break
61
- visible_history_count = len(truncated_others[max(0, history_start):])
63
+ included_history_start = max(0, history_start)
64
+ visible_history_count = len(truncated_others[included_history_start:])
65
+ included_others = others[included_history_start:]
62
66
  sent_session_messages = visible_history_count + len(images)
63
67
  total_session_messages = len(self.agent.session.messages) if self.agent.session else 0
64
68
  persistent_session_messages = sum(
@@ -69,6 +73,39 @@ class StatusManager:
69
73
  1 for msg in session_messages
70
74
  if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
71
75
  )
76
+ wrappers = [msg for msg in all_session_messages if isinstance(msg, ToolResultWrapper)]
77
+ active_wrappers = [
78
+ msg for msg in wrappers
79
+ if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
80
+ ]
81
+ vanished_wrappers = [msg for msg in wrappers if msg.get("turns_left", -1) == 0]
82
+
83
+ def wrapper_token_count(messages):
84
+ total = 0
85
+ for msg in messages:
86
+ msg_copy = msg.copy()
87
+ if msg_copy.get("content") and isinstance(msg_copy.content, str):
88
+ msg_copy.content = truncate(msg_copy.content, max_tokens=max_input_tokens, model=self.agent.config.model)
89
+ total += context_manager.response_input_token_count([msg_copy], response_tools=[])
90
+ return total
91
+
92
+ active_context_wrappers = [msg for msg in included_others if isinstance(msg, ToolResultWrapper)]
93
+ active_wrapper_tokens = wrapper_token_count(active_context_wrappers)
94
+ vanished_wrapper_tokens = wrapper_token_count(vanished_wrappers)
95
+ base_context_tokens = max(0, current_count - active_wrapper_tokens)
96
+ if self.agent.session:
97
+ compaction_plan = self.agent.session.compaction_plan(
98
+ max_input_tokens=max_input_tokens,
99
+ model=self.agent.config.model,
100
+ max_compacted_turns=None,
101
+ )
102
+ compactable_turns = compaction_plan.completed_turn_count
103
+ compactable_messages = compaction_plan.compacted_message_count
104
+ compactable_tokens = compaction_plan.compacted_token_count
105
+ else:
106
+ compactable_turns = 0
107
+ compactable_messages = 0
108
+ compactable_tokens = 0
72
109
  percent = (current_count / input_limit * 100) if input_limit else 0
73
110
  status = modict(
74
111
  used_tokens=current_count,
@@ -84,6 +121,15 @@ class StatusManager:
84
121
  sent_session_message_percent=(sent_session_messages / total_session_messages * 100) if total_session_messages else 0,
85
122
  persistent_session_messages=persistent_session_messages,
86
123
  temporary_session_messages=temporary_session_messages,
124
+ active_wrapper_messages=len(active_wrappers),
125
+ vanished_wrapper_messages=len(vanished_wrappers),
126
+ total_wrapper_messages=len(wrappers),
127
+ base_context_tokens=base_context_tokens,
128
+ active_wrapper_tokens=active_wrapper_tokens,
129
+ vanished_wrapper_tokens=vanished_wrapper_tokens,
130
+ compactable_turns=compactable_turns,
131
+ compactable_messages=compactable_messages,
132
+ compactable_tokens=compactable_tokens,
87
133
  context_session_messages=len(session_messages),
88
134
  image_messages=len(images),
89
135
  pending_messages=len(self.agent.mainloop_manager.pending),
@@ -116,6 +162,15 @@ class StatusManager:
116
162
  sent_session_message_percent=estimated.sent_session_message_percent,
117
163
  persistent_session_messages=estimated.get("persistent_session_messages", 0),
118
164
  temporary_session_messages=estimated.get("temporary_session_messages", 0),
165
+ active_wrapper_messages=estimated.get("active_wrapper_messages", 0),
166
+ vanished_wrapper_messages=estimated.get("vanished_wrapper_messages", 0),
167
+ total_wrapper_messages=estimated.get("total_wrapper_messages", 0),
168
+ base_context_tokens=estimated.get("base_context_tokens", estimated.used_tokens),
169
+ active_wrapper_tokens=estimated.get("active_wrapper_tokens", 0),
170
+ vanished_wrapper_tokens=estimated.get("vanished_wrapper_tokens", 0),
171
+ compactable_turns=estimated.get("compactable_turns", 0),
172
+ compactable_messages=estimated.get("compactable_messages", 0),
173
+ compactable_tokens=estimated.get("compactable_tokens", 0),
119
174
  quota_5h_percent=None,
120
175
  quota_7d_percent=None,
121
176
  model=self.agent.config.get("model") or "gpt-5.4",
@@ -143,6 +198,15 @@ class StatusManager:
143
198
  sent_session_message_percent=estimated.sent_session_message_percent,
144
199
  persistent_session_messages=estimated.get("persistent_session_messages", 0),
145
200
  temporary_session_messages=estimated.get("temporary_session_messages", 0),
201
+ active_wrapper_messages=estimated.get("active_wrapper_messages", 0),
202
+ vanished_wrapper_messages=estimated.get("vanished_wrapper_messages", 0),
203
+ total_wrapper_messages=estimated.get("total_wrapper_messages", 0),
204
+ base_context_tokens=estimated.get("base_context_tokens", estimated.used_tokens),
205
+ active_wrapper_tokens=estimated.get("active_wrapper_tokens", 0),
206
+ vanished_wrapper_tokens=estimated.get("vanished_wrapper_tokens", 0),
207
+ compactable_turns=estimated.get("compactable_turns", 0),
208
+ compactable_messages=estimated.get("compactable_messages", 0),
209
+ compactable_tokens=estimated.get("compactable_tokens", 0),
146
210
  model=self.agent.config.get("model") or "gpt-5.4",
147
211
  busy=self.agent.mainloop_manager.busy,
148
212
  current_turn_id=self.agent.mainloop_manager.current_turn_id,
@@ -167,6 +231,15 @@ class StatusManager:
167
231
  sent_session_message_percent=estimated.sent_session_message_percent,
168
232
  persistent_session_messages=estimated.get("persistent_session_messages", 0),
169
233
  temporary_session_messages=estimated.get("temporary_session_messages", 0),
234
+ active_wrapper_messages=estimated.get("active_wrapper_messages", 0),
235
+ vanished_wrapper_messages=estimated.get("vanished_wrapper_messages", 0),
236
+ total_wrapper_messages=estimated.get("total_wrapper_messages", 0),
237
+ base_context_tokens=estimated.get("base_context_tokens", estimated.used_tokens),
238
+ active_wrapper_tokens=estimated.get("active_wrapper_tokens", 0),
239
+ vanished_wrapper_tokens=estimated.get("vanished_wrapper_tokens", 0),
240
+ compactable_turns=estimated.get("compactable_turns", 0),
241
+ compactable_messages=estimated.get("compactable_messages", 0),
242
+ compactable_tokens=estimated.get("compactable_tokens", 0),
170
243
  model=self.agent.config.get("model") or "gpt-5.4",
171
244
  busy=self.agent.mainloop_manager.busy,
172
245
  current_turn_id=self.agent.mainloop_manager.current_turn_id,
@@ -193,6 +266,15 @@ class StatusManager:
193
266
  sent_session_message_percent=context_status.get("sent_session_message_percent", 0),
194
267
  persistent_session_messages=context_status.get("persistent_session_messages", 0),
195
268
  temporary_session_messages=context_status.get("temporary_session_messages", 0),
269
+ active_wrapper_messages=context_status.get("active_wrapper_messages", 0),
270
+ vanished_wrapper_messages=context_status.get("vanished_wrapper_messages", 0),
271
+ total_wrapper_messages=context_status.get("total_wrapper_messages", 0),
272
+ base_context_tokens=context_status.get("base_context_tokens", max(0, input_tokens)),
273
+ active_wrapper_tokens=context_status.get("active_wrapper_tokens", 0),
274
+ vanished_wrapper_tokens=context_status.get("vanished_wrapper_tokens", 0),
275
+ compactable_turns=context_status.get("compactable_turns", 0),
276
+ compactable_messages=context_status.get("compactable_messages", 0),
277
+ compactable_tokens=context_status.get("compactable_tokens", 0),
196
278
  quota_5h_percent=wham.quota_5h_percent,
197
279
  quota_7d_percent=wham.quota_7d_percent,
198
280
  model=model or self.agent.config.get("model") or "gpt-5.4",
@@ -801,20 +801,34 @@ class Chat:
801
801
  context_style = cls._percent_style(percent)
802
802
  used = cls._format_ktok(status.get("context_current_tokens") or 0)
803
803
  limit = cls._format_ktok(status.get("context_limit_tokens") or 0)
804
+ context_segments = cls._context_token_status_segments(status, used, limit)
804
805
  quota_5h = cls._format_percent(status.get("quota_5h_percent"))
805
806
  quota_7d = cls._format_percent(status.get("quota_7d_percent"))
806
807
  sent_messages = int(status.get("sent_session_messages") or 0)
807
808
  persistent_messages = status.get("persistent_session_messages")
808
809
  temporary_messages = status.get("temporary_session_messages")
810
+ active_wrapper_messages = status.get("active_wrapper_messages")
811
+ vanished_wrapper_messages = status.get("vanished_wrapper_messages")
809
812
  total_messages = int(status.get("total_session_messages") or 0)
813
+ compactable_turns = int(status.get("compactable_turns") or 0)
810
814
  model = status.get("model") or "-"
811
- message_segments = cls._message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages)
815
+ message_segments = cls._message_status_segments(
816
+ sent_messages,
817
+ persistent_messages,
818
+ temporary_messages,
819
+ total_messages,
820
+ active_wrapper_messages=active_wrapper_messages,
821
+ vanished_wrapper_messages=vanished_wrapper_messages,
822
+ )
812
823
  return Text.assemble(
813
824
  (" ctx ", "bold #c9d1d9 on #161b22"),
814
- (f"{used}/{limit} ktok ", "#8b949e on #161b22"),
825
+ *context_segments,
826
+ (" ", "#8b949e on #161b22"),
815
827
  (f"{percent:.1f}%", f"{context_style} on #161b22"),
816
828
  (" msg ", "#6e7681 on #161b22"),
817
829
  *message_segments,
830
+ (" cmp ", "#6e7681 on #161b22"),
831
+ (str(compactable_turns), "#c9d1d9 on #161b22"),
818
832
  (" 5h ", "#6e7681 on #161b22"),
819
833
  (quota_5h, f"{cls._percent_style(status.get('quota_5h_percent'))} on #161b22"),
820
834
  (" 7d ", "#6e7681 on #161b22"),
@@ -823,18 +837,41 @@ class Chat:
823
837
  (str(model), "#c9d1d9 on #161b22"),
824
838
  )
825
839
 
840
+ @classmethod
841
+ def _context_token_status_segments(cls, status, used, limit):
842
+ if "base_context_tokens" not in status and "active_wrapper_tokens" not in status and "vanished_wrapper_tokens" not in status:
843
+ return ((f"{used}/{limit} ktok", "#8b949e on #161b22"),)
844
+ base = cls._format_ktok(status.get("base_context_tokens") or 0)
845
+ active_wrappers = cls._format_ktok(status.get("active_wrapper_tokens") or 0)
846
+ vanished_wrappers = cls._format_ktok(status.get("vanished_wrapper_tokens") or 0)
847
+ return (
848
+ (base, "#c9d1d9 on #161b22"),
849
+ (f"+{active_wrappers}", "italic #f0883e on #161b22"),
850
+ (f"+{vanished_wrappers}", "italic #6e7681 on #161b22"),
851
+ (f"/{limit} ktok", "#8b949e on #161b22"),
852
+ )
853
+
826
854
  @staticmethod
827
- def _message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages):
828
- if persistent_messages is None and temporary_messages is None:
855
+ def _message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages, *, active_wrapper_messages=None, vanished_wrapper_messages=None):
856
+ if persistent_messages is None and temporary_messages is None and active_wrapper_messages is None and vanished_wrapper_messages is None:
829
857
  return ((f"{sent_messages}/{total_messages}", "#c9d1d9 on #161b22"),)
830
858
  try:
831
859
  persistent = int(persistent_messages or 0)
832
- temporary = int(temporary_messages or 0)
860
+ if active_wrapper_messages is None and vanished_wrapper_messages is None:
861
+ temporary = int(temporary_messages or 0)
862
+ return (
863
+ (str(persistent), "#c9d1d9 on #161b22"),
864
+ (f"+{temporary}", "italic #f0883e on #161b22"),
865
+ (f"/{total_messages}", "#c9d1d9 on #161b22"),
866
+ )
867
+ active_wrappers = int(active_wrapper_messages or 0)
868
+ vanished_wrappers = int(vanished_wrapper_messages or 0)
833
869
  except Exception:
834
870
  return ((f"{sent_messages}/{total_messages}", "#c9d1d9 on #161b22"),)
835
871
  return (
836
872
  (str(persistent), "#c9d1d9 on #161b22"),
837
- (f"+{temporary}", "italic #f0883e on #161b22"),
873
+ (f"+{active_wrappers}", "italic #f0883e on #161b22"),
874
+ (f"+{vanished_wrappers}", "italic #6e7681 on #161b22"),
838
875
  (f"/{total_messages}", "#c9d1d9 on #161b22"),
839
876
  )
840
877
 
@@ -105,18 +105,21 @@ def add_line_numbers(text: str, start_line: int = 1, total_lines: int | None = N
105
105
  return _number_lines(text.splitlines(), start_line=start_line, total_lines=total_lines)
106
106
 
107
107
 
108
- def pattern_snippets(text: str, pattern, context_lines: int = 10, regex_pattern: bool = True) -> str:
108
+ def pattern_line_numbers(text: str, pattern, regex_pattern: bool = True) -> list[int]:
109
109
  pattern_text = str(pattern)
110
110
  lines = text.splitlines()
111
- total_lines = len(lines)
112
111
  if regex_pattern:
113
112
  try:
114
113
  compiled_pattern = re.compile(pattern_text)
115
114
  except re.error as exc:
116
115
  raise ValueError(f"Invalid regex pattern {pattern_text!r}: {exc}") from exc
117
- matches = [index for index, line in enumerate(lines, start=1) if compiled_pattern.search(line)]
118
- else:
119
- matches = [index for index, line in enumerate(lines, start=1) if pattern_text in line]
116
+ return [index for index, line in enumerate(lines, start=1) if compiled_pattern.search(line)]
117
+ return [index for index, line in enumerate(lines, start=1) if pattern_text in line]
118
+
119
+
120
+ def _numbered_snippets_for_matches(text: str, matches: list[int], context_lines: int = 10) -> str:
121
+ lines = text.splitlines()
122
+ total_lines = len(lines)
120
123
  if not matches:
121
124
  return _number_lines(lines, start_line=1, total_lines=total_lines)
122
125
 
@@ -139,6 +142,14 @@ def pattern_snippets(text: str, pattern, context_lines: int = 10, regex_pattern:
139
142
  )
140
143
  return "\n\n".join(parts)
141
144
 
145
+
146
+ def pattern_snippets(text: str, pattern, context_lines: int = 10, regex_pattern: bool = True) -> str:
147
+ return _numbered_snippets_for_matches(
148
+ text,
149
+ pattern_line_numbers(text, pattern, regex_pattern=regex_pattern),
150
+ context_lines=context_lines,
151
+ )
152
+
142
153
  def short_id(length=8):
143
154
  return "".join(random.choices(string.ascii_letters,k=length))
144
155
 
@@ -461,14 +472,45 @@ class Logger:
461
472
  with open(file,'w',encoding="utf-8") as f:
462
473
  f.write(str(content)+'\n')
463
474
 
464
- def _render_numbered_read_text(text: str, start_at_line=1, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
465
- effective_start_at_line = int(start_at_line or 1)
475
+ def _render_numbered_read_text(text: str, start_at_line=1, end_at_line=None, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
476
+ effective_start_at_line = max(1, int(start_at_line or 1))
466
477
  total_lines = len(text.splitlines())
467
478
  if pattern:
468
479
  text = pattern_snippets(text, pattern, context_lines=pattern_context_lines, regex_pattern=regex)
469
480
  if max_tokens > 0:
470
481
  text = truncate(text, max_tokens=max_tokens, start_line=1)
471
482
  return text
483
+
484
+ if end_at_line is not None:
485
+ effective_end_at_line = int(end_at_line)
486
+ if effective_end_at_line < effective_start_at_line:
487
+ raise ValueError("end_at_line must be greater than or equal to start_at_line")
488
+ lines = text.splitlines(keepends=True)
489
+ if effective_start_at_line > len(lines):
490
+ text = ""
491
+ else:
492
+ selected_lines = lines[effective_start_at_line - 1:effective_end_at_line]
493
+ if max_tokens > 0:
494
+ result_lines = []
495
+ current_tokens = 0
496
+ for line in selected_lines:
497
+ line_tokens = token_count(line)
498
+ if current_tokens + line_tokens <= max_tokens:
499
+ result_lines.append(line)
500
+ current_tokens += line_tokens
501
+ else:
502
+ break
503
+ text = ''.join(result_lines)
504
+ if len(result_lines) < len(selected_lines):
505
+ if not text.endswith('\n'):
506
+ text += '\n'
507
+ first_truncated = effective_start_at_line + len(result_lines)
508
+ last_truncated = effective_start_at_line + len(selected_lines) - 1
509
+ text += f"\n...\n\n[Lines {first_truncated}-{last_truncated} truncated]\n"
510
+ else:
511
+ text = ''.join(selected_lines)
512
+ return add_line_numbers(text, start_line=effective_start_at_line, total_lines=total_lines)
513
+
472
514
  # Apply pagination and truncation - truncate function handles smart chunking internally
473
515
  if max_tokens > 0:
474
516
  text = truncate(text, max_tokens=max_tokens, start_line=effective_start_at_line)
@@ -502,24 +544,114 @@ def _view_search_snippets(text: str, search_query, context_lines: int = 10) -> s
502
544
  return "\n\n".join(parts)
503
545
 
504
546
 
505
- def read_document_content(source, start_at_line=1, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
506
- """Strictly read a local UTF-8 text file without extraction, URL fetching, or directory traversal."""
547
+ READ_DIRECTORY_EXCLUDES = {
548
+ ".git",
549
+ ".hg",
550
+ ".svn",
551
+ ".mypy_cache",
552
+ ".pytest_cache",
553
+ ".ruff_cache",
554
+ ".tox",
555
+ ".venv",
556
+ "__pycache__",
557
+ "build",
558
+ "dist",
559
+ "node_modules",
560
+ }
561
+
562
+
563
+ def _directory_entries(path):
564
+ try:
565
+ entries = list(os.scandir(path))
566
+ except OSError as exc:
567
+ raise OSError(f"failed to list directory {path}: {exc}") from exc
568
+ entries = [entry for entry in entries if entry.name not in READ_DIRECTORY_EXCLUDES]
569
+ return sorted(entries, key=lambda entry: (not entry.is_dir(follow_symlinks=False), entry.name.lower()))
570
+
571
+
572
+ def _render_directory_tree(path, max_tokens=8000):
573
+ root = os.path.abspath(path)
574
+ lines = [f"{os.path.basename(root) or root}/"]
575
+ entries = _directory_entries(root)
576
+ for index, entry in enumerate(entries):
577
+ is_last = index == len(entries) - 1
578
+ connector = "└── " if is_last else "├── "
579
+ child_prefix = " " if is_last else "│ "
580
+ suffix = "/" if entry.is_dir(follow_symlinks=False) else ""
581
+ lines.append(f"{connector}{entry.name}{suffix}")
582
+ if entry.is_dir(follow_symlinks=False):
583
+ children = _directory_entries(entry.path)
584
+ for child_index, child in enumerate(children):
585
+ child_connector = "└── " if child_index == len(children) - 1 else "├── "
586
+ child_suffix = "/" if child.is_dir(follow_symlinks=False) else ""
587
+ lines.append(f"{child_prefix}{child_connector}{child.name}{child_suffix}")
588
+ text = "\n".join(lines)
589
+ if max_tokens > 0:
590
+ text = truncate(text, max_tokens=max_tokens, start_line=1)
591
+ return text
592
+
593
+
594
+ def _iter_directory_files(path):
595
+ for root, dirnames, filenames in os.walk(path):
596
+ dirnames[:] = sorted(
597
+ [dirname for dirname in dirnames if dirname not in READ_DIRECTORY_EXCLUDES],
598
+ key=str.lower,
599
+ )
600
+ for filename in sorted(filenames, key=str.lower):
601
+ yield os.path.join(root, filename)
602
+
603
+
604
+ def _read_utf8_text_file(path):
605
+ with open(path, "rb") as f:
606
+ data = f.read()
607
+ try:
608
+ return data.decode("utf-8")
609
+ except UnicodeDecodeError as exc:
610
+ raise UnicodeDecodeError(exc.encoding, exc.object, exc.start, exc.end, f"{exc.reason}. read requires UTF-8 text files; use view for extracted content.") from exc
611
+
612
+
613
+ def _directory_pattern_search(path, pattern, max_tokens=8000, pattern_context_lines=10, regex=True):
614
+ pattern_text = str(pattern)
615
+ parts = []
616
+ skipped = 0
617
+ for file_path in _iter_directory_files(path):
618
+ try:
619
+ text = _read_utf8_text_file(file_path)
620
+ except UnicodeDecodeError:
621
+ skipped += 1
622
+ continue
623
+ matches = pattern_line_numbers(text, pattern_text, regex_pattern=regex)
624
+ if not matches:
625
+ continue
626
+ snippet = _numbered_snippets_for_matches(text, matches, context_lines=pattern_context_lines)
627
+ rel_path = os.path.relpath(file_path, path)
628
+ parts.append(f"--- file: {rel_path} ---\n{snippet}")
629
+ if not parts:
630
+ result = f"No matches for pattern: {pattern_text!r}"
631
+ else:
632
+ result = "\n\n".join(parts)
633
+ if skipped:
634
+ result += f"\n\n[Skipped {skipped} non-UTF-8 file(s)]"
635
+ if max_tokens > 0:
636
+ result = truncate(result, max_tokens=max_tokens, start_line=1)
637
+ return result
638
+
639
+
640
+ def read_document_content(source, start_at_line=1, end_at_line=None, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
641
+ """Read local UTF-8 files, list directories, or recursively search directories by pattern."""
507
642
  if not isinstance(source, (str, os.PathLike)):
508
- raise TypeError("read only accepts a local file path. Use view for objects or extracted document content.")
643
+ raise TypeError("read only accepts a local file or directory path. Use view for objects or extracted document content.")
509
644
  path = os.fspath(source)
510
645
  if path.startswith(("http://", "https://")):
511
- raise ValueError("read only accepts local UTF-8 text files. Use view for URLs.")
646
+ raise ValueError("read only accepts local UTF-8 files or directories. Use view for URLs.")
512
647
  if os.path.isdir(path):
513
- raise ValueError("read only accepts local UTF-8 text files. Use view for directories.")
648
+ if pattern:
649
+ return _directory_pattern_search(path, pattern, max_tokens=max_tokens, pattern_context_lines=pattern_context_lines, regex=regex)
650
+ return _render_directory_tree(path, max_tokens=max_tokens)
514
651
  if not os.path.isfile(path):
515
652
  raise FileNotFoundError(f"File not found: {path}")
516
- with open(path, "rb") as f:
517
- data = f.read()
518
- try:
519
- text = data.decode("utf-8")
520
- except UnicodeDecodeError as exc:
521
- raise UnicodeDecodeError(exc.encoding, exc.object, exc.start, exc.end, f"{exc.reason}. read requires UTF-8 text files; use view for extracted content.") from exc
522
- return _render_numbered_read_text(text, start_at_line=start_at_line, max_tokens=max_tokens, pattern=pattern, pattern_context_lines=pattern_context_lines, regex=regex)
653
+ text = _read_utf8_text_file(path)
654
+ return _render_numbered_read_text(text, start_at_line=start_at_line, end_at_line=end_at_line, max_tokens=max_tokens, pattern=pattern, pattern_context_lines=pattern_context_lines, regex=regex)
523
655
 
524
656
 
525
657
  def view_document_content(source, start_at_line=1, max_tokens=8000, search_query=None, search_context_lines=10):
@@ -0,0 +1 @@
1
+ __version__ = "0.1.27"