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.
- codemaster_cli-1.0.1.dist-info/METADATA +645 -0
- codemaster_cli-1.0.1.dist-info/RECORD +174 -0
- codemaster_cli-1.0.1.dist-info/WHEEL +4 -0
- codemaster_cli-1.0.1.dist-info/entry_points.txt +3 -0
- vibe/__init__.py +6 -0
- vibe/acp/__init__.py +0 -0
- vibe/acp/acp_agent_loop.py +746 -0
- vibe/acp/entrypoint.py +81 -0
- vibe/acp/tools/__init__.py +0 -0
- vibe/acp/tools/base.py +100 -0
- vibe/acp/tools/builtins/bash.py +134 -0
- vibe/acp/tools/builtins/read_file.py +54 -0
- vibe/acp/tools/builtins/search_replace.py +129 -0
- vibe/acp/tools/builtins/todo.py +65 -0
- vibe/acp/tools/builtins/write_file.py +98 -0
- vibe/acp/tools/session_update.py +118 -0
- vibe/acp/utils.py +213 -0
- vibe/cli/__init__.py +0 -0
- vibe/cli/autocompletion/__init__.py +0 -0
- vibe/cli/autocompletion/base.py +22 -0
- vibe/cli/autocompletion/path_completion.py +177 -0
- vibe/cli/autocompletion/slash_command.py +99 -0
- vibe/cli/cli.py +229 -0
- vibe/cli/clipboard.py +69 -0
- vibe/cli/commands.py +116 -0
- vibe/cli/entrypoint.py +173 -0
- vibe/cli/history_manager.py +91 -0
- vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
- vibe/cli/plan_offer/decide_plan_offer.py +87 -0
- vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
- vibe/cli/terminal_setup.py +323 -0
- vibe/cli/textual_ui/__init__.py +0 -0
- vibe/cli/textual_ui/ansi_markdown.py +58 -0
- vibe/cli/textual_ui/app.py +1546 -0
- vibe/cli/textual_ui/app.tcss +1020 -0
- vibe/cli/textual_ui/external_editor.py +32 -0
- vibe/cli/textual_ui/handlers/__init__.py +5 -0
- vibe/cli/textual_ui/handlers/event_handler.py +147 -0
- vibe/cli/textual_ui/widgets/__init__.py +0 -0
- vibe/cli/textual_ui/widgets/approval_app.py +192 -0
- vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
- vibe/cli/textual_ui/widgets/banner/petit_chat.py +176 -0
- vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
- vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
- vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
- vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
- vibe/cli/textual_ui/widgets/compact.py +41 -0
- vibe/cli/textual_ui/widgets/config_app.py +171 -0
- vibe/cli/textual_ui/widgets/context_progress.py +30 -0
- vibe/cli/textual_ui/widgets/load_more.py +43 -0
- vibe/cli/textual_ui/widgets/loading.py +201 -0
- vibe/cli/textual_ui/widgets/messages.py +277 -0
- vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
- vibe/cli/textual_ui/widgets/path_display.py +28 -0
- vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
- vibe/cli/textual_ui/widgets/question_app.py +496 -0
- vibe/cli/textual_ui/widgets/spinner.py +194 -0
- vibe/cli/textual_ui/widgets/status_message.py +76 -0
- vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
- vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
- vibe/cli/textual_ui/widgets/tools.py +201 -0
- vibe/cli/textual_ui/windowing/__init__.py +29 -0
- vibe/cli/textual_ui/windowing/history.py +105 -0
- vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
- vibe/cli/textual_ui/windowing/state.py +105 -0
- vibe/cli/update_notifier/__init__.py +47 -0
- vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
- vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
- vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
- vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
- vibe/cli/update_notifier/ports/update_gateway.py +53 -0
- vibe/cli/update_notifier/update.py +139 -0
- vibe/cli/update_notifier/whats_new.py +49 -0
- vibe/core/__init__.py +5 -0
- vibe/core/agent_loop.py +1075 -0
- vibe/core/agents/__init__.py +31 -0
- vibe/core/agents/manager.py +166 -0
- vibe/core/agents/models.py +143 -0
- vibe/core/auth/__init__.py +6 -0
- vibe/core/auth/crypto.py +137 -0
- vibe/core/auth/github.py +178 -0
- vibe/core/autocompletion/__init__.py +0 -0
- vibe/core/autocompletion/completers.py +257 -0
- vibe/core/autocompletion/file_indexer/__init__.py +10 -0
- vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
- vibe/core/autocompletion/file_indexer/indexer.py +179 -0
- vibe/core/autocompletion/file_indexer/store.py +169 -0
- vibe/core/autocompletion/file_indexer/watcher.py +71 -0
- vibe/core/autocompletion/fuzzy.py +189 -0
- vibe/core/autocompletion/path_prompt.py +108 -0
- vibe/core/autocompletion/path_prompt_adapter.py +149 -0
- vibe/core/config.py +983 -0
- vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
- vibe/core/llm/__init__.py +0 -0
- vibe/core/llm/backend/anthropic.py +630 -0
- vibe/core/llm/backend/base.py +38 -0
- vibe/core/llm/backend/factory.py +7 -0
- vibe/core/llm/backend/generic.py +425 -0
- vibe/core/llm/backend/mistral.py +381 -0
- vibe/core/llm/backend/vertex.py +115 -0
- vibe/core/llm/exceptions.py +195 -0
- vibe/core/llm/format.py +184 -0
- vibe/core/llm/message_utils.py +24 -0
- vibe/core/llm/types.py +120 -0
- vibe/core/middleware.py +209 -0
- vibe/core/output_formatters.py +85 -0
- vibe/core/paths/__init__.py +0 -0
- vibe/core/paths/config_paths.py +68 -0
- vibe/core/paths/global_paths.py +40 -0
- vibe/core/programmatic.py +56 -0
- vibe/core/prompts/__init__.py +33 -0
- vibe/core/prompts/cli.md +111 -0
- vibe/core/prompts/compact.md +48 -0
- vibe/core/prompts/dangerous_directory.md +5 -0
- vibe/core/prompts/explore.md +50 -0
- vibe/core/prompts/gitmaster.md +38 -0
- vibe/core/prompts/project_context.md +8 -0
- vibe/core/prompts/tests.md +1 -0
- vibe/core/proxy_setup.py +65 -0
- vibe/core/session/session_loader.py +222 -0
- vibe/core/session/session_logger.py +318 -0
- vibe/core/session/session_migration.py +41 -0
- vibe/core/skills/__init__.py +7 -0
- vibe/core/skills/manager.py +132 -0
- vibe/core/skills/models.py +92 -0
- vibe/core/skills/parser.py +39 -0
- vibe/core/system_prompt.py +466 -0
- vibe/core/telemetry/__init__.py +0 -0
- vibe/core/telemetry/send.py +185 -0
- vibe/core/teleport/errors.py +9 -0
- vibe/core/teleport/git.py +196 -0
- vibe/core/teleport/nuage.py +180 -0
- vibe/core/teleport/teleport.py +208 -0
- vibe/core/teleport/types.py +54 -0
- vibe/core/tools/base.py +338 -0
- vibe/core/tools/builtins/ask_user_question.py +134 -0
- vibe/core/tools/builtins/bash.py +454 -0
- vibe/core/tools/builtins/git_clone.py +861 -0
- vibe/core/tools/builtins/grep.py +310 -0
- vibe/core/tools/builtins/prompts/__init__.py +0 -0
- vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
- vibe/core/tools/builtins/prompts/bash.md +73 -0
- vibe/core/tools/builtins/prompts/git_clone.md +43 -0
- vibe/core/tools/builtins/prompts/gitmaster.md +38 -0
- vibe/core/tools/builtins/prompts/grep.md +4 -0
- vibe/core/tools/builtins/prompts/read_file.md +13 -0
- vibe/core/tools/builtins/prompts/search_replace.md +43 -0
- vibe/core/tools/builtins/prompts/task.md +24 -0
- vibe/core/tools/builtins/prompts/todo.md +199 -0
- vibe/core/tools/builtins/prompts/write_file.md +42 -0
- vibe/core/tools/builtins/read_file.py +222 -0
- vibe/core/tools/builtins/search_replace.py +456 -0
- vibe/core/tools/builtins/task.py +154 -0
- vibe/core/tools/builtins/todo.py +134 -0
- vibe/core/tools/builtins/write_file.py +160 -0
- vibe/core/tools/manager.py +341 -0
- vibe/core/tools/mcp.py +397 -0
- vibe/core/tools/ui.py +68 -0
- vibe/core/trusted_folders.py +86 -0
- vibe/core/types.py +401 -0
- vibe/core/utils.py +396 -0
- vibe/setup/onboarding/__init__.py +39 -0
- vibe/setup/onboarding/base.py +14 -0
- vibe/setup/onboarding/onboarding.tcss +134 -0
- vibe/setup/onboarding/screens/__init__.py +5 -0
- vibe/setup/onboarding/screens/api_key.py +200 -0
- vibe/setup/onboarding/screens/provider_selection.py +184 -0
- vibe/setup/onboarding/screens/welcome.py +136 -0
- vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
- vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
- 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"
|