codemaster-cli 1.0.1__py3-none-any.whl

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 (174) hide show
  1. codemaster_cli-1.0.1.dist-info/METADATA +645 -0
  2. codemaster_cli-1.0.1.dist-info/RECORD +174 -0
  3. codemaster_cli-1.0.1.dist-info/WHEEL +4 -0
  4. codemaster_cli-1.0.1.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +229 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +173 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +176 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +166 -0
  81. vibe/core/agents/models.py +143 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +983 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +33 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/gitmaster.md +38 -0
  120. vibe/core/prompts/project_context.md +8 -0
  121. vibe/core/prompts/tests.md +1 -0
  122. vibe/core/proxy_setup.py +65 -0
  123. vibe/core/session/session_loader.py +222 -0
  124. vibe/core/session/session_logger.py +318 -0
  125. vibe/core/session/session_migration.py +41 -0
  126. vibe/core/skills/__init__.py +7 -0
  127. vibe/core/skills/manager.py +132 -0
  128. vibe/core/skills/models.py +92 -0
  129. vibe/core/skills/parser.py +39 -0
  130. vibe/core/system_prompt.py +466 -0
  131. vibe/core/telemetry/__init__.py +0 -0
  132. vibe/core/telemetry/send.py +185 -0
  133. vibe/core/teleport/errors.py +9 -0
  134. vibe/core/teleport/git.py +196 -0
  135. vibe/core/teleport/nuage.py +180 -0
  136. vibe/core/teleport/teleport.py +208 -0
  137. vibe/core/teleport/types.py +54 -0
  138. vibe/core/tools/base.py +338 -0
  139. vibe/core/tools/builtins/ask_user_question.py +134 -0
  140. vibe/core/tools/builtins/bash.py +454 -0
  141. vibe/core/tools/builtins/git_clone.py +861 -0
  142. vibe/core/tools/builtins/grep.py +310 -0
  143. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  144. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  145. vibe/core/tools/builtins/prompts/bash.md +73 -0
  146. vibe/core/tools/builtins/prompts/git_clone.md +43 -0
  147. vibe/core/tools/builtins/prompts/gitmaster.md +38 -0
  148. vibe/core/tools/builtins/prompts/grep.md +4 -0
  149. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  150. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  151. vibe/core/tools/builtins/prompts/task.md +24 -0
  152. vibe/core/tools/builtins/prompts/todo.md +199 -0
  153. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  154. vibe/core/tools/builtins/read_file.py +222 -0
  155. vibe/core/tools/builtins/search_replace.py +456 -0
  156. vibe/core/tools/builtins/task.py +154 -0
  157. vibe/core/tools/builtins/todo.py +134 -0
  158. vibe/core/tools/builtins/write_file.py +160 -0
  159. vibe/core/tools/manager.py +341 -0
  160. vibe/core/tools/mcp.py +397 -0
  161. vibe/core/tools/ui.py +68 -0
  162. vibe/core/trusted_folders.py +86 -0
  163. vibe/core/types.py +401 -0
  164. vibe/core/utils.py +396 -0
  165. vibe/setup/onboarding/__init__.py +39 -0
  166. vibe/setup/onboarding/base.py +14 -0
  167. vibe/setup/onboarding/onboarding.tcss +134 -0
  168. vibe/setup/onboarding/screens/__init__.py +5 -0
  169. vibe/setup/onboarding/screens/api_key.py +200 -0
  170. vibe/setup/onboarding/screens/provider_selection.py +184 -0
  171. vibe/setup/onboarding/screens/welcome.py +136 -0
  172. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  173. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  174. vibe/whats_new.md +5 -0
@@ -0,0 +1,199 @@
1
+ Use the `todo` tool to manage a simple task list. This tool helps you track tasks and their progress.
2
+
3
+ ## How it works
4
+
5
+ - **Reading:** Use `action: "read"` to view the current todo list
6
+ - **Writing:** Use `action: "write"` with the complete `todos` list to update. You must provide the ENTIRE list - this replaces everything.
7
+
8
+ ## Todo Structure
9
+ Each todo item has:
10
+ - `id`: A unique string identifier (e.g., "1", "2", "task-a")
11
+ - `content`: The task description
12
+ - `status`: One of: "pending", "in_progress", "completed", "cancelled"
13
+ - `priority`: One of: "high", "medium", "low"
14
+
15
+ ## When to Use This Tool
16
+
17
+ **Use proactively for:**
18
+ - Complex multi-step tasks (3+ distinct steps)
19
+ - Non-trivial tasks requiring careful planning
20
+ - Multiple tasks provided by the user (numbered or comma-separated)
21
+ - Tracking progress on ongoing work
22
+ - After receiving new instructions - immediately capture requirements
23
+ - When starting work - mark task as in_progress BEFORE beginning
24
+ - After completing work - mark as completed and add any follow-up tasks discovered
25
+
26
+ **Skip this tool for:**
27
+ - Single, straightforward tasks
28
+ - Trivial operations (< 3 simple steps)
29
+ - Purely conversational or informational requests
30
+ - Tasks that provide no organizational benefit
31
+
32
+ ## Task Management Best Practices
33
+
34
+ 1. **Status Management:**
35
+ - Only ONE task should be `in_progress` at a time
36
+ - Mark tasks `in_progress` BEFORE starting work on them
37
+ - Mark tasks `completed` IMMEDIATELY after finishing
38
+ - Keep tasks `in_progress` if blocked or encountering errors
39
+
40
+ 2. **Task Completion Rules:**
41
+ - ONLY mark as `completed` when FULLY accomplished
42
+ - Never mark complete if tests are failing, implementation is partial, or errors are unresolved
43
+ - When blocked, create a new task describing what needs resolution
44
+
45
+ 3. **Task Organization:**
46
+ - Create specific, actionable items
47
+ - Break complex tasks into manageable steps
48
+ - Use clear, descriptive task names
49
+ - Remove irrelevant tasks entirely (don't just mark cancelled)
50
+
51
+ ## Examples
52
+
53
+ **Example 1: Reading todos**
54
+ ```json
55
+ {
56
+ "action": "read"
57
+ }
58
+ ```
59
+
60
+ **Example 2: Initial task creation (user requests multiple features)**
61
+ ```json
62
+ {
63
+ "action": "write",
64
+ "todos": [
65
+ {
66
+ "id": "1",
67
+ "content": "Add dark mode toggle to settings",
68
+ "status": "pending",
69
+ "priority": "high"
70
+ },
71
+ {
72
+ "id": "2",
73
+ "content": "Implement theme context/state management",
74
+ "status": "pending",
75
+ "priority": "high"
76
+ },
77
+ {
78
+ "id": "3",
79
+ "content": "Update components for theme switching",
80
+ "status": "pending",
81
+ "priority": "medium"
82
+ },
83
+ {
84
+ "id": "4",
85
+ "content": "Run tests and verify build",
86
+ "status": "pending",
87
+ "priority": "medium"
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ **Example 3: Starting work (marking one task in_progress)**
94
+ ```json
95
+ {
96
+ "action": "write",
97
+ "todos": [
98
+ {
99
+ "id": "1",
100
+ "content": "Add dark mode toggle to settings",
101
+ "status": "in_progress",
102
+ "priority": "high"
103
+ },
104
+ {
105
+ "id": "2",
106
+ "content": "Implement theme context/state management",
107
+ "status": "pending",
108
+ "priority": "high"
109
+ },
110
+ {
111
+ "id": "3",
112
+ "content": "Update components for theme switching",
113
+ "status": "pending",
114
+ "priority": "medium"
115
+ },
116
+ {
117
+ "id": "4",
118
+ "content": "Run tests and verify build",
119
+ "status": "pending",
120
+ "priority": "medium"
121
+ }
122
+ ]
123
+ }
124
+ ```
125
+
126
+ **Example 4: Completing task and adding discovered subtask**
127
+ ```json
128
+ {
129
+ "action": "write",
130
+ "todos": [
131
+ {
132
+ "id": "1",
133
+ "content": "Add dark mode toggle to settings",
134
+ "status": "completed",
135
+ "priority": "high"
136
+ },
137
+ {
138
+ "id": "2",
139
+ "content": "Implement theme context/state management",
140
+ "status": "in_progress",
141
+ "priority": "high"
142
+ },
143
+ {
144
+ "id": "3",
145
+ "content": "Update components for theme switching",
146
+ "status": "pending",
147
+ "priority": "medium"
148
+ },
149
+ {
150
+ "id": "4",
151
+ "content": "Fix TypeScript errors in theme types",
152
+ "status": "pending",
153
+ "priority": "high"
154
+ },
155
+ {
156
+ "id": "5",
157
+ "content": "Run tests and verify build",
158
+ "status": "pending",
159
+ "priority": "medium"
160
+ }
161
+ ]
162
+ }
163
+ ```
164
+
165
+ **Example 5: Handling blockers (keeping task in_progress)**
166
+ ```json
167
+ {
168
+ "action": "write",
169
+ "todos": [
170
+ {
171
+ "id": "1",
172
+ "content": "Deploy to production",
173
+ "status": "in_progress",
174
+ "priority": "high"
175
+ },
176
+ {
177
+ "id": "2",
178
+ "content": "BLOCKER: Fix failing deployment pipeline",
179
+ "status": "pending",
180
+ "priority": "high"
181
+ },
182
+ {
183
+ "id": "3",
184
+ "content": "Update documentation",
185
+ "status": "pending",
186
+ "priority": "low"
187
+ }
188
+ ]
189
+ }
190
+ ```
191
+
192
+ ## Common Scenarios
193
+
194
+ **Multi-file refactoring:** Create todos for each file that needs updating
195
+ **Performance optimization:** List specific bottlenecks as individual tasks
196
+ **Bug fixing:** Track reproduction, diagnosis, fix, and verification as separate tasks
197
+ **Feature implementation:** Break down into UI, logic, tests, and documentation tasks
198
+
199
+ Remember: When writing, you must include ALL todos you want to keep. Any todo not in the list will be removed. Be proactive with task management to demonstrate thoroughness and ensure all requirements are completed successfully.
@@ -0,0 +1,42 @@
1
+ Use `write_file` to write content to a file.
2
+
3
+ **Arguments:**
4
+ - `path`: The file path (relative or absolute)
5
+ - `content`: The content to write to the file
6
+ - `overwrite`: Must be set to `true` to overwrite an existing file (default: `false`)
7
+
8
+ **IMPORTANT SAFETY RULES:**
9
+
10
+ - By default, the tool will **fail if the file already exists** to prevent accidental data loss
11
+ - To **overwrite** an existing file, you **MUST** set `overwrite: true`
12
+ - To **create a new file**, just provide the `path` and `content` (overwrite defaults to false)
13
+ - If parent directories don't exist, they will be created automatically
14
+
15
+ **BEST PRACTICES:**
16
+
17
+ - **ALWAYS** use the `read_file` tool first before overwriting an existing file to understand its current contents
18
+ - **ALWAYS** prefer using `search_replace` to edit existing files rather than overwriting them completely
19
+ - **NEVER** write new files unless explicitly required - prefer modifying existing files
20
+ - **NEVER** proactively create documentation files (*.md) or README files unless explicitly requested
21
+ - **AVOID** using emojis in file content unless the user explicitly requests them
22
+
23
+ **Usage Examples:**
24
+
25
+ ```python
26
+ # Create a new file (will error if file exists)
27
+ write_file(
28
+ path="src/new_module.py",
29
+ content="def hello():\n return 'Hello World'"
30
+ )
31
+
32
+ # Overwrite an existing file (must read it first!)
33
+ # First: read_file(path="src/existing.py")
34
+ # Then:
35
+ write_file(
36
+ path="src/existing.py",
37
+ content="# Updated content\ndef new_function():\n pass",
38
+ overwrite=True
39
+ )
40
+ ```
41
+
42
+ **Remember:** For editing existing files, prefer `search_replace` over `write_file` to preserve unchanged portions and avoid accidental data loss.
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncGenerator
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, ClassVar, NamedTuple, final
6
+
7
+ import anyio
8
+ from pydantic import BaseModel, Field
9
+
10
+ from vibe.core.tools.base import (
11
+ BaseTool,
12
+ BaseToolConfig,
13
+ BaseToolState,
14
+ InvokeContext,
15
+ ToolError,
16
+ ToolPermission,
17
+ )
18
+ from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
19
+ from vibe.core.types import ToolStreamEvent
20
+
21
+ if TYPE_CHECKING:
22
+ from vibe.core.types import ToolCallEvent, ToolResultEvent
23
+
24
+
25
+ class _ReadResult(NamedTuple):
26
+ lines: list[str]
27
+ bytes_read: int
28
+ was_truncated: bool
29
+
30
+
31
+ class ReadFileArgs(BaseModel):
32
+ path: str
33
+ offset: int = Field(
34
+ default=0,
35
+ description="Line number to start reading from (0-indexed, inclusive).",
36
+ )
37
+ limit: int | None = Field(
38
+ default=None, description="Maximum number of lines to read."
39
+ )
40
+
41
+
42
+ class ReadFileResult(BaseModel):
43
+ path: str
44
+ content: str
45
+ lines_read: int
46
+ was_truncated: bool = Field(
47
+ description="True if the reading was stopped due to the max_read_bytes limit."
48
+ )
49
+
50
+
51
+ class ReadFileToolConfig(BaseToolConfig):
52
+ permission: ToolPermission = ToolPermission.ALWAYS
53
+
54
+ max_read_bytes: int = Field(
55
+ default=64_000, description="Maximum total bytes to read from a file in one go."
56
+ )
57
+ max_state_history: int = Field(
58
+ default=10, description="Number of recently read files to remember in state."
59
+ )
60
+
61
+
62
+ class ReadFileState(BaseToolState):
63
+ recently_read_files: list[str] = Field(default_factory=list)
64
+
65
+
66
+ class ReadFile(
67
+ BaseTool[ReadFileArgs, ReadFileResult, ReadFileToolConfig, ReadFileState],
68
+ ToolUIData[ReadFileArgs, ReadFileResult],
69
+ ):
70
+ description: ClassVar[str] = (
71
+ "Read a UTF-8 file, returning content from a specific line range. "
72
+ "Reading is capped by a byte limit for safety."
73
+ )
74
+
75
+ @final
76
+ async def run(
77
+ self, args: ReadFileArgs, ctx: InvokeContext | None = None
78
+ ) -> AsyncGenerator[ToolStreamEvent | ReadFileResult, None]:
79
+ file_path = self._prepare_and_validate_path(args)
80
+
81
+ read_result = await self._read_file(args, file_path)
82
+
83
+ self._update_state_history(file_path)
84
+
85
+ yield ReadFileResult(
86
+ path=str(file_path),
87
+ content="".join(read_result.lines),
88
+ lines_read=len(read_result.lines),
89
+ was_truncated=read_result.was_truncated,
90
+ )
91
+
92
+ def check_allowlist_denylist(self, args: ReadFileArgs) -> ToolPermission | None:
93
+ import fnmatch
94
+
95
+ file_path = Path(args.path).expanduser()
96
+ if not file_path.is_absolute():
97
+ file_path = Path.cwd() / file_path
98
+ file_str = str(file_path)
99
+
100
+ for pattern in self.config.denylist:
101
+ if fnmatch.fnmatch(file_str, pattern):
102
+ return ToolPermission.NEVER
103
+
104
+ for pattern in self.config.allowlist:
105
+ if fnmatch.fnmatch(file_str, pattern):
106
+ return ToolPermission.ALWAYS
107
+
108
+ return None
109
+
110
+ def _prepare_and_validate_path(self, args: ReadFileArgs) -> Path:
111
+ self._validate_inputs(args)
112
+
113
+ file_path = Path(args.path).expanduser()
114
+ if not file_path.is_absolute():
115
+ file_path = Path.cwd() / file_path
116
+
117
+ self._validate_path(file_path)
118
+ return file_path
119
+
120
+ async def _read_file(self, args: ReadFileArgs, file_path: Path) -> _ReadResult:
121
+ try:
122
+ lines_to_return: list[str] = []
123
+ bytes_read = 0
124
+ was_truncated = False
125
+
126
+ async with await anyio.Path(file_path).open(
127
+ encoding="utf-8", errors="ignore"
128
+ ) as f:
129
+ line_index = 0
130
+ async for line in f:
131
+ if line_index < args.offset:
132
+ line_index += 1
133
+ continue
134
+
135
+ if args.limit is not None and len(lines_to_return) >= args.limit:
136
+ break
137
+
138
+ line_bytes = len(line.encode("utf-8"))
139
+ if bytes_read + line_bytes > self.config.max_read_bytes:
140
+ was_truncated = True
141
+ break
142
+
143
+ lines_to_return.append(line)
144
+ bytes_read += line_bytes
145
+ line_index += 1
146
+
147
+ return _ReadResult(
148
+ lines=lines_to_return,
149
+ bytes_read=bytes_read,
150
+ was_truncated=was_truncated,
151
+ )
152
+
153
+ except OSError as exc:
154
+ raise ToolError(f"Error reading {file_path}: {exc}") from exc
155
+
156
+ def _validate_inputs(self, args: ReadFileArgs) -> None:
157
+ if not args.path.strip():
158
+ raise ToolError("Path cannot be empty")
159
+ if args.offset < 0:
160
+ raise ToolError("Offset cannot be negative")
161
+ if args.limit is not None and args.limit <= 0:
162
+ raise ToolError("Limit, if provided, must be a positive number")
163
+
164
+ def _validate_path(self, file_path: Path) -> None:
165
+ try:
166
+ resolved_path = file_path.resolve()
167
+ except ValueError:
168
+ raise ToolError(
169
+ f"Security error: Cannot read path '{file_path}' outside of the project directory '{Path.cwd()}'."
170
+ )
171
+ except FileNotFoundError:
172
+ raise ToolError(f"File not found at: {file_path}")
173
+
174
+ if not resolved_path.exists():
175
+ raise ToolError(f"File not found at: {file_path}")
176
+ if resolved_path.is_dir():
177
+ raise ToolError(f"Path is a directory, not a file: {file_path}")
178
+
179
+ def _update_state_history(self, file_path: Path) -> None:
180
+ self.state.recently_read_files.append(str(file_path.resolve()))
181
+ if len(self.state.recently_read_files) > self.config.max_state_history:
182
+ self.state.recently_read_files.pop(0)
183
+
184
+ @classmethod
185
+ def get_call_display(cls, event: ToolCallEvent) -> ToolCallDisplay:
186
+ if not isinstance(event.args, ReadFileArgs):
187
+ return ToolCallDisplay(summary="read_file")
188
+
189
+ summary = f"Reading {event.args.path}"
190
+ if event.args.offset > 0 or event.args.limit is not None:
191
+ parts = []
192
+ if event.args.offset > 0:
193
+ parts.append(f"from line {event.args.offset}")
194
+ if event.args.limit is not None:
195
+ parts.append(f"limit {event.args.limit} lines")
196
+ summary += f" ({', '.join(parts)})"
197
+
198
+ return ToolCallDisplay(summary=summary)
199
+
200
+ @classmethod
201
+ def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
202
+ if not isinstance(event.result, ReadFileResult):
203
+ return ToolResultDisplay(
204
+ success=False, message=event.error or event.skip_reason or "No result"
205
+ )
206
+
207
+ path_obj = Path(event.result.path)
208
+ message = f"Read {event.result.lines_read} line{'' if event.result.lines_read <= 1 else 's'} from {path_obj.name}"
209
+ if event.result.was_truncated:
210
+ message += " (truncated)"
211
+
212
+ return ToolResultDisplay(
213
+ success=True,
214
+ message=message,
215
+ warnings=["File was truncated due to size limit"]
216
+ if event.result.was_truncated
217
+ else [],
218
+ )
219
+
220
+ @classmethod
221
+ def get_status_text(cls) -> str:
222
+ return "Reading file"