ripperdoc 0.2.3__py3-none-any.whl → 0.2.5__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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/__main__.py +0 -5
- ripperdoc/cli/cli.py +37 -16
- ripperdoc/cli/commands/__init__.py +2 -0
- ripperdoc/cli/commands/agents_cmd.py +12 -9
- ripperdoc/cli/commands/compact_cmd.py +7 -3
- ripperdoc/cli/commands/context_cmd.py +35 -15
- ripperdoc/cli/commands/doctor_cmd.py +27 -14
- ripperdoc/cli/commands/exit_cmd.py +1 -1
- ripperdoc/cli/commands/mcp_cmd.py +13 -8
- ripperdoc/cli/commands/memory_cmd.py +5 -5
- ripperdoc/cli/commands/models_cmd.py +47 -16
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +1 -2
- ripperdoc/cli/commands/tasks_cmd.py +24 -13
- ripperdoc/cli/ui/rich_ui.py +523 -396
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/agents.py +172 -4
- ripperdoc/core/config.py +130 -6
- ripperdoc/core/default_tools.py +13 -2
- ripperdoc/core/permissions.py +20 -14
- ripperdoc/core/providers/__init__.py +31 -15
- ripperdoc/core/providers/anthropic.py +122 -8
- ripperdoc/core/providers/base.py +93 -15
- ripperdoc/core/providers/gemini.py +539 -96
- ripperdoc/core/providers/openai.py +371 -26
- ripperdoc/core/query.py +301 -62
- ripperdoc/core/query_utils.py +51 -7
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +79 -67
- ripperdoc/core/tool.py +15 -6
- ripperdoc/sdk/client.py +14 -1
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +82 -26
- ripperdoc/tools/bash_tool.py +356 -209
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +53 -10
- ripperdoc/tools/file_read_tool.py +17 -7
- ripperdoc/tools/file_write_tool.py +49 -13
- ripperdoc/tools/glob_tool.py +10 -9
- ripperdoc/tools/grep_tool.py +182 -51
- ripperdoc/tools/ls_tool.py +6 -6
- ripperdoc/tools/mcp_tools.py +172 -413
- ripperdoc/tools/multi_edit_tool.py +49 -9
- ripperdoc/tools/notebook_edit_tool.py +57 -13
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +91 -9
- ripperdoc/tools/todo_tool.py +12 -12
- ripperdoc/tools/tool_search_tool.py +5 -6
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/file_watch.py +5 -4
- ripperdoc/utils/json_utils.py +4 -4
- ripperdoc/utils/log.py +3 -3
- ripperdoc/utils/mcp.py +82 -22
- ripperdoc/utils/memory.py +9 -6
- ripperdoc/utils/message_compaction.py +19 -16
- ripperdoc/utils/messages.py +73 -8
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/permissions/__init__.py +7 -1
- ripperdoc/utils/permissions/path_validation_utils.py +5 -3
- ripperdoc/utils/permissions/shell_command_validation.py +496 -18
- ripperdoc/utils/prompt.py +1 -1
- ripperdoc/utils/safe_get_cwd.py +5 -2
- ripperdoc/utils/session_history.py +38 -19
- ripperdoc/utils/todo.py +6 -2
- ripperdoc/utils/token_estimation.py +34 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/METADATA +14 -1
- ripperdoc-0.2.5.dist-info/RECORD +107 -0
- ripperdoc-0.2.3.dist-info/RECORD +0 -95
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Exit plan mode tool for presenting implementation plans.
|
|
2
|
+
|
|
3
|
+
This tool allows the AI to exit plan mode and present an implementation
|
|
4
|
+
plan to the user for approval before starting to code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from textwrap import dedent
|
|
10
|
+
from typing import AsyncGenerator, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from ripperdoc.core.tool import (
|
|
15
|
+
Tool,
|
|
16
|
+
ToolOutput,
|
|
17
|
+
ToolResult,
|
|
18
|
+
ToolUseContext,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
)
|
|
21
|
+
from ripperdoc.utils.log import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger()
|
|
24
|
+
|
|
25
|
+
TOOL_NAME = "ExitPlanMode"
|
|
26
|
+
|
|
27
|
+
EXIT_PLAN_MODE_PROMPT = dedent(
|
|
28
|
+
"""\
|
|
29
|
+
Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.
|
|
30
|
+
|
|
31
|
+
## How This Tool Works
|
|
32
|
+
- You should have already written your plan to the plan file specified in the plan mode system message
|
|
33
|
+
- This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote
|
|
34
|
+
- This tool simply signals that you're done planning and ready for the user to review and approve
|
|
35
|
+
- The user will see the contents of your plan file when they review it
|
|
36
|
+
|
|
37
|
+
## When to Use This Tool
|
|
38
|
+
IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.
|
|
39
|
+
|
|
40
|
+
## Handling Ambiguity in Plans
|
|
41
|
+
Before using this tool, ensure your plan is clear and unambiguous. If there are multiple valid approaches or unclear requirements:
|
|
42
|
+
1. Use the AskUserQuestion tool to clarify with the user
|
|
43
|
+
2. Ask about specific implementation choices (e.g., architectural patterns, which library to use)
|
|
44
|
+
3. Clarify any assumptions that could affect the implementation
|
|
45
|
+
4. Edit your plan file to incorporate user feedback
|
|
46
|
+
5. Only proceed with ExitPlanMode after resolving ambiguities and updating the plan file
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
1. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.
|
|
51
|
+
2. Initial task: "Help me implement yank mode for vim" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.
|
|
52
|
+
3. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach.
|
|
53
|
+
"""
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ExitPlanModeToolInput(BaseModel):
|
|
58
|
+
"""Input for the ExitPlanMode tool."""
|
|
59
|
+
|
|
60
|
+
plan: str = Field(
|
|
61
|
+
description="The plan you came up with, that you want to run by the user for approval. "
|
|
62
|
+
"Supports markdown. The plan should be pretty concise."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ExitPlanModeToolOutput(BaseModel):
|
|
67
|
+
"""Output from the ExitPlanMode tool."""
|
|
68
|
+
|
|
69
|
+
plan: str
|
|
70
|
+
is_agent: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ExitPlanModeTool(Tool[ExitPlanModeToolInput, ExitPlanModeToolOutput]):
|
|
74
|
+
"""Tool for exiting plan mode and presenting a plan for approval."""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def name(self) -> str:
|
|
78
|
+
return TOOL_NAME
|
|
79
|
+
|
|
80
|
+
async def description(self) -> str:
|
|
81
|
+
return "Prompts the user to exit plan mode and start coding"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def input_schema(self) -> type[ExitPlanModeToolInput]:
|
|
85
|
+
return ExitPlanModeToolInput
|
|
86
|
+
|
|
87
|
+
async def prompt(self, safe_mode: bool = False) -> str: # noqa: ARG002
|
|
88
|
+
return EXIT_PLAN_MODE_PROMPT
|
|
89
|
+
|
|
90
|
+
def user_facing_name(self) -> str:
|
|
91
|
+
return "Exit plan mode"
|
|
92
|
+
|
|
93
|
+
def is_read_only(self) -> bool:
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def is_concurrency_safe(self) -> bool:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def needs_permissions(
|
|
100
|
+
self,
|
|
101
|
+
input_data: Optional[ExitPlanModeToolInput] = None, # noqa: ARG002
|
|
102
|
+
) -> bool:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
async def validate_input(
|
|
106
|
+
self,
|
|
107
|
+
input_data: ExitPlanModeToolInput,
|
|
108
|
+
context: Optional[ToolUseContext] = None, # noqa: ARG002
|
|
109
|
+
) -> ValidationResult:
|
|
110
|
+
"""Validate that plan is not empty."""
|
|
111
|
+
if not input_data.plan or not input_data.plan.strip():
|
|
112
|
+
return ValidationResult(
|
|
113
|
+
result=False,
|
|
114
|
+
message="Plan cannot be empty",
|
|
115
|
+
)
|
|
116
|
+
return ValidationResult(result=True)
|
|
117
|
+
|
|
118
|
+
def render_result_for_assistant(self, output: ExitPlanModeToolOutput) -> str:
|
|
119
|
+
"""Render the tool output for the AI assistant."""
|
|
120
|
+
return f"Exit plan mode and start coding now. Plan:\n{output.plan}"
|
|
121
|
+
|
|
122
|
+
def render_tool_use_message(
|
|
123
|
+
self,
|
|
124
|
+
input_data: ExitPlanModeToolInput,
|
|
125
|
+
verbose: bool = False, # noqa: ARG002
|
|
126
|
+
) -> str:
|
|
127
|
+
"""Render the tool use message for display."""
|
|
128
|
+
plan = input_data.plan
|
|
129
|
+
snippet = f"{plan[:77]}..." if len(plan) > 80 else plan
|
|
130
|
+
return f"Share plan for approval: {snippet}"
|
|
131
|
+
|
|
132
|
+
async def call(
|
|
133
|
+
self,
|
|
134
|
+
input_data: ExitPlanModeToolInput,
|
|
135
|
+
context: ToolUseContext,
|
|
136
|
+
) -> AsyncGenerator[ToolOutput, None]:
|
|
137
|
+
"""Execute the tool to exit plan mode."""
|
|
138
|
+
# Invoke the exit plan mode callback if available
|
|
139
|
+
if context.on_exit_plan_mode:
|
|
140
|
+
try:
|
|
141
|
+
context.on_exit_plan_mode()
|
|
142
|
+
except (RuntimeError, ValueError, TypeError):
|
|
143
|
+
logger.debug("[exit_plan_mode_tool] Failed to call on_exit_plan_mode")
|
|
144
|
+
|
|
145
|
+
is_agent = bool(context.agent_id)
|
|
146
|
+
output = ExitPlanModeToolOutput(
|
|
147
|
+
plan=input_data.plan,
|
|
148
|
+
is_agent=is_agent,
|
|
149
|
+
)
|
|
150
|
+
yield ToolResult(
|
|
151
|
+
data=output,
|
|
152
|
+
result_for_assistant=self.render_result_for_assistant(output),
|
|
153
|
+
)
|
|
@@ -4,6 +4,7 @@ Allows the AI to edit files by replacing text.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import AsyncGenerator, List, Optional
|
|
8
9
|
from pydantic import BaseModel, Field
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ from ripperdoc.core.tool import (
|
|
|
17
18
|
)
|
|
18
19
|
from ripperdoc.utils.log import get_logger
|
|
19
20
|
from ripperdoc.utils.file_watch import record_snapshot
|
|
21
|
+
from ripperdoc.utils.path_ignore import check_path_for_tool
|
|
20
22
|
|
|
21
23
|
logger = get_logger()
|
|
22
24
|
|
|
@@ -108,20 +110,59 @@ match exactly (including whitespace and indentation)."""
|
|
|
108
110
|
) -> ValidationResult:
|
|
109
111
|
# Check if file exists
|
|
110
112
|
if not os.path.exists(input_data.file_path):
|
|
111
|
-
return ValidationResult(
|
|
113
|
+
return ValidationResult(
|
|
114
|
+
result=False,
|
|
115
|
+
message=f"File not found: {input_data.file_path}",
|
|
116
|
+
error_code=1,
|
|
117
|
+
)
|
|
112
118
|
|
|
113
119
|
# Check if it's a file
|
|
114
120
|
if not os.path.isfile(input_data.file_path):
|
|
115
121
|
return ValidationResult(
|
|
116
|
-
result=False,
|
|
122
|
+
result=False,
|
|
123
|
+
message=f"Path is not a file: {input_data.file_path}",
|
|
124
|
+
error_code=2,
|
|
117
125
|
)
|
|
118
126
|
|
|
119
127
|
# Check that old_string and new_string are different
|
|
120
128
|
if input_data.old_string == input_data.new_string:
|
|
121
129
|
return ValidationResult(
|
|
122
|
-
result=False,
|
|
130
|
+
result=False,
|
|
131
|
+
message="old_string and new_string must be different",
|
|
132
|
+
error_code=3,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Check if file has been read before editing
|
|
136
|
+
file_state_cache = getattr(context, "file_state_cache", {}) if context else {}
|
|
137
|
+
file_path = os.path.abspath(input_data.file_path)
|
|
138
|
+
file_snapshot = file_state_cache.get(file_path)
|
|
139
|
+
|
|
140
|
+
if not file_snapshot:
|
|
141
|
+
return ValidationResult(
|
|
142
|
+
result=False,
|
|
143
|
+
message="File has not been read yet. Read it first before editing.",
|
|
144
|
+
error_code=4,
|
|
123
145
|
)
|
|
124
146
|
|
|
147
|
+
# Check if file has been modified since it was read
|
|
148
|
+
try:
|
|
149
|
+
current_mtime = os.path.getmtime(file_path)
|
|
150
|
+
if current_mtime > file_snapshot.timestamp:
|
|
151
|
+
return ValidationResult(
|
|
152
|
+
result=False,
|
|
153
|
+
message="File has been modified since read, either by the user or by a linter. "
|
|
154
|
+
"Read it again before attempting to edit it.",
|
|
155
|
+
error_code=5,
|
|
156
|
+
)
|
|
157
|
+
except OSError:
|
|
158
|
+
pass # File mtime check failed, proceed anyway
|
|
159
|
+
|
|
160
|
+
# Check if path is ignored (warning for edit operations)
|
|
161
|
+
file_path_obj = Path(file_path)
|
|
162
|
+
should_proceed, warning_msg = check_path_for_tool(file_path_obj, tool_name="Edit", warn_only=True)
|
|
163
|
+
if warning_msg:
|
|
164
|
+
logger.warning("[file_edit_tool] %s", warning_msg)
|
|
165
|
+
|
|
125
166
|
return ValidationResult(result=True)
|
|
126
167
|
|
|
127
168
|
def render_result_for_assistant(self, output: FileEditToolOutput) -> str:
|
|
@@ -192,9 +233,10 @@ match exactly (including whitespace and indentation)."""
|
|
|
192
233
|
new_content,
|
|
193
234
|
getattr(context, "file_state_cache", {}),
|
|
194
235
|
)
|
|
195
|
-
except
|
|
196
|
-
logger.
|
|
197
|
-
"[file_edit_tool] Failed to record file snapshot",
|
|
236
|
+
except (OSError, IOError, RuntimeError) as exc:
|
|
237
|
+
logger.warning(
|
|
238
|
+
"[file_edit_tool] Failed to record file snapshot: %s: %s",
|
|
239
|
+
type(exc).__name__, exc,
|
|
198
240
|
extra={"file_path": input_data.file_path},
|
|
199
241
|
)
|
|
200
242
|
|
|
@@ -283,10 +325,11 @@ match exactly (including whitespace and indentation)."""
|
|
|
283
325
|
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
284
326
|
)
|
|
285
327
|
|
|
286
|
-
except
|
|
287
|
-
logger.
|
|
288
|
-
"[file_edit_tool] Error editing file",
|
|
289
|
-
|
|
328
|
+
except (OSError, IOError, PermissionError, UnicodeDecodeError, ValueError) as e:
|
|
329
|
+
logger.warning(
|
|
330
|
+
"[file_edit_tool] Error editing file: %s: %s",
|
|
331
|
+
type(e).__name__, e,
|
|
332
|
+
extra={"file_path": input_data.file_path},
|
|
290
333
|
)
|
|
291
334
|
error_output = FileEditToolOutput(
|
|
292
335
|
file_path=input_data.file_path,
|
|
@@ -4,6 +4,7 @@ Allows the AI to read file contents.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import AsyncGenerator, List, Optional
|
|
8
9
|
from pydantic import BaseModel, Field
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ from ripperdoc.core.tool import (
|
|
|
17
18
|
)
|
|
18
19
|
from ripperdoc.utils.log import get_logger
|
|
19
20
|
from ripperdoc.utils.file_watch import record_snapshot
|
|
21
|
+
from ripperdoc.utils.path_ignore import check_path_for_tool, is_path_ignored
|
|
20
22
|
|
|
21
23
|
logger = get_logger()
|
|
22
24
|
|
|
@@ -102,6 +104,12 @@ and limit to read only a portion of the file."""
|
|
|
102
104
|
result=False, message=f"Path is not a file: {input_data.file_path}"
|
|
103
105
|
)
|
|
104
106
|
|
|
107
|
+
# Check if path is ignored (warning only for read operations)
|
|
108
|
+
file_path = Path(input_data.file_path)
|
|
109
|
+
should_proceed, warning_msg = check_path_for_tool(file_path, tool_name="Read", warn_only=True)
|
|
110
|
+
if warning_msg:
|
|
111
|
+
logger.info("[file_read_tool] %s", warning_msg)
|
|
112
|
+
|
|
105
113
|
return ValidationResult(result=True)
|
|
106
114
|
|
|
107
115
|
def render_result_for_assistant(self, output: FileReadToolOutput) -> str:
|
|
@@ -153,9 +161,10 @@ and limit to read only a portion of the file."""
|
|
|
153
161
|
offset=offset,
|
|
154
162
|
limit=limit,
|
|
155
163
|
)
|
|
156
|
-
except
|
|
157
|
-
logger.
|
|
158
|
-
"[file_read_tool] Failed to record file snapshot",
|
|
164
|
+
except (OSError, IOError, RuntimeError) as exc:
|
|
165
|
+
logger.warning(
|
|
166
|
+
"[file_read_tool] Failed to record file snapshot: %s: %s",
|
|
167
|
+
type(exc).__name__, exc,
|
|
159
168
|
extra={"file_path": input_data.file_path},
|
|
160
169
|
)
|
|
161
170
|
|
|
@@ -171,10 +180,11 @@ and limit to read only a portion of the file."""
|
|
|
171
180
|
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
172
181
|
)
|
|
173
182
|
|
|
174
|
-
except
|
|
175
|
-
logger.
|
|
176
|
-
"[file_read_tool] Error reading file",
|
|
177
|
-
|
|
183
|
+
except (OSError, IOError, UnicodeDecodeError, ValueError) as e:
|
|
184
|
+
logger.warning(
|
|
185
|
+
"[file_read_tool] Error reading file: %s: %s",
|
|
186
|
+
type(e).__name__, e,
|
|
187
|
+
extra={"file_path": input_data.file_path},
|
|
178
188
|
)
|
|
179
189
|
# Create an error output
|
|
180
190
|
error_output = FileReadToolOutput(
|
|
@@ -18,6 +18,7 @@ from ripperdoc.core.tool import (
|
|
|
18
18
|
)
|
|
19
19
|
from ripperdoc.utils.log import get_logger
|
|
20
20
|
from ripperdoc.utils.file_watch import record_snapshot
|
|
21
|
+
from ripperdoc.utils.path_ignore import check_path_for_tool
|
|
21
22
|
|
|
22
23
|
logger = get_logger()
|
|
23
24
|
|
|
@@ -92,18 +93,51 @@ NEVER write new files unless explicitly required by the user."""
|
|
|
92
93
|
async def validate_input(
|
|
93
94
|
self, input_data: FileWriteToolInput, context: Optional[ToolUseContext] = None
|
|
94
95
|
) -> ValidationResult:
|
|
95
|
-
# Check if file already exists (warning)
|
|
96
|
-
if os.path.exists(input_data.file_path):
|
|
97
|
-
# In safe mode, this should be handled by permissions
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
96
|
# Check if parent directory exists
|
|
101
97
|
parent = Path(input_data.file_path).parent
|
|
102
98
|
if not parent.exists():
|
|
103
99
|
return ValidationResult(
|
|
104
|
-
result=False,
|
|
100
|
+
result=False,
|
|
101
|
+
message=f"Parent directory does not exist: {parent}",
|
|
102
|
+
error_code=1,
|
|
105
103
|
)
|
|
106
104
|
|
|
105
|
+
file_path = os.path.abspath(input_data.file_path)
|
|
106
|
+
|
|
107
|
+
# If file doesn't exist, it's a new file - allow without reading first
|
|
108
|
+
if not os.path.exists(file_path):
|
|
109
|
+
return ValidationResult(result=True)
|
|
110
|
+
|
|
111
|
+
# File exists - check if it has been read before writing
|
|
112
|
+
file_state_cache = getattr(context, "file_state_cache", {}) if context else {}
|
|
113
|
+
file_snapshot = file_state_cache.get(file_path)
|
|
114
|
+
|
|
115
|
+
if not file_snapshot:
|
|
116
|
+
return ValidationResult(
|
|
117
|
+
result=False,
|
|
118
|
+
message="File has not been read yet. Read it first before writing to it.",
|
|
119
|
+
error_code=2,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Check if file has been modified since it was read
|
|
123
|
+
try:
|
|
124
|
+
current_mtime = os.path.getmtime(file_path)
|
|
125
|
+
if current_mtime > file_snapshot.timestamp:
|
|
126
|
+
return ValidationResult(
|
|
127
|
+
result=False,
|
|
128
|
+
message="File has been modified since read, either by the user or by a linter. "
|
|
129
|
+
"Read it again before attempting to write it.",
|
|
130
|
+
error_code=3,
|
|
131
|
+
)
|
|
132
|
+
except OSError:
|
|
133
|
+
pass # File mtime check failed, proceed anyway
|
|
134
|
+
|
|
135
|
+
# Check if path is ignored (warning for write operations)
|
|
136
|
+
file_path_obj = Path(file_path)
|
|
137
|
+
should_proceed, warning_msg = check_path_for_tool(file_path_obj, tool_name="Write", warn_only=True)
|
|
138
|
+
if warning_msg:
|
|
139
|
+
logger.warning("[file_write_tool] %s", warning_msg)
|
|
140
|
+
|
|
107
141
|
return ValidationResult(result=True)
|
|
108
142
|
|
|
109
143
|
def render_result_for_assistant(self, output: FileWriteToolOutput) -> str:
|
|
@@ -132,9 +166,10 @@ NEVER write new files unless explicitly required by the user."""
|
|
|
132
166
|
input_data.content,
|
|
133
167
|
getattr(context, "file_state_cache", {}),
|
|
134
168
|
)
|
|
135
|
-
except
|
|
136
|
-
logger.
|
|
137
|
-
"[file_write_tool] Failed to record file snapshot",
|
|
169
|
+
except (OSError, IOError, RuntimeError) as exc:
|
|
170
|
+
logger.warning(
|
|
171
|
+
"[file_write_tool] Failed to record file snapshot: %s: %s",
|
|
172
|
+
type(exc).__name__, exc,
|
|
138
173
|
extra={"file_path": input_data.file_path},
|
|
139
174
|
)
|
|
140
175
|
|
|
@@ -149,10 +184,11 @@ NEVER write new files unless explicitly required by the user."""
|
|
|
149
184
|
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
150
185
|
)
|
|
151
186
|
|
|
152
|
-
except
|
|
153
|
-
logger.
|
|
154
|
-
"[file_write_tool] Error writing file",
|
|
155
|
-
|
|
187
|
+
except (OSError, IOError, PermissionError, UnicodeEncodeError) as e:
|
|
188
|
+
logger.warning(
|
|
189
|
+
"[file_write_tool] Error writing file: %s: %s",
|
|
190
|
+
type(e).__name__, e,
|
|
191
|
+
extra={"file_path": input_data.file_path},
|
|
156
192
|
)
|
|
157
193
|
error_output = FileWriteToolOutput(
|
|
158
194
|
file_path=input_data.file_path,
|
ripperdoc/tools/glob_tool.py
CHANGED
|
@@ -76,7 +76,7 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
|
|
|
76
76
|
),
|
|
77
77
|
]
|
|
78
78
|
|
|
79
|
-
async def prompt(self,
|
|
79
|
+
async def prompt(self, _safe_mode: bool = False) -> str:
|
|
80
80
|
return GLOB_USAGE
|
|
81
81
|
|
|
82
82
|
def is_read_only(self) -> bool:
|
|
@@ -85,11 +85,11 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
|
|
|
85
85
|
def is_concurrency_safe(self) -> bool:
|
|
86
86
|
return True
|
|
87
87
|
|
|
88
|
-
def needs_permissions(self,
|
|
88
|
+
def needs_permissions(self, _input_data: Optional[GlobToolInput] = None) -> bool:
|
|
89
89
|
return False
|
|
90
90
|
|
|
91
91
|
async def validate_input(
|
|
92
|
-
self,
|
|
92
|
+
self, _input_data: GlobToolInput, _context: Optional[ToolUseContext] = None
|
|
93
93
|
) -> ValidationResult:
|
|
94
94
|
return ValidationResult(result=True)
|
|
95
95
|
|
|
@@ -103,7 +103,7 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
|
|
|
103
103
|
lines.append("(Results are truncated. Consider using a more specific path or pattern.)")
|
|
104
104
|
return "\n".join(lines)
|
|
105
105
|
|
|
106
|
-
def render_tool_use_message(self, input_data: GlobToolInput,
|
|
106
|
+
def render_tool_use_message(self, input_data: GlobToolInput, _verbose: bool = False) -> str:
|
|
107
107
|
"""Format the tool use for display."""
|
|
108
108
|
if not input_data.pattern:
|
|
109
109
|
return "Glob"
|
|
@@ -123,7 +123,7 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
|
|
|
123
123
|
except ValueError:
|
|
124
124
|
relative_path = None
|
|
125
125
|
|
|
126
|
-
if
|
|
126
|
+
if _verbose or not relative_path or str(relative_path) == ".":
|
|
127
127
|
rendered_path = str(absolute_path)
|
|
128
128
|
else:
|
|
129
129
|
rendered_path = str(relative_path)
|
|
@@ -132,7 +132,7 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
|
|
|
132
132
|
return f'pattern: "{input_data.pattern}"{path_fragment}'
|
|
133
133
|
|
|
134
134
|
async def call(
|
|
135
|
-
self, input_data: GlobToolInput,
|
|
135
|
+
self, input_data: GlobToolInput, _context: ToolUseContext
|
|
136
136
|
) -> AsyncGenerator[ToolOutput, None]:
|
|
137
137
|
"""Find files matching the pattern."""
|
|
138
138
|
|
|
@@ -166,9 +166,10 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
|
|
|
166
166
|
data=output, result_for_assistant=self.render_result_for_assistant(output)
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
-
except
|
|
170
|
-
logger.
|
|
171
|
-
"[glob_tool] Error executing glob",
|
|
169
|
+
except (OSError, RuntimeError, ValueError) as e:
|
|
170
|
+
logger.warning(
|
|
171
|
+
"[glob_tool] Error executing glob: %s: %s",
|
|
172
|
+
type(e).__name__, e,
|
|
172
173
|
extra={"pattern": input_data.pattern, "path": input_data.path},
|
|
173
174
|
)
|
|
174
175
|
error_output = GlobToolOutput(matches=[], pattern=input_data.pattern, count=0)
|