ripperdoc 0.2.7__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 (87) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +33 -115
  3. ripperdoc/cli/commands/__init__.py +70 -6
  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/help_cmd.py +11 -1
  10. ripperdoc/cli/commands/hooks_cmd.py +610 -0
  11. ripperdoc/cli/commands/models_cmd.py +26 -9
  12. ripperdoc/cli/commands/permissions_cmd.py +57 -37
  13. ripperdoc/cli/commands/resume_cmd.py +6 -4
  14. ripperdoc/cli/commands/status_cmd.py +4 -4
  15. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  16. ripperdoc/cli/ui/file_mention_completer.py +64 -8
  17. ripperdoc/cli/ui/interrupt_handler.py +3 -4
  18. ripperdoc/cli/ui/message_display.py +5 -3
  19. ripperdoc/cli/ui/panels.py +13 -10
  20. ripperdoc/cli/ui/provider_options.py +247 -0
  21. ripperdoc/cli/ui/rich_ui.py +196 -77
  22. ripperdoc/cli/ui/spinner.py +25 -1
  23. ripperdoc/cli/ui/tool_renderers.py +8 -2
  24. ripperdoc/cli/ui/wizard.py +215 -0
  25. ripperdoc/core/agents.py +9 -3
  26. ripperdoc/core/config.py +49 -12
  27. ripperdoc/core/custom_commands.py +412 -0
  28. ripperdoc/core/default_tools.py +11 -2
  29. ripperdoc/core/hooks/__init__.py +99 -0
  30. ripperdoc/core/hooks/config.py +301 -0
  31. ripperdoc/core/hooks/events.py +535 -0
  32. ripperdoc/core/hooks/executor.py +496 -0
  33. ripperdoc/core/hooks/integration.py +344 -0
  34. ripperdoc/core/hooks/manager.py +745 -0
  35. ripperdoc/core/permissions.py +40 -8
  36. ripperdoc/core/providers/anthropic.py +548 -68
  37. ripperdoc/core/providers/gemini.py +70 -5
  38. ripperdoc/core/providers/openai.py +60 -5
  39. ripperdoc/core/query.py +140 -39
  40. ripperdoc/core/query_utils.py +2 -0
  41. ripperdoc/core/skills.py +9 -3
  42. ripperdoc/core/system_prompt.py +4 -2
  43. ripperdoc/core/tool.py +9 -5
  44. ripperdoc/sdk/client.py +2 -2
  45. ripperdoc/tools/ask_user_question_tool.py +5 -3
  46. ripperdoc/tools/background_shell.py +2 -1
  47. ripperdoc/tools/bash_output_tool.py +1 -1
  48. ripperdoc/tools/bash_tool.py +30 -20
  49. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  50. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  51. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  52. ripperdoc/tools/file_edit_tool.py +8 -4
  53. ripperdoc/tools/file_read_tool.py +9 -5
  54. ripperdoc/tools/file_write_tool.py +9 -5
  55. ripperdoc/tools/glob_tool.py +3 -2
  56. ripperdoc/tools/grep_tool.py +3 -2
  57. ripperdoc/tools/kill_bash_tool.py +1 -1
  58. ripperdoc/tools/ls_tool.py +1 -1
  59. ripperdoc/tools/mcp_tools.py +13 -10
  60. ripperdoc/tools/multi_edit_tool.py +8 -7
  61. ripperdoc/tools/notebook_edit_tool.py +7 -4
  62. ripperdoc/tools/skill_tool.py +1 -1
  63. ripperdoc/tools/task_tool.py +5 -4
  64. ripperdoc/tools/todo_tool.py +2 -2
  65. ripperdoc/tools/tool_search_tool.py +3 -2
  66. ripperdoc/utils/conversation_compaction.py +11 -7
  67. ripperdoc/utils/file_watch.py +8 -2
  68. ripperdoc/utils/json_utils.py +2 -1
  69. ripperdoc/utils/mcp.py +11 -3
  70. ripperdoc/utils/memory.py +4 -2
  71. ripperdoc/utils/message_compaction.py +21 -7
  72. ripperdoc/utils/message_formatting.py +11 -7
  73. ripperdoc/utils/messages.py +105 -66
  74. ripperdoc/utils/path_ignore.py +38 -12
  75. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  76. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  77. ripperdoc/utils/safe_get_cwd.py +2 -1
  78. ripperdoc/utils/session_history.py +13 -6
  79. ripperdoc/utils/todo.py +2 -1
  80. ripperdoc/utils/token_estimation.py +6 -1
  81. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
  82. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  83. ripperdoc-0.2.7.dist-info/RECORD +0 -113
  84. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  85. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  86. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  87. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,344 @@
1
+ """Integration helpers for hooks with tool execution.
2
+
3
+ This module provides convenient integration points for running hooks
4
+ as part of tool execution flows.
5
+ """
6
+
7
+ from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, Union
8
+
9
+ from ripperdoc.core.hooks.manager import HookManager, hook_manager
10
+ from ripperdoc.utils.log import get_logger
11
+
12
+ logger = get_logger()
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ class HookInterceptor:
18
+ """Provides hook interception for tool execution.
19
+
20
+ This class wraps tool execution with pre/post hooks,
21
+ handling blocking decisions and context injection.
22
+ """
23
+
24
+ def __init__(self, manager: Optional[HookManager] = None):
25
+ """Initialize the interceptor.
26
+
27
+ Args:
28
+ manager: HookManager to use, defaults to global instance
29
+ """
30
+ self.manager = manager or hook_manager
31
+
32
+ def check_pre_tool_use(
33
+ self, tool_name: str, tool_input: Dict[str, Any]
34
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
35
+ """Check if a tool call should proceed based on PreToolUse hooks.
36
+
37
+ Args:
38
+ tool_name: Name of the tool
39
+ tool_input: Tool input parameters
40
+
41
+ Returns:
42
+ Tuple of (should_proceed, block_reason, additional_context)
43
+ - should_proceed: True if tool should execute
44
+ - block_reason: Reason if blocked, None otherwise
45
+ - additional_context: Additional context to add if any
46
+ """
47
+ result = self.manager.run_pre_tool_use(tool_name, tool_input)
48
+
49
+ if result.should_block:
50
+ logger.info(
51
+ f"Tool {tool_name} blocked by hook",
52
+ extra={"reason": result.block_reason},
53
+ )
54
+ return False, result.block_reason, result.additional_context
55
+
56
+ if result.should_ask:
57
+ # For 'ask' decision, we return a special flag
58
+ # The caller should prompt the user for confirmation
59
+ return False, "USER_CONFIRMATION_REQUIRED", result.additional_context
60
+
61
+ return True, None, result.additional_context
62
+
63
+ async def check_pre_tool_use_async(
64
+ self, tool_name: str, tool_input: Dict[str, Any]
65
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
66
+ """Async version of check_pre_tool_use."""
67
+ result = await self.manager.run_pre_tool_use_async(tool_name, tool_input)
68
+
69
+ if result.should_block:
70
+ logger.info(
71
+ f"Tool {tool_name} blocked by hook",
72
+ extra={"reason": result.block_reason},
73
+ )
74
+ return False, result.block_reason, result.additional_context
75
+
76
+ if result.should_ask:
77
+ return False, "USER_CONFIRMATION_REQUIRED", result.additional_context
78
+
79
+ return True, None, result.additional_context
80
+
81
+ def run_post_tool_use(
82
+ self,
83
+ tool_name: str,
84
+ tool_input: Dict[str, Any],
85
+ tool_output: Any = None,
86
+ tool_error: Optional[str] = None,
87
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
88
+ """Run PostToolUse hooks after tool execution.
89
+
90
+ Args:
91
+ tool_name: Name of the tool
92
+ tool_input: Tool input parameters
93
+ tool_output: Output from the tool
94
+ tool_error: Error message if tool failed
95
+
96
+ Returns:
97
+ Tuple of (should_continue, block_reason, additional_context)
98
+ """
99
+ result = self.manager.run_post_tool_use(tool_name, tool_input, tool_output, tool_error)
100
+
101
+ if result.should_block:
102
+ return False, result.block_reason, result.additional_context
103
+
104
+ return True, None, result.additional_context
105
+
106
+ async def run_post_tool_use_async(
107
+ self,
108
+ tool_name: str,
109
+ tool_input: Dict[str, Any],
110
+ tool_output: Any = None,
111
+ tool_error: Optional[str] = None,
112
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
113
+ """Async version of run_post_tool_use."""
114
+ result = await self.manager.run_post_tool_use_async(
115
+ tool_name, tool_input, tool_output, tool_error
116
+ )
117
+
118
+ if result.should_block:
119
+ return False, result.block_reason, result.additional_context
120
+
121
+ return True, None, result.additional_context
122
+
123
+ def wrap_tool_execution(
124
+ self,
125
+ tool_name: str,
126
+ tool_input: Dict[str, Any],
127
+ execute_fn: Callable[[], T],
128
+ ) -> Tuple[bool, Union[T, str, None], Optional[str]]:
129
+ """Wrap synchronous tool execution with pre/post hooks.
130
+
131
+ Args:
132
+ tool_name: Name of the tool
133
+ tool_input: Tool input parameters
134
+ execute_fn: Function to execute the tool
135
+
136
+ Returns:
137
+ Tuple of (success, result_or_error, additional_context)
138
+ """
139
+ # Run pre-tool hooks
140
+ should_proceed, block_reason, pre_context = self.check_pre_tool_use(tool_name, tool_input)
141
+
142
+ if not should_proceed:
143
+ return False, block_reason or "Blocked by hook", pre_context
144
+
145
+ # Execute the tool
146
+ try:
147
+ result = execute_fn()
148
+ tool_error = None
149
+ except Exception as e:
150
+ result = None
151
+ tool_error = str(e)
152
+
153
+ # Run post-tool hooks
154
+ _, _, post_context = self.run_post_tool_use(tool_name, tool_input, result, tool_error)
155
+
156
+ # Combine contexts
157
+ combined_context = None
158
+ if pre_context or post_context:
159
+ parts = [c for c in [pre_context, post_context] if c]
160
+ combined_context = "\n".join(parts) if parts else None
161
+
162
+ if tool_error:
163
+ return False, tool_error or "", combined_context
164
+
165
+ return True, result, combined_context
166
+
167
+ async def wrap_tool_execution_async(
168
+ self,
169
+ tool_name: str,
170
+ tool_input: Dict[str, Any],
171
+ execute_fn: Callable[[], T],
172
+ ) -> Tuple[bool, Union[T, str, None], Optional[str]]:
173
+ """Wrap async tool execution with pre/post hooks."""
174
+ # Run pre-tool hooks
175
+ should_proceed, block_reason, pre_context = await self.check_pre_tool_use_async(
176
+ tool_name, tool_input
177
+ )
178
+
179
+ if not should_proceed:
180
+ return False, block_reason or "Blocked by hook", pre_context
181
+
182
+ # Execute the tool
183
+ try:
184
+ import asyncio
185
+
186
+ if asyncio.iscoroutinefunction(execute_fn):
187
+ result = await execute_fn()
188
+ else:
189
+ result = execute_fn()
190
+ tool_error = None
191
+ except Exception as e:
192
+ result = None
193
+ tool_error = str(e)
194
+
195
+ # Run post-tool hooks
196
+ _, _, post_context = await self.run_post_tool_use_async(
197
+ tool_name, tool_input, result, tool_error
198
+ )
199
+
200
+ # Combine contexts
201
+ combined_context = None
202
+ if pre_context or post_context:
203
+ parts = [c for c in [pre_context, post_context] if c]
204
+ combined_context = "\n".join(parts) if parts else None
205
+
206
+ if tool_error:
207
+ return False, tool_error or "", combined_context
208
+
209
+ return True, result, combined_context
210
+
211
+
212
+ # Global interceptor instance
213
+ hook_interceptor = HookInterceptor()
214
+
215
+
216
+ def check_pre_tool_use(
217
+ tool_name: str, tool_input: Dict[str, Any]
218
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
219
+ """Convenience function to check pre-tool hooks using global interceptor."""
220
+ return hook_interceptor.check_pre_tool_use(tool_name, tool_input)
221
+
222
+
223
+ async def check_pre_tool_use_async(
224
+ tool_name: str, tool_input: Dict[str, Any]
225
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
226
+ """Async convenience function to check pre-tool hooks."""
227
+ return await hook_interceptor.check_pre_tool_use_async(tool_name, tool_input)
228
+
229
+
230
+ def run_post_tool_use(
231
+ tool_name: str,
232
+ tool_input: Dict[str, Any],
233
+ tool_output: Any = None,
234
+ tool_error: Optional[str] = None,
235
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
236
+ """Convenience function to run post-tool hooks using global interceptor."""
237
+ return hook_interceptor.run_post_tool_use(tool_name, tool_input, tool_output, tool_error)
238
+
239
+
240
+ async def run_post_tool_use_async(
241
+ tool_name: str,
242
+ tool_input: Dict[str, Any],
243
+ tool_output: Any = None,
244
+ tool_error: Optional[str] = None,
245
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
246
+ """Async convenience function to run post-tool hooks."""
247
+ return await hook_interceptor.run_post_tool_use_async(
248
+ tool_name, tool_input, tool_output, tool_error
249
+ )
250
+
251
+
252
+ def check_user_prompt(prompt: str) -> Tuple[bool, Optional[str], Optional[str]]:
253
+ """Check if a user prompt should be processed.
254
+
255
+ Args:
256
+ prompt: The user's prompt text
257
+
258
+ Returns:
259
+ Tuple of (should_process, block_reason, additional_context)
260
+ """
261
+ result = hook_manager.run_user_prompt_submit(prompt)
262
+
263
+ if result.should_block:
264
+ return False, result.block_reason, result.additional_context
265
+
266
+ return True, None, result.additional_context
267
+
268
+
269
+ async def check_user_prompt_async(
270
+ prompt: str,
271
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
272
+ """Async version of check_user_prompt."""
273
+ result = await hook_manager.run_user_prompt_submit_async(prompt)
274
+
275
+ if result.should_block:
276
+ return False, result.block_reason, result.additional_context
277
+
278
+ return True, None, result.additional_context
279
+
280
+
281
+ def notify_session_start(trigger: str) -> Optional[str]:
282
+ """Notify hooks that a session is starting.
283
+
284
+ Args:
285
+ trigger: "startup", "resume", "clear", or "compact"
286
+
287
+ Returns:
288
+ Additional context from hooks, if any
289
+ """
290
+ result = hook_manager.run_session_start(trigger)
291
+ return result.additional_context
292
+
293
+
294
+ def notify_session_end(
295
+ trigger: str,
296
+ duration_seconds: Optional[float] = None,
297
+ message_count: int = 0,
298
+ ) -> Optional[str]:
299
+ """Notify hooks that a session is ending.
300
+
301
+ Args:
302
+ trigger: "clear", "logout", "prompt_input_exit", or "other"
303
+ duration_seconds: How long the session lasted
304
+ message_count: Number of messages in the session
305
+
306
+ Returns:
307
+ Additional context from hooks, if any
308
+ """
309
+ result = hook_manager.run_session_end(trigger, duration_seconds, message_count)
310
+ return result.additional_context
311
+
312
+
313
+ def check_stop(
314
+ reason: Optional[str] = None, stop_sequence: Optional[str] = None
315
+ ) -> Tuple[bool, Optional[str]]:
316
+ """Check if the agent should stop.
317
+
318
+ Args:
319
+ reason: Why the agent wants to stop
320
+ stop_sequence: The stop sequence encountered, if any
321
+
322
+ Returns:
323
+ Tuple of (should_stop, continue_reason)
324
+ - should_stop: True if agent should stop
325
+ - continue_reason: Reason to continue if blocked
326
+ """
327
+ result = hook_manager.run_stop(False, reason, stop_sequence)
328
+
329
+ if result.should_block:
330
+ return False, result.block_reason
331
+
332
+ return True, None
333
+
334
+
335
+ async def check_stop_async(
336
+ reason: Optional[str] = None, stop_sequence: Optional[str] = None
337
+ ) -> Tuple[bool, Optional[str]]:
338
+ """Async version of check_stop."""
339
+ result = await hook_manager.run_stop_async(False, reason, stop_sequence)
340
+
341
+ if result.should_block:
342
+ return False, result.block_reason
343
+
344
+ return True, None