ripperdoc 0.2.8__py3-none-any.whl → 0.2.9__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 (84) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +28 -115
  3. ripperdoc/cli/commands/__init__.py +0 -1
  4. ripperdoc/cli/commands/agents_cmd.py +6 -3
  5. ripperdoc/cli/commands/clear_cmd.py +1 -4
  6. ripperdoc/cli/commands/config_cmd.py +1 -1
  7. ripperdoc/cli/commands/context_cmd.py +3 -2
  8. ripperdoc/cli/commands/doctor_cmd.py +18 -4
  9. ripperdoc/cli/commands/hooks_cmd.py +27 -53
  10. ripperdoc/cli/commands/models_cmd.py +26 -9
  11. ripperdoc/cli/commands/permissions_cmd.py +27 -9
  12. ripperdoc/cli/commands/resume_cmd.py +5 -3
  13. ripperdoc/cli/commands/status_cmd.py +4 -4
  14. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  15. ripperdoc/cli/ui/file_mention_completer.py +2 -1
  16. ripperdoc/cli/ui/interrupt_handler.py +2 -3
  17. ripperdoc/cli/ui/message_display.py +4 -2
  18. ripperdoc/cli/ui/provider_options.py +247 -0
  19. ripperdoc/cli/ui/rich_ui.py +110 -59
  20. ripperdoc/cli/ui/spinner.py +25 -1
  21. ripperdoc/cli/ui/tool_renderers.py +8 -2
  22. ripperdoc/cli/ui/wizard.py +215 -0
  23. ripperdoc/core/agents.py +9 -3
  24. ripperdoc/core/config.py +49 -12
  25. ripperdoc/core/custom_commands.py +7 -6
  26. ripperdoc/core/default_tools.py +11 -2
  27. ripperdoc/core/hooks/config.py +1 -3
  28. ripperdoc/core/hooks/events.py +23 -28
  29. ripperdoc/core/hooks/executor.py +4 -6
  30. ripperdoc/core/hooks/integration.py +12 -21
  31. ripperdoc/core/hooks/manager.py +40 -15
  32. ripperdoc/core/permissions.py +40 -8
  33. ripperdoc/core/providers/anthropic.py +109 -36
  34. ripperdoc/core/providers/gemini.py +70 -5
  35. ripperdoc/core/providers/openai.py +60 -5
  36. ripperdoc/core/query.py +82 -38
  37. ripperdoc/core/query_utils.py +2 -0
  38. ripperdoc/core/skills.py +9 -3
  39. ripperdoc/core/system_prompt.py +4 -2
  40. ripperdoc/core/tool.py +9 -5
  41. ripperdoc/sdk/client.py +2 -2
  42. ripperdoc/tools/ask_user_question_tool.py +5 -3
  43. ripperdoc/tools/background_shell.py +2 -1
  44. ripperdoc/tools/bash_output_tool.py +1 -1
  45. ripperdoc/tools/bash_tool.py +26 -16
  46. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  47. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  48. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  49. ripperdoc/tools/file_edit_tool.py +8 -4
  50. ripperdoc/tools/file_read_tool.py +8 -4
  51. ripperdoc/tools/file_write_tool.py +9 -5
  52. ripperdoc/tools/glob_tool.py +3 -2
  53. ripperdoc/tools/grep_tool.py +3 -2
  54. ripperdoc/tools/kill_bash_tool.py +1 -1
  55. ripperdoc/tools/ls_tool.py +1 -1
  56. ripperdoc/tools/mcp_tools.py +13 -10
  57. ripperdoc/tools/multi_edit_tool.py +8 -7
  58. ripperdoc/tools/notebook_edit_tool.py +7 -4
  59. ripperdoc/tools/skill_tool.py +1 -1
  60. ripperdoc/tools/task_tool.py +5 -4
  61. ripperdoc/tools/todo_tool.py +2 -2
  62. ripperdoc/tools/tool_search_tool.py +3 -2
  63. ripperdoc/utils/conversation_compaction.py +8 -4
  64. ripperdoc/utils/file_watch.py +8 -2
  65. ripperdoc/utils/json_utils.py +2 -1
  66. ripperdoc/utils/mcp.py +11 -3
  67. ripperdoc/utils/memory.py +4 -2
  68. ripperdoc/utils/message_compaction.py +21 -7
  69. ripperdoc/utils/message_formatting.py +11 -7
  70. ripperdoc/utils/messages.py +105 -66
  71. ripperdoc/utils/path_ignore.py +35 -8
  72. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  73. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  74. ripperdoc/utils/safe_get_cwd.py +2 -1
  75. ripperdoc/utils/session_history.py +13 -6
  76. ripperdoc/utils/todo.py +2 -1
  77. ripperdoc/utils/token_estimation.py +6 -1
  78. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
  79. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  80. ripperdoc-0.2.8.dist-info/RECORD +0 -121
  81. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  82. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  83. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  84. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
@@ -150,7 +150,7 @@ build projects, run tests, and interact with the file system."""
150
150
  ),
151
151
  ]
152
152
 
153
- async def prompt(self, safe_mode: bool = False) -> str:
153
+ async def prompt(self, yolo_mode: bool = False) -> str:
154
154
  sandbox_available = is_sandbox_available()
155
155
  try:
156
156
  current_shell = find_suitable_shell()
@@ -398,6 +398,11 @@ build projects, run tests, and interact with the file system."""
398
398
 
399
399
  validation = validate_shell_command(input_data.command)
400
400
  if validation.behavior == "ask":
401
+ # In yolo mode, allow shell metacharacters
402
+ if context and hasattr(context, 'yolo_mode') and context.yolo_mode \
403
+ and "shell metacharacters" in validation.message:
404
+ # Allow commands with shell metacharacters in yolo mode
405
+ return ValidationResult(result=True)
401
406
  return ValidationResult(result=False, message=validation.message)
402
407
 
403
408
  return ValidationResult(result=True)
@@ -505,9 +510,7 @@ build projects, run tests, and interact with the file system."""
505
510
 
506
511
  return command, False
507
512
 
508
- def _create_error_output(
509
- self, command: str, stderr: str, sandbox: bool
510
- ) -> BashToolOutput:
513
+ def _create_error_output(self, command: str, stderr: str, sandbox: bool) -> BashToolOutput:
511
514
  """Create a standardized error output."""
512
515
  return BashToolOutput(
513
516
  stdout="",
@@ -531,9 +534,13 @@ build projects, run tests, and interact with the file system."""
531
534
  return command, None, None
532
535
 
533
536
  if not is_sandbox_available():
534
- return None, self._create_error_output(
535
- command, "Sandbox mode requested but not available on this system", True
536
- ), None
537
+ return (
538
+ None,
539
+ self._create_error_output(
540
+ command, "Sandbox mode requested but not available on this system", True
541
+ ),
542
+ None,
543
+ )
537
544
 
538
545
  try:
539
546
  wrapper = create_sandbox_wrapper(command)
@@ -541,12 +548,15 @@ build projects, run tests, and interact with the file system."""
541
548
  except (OSError, RuntimeError, ValueError) as exc:
542
549
  logger.warning(
543
550
  "[bash_tool] Failed to enable sandbox: %s: %s",
544
- type(exc).__name__, exc,
551
+ type(exc).__name__,
552
+ exc,
545
553
  extra={"command": command},
546
554
  )
547
- return None, self._create_error_output(
548
- command, f"Failed to enable sandbox: {exc}", True
549
- ), None
555
+ return (
556
+ None,
557
+ self._create_error_output(command, f"Failed to enable sandbox: {exc}", True),
558
+ None,
559
+ )
550
560
 
551
561
  async def _run_background_command(
552
562
  self,
@@ -570,7 +580,8 @@ build projects, run tests, and interact with the file system."""
570
580
  # pragma: no cover - defensive import
571
581
  logger.warning(
572
582
  "[bash_tool] Failed to import background shell runner: %s: %s",
573
- type(e).__name__, e,
583
+ type(e).__name__,
584
+ e,
574
585
  extra={"command": effective_command},
575
586
  )
576
587
  return self._create_error_output(
@@ -696,9 +707,7 @@ build projects, run tests, and interact with the file system."""
696
707
  # Store results in a way that the caller can access
697
708
  self._last_execution_result = (stdout_lines, stderr_lines, timed_out)
698
709
 
699
- async def _drain_stream(
700
- self, stream: Optional[asyncio.StreamReader], sink: list[str]
701
- ) -> None:
710
+ async def _drain_stream(self, stream: Optional[asyncio.StreamReader], sink: list[str]) -> None:
702
711
  """Drain any remaining data from a stream."""
703
712
  if not stream:
704
713
  return
@@ -969,7 +978,8 @@ build projects, run tests, and interact with the file system."""
969
978
  raise # Re-raise cancellation
970
979
  logger.warning(
971
980
  "[bash_tool] Error executing command: %s: %s",
972
- type(e).__name__, e,
981
+ type(e).__name__,
982
+ e,
973
983
  extra={"command": effective_command},
974
984
  )
975
985
  error_output = self._create_error_output(
@@ -74,7 +74,8 @@ def _annotation_flag(tool_info: Any, key: str) -> bool:
74
74
  except (AttributeError, TypeError, KeyError) as exc:
75
75
  logger.debug(
76
76
  "[mcp_tools] Failed to read annotation flag: %s: %s",
77
- type(exc).__name__, exc,
77
+ type(exc).__name__,
78
+ exc,
78
79
  )
79
80
  return False
80
81
  return False
@@ -204,7 +205,7 @@ class DynamicMcpTool(Tool[BaseModel, McpToolCallOutput]):
204
205
  def input_schema(self) -> type[BaseModel]:
205
206
  return self._input_model
206
207
 
207
- async def prompt(self, _safe_mode: bool = False) -> str:
208
+ async def prompt(self, _yolo_mode: bool = False) -> str:
208
209
  return await self.description()
209
210
 
210
211
  def is_read_only(self) -> bool:
@@ -321,7 +322,14 @@ class DynamicMcpTool(Tool[BaseModel, McpToolCallOutput]):
321
322
  data=annotated_output,
322
323
  result_for_assistant=final_text,
323
324
  )
324
- except (OSError, RuntimeError, ConnectionError, ValueError, KeyError, TypeError) as exc: # pragma: no cover - runtime errors
325
+ except (
326
+ OSError,
327
+ RuntimeError,
328
+ ConnectionError,
329
+ ValueError,
330
+ KeyError,
331
+ TypeError,
332
+ ) as exc: # pragma: no cover - runtime errors
325
333
  output = McpToolCallOutput(
326
334
  server=self.server_name,
327
335
  tool=self.tool_info.name,
@@ -333,7 +341,8 @@ class DynamicMcpTool(Tool[BaseModel, McpToolCallOutput]):
333
341
  )
334
342
  logger.warning(
335
343
  "Error calling MCP tool: %s: %s",
336
- type(exc).__name__, exc,
344
+ type(exc).__name__,
345
+ exc,
337
346
  extra={
338
347
  "server": self.server_name,
339
348
  "tool": self.tool_info.name,
@@ -381,10 +390,16 @@ def load_dynamic_mcp_tools_sync(project_path: Optional[Path] = None) -> List[Dyn
381
390
 
382
391
  try:
383
392
  return asyncio.run(_load_and_keep())
384
- except (OSError, RuntimeError, ConnectionError, ValueError) as exc: # pragma: no cover - SDK/runtime failures
393
+ except (
394
+ OSError,
395
+ RuntimeError,
396
+ ConnectionError,
397
+ ValueError,
398
+ ) as exc: # pragma: no cover - SDK/runtime failures
385
399
  logger.warning(
386
400
  "Failed to initialize MCP runtime for dynamic tools (sync): %s: %s",
387
- type(exc).__name__, exc,
401
+ type(exc).__name__,
402
+ exc,
388
403
  )
389
404
  return []
390
405
 
@@ -393,10 +408,16 @@ async def load_dynamic_mcp_tools_async(project_path: Optional[Path] = None) -> L
393
408
  """Async loader for MCP tools when already in an event loop."""
394
409
  try:
395
410
  runtime = await ensure_mcp_runtime(project_path)
396
- except (OSError, RuntimeError, ConnectionError, ValueError) as exc: # pragma: no cover - SDK/runtime failures
411
+ except (
412
+ OSError,
413
+ RuntimeError,
414
+ ConnectionError,
415
+ ValueError,
416
+ ) as exc: # pragma: no cover - SDK/runtime failures
397
417
  logger.warning(
398
418
  "Failed to initialize MCP runtime for dynamic tools (async): %s: %s",
399
- type(exc).__name__, exc,
419
+ type(exc).__name__,
420
+ exc,
400
421
  )
401
422
  return []
402
423
  return _build_dynamic_mcp_tools(runtime)
@@ -151,7 +151,7 @@ class EnterPlanModeTool(Tool[EnterPlanModeToolInput, EnterPlanModeToolOutput]):
151
151
  def input_schema(self) -> type[EnterPlanModeToolInput]:
152
152
  return EnterPlanModeToolInput
153
153
 
154
- async def prompt(self, safe_mode: bool = False) -> str: # noqa: ARG002
154
+ async def prompt(self, yolo_mode: bool = False) -> str: # noqa: ARG002
155
155
  return ENTER_PLAN_MODE_PROMPT
156
156
 
157
157
  def user_facing_name(self) -> str:
@@ -84,7 +84,7 @@ class ExitPlanModeTool(Tool[ExitPlanModeToolInput, ExitPlanModeToolOutput]):
84
84
  def input_schema(self) -> type[ExitPlanModeToolInput]:
85
85
  return ExitPlanModeToolInput
86
86
 
87
- async def prompt(self, safe_mode: bool = False) -> str: # noqa: ARG002
87
+ async def prompt(self, yolo_mode: bool = False) -> str: # noqa: ARG002
88
88
  return EXIT_PLAN_MODE_PROMPT
89
89
 
90
90
  def user_facing_name(self) -> str:
@@ -84,7 +84,7 @@ match exactly (including whitespace and indentation)."""
84
84
  ),
85
85
  ]
86
86
 
87
- async def prompt(self, safe_mode: bool = False) -> str:
87
+ async def prompt(self, yolo_mode: bool = False) -> str:
88
88
  return (
89
89
  "Performs exact string replacements in files.\n\n"
90
90
  "Usage:\n"
@@ -159,7 +159,9 @@ match exactly (including whitespace and indentation)."""
159
159
 
160
160
  # Check if path is ignored (warning for edit operations)
161
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)
162
+ should_proceed, warning_msg = check_path_for_tool(
163
+ file_path_obj, tool_name="Edit", warn_only=True
164
+ )
163
165
  if warning_msg:
164
166
  logger.warning("[file_edit_tool] %s", warning_msg)
165
167
 
@@ -238,7 +240,8 @@ match exactly (including whitespace and indentation)."""
238
240
  except (OSError, IOError, RuntimeError) as exc:
239
241
  logger.warning(
240
242
  "[file_edit_tool] Failed to record file snapshot: %s: %s",
241
- type(exc).__name__, exc,
243
+ type(exc).__name__,
244
+ exc,
242
245
  extra={"file_path": abs_file_path},
243
246
  )
244
247
 
@@ -330,7 +333,8 @@ match exactly (including whitespace and indentation)."""
330
333
  except (OSError, IOError, PermissionError, UnicodeDecodeError, ValueError) as e:
331
334
  logger.warning(
332
335
  "[file_edit_tool] Error editing file: %s: %s",
333
- type(e).__name__, e,
336
+ type(e).__name__,
337
+ e,
334
338
  extra={"file_path": input_data.file_path},
335
339
  )
336
340
  error_output = FileEditToolOutput(
@@ -70,7 +70,7 @@ and limit to read only a portion of the file."""
70
70
  ),
71
71
  ]
72
72
 
73
- async def prompt(self, safe_mode: bool = False) -> str:
73
+ async def prompt(self, yolo_mode: bool = False) -> str:
74
74
  return (
75
75
  "Read a file from the local filesystem.\n\n"
76
76
  "Usage:\n"
@@ -106,7 +106,9 @@ and limit to read only a portion of the file."""
106
106
 
107
107
  # Check if path is ignored (warning only for read operations)
108
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)
109
+ should_proceed, warning_msg = check_path_for_tool(
110
+ file_path, tool_name="Read", warn_only=True
111
+ )
110
112
  if warning_msg:
111
113
  logger.info("[file_read_tool] %s", warning_msg)
112
114
 
@@ -166,7 +168,8 @@ and limit to read only a portion of the file."""
166
168
  except (OSError, IOError, RuntimeError) as exc:
167
169
  logger.warning(
168
170
  "[file_read_tool] Failed to record file snapshot: %s: %s",
169
- type(exc).__name__, exc,
171
+ type(exc).__name__,
172
+ exc,
170
173
  extra={"file_path": input_data.file_path},
171
174
  )
172
175
 
@@ -185,7 +188,8 @@ and limit to read only a portion of the file."""
185
188
  except (OSError, IOError, UnicodeDecodeError, ValueError) as e:
186
189
  logger.warning(
187
190
  "[file_read_tool] Error reading file: %s: %s",
188
- type(e).__name__, e,
191
+ type(e).__name__,
192
+ e,
189
193
  extra={"file_path": input_data.file_path},
190
194
  )
191
195
  # Create an error output
@@ -72,10 +72,10 @@ the file if it already exists."""
72
72
  ),
73
73
  ]
74
74
 
75
- async def prompt(self, safe_mode: bool = False) -> str:
75
+ async def prompt(self, yolo_mode: bool = False) -> str:
76
76
  prompt = """Use the Write tool to create new files. """
77
77
 
78
- if safe_mode:
78
+ if not yolo_mode:
79
79
  prompt += """IMPORTANT: You must ALWAYS prefer editing existing files.
80
80
  NEVER write new files unless explicitly required by the user."""
81
81
 
@@ -134,7 +134,9 @@ NEVER write new files unless explicitly required by the user."""
134
134
 
135
135
  # Check if path is ignored (warning for write operations)
136
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)
137
+ should_proceed, warning_msg = check_path_for_tool(
138
+ file_path_obj, tool_name="Write", warn_only=True
139
+ )
138
140
  if warning_msg:
139
141
  logger.warning("[file_write_tool] %s", warning_msg)
140
142
 
@@ -171,7 +173,8 @@ NEVER write new files unless explicitly required by the user."""
171
173
  except (OSError, IOError, RuntimeError) as exc:
172
174
  logger.warning(
173
175
  "[file_write_tool] Failed to record file snapshot: %s: %s",
174
- type(exc).__name__, exc,
176
+ type(exc).__name__,
177
+ exc,
175
178
  extra={"file_path": abs_file_path},
176
179
  )
177
180
 
@@ -189,7 +192,8 @@ NEVER write new files unless explicitly required by the user."""
189
192
  except (OSError, IOError, PermissionError, UnicodeEncodeError) as e:
190
193
  logger.warning(
191
194
  "[file_write_tool] Error writing file: %s: %s",
192
- type(e).__name__, e,
195
+ type(e).__name__,
196
+ e,
193
197
  extra={"file_path": input_data.file_path},
194
198
  )
195
199
  error_output = FileWriteToolOutput(
@@ -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, _yolo_mode: bool = False) -> str:
80
80
  return GLOB_USAGE
81
81
 
82
82
  def is_read_only(self) -> bool:
@@ -169,7 +169,8 @@ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
169
169
  except (OSError, RuntimeError, ValueError) as e:
170
170
  logger.warning(
171
171
  "[glob_tool] Error executing glob: %s: %s",
172
- type(e).__name__, e,
172
+ type(e).__name__,
173
+ e,
173
174
  extra={"pattern": input_data.pattern, "path": input_data.path},
174
175
  )
175
176
  error_output = GlobToolOutput(matches=[], pattern=input_data.pattern, count=0)
@@ -148,7 +148,7 @@ class GrepTool(Tool[GrepToolInput, GrepToolOutput]):
148
148
  ),
149
149
  ]
150
150
 
151
- async def prompt(self, _safe_mode: bool = False) -> str:
151
+ async def prompt(self, _yolo_mode: bool = False) -> str:
152
152
  return GREP_USAGE
153
153
 
154
154
  def is_read_only(self) -> bool:
@@ -358,7 +358,8 @@ class GrepTool(Tool[GrepToolInput, GrepToolOutput]):
358
358
  except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as e:
359
359
  logger.warning(
360
360
  "[grep_tool] Error executing grep: %s: %s",
361
- type(e).__name__, e,
361
+ type(e).__name__,
362
+ e,
362
363
  extra={"pattern": input_data.pattern, "path": input_data.path},
363
364
  )
364
365
  error_output = GrepToolOutput(
@@ -53,7 +53,7 @@ class KillBashTool(Tool[KillBashInput, KillBashOutput]):
53
53
  async def description(self) -> str:
54
54
  return "Kill a background bash shell by ID"
55
55
 
56
- async def prompt(self, safe_mode: bool = False) -> str:
56
+ async def prompt(self, yolo_mode: bool = False) -> str:
57
57
  return KILL_BASH_PROMPT
58
58
 
59
59
  @property
@@ -320,7 +320,7 @@ class LSTool(Tool[LSToolInput, LSToolOutput]):
320
320
  ),
321
321
  ]
322
322
 
323
- async def prompt(self, safe_mode: bool = False) -> str:
323
+ async def prompt(self, yolo_mode: bool = False) -> str:
324
324
  return (
325
325
  "Lists files and directories in a given path. The path parameter must be an absolute path, "
326
326
  "not a relative path. You can optionally provide an array of glob patterns to ignore with "
@@ -47,6 +47,7 @@ DEFAULT_MCP_WARNING_FRACTION = 0.8
47
47
  # Base class for MCP tools to reduce code duplication
48
48
  # =============================================================================
49
49
 
50
+
50
51
  class BaseMcpTool(Tool): # type: ignore[type-arg]
51
52
  """Base class for MCP tools with common default implementations.
52
53
 
@@ -76,9 +77,7 @@ class BaseMcpTool(Tool): # type: ignore[type-arg]
76
77
  runtime = await ensure_mcp_runtime()
77
78
  server_names = {s.name for s in runtime.servers}
78
79
  if server_name not in server_names:
79
- return ValidationResult(
80
- result=False, message=f"Unknown MCP server '{server_name}'."
81
- )
80
+ return ValidationResult(result=False, message=f"Unknown MCP server '{server_name}'.")
82
81
  return ValidationResult(result=True)
83
82
 
84
83
 
@@ -160,7 +159,7 @@ class ListMcpServersTool(BaseMcpTool, Tool[ListMcpServersInput, ListMcpServersOu
160
159
  def input_schema(self) -> type[ListMcpServersInput]:
161
160
  return ListMcpServersInput
162
161
 
163
- async def prompt(self, _safe_mode: bool = False) -> str:
162
+ async def prompt(self, _yolo_mode: bool = False) -> str:
164
163
  servers = await load_mcp_servers_async()
165
164
  return format_mcp_instructions(servers)
166
165
 
@@ -243,7 +242,7 @@ class ListMcpResourcesTool(BaseMcpTool, Tool[ListMcpResourcesInput, ListMcpResou
243
242
  def input_schema(self) -> type[ListMcpResourcesInput]:
244
243
  return ListMcpResourcesInput
245
244
 
246
- async def prompt(self, _safe_mode: bool = False) -> str:
245
+ async def prompt(self, _yolo_mode: bool = False) -> str:
247
246
  return (
248
247
  "List available resources from configured MCP servers.\n"
249
248
  "Each returned resource will include all standard MCP resource fields plus a 'server' field\n"
@@ -268,7 +267,8 @@ class ListMcpResourcesTool(BaseMcpTool, Tool[ListMcpResourcesInput, ListMcpResou
268
267
  except (TypeError, ValueError) as exc:
269
268
  logger.warning(
270
269
  "[mcp_tools] Failed to serialize MCP resources for assistant output: %s: %s",
271
- type(exc).__name__, exc,
270
+ type(exc).__name__,
271
+ exc,
272
272
  )
273
273
  return str(output.resources)
274
274
 
@@ -314,7 +314,8 @@ class ListMcpResourcesTool(BaseMcpTool, Tool[ListMcpResourcesInput, ListMcpResou
314
314
  # pragma: no cover - runtime errors
315
315
  logger.warning(
316
316
  "Failed to fetch resources from MCP server: %s: %s",
317
- type(exc).__name__, exc,
317
+ type(exc).__name__,
318
+ exc,
318
319
  extra={"server": server.name},
319
320
  )
320
321
  fetched = []
@@ -394,7 +395,7 @@ class ReadMcpResourceTool(BaseMcpTool, Tool[ReadMcpResourceInput, ReadMcpResourc
394
395
  def input_schema(self) -> type[ReadMcpResourceInput]:
395
396
  return ReadMcpResourceInput
396
397
 
397
- async def prompt(self, _safe_mode: bool = False) -> str:
398
+ async def prompt(self, _yolo_mode: bool = False) -> str:
398
399
  return (
399
400
  "Reads a specific resource from an MCP server, identified by server name and resource URI.\n\n"
400
401
  "Parameters:\n"
@@ -482,7 +483,8 @@ class ReadMcpResourceTool(BaseMcpTool, Tool[ReadMcpResourceInput, ReadMcpResourc
482
483
  except (ValueError, binascii.Error) as exc:
483
484
  logger.warning(
484
485
  "[mcp_tools] Failed to decode base64 blob content: %s: %s",
485
- type(exc).__name__, exc,
486
+ type(exc).__name__,
487
+ exc,
486
488
  extra={"server": input_data.server, "uri": input_data.uri},
487
489
  )
488
490
  raw_bytes = None
@@ -515,7 +517,8 @@ class ReadMcpResourceTool(BaseMcpTool, Tool[ReadMcpResourceInput, ReadMcpResourc
515
517
  # pragma: no cover - runtime errors
516
518
  logger.warning(
517
519
  "Error reading MCP resource: %s: %s",
518
- type(exc).__name__, exc,
520
+ type(exc).__name__,
521
+ exc,
519
522
  extra={"server": input_data.server, "uri": input_data.uri},
520
523
  )
521
524
  content_text = f"Error reading MCP resource: {exc}"
@@ -149,7 +149,7 @@ class MultiEditTool(Tool[MultiEditToolInput, MultiEditToolOutput]):
149
149
  ),
150
150
  ]
151
151
 
152
- async def prompt(self, safe_mode: bool = False) -> str:
152
+ async def prompt(self, yolo_mode: bool = False) -> str:
153
153
  return MULTI_EDIT_DESCRIPTION
154
154
 
155
155
  def is_read_only(self) -> bool:
@@ -190,9 +190,7 @@ class MultiEditTool(Tool[MultiEditToolInput, MultiEditToolOutput]):
190
190
 
191
191
  # Check if this is a file creation (first edit has empty old_string)
192
192
  is_creation = (
193
- not path.exists()
194
- and len(input_data.edits) > 0
195
- and input_data.edits[0].old_string == ""
193
+ not path.exists() and len(input_data.edits) > 0 and input_data.edits[0].old_string == ""
196
194
  )
197
195
 
198
196
  # If file exists, check if it has been read before editing
@@ -350,7 +348,8 @@ class MultiEditTool(Tool[MultiEditToolInput, MultiEditToolOutput]):
350
348
  # pragma: no cover - unlikely permission issue
351
349
  logger.warning(
352
350
  "[multi_edit_tool] Error reading file before edits: %s: %s",
353
- type(exc).__name__, exc,
351
+ type(exc).__name__,
352
+ exc,
354
353
  extra={"file_path": str(file_path)},
355
354
  )
356
355
  output = MultiEditToolOutput(
@@ -408,13 +407,15 @@ class MultiEditTool(Tool[MultiEditToolInput, MultiEditToolOutput]):
408
407
  except (OSError, IOError, RuntimeError) as exc:
409
408
  logger.warning(
410
409
  "[multi_edit_tool] Failed to record file snapshot: %s: %s",
411
- type(exc).__name__, exc,
410
+ type(exc).__name__,
411
+ exc,
412
412
  extra={"file_path": str(file_path)},
413
413
  )
414
414
  except (OSError, IOError, PermissionError, UnicodeDecodeError) as exc:
415
415
  logger.warning(
416
416
  "[multi_edit_tool] Error writing edited file: %s: %s",
417
- type(exc).__name__, exc,
417
+ type(exc).__name__,
418
+ exc,
418
419
  extra={"file_path": str(file_path)},
419
420
  )
420
421
  output = MultiEditToolOutput(
@@ -122,7 +122,7 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
122
122
  ),
123
123
  ]
124
124
 
125
- async def prompt(self, safe_mode: bool = False) -> str:
125
+ async def prompt(self, yolo_mode: bool = False) -> str:
126
126
  return NOTEBOOK_EDIT_DESCRIPTION
127
127
 
128
128
  def is_read_only(self) -> bool:
@@ -204,7 +204,8 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
204
204
  except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
205
205
  logger.warning(
206
206
  "Failed to parse notebook: %s: %s",
207
- type(exc).__name__, exc,
207
+ type(exc).__name__,
208
+ exc,
208
209
  extra={"path": str(path)},
209
210
  )
210
211
  return ValidationResult(
@@ -325,7 +326,8 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
325
326
  except (OSError, IOError, RuntimeError) as exc:
326
327
  logger.warning(
327
328
  "[notebook_edit_tool] Failed to record file snapshot: %s: %s",
328
- type(exc).__name__, exc,
329
+ type(exc).__name__,
330
+ exc,
329
331
  extra={"file_path": abs_notebook_path},
330
332
  )
331
333
 
@@ -344,7 +346,8 @@ class NotebookEditTool(Tool[NotebookEditInput, NotebookEditOutput]):
344
346
  # pragma: no cover - error path
345
347
  logger.warning(
346
348
  "Error editing notebook: %s: %s",
347
- type(exc).__name__, exc,
349
+ type(exc).__name__,
350
+ exc,
348
351
  extra={"path": input_data.notebook_path},
349
352
  )
350
353
  output = NotebookEditOutput(
@@ -82,7 +82,7 @@ class SkillTool(Tool[SkillToolInput, SkillToolOutput]):
82
82
  ),
83
83
  ]
84
84
 
85
- async def prompt(self, safe_mode: bool = False) -> str: # noqa: ARG002
85
+ async def prompt(self, yolo_mode: bool = False) -> str: # noqa: ARG002
86
86
  return (
87
87
  "Load a skill by name to read its SKILL.md content. "
88
88
  "Only call this when the skill description is clearly relevant. "
@@ -69,8 +69,8 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
69
69
  def input_schema(self) -> type[TaskToolInput]:
70
70
  return TaskToolInput
71
71
 
72
- async def prompt(self, safe_mode: bool = False) -> str:
73
- del safe_mode
72
+ async def prompt(self, yolo_mode: bool = False) -> str:
73
+ del yolo_mode
74
74
  clear_agent_cache()
75
75
  agents: AgentLoadResult = load_agent_definitions()
76
76
 
@@ -221,7 +221,7 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
221
221
 
222
222
  subagent_context = QueryContext(
223
223
  tools=typed_agent_tools,
224
- safe_mode=context.safe_mode,
224
+ yolo_mode=context.yolo_mode,
225
225
  verbose=context.verbose,
226
226
  model=target_agent.model or "task",
227
227
  )
@@ -370,7 +370,8 @@ class TaskTool(Tool[TaskToolInput, TaskToolOutput]):
370
370
  except (TypeError, ValueError) as exc:
371
371
  logger.warning(
372
372
  "[task_tool] Failed to serialize tool_use input: %s: %s",
373
- type(exc).__name__, exc,
373
+ type(exc).__name__,
374
+ exc,
374
375
  extra={"tool_use_input": str(inp)[:200]},
375
376
  )
376
377
  serialized = str(inp)
@@ -309,7 +309,7 @@ class TodoWriteTool(Tool[TodoWriteToolInput, TodoToolOutput]):
309
309
  ),
310
310
  ]
311
311
 
312
- async def prompt(self, _safe_mode: bool = False) -> str:
312
+ async def prompt(self, _yolo_mode: bool = False) -> str:
313
313
  return TODO_WRITE_PROMPT
314
314
 
315
315
  def is_read_only(self) -> bool:
@@ -403,7 +403,7 @@ class TodoReadTool(Tool[TodoReadToolInput, TodoToolOutput]):
403
403
  ),
404
404
  ]
405
405
 
406
- async def prompt(self, _safe_mode: bool = False) -> str:
406
+ async def prompt(self, _yolo_mode: bool = False) -> str:
407
407
  return (
408
408
  "Use TodoRead to fetch the current todo list before making progress or when you need "
409
409
  "to confirm the next action. You can request only the next actionable item or filter "
@@ -106,7 +106,7 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
106
106
  ),
107
107
  ]
108
108
 
109
- async def prompt(self, safe_mode: bool = False) -> str: # noqa: ARG002
109
+ async def prompt(self, yolo_mode: bool = False) -> str: # noqa: ARG002
110
110
  return (
111
111
  "Search for a tool by providing a short description (e.g., 'query database', 'render notebook'). "
112
112
  "Use names to activate tools you've already discovered. "
@@ -193,7 +193,8 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
193
193
  description = ""
194
194
  logger.warning(
195
195
  "[tool_search] Failed to build tool description: %s: %s",
196
- type(exc).__name__, exc,
196
+ type(exc).__name__,
197
+ exc,
197
198
  extra={"tool_name": getattr(tool, "name", None)},
198
199
  )
199
200
  doc_text = " ".join([name, tool.user_facing_name(), description])