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.
Files changed (76) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/__main__.py +0 -5
  3. ripperdoc/cli/cli.py +37 -16
  4. ripperdoc/cli/commands/__init__.py +2 -0
  5. ripperdoc/cli/commands/agents_cmd.py +12 -9
  6. ripperdoc/cli/commands/compact_cmd.py +7 -3
  7. ripperdoc/cli/commands/context_cmd.py +35 -15
  8. ripperdoc/cli/commands/doctor_cmd.py +27 -14
  9. ripperdoc/cli/commands/exit_cmd.py +1 -1
  10. ripperdoc/cli/commands/mcp_cmd.py +13 -8
  11. ripperdoc/cli/commands/memory_cmd.py +5 -5
  12. ripperdoc/cli/commands/models_cmd.py +47 -16
  13. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  14. ripperdoc/cli/commands/resume_cmd.py +1 -2
  15. ripperdoc/cli/commands/tasks_cmd.py +24 -13
  16. ripperdoc/cli/ui/rich_ui.py +523 -396
  17. ripperdoc/cli/ui/tool_renderers.py +298 -0
  18. ripperdoc/core/agents.py +172 -4
  19. ripperdoc/core/config.py +130 -6
  20. ripperdoc/core/default_tools.py +13 -2
  21. ripperdoc/core/permissions.py +20 -14
  22. ripperdoc/core/providers/__init__.py +31 -15
  23. ripperdoc/core/providers/anthropic.py +122 -8
  24. ripperdoc/core/providers/base.py +93 -15
  25. ripperdoc/core/providers/gemini.py +539 -96
  26. ripperdoc/core/providers/openai.py +371 -26
  27. ripperdoc/core/query.py +301 -62
  28. ripperdoc/core/query_utils.py +51 -7
  29. ripperdoc/core/skills.py +295 -0
  30. ripperdoc/core/system_prompt.py +79 -67
  31. ripperdoc/core/tool.py +15 -6
  32. ripperdoc/sdk/client.py +14 -1
  33. ripperdoc/tools/ask_user_question_tool.py +431 -0
  34. ripperdoc/tools/background_shell.py +82 -26
  35. ripperdoc/tools/bash_tool.py +356 -209
  36. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  37. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  38. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  39. ripperdoc/tools/file_edit_tool.py +53 -10
  40. ripperdoc/tools/file_read_tool.py +17 -7
  41. ripperdoc/tools/file_write_tool.py +49 -13
  42. ripperdoc/tools/glob_tool.py +10 -9
  43. ripperdoc/tools/grep_tool.py +182 -51
  44. ripperdoc/tools/ls_tool.py +6 -6
  45. ripperdoc/tools/mcp_tools.py +172 -413
  46. ripperdoc/tools/multi_edit_tool.py +49 -9
  47. ripperdoc/tools/notebook_edit_tool.py +57 -13
  48. ripperdoc/tools/skill_tool.py +205 -0
  49. ripperdoc/tools/task_tool.py +91 -9
  50. ripperdoc/tools/todo_tool.py +12 -12
  51. ripperdoc/tools/tool_search_tool.py +5 -6
  52. ripperdoc/utils/coerce.py +34 -0
  53. ripperdoc/utils/context_length_errors.py +252 -0
  54. ripperdoc/utils/file_watch.py +5 -4
  55. ripperdoc/utils/json_utils.py +4 -4
  56. ripperdoc/utils/log.py +3 -3
  57. ripperdoc/utils/mcp.py +82 -22
  58. ripperdoc/utils/memory.py +9 -6
  59. ripperdoc/utils/message_compaction.py +19 -16
  60. ripperdoc/utils/messages.py +73 -8
  61. ripperdoc/utils/path_ignore.py +677 -0
  62. ripperdoc/utils/permissions/__init__.py +7 -1
  63. ripperdoc/utils/permissions/path_validation_utils.py +5 -3
  64. ripperdoc/utils/permissions/shell_command_validation.py +496 -18
  65. ripperdoc/utils/prompt.py +1 -1
  66. ripperdoc/utils/safe_get_cwd.py +5 -2
  67. ripperdoc/utils/session_history.py +38 -19
  68. ripperdoc/utils/todo.py +6 -2
  69. ripperdoc/utils/token_estimation.py +34 -0
  70. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/METADATA +14 -1
  71. ripperdoc-0.2.5.dist-info/RECORD +107 -0
  72. ripperdoc-0.2.3.dist-info/RECORD +0 -95
  73. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/WHEEL +0 -0
  74. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/entry_points.txt +0 -0
  75. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/licenses/LICENSE +0 -0
  76. {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(result=False, message=f"File not found: {input_data.file_path}")
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, message=f"Path is not a file: {input_data.file_path}"
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, message="old_string and new_string must be different"
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 Exception:
196
- logger.exception(
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 Exception as e:
287
- logger.exception(
288
- "[file_edit_tool] Error editing file",
289
- extra={"file_path": input_data.file_path, "error": str(e)},
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 Exception:
157
- logger.exception(
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 Exception as e:
175
- logger.exception(
176
- "[file_read_tool] Error reading file",
177
- extra={"file_path": input_data.file_path, "error": str(e)},
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, message=f"Parent directory does not exist: {parent}"
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 Exception:
136
- logger.exception(
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 Exception as e:
153
- logger.exception(
154
- "[file_write_tool] Error writing file",
155
- extra={"file_path": input_data.file_path, "error": str(e)},
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,
@@ -76,7 +76,7 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
76
76
  ),
77
77
  ]
78
78
 
79
- async def prompt(self, safe_mode: bool = False) -> str:
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, input_data: Optional[GlobToolInput] = None) -> bool:
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, input_data: GlobToolInput, context: Optional[ToolUseContext] = None
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, verbose: bool = False) -> str:
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 verbose or not relative_path or str(relative_path) == ".":
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, context: ToolUseContext
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 Exception as e:
170
- logger.exception(
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)