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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +28 -115
- ripperdoc/cli/commands/__init__.py +0 -1
- 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/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +5 -3
- 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/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +110 -59
- 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 +7 -6
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +23 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +82 -38
- 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 +26 -16
- 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 +8 -4
- 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 +8 -4
- 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 +35 -8
- 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.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
ripperdoc/core/agents.py
CHANGED
|
@@ -278,10 +278,15 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
|
|
|
278
278
|
body = "\n".join(lines[idx + 1 :])
|
|
279
279
|
try:
|
|
280
280
|
frontmatter = yaml.safe_load(frontmatter_text) or {}
|
|
281
|
-
except (
|
|
281
|
+
except (
|
|
282
|
+
yaml.YAMLError,
|
|
283
|
+
ValueError,
|
|
284
|
+
TypeError,
|
|
285
|
+
) as exc: # pragma: no cover - defensive
|
|
282
286
|
logger.warning(
|
|
283
287
|
"Invalid frontmatter in agent file: %s: %s",
|
|
284
|
-
type(exc).__name__,
|
|
288
|
+
type(exc).__name__,
|
|
289
|
+
exc,
|
|
285
290
|
extra={"error": str(exc)},
|
|
286
291
|
)
|
|
287
292
|
return {"__error__": f"Invalid frontmatter: {exc}"}, body
|
|
@@ -312,7 +317,8 @@ def _parse_agent_file(
|
|
|
312
317
|
except (OSError, IOError, UnicodeDecodeError) as exc:
|
|
313
318
|
logger.warning(
|
|
314
319
|
"Failed to read agent file: %s: %s",
|
|
315
|
-
type(exc).__name__,
|
|
320
|
+
type(exc).__name__,
|
|
321
|
+
exc,
|
|
316
322
|
extra={"error": str(exc), "path": str(path)},
|
|
317
323
|
)
|
|
318
324
|
return None, f"Failed to read agent file {path}: {exc}"
|
ripperdoc/core/config.py
CHANGED
|
@@ -7,8 +7,8 @@ including API keys, model settings, and user preferences.
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Dict, Optional, Literal
|
|
11
|
-
from pydantic import BaseModel, Field
|
|
10
|
+
from typing import Any, Dict, Optional, Literal
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
12
12
|
from enum import Enum
|
|
13
13
|
|
|
14
14
|
from ripperdoc.utils.log import get_logger
|
|
@@ -111,7 +111,7 @@ class ModelProfile(BaseModel):
|
|
|
111
111
|
# interactions into plain text to support providers that reject tool roles.
|
|
112
112
|
openai_tool_mode: Literal["native", "text"] = "native"
|
|
113
113
|
# Optional override for thinking protocol handling (e.g., "deepseek", "openrouter",
|
|
114
|
-
# "qwen", "gemini_openai", "
|
|
114
|
+
# "qwen", "gemini_openai", "openai"). When unset, provider heuristics are used.
|
|
115
115
|
thinking_mode: Optional[str] = None
|
|
116
116
|
# Pricing (USD per 1M tokens). Leave as 0 to skip cost calculation.
|
|
117
117
|
input_cost_per_million_tokens: float = 0.0
|
|
@@ -130,7 +130,7 @@ class ModelPointers(BaseModel):
|
|
|
130
130
|
class GlobalConfig(BaseModel):
|
|
131
131
|
"""Global configuration stored in ~/.ripperdoc.json"""
|
|
132
132
|
|
|
133
|
-
model_config = {"protected_namespaces": ()}
|
|
133
|
+
model_config = {"protected_namespaces": (), "populate_by_name": True}
|
|
134
134
|
|
|
135
135
|
# Model configuration
|
|
136
136
|
model_profiles: Dict[str, ModelProfile] = Field(default_factory=dict)
|
|
@@ -139,7 +139,8 @@ class GlobalConfig(BaseModel):
|
|
|
139
139
|
# User preferences
|
|
140
140
|
theme: str = "dark"
|
|
141
141
|
verbose: bool = False
|
|
142
|
-
|
|
142
|
+
yolo_mode: bool = Field(default=False)
|
|
143
|
+
show_full_thinking: bool = Field(default=False)
|
|
143
144
|
auto_compact_enabled: bool = True
|
|
144
145
|
context_token_limit: Optional[int] = None
|
|
145
146
|
|
|
@@ -154,6 +155,18 @@ class GlobalConfig(BaseModel):
|
|
|
154
155
|
# Statistics
|
|
155
156
|
num_startups: int = 0
|
|
156
157
|
|
|
158
|
+
@model_validator(mode="before")
|
|
159
|
+
@classmethod
|
|
160
|
+
def _migrate_safe_mode(cls, data: Any) -> Any:
|
|
161
|
+
"""Translate legacy safe_mode to the new yolo_mode flag."""
|
|
162
|
+
if isinstance(data, dict) and "safe_mode" in data and "yolo_mode" not in data:
|
|
163
|
+
data = dict(data)
|
|
164
|
+
try:
|
|
165
|
+
data["yolo_mode"] = not bool(data.pop("safe_mode"))
|
|
166
|
+
except Exception:
|
|
167
|
+
data["yolo_mode"] = False
|
|
168
|
+
return data
|
|
169
|
+
|
|
157
170
|
|
|
158
171
|
class ProjectConfig(BaseModel):
|
|
159
172
|
"""Project-specific configuration stored in .ripperdoc/config.json"""
|
|
@@ -167,7 +180,7 @@ class ProjectConfig(BaseModel):
|
|
|
167
180
|
# Path ignore patterns (gitignore-style)
|
|
168
181
|
ignore_patterns: list[str] = Field(
|
|
169
182
|
default_factory=list,
|
|
170
|
-
description="Gitignore-style patterns for paths to ignore in file operations"
|
|
183
|
+
description="Gitignore-style patterns for paths to ignore in file operations",
|
|
171
184
|
)
|
|
172
185
|
|
|
173
186
|
# Context
|
|
@@ -222,10 +235,18 @@ class ConfigManager:
|
|
|
222
235
|
"profile_count": len(self._global_config.model_profiles),
|
|
223
236
|
},
|
|
224
237
|
)
|
|
225
|
-
except (
|
|
238
|
+
except (
|
|
239
|
+
json.JSONDecodeError,
|
|
240
|
+
OSError,
|
|
241
|
+
IOError,
|
|
242
|
+
UnicodeDecodeError,
|
|
243
|
+
ValueError,
|
|
244
|
+
TypeError,
|
|
245
|
+
) as e:
|
|
226
246
|
logger.warning(
|
|
227
247
|
"Error loading global config: %s: %s",
|
|
228
|
-
type(e).__name__,
|
|
248
|
+
type(e).__name__,
|
|
249
|
+
e,
|
|
229
250
|
extra={"error": str(e)},
|
|
230
251
|
)
|
|
231
252
|
self._global_config = GlobalConfig()
|
|
@@ -276,10 +297,18 @@ class ConfigManager:
|
|
|
276
297
|
"allowed_tools": len(self._project_config.allowed_tools),
|
|
277
298
|
},
|
|
278
299
|
)
|
|
279
|
-
except (
|
|
300
|
+
except (
|
|
301
|
+
json.JSONDecodeError,
|
|
302
|
+
OSError,
|
|
303
|
+
IOError,
|
|
304
|
+
UnicodeDecodeError,
|
|
305
|
+
ValueError,
|
|
306
|
+
TypeError,
|
|
307
|
+
) as e:
|
|
280
308
|
logger.warning(
|
|
281
309
|
"Error loading project config: %s: %s",
|
|
282
|
-
type(e).__name__,
|
|
310
|
+
type(e).__name__,
|
|
311
|
+
e,
|
|
283
312
|
extra={"error": str(e), "path": str(config_path)},
|
|
284
313
|
)
|
|
285
314
|
self._project_config = ProjectConfig()
|
|
@@ -344,10 +373,18 @@ class ConfigManager:
|
|
|
344
373
|
"project_path": str(self.current_project_path),
|
|
345
374
|
},
|
|
346
375
|
)
|
|
347
|
-
except (
|
|
376
|
+
except (
|
|
377
|
+
json.JSONDecodeError,
|
|
378
|
+
OSError,
|
|
379
|
+
IOError,
|
|
380
|
+
UnicodeDecodeError,
|
|
381
|
+
ValueError,
|
|
382
|
+
TypeError,
|
|
383
|
+
) as e:
|
|
348
384
|
logger.warning(
|
|
349
385
|
"Error loading project-local config: %s: %s",
|
|
350
|
-
type(e).__name__,
|
|
386
|
+
type(e).__name__,
|
|
387
|
+
e,
|
|
351
388
|
extra={"error": str(e), "path": str(config_path)},
|
|
352
389
|
)
|
|
353
390
|
self._project_local_config = ProjectLocalConfig()
|
|
@@ -14,7 +14,6 @@ Features:
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import asyncio
|
|
18
17
|
import re
|
|
19
18
|
import subprocess
|
|
20
19
|
from dataclasses import dataclass, field
|
|
@@ -24,7 +23,6 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
|
24
23
|
|
|
25
24
|
import yaml
|
|
26
25
|
|
|
27
|
-
from ripperdoc.utils.coerce import parse_boolish, parse_optional_int
|
|
28
26
|
from ripperdoc.utils.log import get_logger
|
|
29
27
|
|
|
30
28
|
logger = get_logger()
|
|
@@ -96,13 +94,14 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
|
|
|
96
94
|
for idx in range(1, len(lines)):
|
|
97
95
|
if lines[idx].strip() == "---":
|
|
98
96
|
frontmatter_text = "\n".join(lines[1:idx])
|
|
99
|
-
body = "\n".join(lines[idx + 1:])
|
|
97
|
+
body = "\n".join(lines[idx + 1 :])
|
|
100
98
|
try:
|
|
101
99
|
frontmatter = yaml.safe_load(frontmatter_text) or {}
|
|
102
100
|
except (yaml.YAMLError, ValueError, TypeError) as exc:
|
|
103
101
|
logger.warning(
|
|
104
102
|
"[custom_commands] Invalid frontmatter: %s: %s",
|
|
105
|
-
type(exc).__name__,
|
|
103
|
+
type(exc).__name__,
|
|
104
|
+
exc,
|
|
106
105
|
)
|
|
107
106
|
return {"__error__": f"Invalid frontmatter: {exc}"}, body
|
|
108
107
|
return frontmatter, body
|
|
@@ -163,7 +162,8 @@ def _load_command_file(
|
|
|
163
162
|
except (OSError, IOError, UnicodeDecodeError) as exc:
|
|
164
163
|
logger.warning(
|
|
165
164
|
"[custom_commands] Failed to read command file: %s: %s",
|
|
166
|
-
type(exc).__name__,
|
|
165
|
+
type(exc).__name__,
|
|
166
|
+
exc,
|
|
167
167
|
extra={"path": str(path)},
|
|
168
168
|
)
|
|
169
169
|
return None, CustomCommandLoadError(path=path, reason=f"Failed to read file: {exc}")
|
|
@@ -239,7 +239,8 @@ def _load_commands_from_dir(
|
|
|
239
239
|
except OSError as exc:
|
|
240
240
|
logger.warning(
|
|
241
241
|
"[custom_commands] Failed to scan command directory: %s: %s",
|
|
242
|
-
type(exc).__name__,
|
|
242
|
+
type(exc).__name__,
|
|
243
|
+
exc,
|
|
243
244
|
extra={"path": str(commands_dir)},
|
|
244
245
|
)
|
|
245
246
|
|
ripperdoc/core/default_tools.py
CHANGED
|
@@ -68,11 +68,20 @@ def get_default_tools() -> List[Tool[Any, Any]]:
|
|
|
68
68
|
if isinstance(tool, Tool):
|
|
69
69
|
base_tools.append(tool)
|
|
70
70
|
dynamic_tools.append(tool)
|
|
71
|
-
except (
|
|
71
|
+
except (
|
|
72
|
+
ImportError,
|
|
73
|
+
ModuleNotFoundError,
|
|
74
|
+
OSError,
|
|
75
|
+
RuntimeError,
|
|
76
|
+
ConnectionError,
|
|
77
|
+
ValueError,
|
|
78
|
+
TypeError,
|
|
79
|
+
) as exc:
|
|
72
80
|
# If MCP runtime is not available, continue with base tools only.
|
|
73
81
|
logger.warning(
|
|
74
82
|
"[default_tools] Failed to load dynamic MCP tools: %s: %s",
|
|
75
|
-
type(exc).__name__,
|
|
83
|
+
type(exc).__name__,
|
|
84
|
+
exc,
|
|
76
85
|
)
|
|
77
86
|
|
|
78
87
|
task_tool = TaskTool(lambda: base_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"]
|
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
|