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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +33 -115
- ripperdoc/cli/commands/__init__.py +70 -6
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +610 -0
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +57 -37
- ripperdoc/cli/commands/resume_cmd.py +6 -4
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +64 -8
- ripperdoc/cli/ui/interrupt_handler.py +3 -4
- ripperdoc/cli/ui/message_display.py +5 -3
- ripperdoc/cli/ui/panels.py +13 -10
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +196 -77
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +412 -0
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +301 -0
- ripperdoc/core/hooks/events.py +535 -0
- ripperdoc/core/hooks/executor.py +496 -0
- ripperdoc/core/hooks/integration.py +344 -0
- ripperdoc/core/hooks/manager.py +745 -0
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +548 -68
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +140 -39
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +30 -20
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +8 -4
- ripperdoc/tools/file_read_tool.py +9 -5
- ripperdoc/tools/file_write_tool.py +9 -5
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +11 -7
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +38 -12
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.7.dist-info/RECORD +0 -113
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {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
|