ripperdoc 0.2.8__py3-none-any.whl → 0.2.10__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 +257 -123
- ripperdoc/cli/commands/__init__.py +2 -1
- ripperdoc/cli/commands/agents_cmd.py +138 -8
- ripperdoc/cli/commands/clear_cmd.py +9 -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/exit_cmd.py +1 -0
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +27 -10
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +9 -3
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +403 -81
- ripperdoc/cli/ui/spinner.py +54 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +213 -0
- ripperdoc/core/agents.py +19 -6
- ripperdoc/core/config.py +51 -17
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +101 -12
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +27 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +118 -12
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +89 -24
- ripperdoc/core/query.py +273 -68
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +17 -8
- ripperdoc/sdk/client.py +79 -4
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +307 -135
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +63 -24
- 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 +167 -54
- ripperdoc/tools/file_read_tool.py +28 -4
- ripperdoc/tools/file_write_tool.py +13 -10
- 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/lsp_tool.py +615 -0
- 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 +519 -69
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +9 -5
- ripperdoc/utils/file_watch.py +214 -5
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +14 -7
- ripperdoc/utils/messages.py +126 -67
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/session_stats.py +293 -0
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- ripperdoc-0.2.10.dist-info/RECORD +129 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
ripperdoc/core/default_tools.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any, List
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
6
|
|
|
7
7
|
from ripperdoc.core.tool import Tool
|
|
8
8
|
|
|
@@ -17,6 +17,7 @@ from ripperdoc.tools.file_write_tool import FileWriteTool
|
|
|
17
17
|
from ripperdoc.tools.glob_tool import GlobTool
|
|
18
18
|
from ripperdoc.tools.ls_tool import LSTool
|
|
19
19
|
from ripperdoc.tools.grep_tool import GrepTool
|
|
20
|
+
from ripperdoc.tools.lsp_tool import LspTool
|
|
20
21
|
from ripperdoc.tools.skill_tool import SkillTool
|
|
21
22
|
from ripperdoc.tools.todo_tool import TodoReadTool, TodoWriteTool
|
|
22
23
|
from ripperdoc.tools.ask_user_question_tool import AskUserQuestionTool
|
|
@@ -34,8 +35,73 @@ from ripperdoc.utils.log import get_logger
|
|
|
34
35
|
|
|
35
36
|
logger = get_logger()
|
|
36
37
|
|
|
38
|
+
# Canonical tool names for --tools filtering
|
|
39
|
+
BUILTIN_TOOL_NAMES = [
|
|
40
|
+
"Bash",
|
|
41
|
+
"BashOutput",
|
|
42
|
+
"KillBash",
|
|
43
|
+
"Read",
|
|
44
|
+
"Edit",
|
|
45
|
+
"MultiEdit",
|
|
46
|
+
"NotebookEdit",
|
|
47
|
+
"Write",
|
|
48
|
+
"Glob",
|
|
49
|
+
"LS",
|
|
50
|
+
"Grep",
|
|
51
|
+
"LSP",
|
|
52
|
+
"Skill",
|
|
53
|
+
"TodoRead",
|
|
54
|
+
"TodoWrite",
|
|
55
|
+
"AskUserQuestion",
|
|
56
|
+
"EnterPlanMode",
|
|
57
|
+
"ExitPlanMode",
|
|
58
|
+
"ToolSearch",
|
|
59
|
+
"ListMcpServers",
|
|
60
|
+
"ListMcpResources",
|
|
61
|
+
"ReadMcpResource",
|
|
62
|
+
"Task",
|
|
63
|
+
]
|
|
37
64
|
|
|
38
|
-
|
|
65
|
+
|
|
66
|
+
def filter_tools_by_names(
|
|
67
|
+
tools: List[Tool[Any, Any]], tool_names: List[str]
|
|
68
|
+
) -> List[Tool[Any, Any]]:
|
|
69
|
+
"""Filter a tool list to only include tools with matching names.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
tools: The full list of tools to filter.
|
|
73
|
+
tool_names: List of tool names to include.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Filtered list of tools. If Task is included, it's recreated with
|
|
77
|
+
the filtered base tools.
|
|
78
|
+
"""
|
|
79
|
+
if not tool_names:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
name_set = set(tool_names)
|
|
83
|
+
filtered: List[Tool[Any, Any]] = []
|
|
84
|
+
has_task = False
|
|
85
|
+
|
|
86
|
+
for tool in tools:
|
|
87
|
+
tool_name = getattr(tool, "name", tool.__class__.__name__)
|
|
88
|
+
if tool_name in name_set:
|
|
89
|
+
if tool_name == "Task":
|
|
90
|
+
has_task = True
|
|
91
|
+
else:
|
|
92
|
+
filtered.append(tool)
|
|
93
|
+
|
|
94
|
+
# If Task is requested, recreate it with the filtered base tools
|
|
95
|
+
if has_task:
|
|
96
|
+
def _filtered_base_provider() -> List[Tool[Any, Any]]:
|
|
97
|
+
return [t for t in filtered if getattr(t, "name", None) != "Task"]
|
|
98
|
+
|
|
99
|
+
filtered.append(TaskTool(_filtered_base_provider))
|
|
100
|
+
|
|
101
|
+
return filtered
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_default_tools(allowed_tools: Optional[List[str]] = None) -> List[Tool[Any, Any]]:
|
|
39
105
|
"""Construct the default tool set (base tools + Task subagent launcher)."""
|
|
40
106
|
base_tools: List[Tool[Any, Any]] = [
|
|
41
107
|
BashTool(),
|
|
@@ -49,6 +115,7 @@ def get_default_tools() -> List[Tool[Any, Any]]:
|
|
|
49
115
|
GlobTool(),
|
|
50
116
|
LSTool(),
|
|
51
117
|
GrepTool(),
|
|
118
|
+
LspTool(),
|
|
52
119
|
SkillTool(),
|
|
53
120
|
TodoReadTool(),
|
|
54
121
|
TodoWriteTool(),
|
|
@@ -68,21 +135,43 @@ def get_default_tools() -> List[Tool[Any, Any]]:
|
|
|
68
135
|
if isinstance(tool, Tool):
|
|
69
136
|
base_tools.append(tool)
|
|
70
137
|
dynamic_tools.append(tool)
|
|
71
|
-
except (
|
|
138
|
+
except (
|
|
139
|
+
ImportError,
|
|
140
|
+
ModuleNotFoundError,
|
|
141
|
+
OSError,
|
|
142
|
+
RuntimeError,
|
|
143
|
+
ConnectionError,
|
|
144
|
+
ValueError,
|
|
145
|
+
TypeError,
|
|
146
|
+
) as exc:
|
|
72
147
|
# If MCP runtime is not available, continue with base tools only.
|
|
73
148
|
logger.warning(
|
|
74
149
|
"[default_tools] Failed to load dynamic MCP tools: %s: %s",
|
|
75
|
-
type(exc).__name__,
|
|
150
|
+
type(exc).__name__,
|
|
151
|
+
exc,
|
|
76
152
|
)
|
|
77
153
|
|
|
78
154
|
task_tool = TaskTool(lambda: base_tools)
|
|
79
155
|
all_tools = base_tools + [task_tool]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
156
|
+
|
|
157
|
+
# Apply allowed_tools filter if specified
|
|
158
|
+
if allowed_tools is not None:
|
|
159
|
+
all_tools = filter_tools_by_names(all_tools, allowed_tools)
|
|
160
|
+
logger.debug(
|
|
161
|
+
"[default_tools] Filtered tool inventory",
|
|
162
|
+
extra={
|
|
163
|
+
"allowed_tools": allowed_tools,
|
|
164
|
+
"filtered_tools": len(all_tools),
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
logger.debug(
|
|
169
|
+
"[default_tools] Built tool inventory",
|
|
170
|
+
extra={
|
|
171
|
+
"base_tools": len(base_tools),
|
|
172
|
+
"dynamic_mcp_tools": len(dynamic_tools),
|
|
173
|
+
"total_tools": len(all_tools),
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
88
177
|
return all_tools
|
ripperdoc/core/hooks/config.py
CHANGED
|
@@ -234,9 +234,7 @@ def _parse_hooks_file(data: Dict[str, Any]) -> HooksConfig:
|
|
|
234
234
|
hook_definitions.append(hook_def)
|
|
235
235
|
|
|
236
236
|
if hook_definitions:
|
|
237
|
-
parsed_matchers.append(
|
|
238
|
-
HookMatcher(matcher=matcher_pattern, hooks=hook_definitions)
|
|
239
|
-
)
|
|
237
|
+
parsed_matchers.append(HookMatcher(matcher=matcher_pattern, hooks=hook_definitions))
|
|
240
238
|
|
|
241
239
|
if parsed_matchers:
|
|
242
240
|
parsed_hooks[event_name] = parsed_matchers
|
ripperdoc/core/hooks/events.py
CHANGED
|
@@ -5,10 +5,9 @@ as well as the input/output data structures for each event type.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
import os
|
|
9
8
|
from enum import Enum
|
|
10
|
-
from typing import Any, Dict,
|
|
11
|
-
from pydantic import BaseModel, Field
|
|
9
|
+
from typing import Any, Dict, Literal, Optional, Union
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class HookEvent(str, Enum):
|
|
@@ -73,8 +72,7 @@ class HookInput(BaseModel):
|
|
|
73
72
|
permission_mode: str = "default" # "default", "plan", "acceptEdits", "bypassPermissions"
|
|
74
73
|
hook_event_name: str = ""
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
populate_by_name = True
|
|
75
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
78
76
|
|
|
79
77
|
|
|
80
78
|
class PreToolUseInput(HookInput):
|
|
@@ -150,6 +148,8 @@ class StopInput(HookInput):
|
|
|
150
148
|
|
|
151
149
|
hook_event_name: str = "Stop"
|
|
152
150
|
stop_hook_active: bool = False # True if already continuing from a stop hook
|
|
151
|
+
reason: Optional[str] = None
|
|
152
|
+
stop_sequence: Optional[str] = None
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
class SubagentStopInput(HookInput):
|
|
@@ -210,6 +210,8 @@ class SessionEndInput(HookInput):
|
|
|
210
210
|
|
|
211
211
|
hook_event_name: str = "SessionEnd"
|
|
212
212
|
reason: str = "" # "clear", "logout", "prompt_input_exit", "other"
|
|
213
|
+
duration_seconds: Optional[float] = None
|
|
214
|
+
message_count: Optional[int] = None
|
|
213
215
|
|
|
214
216
|
|
|
215
217
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -232,8 +234,7 @@ class PreToolUseHookOutput(BaseModel):
|
|
|
232
234
|
) # Modified tool input
|
|
233
235
|
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
234
236
|
|
|
235
|
-
|
|
236
|
-
populate_by_name = True
|
|
237
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
237
238
|
|
|
238
239
|
|
|
239
240
|
class PermissionRequestDecision(BaseModel):
|
|
@@ -244,8 +245,7 @@ class PermissionRequestDecision(BaseModel):
|
|
|
244
245
|
message: Optional[str] = None
|
|
245
246
|
interrupt: bool = False
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
populate_by_name = True
|
|
248
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
249
249
|
|
|
250
250
|
|
|
251
251
|
class PermissionRequestHookOutput(BaseModel):
|
|
@@ -254,8 +254,7 @@ class PermissionRequestHookOutput(BaseModel):
|
|
|
254
254
|
hook_event_name: Literal["PermissionRequest"] = "PermissionRequest"
|
|
255
255
|
decision: Optional[PermissionRequestDecision] = None
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
populate_by_name = True
|
|
257
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
259
258
|
|
|
260
259
|
|
|
261
260
|
class PostToolUseHookOutput(BaseModel):
|
|
@@ -264,8 +263,7 @@ class PostToolUseHookOutput(BaseModel):
|
|
|
264
263
|
hook_event_name: Literal["PostToolUse"] = "PostToolUse"
|
|
265
264
|
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
populate_by_name = True
|
|
266
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
269
267
|
|
|
270
268
|
|
|
271
269
|
class UserPromptSubmitHookOutput(BaseModel):
|
|
@@ -274,8 +272,7 @@ class UserPromptSubmitHookOutput(BaseModel):
|
|
|
274
272
|
hook_event_name: Literal["UserPromptSubmit"] = "UserPromptSubmit"
|
|
275
273
|
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
276
274
|
|
|
277
|
-
|
|
278
|
-
populate_by_name = True
|
|
275
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
279
276
|
|
|
280
277
|
|
|
281
278
|
class SessionStartHookOutput(BaseModel):
|
|
@@ -284,8 +281,7 @@ class SessionStartHookOutput(BaseModel):
|
|
|
284
281
|
hook_event_name: Literal["SessionStart"] = "SessionStart"
|
|
285
282
|
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
286
283
|
|
|
287
|
-
|
|
288
|
-
populate_by_name = True
|
|
284
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
289
285
|
|
|
290
286
|
|
|
291
287
|
HookSpecificOutput = Union[
|
|
@@ -325,7 +321,7 @@ class HookOutput(BaseModel):
|
|
|
325
321
|
)
|
|
326
322
|
|
|
327
323
|
# Additional context to inject
|
|
328
|
-
additional_context: Optional[str] = None
|
|
324
|
+
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
329
325
|
|
|
330
326
|
# Raw output (for non-JSON responses)
|
|
331
327
|
raw_output: Optional[str] = None
|
|
@@ -336,8 +332,7 @@ class HookOutput(BaseModel):
|
|
|
336
332
|
exit_code: int = 0
|
|
337
333
|
timed_out: bool = False
|
|
338
334
|
|
|
339
|
-
|
|
340
|
-
populate_by_name = True
|
|
335
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
341
336
|
|
|
342
337
|
@classmethod
|
|
343
338
|
def from_raw(
|
|
@@ -420,10 +415,10 @@ class HookOutput(BaseModel):
|
|
|
420
415
|
# Handle PreToolUse specific fields
|
|
421
416
|
if event_name == "PreToolUse":
|
|
422
417
|
output.hook_specific_output = PreToolUseHookOutput(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
418
|
+
permissionDecision=hso.get("permissionDecision"),
|
|
419
|
+
permissionDecisionReason=hso.get("permissionDecisionReason"),
|
|
420
|
+
updatedInput=hso.get("updatedInput"),
|
|
421
|
+
additionalContext=hso.get("additionalContext"),
|
|
427
422
|
)
|
|
428
423
|
# Map permissionDecision to decision
|
|
429
424
|
perm_decision = hso.get("permissionDecision")
|
|
@@ -444,7 +439,7 @@ class HookOutput(BaseModel):
|
|
|
444
439
|
if isinstance(decision_obj, dict):
|
|
445
440
|
decision_data = PermissionRequestDecision(
|
|
446
441
|
behavior=decision_obj.get("behavior", ""),
|
|
447
|
-
|
|
442
|
+
updatedInput=decision_obj.get("updatedInput"),
|
|
448
443
|
message=decision_obj.get("message"),
|
|
449
444
|
interrupt=decision_obj.get("interrupt", False),
|
|
450
445
|
)
|
|
@@ -462,7 +457,7 @@ class HookOutput(BaseModel):
|
|
|
462
457
|
# Handle PostToolUse specific fields
|
|
463
458
|
elif event_name == "PostToolUse":
|
|
464
459
|
output.hook_specific_output = PostToolUseHookOutput(
|
|
465
|
-
|
|
460
|
+
additionalContext=hso.get("additionalContext"),
|
|
466
461
|
)
|
|
467
462
|
if hso.get("additionalContext"):
|
|
468
463
|
output.additional_context = hso["additionalContext"]
|
|
@@ -470,7 +465,7 @@ class HookOutput(BaseModel):
|
|
|
470
465
|
# Handle UserPromptSubmit specific fields
|
|
471
466
|
elif event_name == "UserPromptSubmit":
|
|
472
467
|
output.hook_specific_output = UserPromptSubmitHookOutput(
|
|
473
|
-
|
|
468
|
+
additionalContext=hso.get("additionalContext"),
|
|
474
469
|
)
|
|
475
470
|
if hso.get("additionalContext"):
|
|
476
471
|
output.additional_context = hso["additionalContext"]
|
|
@@ -478,7 +473,7 @@ class HookOutput(BaseModel):
|
|
|
478
473
|
# Handle SessionStart specific fields
|
|
479
474
|
elif event_name == "SessionStart":
|
|
480
475
|
output.hook_specific_output = SessionStartHookOutput(
|
|
481
|
-
|
|
476
|
+
additionalContext=hso.get("additionalContext"),
|
|
482
477
|
)
|
|
483
478
|
if hso.get("additionalContext"):
|
|
484
479
|
output.additional_context = hso["additionalContext"]
|
|
@@ -520,6 +515,10 @@ class HookOutput(BaseModel):
|
|
|
520
515
|
"""Get updated input from PreToolUse hook."""
|
|
521
516
|
if isinstance(self.hook_specific_output, PreToolUseHookOutput):
|
|
522
517
|
return self.hook_specific_output.updated_input
|
|
518
|
+
if isinstance(self.hook_specific_output, PermissionRequestHookOutput):
|
|
519
|
+
decision = self.hook_specific_output.decision
|
|
520
|
+
if decision and decision.updated_input:
|
|
521
|
+
return decision.updated_input
|
|
523
522
|
if isinstance(self.hook_specific_output, dict):
|
|
524
523
|
return self.hook_specific_output.get("updatedInput")
|
|
525
524
|
return None
|
ripperdoc/core/hooks/executor.py
CHANGED
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
import subprocess
|
|
15
15
|
import tempfile
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import
|
|
17
|
+
from typing import Callable, Dict, Optional, Awaitable
|
|
18
18
|
|
|
19
19
|
from ripperdoc.core.hooks.config import HookDefinition
|
|
20
20
|
from ripperdoc.core.hooks.events import AnyHookInput, HookOutput, HookDecision, SessionStartInput
|
|
@@ -191,7 +191,7 @@ class HookExecutor:
|
|
|
191
191
|
pass
|
|
192
192
|
|
|
193
193
|
# Not JSON, treat as additional context
|
|
194
|
-
return HookOutput(raw_output=response,
|
|
194
|
+
return HookOutput(raw_output=response, additionalContext=response)
|
|
195
195
|
|
|
196
196
|
async def execute_prompt_async(
|
|
197
197
|
self,
|
|
@@ -224,7 +224,7 @@ class HookExecutor:
|
|
|
224
224
|
prompt = self._expand_prompt(hook.prompt, input_data)
|
|
225
225
|
|
|
226
226
|
logger.debug(
|
|
227
|
-
|
|
227
|
+
"Executing prompt hook",
|
|
228
228
|
extra={
|
|
229
229
|
"event": input_data.hook_event_name,
|
|
230
230
|
"timeout": hook.timeout,
|
|
@@ -277,9 +277,7 @@ class HookExecutor:
|
|
|
277
277
|
"""
|
|
278
278
|
# Prompt hooks require async - skip in sync mode
|
|
279
279
|
if hook.is_prompt_hook():
|
|
280
|
-
logger.warning(
|
|
281
|
-
"Prompt hook skipped in sync mode. Use execute_async for prompt hooks."
|
|
282
|
-
)
|
|
280
|
+
logger.warning("Prompt hook skipped in sync mode. Use execute_async for prompt hooks.")
|
|
283
281
|
return HookOutput()
|
|
284
282
|
|
|
285
283
|
return self._execute_command_sync(hook, input_data)
|
|
@@ -4,11 +4,9 @@ This module provides convenient integration points for running hooks
|
|
|
4
4
|
as part of tool execution flows.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from pathlib import Path
|
|
8
7
|
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, Union
|
|
9
8
|
|
|
10
|
-
from ripperdoc.core.hooks.
|
|
11
|
-
from ripperdoc.core.hooks.manager import HookManager, HookResult, hook_manager
|
|
9
|
+
from ripperdoc.core.hooks.manager import HookManager, hook_manager
|
|
12
10
|
from ripperdoc.utils.log import get_logger
|
|
13
11
|
|
|
14
12
|
logger = get_logger()
|
|
@@ -98,9 +96,7 @@ class HookInterceptor:
|
|
|
98
96
|
Returns:
|
|
99
97
|
Tuple of (should_continue, block_reason, additional_context)
|
|
100
98
|
"""
|
|
101
|
-
result = self.manager.run_post_tool_use(
|
|
102
|
-
tool_name, tool_input, tool_output, tool_error
|
|
103
|
-
)
|
|
99
|
+
result = self.manager.run_post_tool_use(tool_name, tool_input, tool_output, tool_error)
|
|
104
100
|
|
|
105
101
|
if result.should_block:
|
|
106
102
|
return False, result.block_reason, result.additional_context
|
|
@@ -129,7 +125,7 @@ class HookInterceptor:
|
|
|
129
125
|
tool_name: str,
|
|
130
126
|
tool_input: Dict[str, Any],
|
|
131
127
|
execute_fn: Callable[[], T],
|
|
132
|
-
) -> Tuple[bool, Union[T, str], Optional[str]]:
|
|
128
|
+
) -> Tuple[bool, Union[T, str, None], Optional[str]]:
|
|
133
129
|
"""Wrap synchronous tool execution with pre/post hooks.
|
|
134
130
|
|
|
135
131
|
Args:
|
|
@@ -141,9 +137,7 @@ class HookInterceptor:
|
|
|
141
137
|
Tuple of (success, result_or_error, additional_context)
|
|
142
138
|
"""
|
|
143
139
|
# Run pre-tool hooks
|
|
144
|
-
should_proceed, block_reason, pre_context = self.check_pre_tool_use(
|
|
145
|
-
tool_name, tool_input
|
|
146
|
-
)
|
|
140
|
+
should_proceed, block_reason, pre_context = self.check_pre_tool_use(tool_name, tool_input)
|
|
147
141
|
|
|
148
142
|
if not should_proceed:
|
|
149
143
|
return False, block_reason or "Blocked by hook", pre_context
|
|
@@ -157,9 +151,7 @@ class HookInterceptor:
|
|
|
157
151
|
tool_error = str(e)
|
|
158
152
|
|
|
159
153
|
# Run post-tool hooks
|
|
160
|
-
_, _, post_context = self.run_post_tool_use(
|
|
161
|
-
tool_name, tool_input, result, tool_error
|
|
162
|
-
)
|
|
154
|
+
_, _, post_context = self.run_post_tool_use(tool_name, tool_input, result, tool_error)
|
|
163
155
|
|
|
164
156
|
# Combine contexts
|
|
165
157
|
combined_context = None
|
|
@@ -168,7 +160,7 @@ class HookInterceptor:
|
|
|
168
160
|
combined_context = "\n".join(parts) if parts else None
|
|
169
161
|
|
|
170
162
|
if tool_error:
|
|
171
|
-
return False, tool_error, combined_context
|
|
163
|
+
return False, tool_error or "", combined_context
|
|
172
164
|
|
|
173
165
|
return True, result, combined_context
|
|
174
166
|
|
|
@@ -177,7 +169,7 @@ class HookInterceptor:
|
|
|
177
169
|
tool_name: str,
|
|
178
170
|
tool_input: Dict[str, Any],
|
|
179
171
|
execute_fn: Callable[[], T],
|
|
180
|
-
) -> Tuple[bool, Union[T, str], Optional[str]]:
|
|
172
|
+
) -> Tuple[bool, Union[T, str, None], Optional[str]]:
|
|
181
173
|
"""Wrap async tool execution with pre/post hooks."""
|
|
182
174
|
# Run pre-tool hooks
|
|
183
175
|
should_proceed, block_reason, pre_context = await self.check_pre_tool_use_async(
|
|
@@ -190,6 +182,7 @@ class HookInterceptor:
|
|
|
190
182
|
# Execute the tool
|
|
191
183
|
try:
|
|
192
184
|
import asyncio
|
|
185
|
+
|
|
193
186
|
if asyncio.iscoroutinefunction(execute_fn):
|
|
194
187
|
result = await execute_fn()
|
|
195
188
|
else:
|
|
@@ -211,7 +204,7 @@ class HookInterceptor:
|
|
|
211
204
|
combined_context = "\n".join(parts) if parts else None
|
|
212
205
|
|
|
213
206
|
if tool_error:
|
|
214
|
-
return False, tool_error, combined_context
|
|
207
|
+
return False, tool_error or "", combined_context
|
|
215
208
|
|
|
216
209
|
return True, result, combined_context
|
|
217
210
|
|
|
@@ -241,9 +234,7 @@ def run_post_tool_use(
|
|
|
241
234
|
tool_error: Optional[str] = None,
|
|
242
235
|
) -> Tuple[bool, Optional[str], Optional[str]]:
|
|
243
236
|
"""Convenience function to run post-tool hooks using global interceptor."""
|
|
244
|
-
return hook_interceptor.run_post_tool_use(
|
|
245
|
-
tool_name, tool_input, tool_output, tool_error
|
|
246
|
-
)
|
|
237
|
+
return hook_interceptor.run_post_tool_use(tool_name, tool_input, tool_output, tool_error)
|
|
247
238
|
|
|
248
239
|
|
|
249
240
|
async def run_post_tool_use_async(
|
|
@@ -333,7 +324,7 @@ def check_stop(
|
|
|
333
324
|
- should_stop: True if agent should stop
|
|
334
325
|
- continue_reason: Reason to continue if blocked
|
|
335
326
|
"""
|
|
336
|
-
result = hook_manager.run_stop(reason, stop_sequence)
|
|
327
|
+
result = hook_manager.run_stop(False, reason, stop_sequence)
|
|
337
328
|
|
|
338
329
|
if result.should_block:
|
|
339
330
|
return False, result.block_reason
|
|
@@ -345,7 +336,7 @@ async def check_stop_async(
|
|
|
345
336
|
reason: Optional[str] = None, stop_sequence: Optional[str] = None
|
|
346
337
|
) -> Tuple[bool, Optional[str]]:
|
|
347
338
|
"""Async version of check_stop."""
|
|
348
|
-
result = await hook_manager.run_stop_async(reason, stop_sequence)
|
|
339
|
+
result = await hook_manager.run_stop_async(False, reason, stop_sequence)
|
|
349
340
|
|
|
350
341
|
if result.should_block:
|
|
351
342
|
return False, result.block_reason
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""LLM callback helper for prompt-based hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ripperdoc.core.hooks.executor import LLMCallback
|
|
8
|
+
from ripperdoc.core.query import query_llm
|
|
9
|
+
from ripperdoc.utils.log import get_logger
|
|
10
|
+
from ripperdoc.utils.messages import AssistantMessage, create_user_message
|
|
11
|
+
|
|
12
|
+
logger = get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _extract_text(message: AssistantMessage) -> str:
|
|
16
|
+
content = message.message.content
|
|
17
|
+
if isinstance(content, str):
|
|
18
|
+
return content
|
|
19
|
+
if isinstance(content, list):
|
|
20
|
+
parts = []
|
|
21
|
+
for block in content:
|
|
22
|
+
text = getattr(block, "text", None) or (
|
|
23
|
+
block.get("text") if isinstance(block, dict) else None
|
|
24
|
+
)
|
|
25
|
+
if text:
|
|
26
|
+
parts.append(str(text))
|
|
27
|
+
return "\n".join(parts)
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_hook_llm_callback(
|
|
32
|
+
*,
|
|
33
|
+
model: str = "quick",
|
|
34
|
+
max_thinking_tokens: int = 0,
|
|
35
|
+
system_prompt: Optional[str] = None,
|
|
36
|
+
) -> LLMCallback:
|
|
37
|
+
"""Build an async callback for prompt hooks using the configured model."""
|
|
38
|
+
|
|
39
|
+
async def _callback(prompt: str) -> str:
|
|
40
|
+
try:
|
|
41
|
+
assistant = await query_llm(
|
|
42
|
+
[create_user_message(prompt)],
|
|
43
|
+
system_prompt or "",
|
|
44
|
+
[],
|
|
45
|
+
max_thinking_tokens=max_thinking_tokens,
|
|
46
|
+
model=model,
|
|
47
|
+
stream=False,
|
|
48
|
+
)
|
|
49
|
+
return _extract_text(assistant).strip()
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
logger.warning(
|
|
52
|
+
"[hooks] Prompt hook LLM callback failed: %s: %s",
|
|
53
|
+
type(exc).__name__,
|
|
54
|
+
exc,
|
|
55
|
+
)
|
|
56
|
+
return f"Prompt hook evaluation failed: {exc}"
|
|
57
|
+
|
|
58
|
+
return _callback
|
|
59
|
+
|