ripperdoc 0.2.6__py3-none-any.whl → 0.2.8__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 +5 -0
- ripperdoc/cli/commands/__init__.py +71 -6
- ripperdoc/cli/commands/clear_cmd.py +1 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -1
- ripperdoc/cli/commands/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +636 -0
- ripperdoc/cli/commands/permissions_cmd.py +36 -34
- ripperdoc/cli/commands/resume_cmd.py +71 -37
- ripperdoc/cli/ui/file_mention_completer.py +276 -0
- ripperdoc/cli/ui/helpers.py +100 -3
- ripperdoc/cli/ui/interrupt_handler.py +175 -0
- ripperdoc/cli/ui/message_display.py +249 -0
- ripperdoc/cli/ui/panels.py +63 -0
- ripperdoc/cli/ui/rich_ui.py +233 -648
- ripperdoc/cli/ui/tool_renderers.py +2 -2
- ripperdoc/core/agents.py +4 -4
- ripperdoc/core/custom_commands.py +411 -0
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +303 -0
- ripperdoc/core/hooks/events.py +540 -0
- ripperdoc/core/hooks/executor.py +498 -0
- ripperdoc/core/hooks/integration.py +353 -0
- ripperdoc/core/hooks/manager.py +720 -0
- ripperdoc/core/providers/anthropic.py +476 -69
- ripperdoc/core/query.py +61 -4
- ripperdoc/core/query_utils.py +1 -1
- ripperdoc/core/tool.py +1 -1
- ripperdoc/tools/bash_tool.py +5 -5
- ripperdoc/tools/file_edit_tool.py +2 -2
- ripperdoc/tools/file_read_tool.py +2 -2
- ripperdoc/tools/multi_edit_tool.py +1 -1
- ripperdoc/utils/conversation_compaction.py +476 -0
- ripperdoc/utils/message_compaction.py +109 -154
- ripperdoc/utils/message_formatting.py +216 -0
- ripperdoc/utils/messages.py +31 -9
- ripperdoc/utils/path_ignore.py +3 -4
- ripperdoc/utils/session_history.py +19 -7
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/METADATA +24 -3
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/RECORD +44 -30
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""Hook event types and data structures.
|
|
2
|
+
|
|
3
|
+
This module defines the event types that can trigger hooks,
|
|
4
|
+
as well as the input/output data structures for each event type.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HookEvent(str, Enum):
|
|
15
|
+
"""Hook event types that can trigger user-defined hooks."""
|
|
16
|
+
|
|
17
|
+
# Tool lifecycle events
|
|
18
|
+
PRE_TOOL_USE = "PreToolUse"
|
|
19
|
+
PERMISSION_REQUEST = "PermissionRequest"
|
|
20
|
+
POST_TOOL_USE = "PostToolUse"
|
|
21
|
+
|
|
22
|
+
# User interaction events
|
|
23
|
+
USER_PROMPT_SUBMIT = "UserPromptSubmit"
|
|
24
|
+
NOTIFICATION = "Notification"
|
|
25
|
+
|
|
26
|
+
# Completion events
|
|
27
|
+
STOP = "Stop"
|
|
28
|
+
SUBAGENT_STOP = "SubagentStop"
|
|
29
|
+
|
|
30
|
+
# Session lifecycle events
|
|
31
|
+
PRE_COMPACT = "PreCompact"
|
|
32
|
+
SESSION_START = "SessionStart"
|
|
33
|
+
SESSION_END = "SessionEnd"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HookDecision(str, Enum):
|
|
37
|
+
"""Decision values that hooks can return to control flow.
|
|
38
|
+
|
|
39
|
+
PreToolUse/PermissionRequest decisions:
|
|
40
|
+
- allow: Bypass permission system, auto-approve the tool call
|
|
41
|
+
- deny: Block the tool call, inform the model
|
|
42
|
+
- ask: Prompt user for confirmation (PreToolUse only)
|
|
43
|
+
|
|
44
|
+
PostToolUse decisions:
|
|
45
|
+
- block: Auto-prompt the model about the issue
|
|
46
|
+
|
|
47
|
+
UserPromptSubmit decisions:
|
|
48
|
+
- block: Reject the prompt, show reason to user
|
|
49
|
+
|
|
50
|
+
Stop/SubagentStop decisions:
|
|
51
|
+
- block: Prevent stopping, reason tells model how to continue
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
ALLOW = "allow"
|
|
55
|
+
DENY = "deny"
|
|
56
|
+
ASK = "ask"
|
|
57
|
+
BLOCK = "block"
|
|
58
|
+
|
|
59
|
+
# Legacy aliases (deprecated but supported)
|
|
60
|
+
APPROVE = "approve" # Use 'allow' instead
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HookInput(BaseModel):
|
|
64
|
+
"""Base class for hook input data.
|
|
65
|
+
|
|
66
|
+
Common fields present in all hook inputs.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Common fields for all hooks
|
|
70
|
+
session_id: Optional[str] = None
|
|
71
|
+
transcript_path: Optional[str] = None # Path to conversation JSON
|
|
72
|
+
cwd: Optional[str] = None # Current working directory
|
|
73
|
+
permission_mode: str = "default" # "default", "plan", "acceptEdits", "bypassPermissions"
|
|
74
|
+
hook_event_name: str = ""
|
|
75
|
+
|
|
76
|
+
class Config:
|
|
77
|
+
populate_by_name = True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PreToolUseInput(HookInput):
|
|
81
|
+
"""Input data for PreToolUse hooks.
|
|
82
|
+
|
|
83
|
+
Runs after Agent creates tool parameters and before processing the tool call.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
hook_event_name: str = "PreToolUse"
|
|
87
|
+
tool_name: str = ""
|
|
88
|
+
tool_input: Dict[str, Any] = Field(default_factory=dict)
|
|
89
|
+
tool_use_id: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PermissionRequestInput(HookInput):
|
|
93
|
+
"""Input data for PermissionRequest hooks.
|
|
94
|
+
|
|
95
|
+
Runs when the user is shown a permission dialog.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
hook_event_name: str = "PermissionRequest"
|
|
99
|
+
tool_name: str = ""
|
|
100
|
+
tool_input: Dict[str, Any] = Field(default_factory=dict)
|
|
101
|
+
tool_use_id: Optional[str] = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PostToolUseInput(HookInput):
|
|
105
|
+
"""Input data for PostToolUse hooks.
|
|
106
|
+
|
|
107
|
+
Runs immediately after a tool completes successfully.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
hook_event_name: str = "PostToolUse"
|
|
111
|
+
tool_name: str = ""
|
|
112
|
+
tool_input: Dict[str, Any] = Field(default_factory=dict)
|
|
113
|
+
tool_response: Any = None # Tool's response/output
|
|
114
|
+
tool_use_id: Optional[str] = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class UserPromptSubmitInput(HookInput):
|
|
118
|
+
"""Input data for UserPromptSubmit hooks.
|
|
119
|
+
|
|
120
|
+
Runs when the user submits a prompt, before Agent processes it.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
hook_event_name: str = "UserPromptSubmit"
|
|
124
|
+
prompt: str = ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class NotificationInput(HookInput):
|
|
128
|
+
"""Input data for Notification hooks.
|
|
129
|
+
|
|
130
|
+
Runs when Ripperdoc sends notifications.
|
|
131
|
+
|
|
132
|
+
notification_type values:
|
|
133
|
+
- permission_prompt: Permission requests
|
|
134
|
+
- idle_prompt: Waiting for user input (after 60+ seconds idle)
|
|
135
|
+
- auth_success: Authentication success
|
|
136
|
+
- elicitation_dialog: MCP tool elicitation
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
hook_event_name: str = "Notification"
|
|
140
|
+
message: str = ""
|
|
141
|
+
notification_type: str = "" # Used as matcher
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class StopInput(HookInput):
|
|
145
|
+
"""Input data for Stop hooks.
|
|
146
|
+
|
|
147
|
+
Runs when the main agent has finished responding.
|
|
148
|
+
Does not run if stoppage occurred due to user interrupt.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
hook_event_name: str = "Stop"
|
|
152
|
+
stop_hook_active: bool = False # True if already continuing from a stop hook
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SubagentStopInput(HookInput):
|
|
156
|
+
"""Input data for SubagentStop hooks.
|
|
157
|
+
|
|
158
|
+
Runs when a subagent (Task tool call) has finished responding.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
hook_event_name: str = "SubagentStop"
|
|
162
|
+
stop_hook_active: bool = False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PreCompactInput(HookInput):
|
|
166
|
+
"""Input data for PreCompact hooks.
|
|
167
|
+
|
|
168
|
+
Runs before a compact operation.
|
|
169
|
+
|
|
170
|
+
trigger values:
|
|
171
|
+
- manual: Invoked from /compact
|
|
172
|
+
- auto: Invoked from auto-compact (due to full context window)
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
hook_event_name: str = "PreCompact"
|
|
176
|
+
trigger: str = "" # "manual" or "auto"
|
|
177
|
+
custom_instructions: str = "" # Custom instructions passed to /compact
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SessionStartInput(HookInput):
|
|
181
|
+
"""Input data for SessionStart hooks.
|
|
182
|
+
|
|
183
|
+
Runs when a session starts or resumes.
|
|
184
|
+
|
|
185
|
+
source values:
|
|
186
|
+
- startup: Fresh start
|
|
187
|
+
- resume: From --resume, --continue, or /resume
|
|
188
|
+
- clear: From /clear
|
|
189
|
+
- compact: From auto or manual compact
|
|
190
|
+
|
|
191
|
+
SessionStart hooks have access to RIPPERDOC_ENV_FILE environment variable,
|
|
192
|
+
which provides a file path where environment variables can be persisted.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
hook_event_name: str = "SessionStart"
|
|
196
|
+
source: str = "" # "startup", "resume", "clear", "compact"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class SessionEndInput(HookInput):
|
|
200
|
+
"""Input data for SessionEnd hooks.
|
|
201
|
+
|
|
202
|
+
Runs when a session ends.
|
|
203
|
+
|
|
204
|
+
reason values:
|
|
205
|
+
- clear: Session cleared with /clear command
|
|
206
|
+
- logout: User logged out
|
|
207
|
+
- prompt_input_exit: User exited while prompt input was visible
|
|
208
|
+
- other: Other exit reasons
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
hook_event_name: str = "SessionEnd"
|
|
212
|
+
reason: str = "" # "clear", "logout", "prompt_input_exit", "other"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
# Hook Output Types
|
|
217
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class PreToolUseHookOutput(BaseModel):
|
|
221
|
+
"""Hook-specific output for PreToolUse."""
|
|
222
|
+
|
|
223
|
+
hook_event_name: Literal["PreToolUse"] = "PreToolUse"
|
|
224
|
+
permission_decision: Optional[str] = Field(
|
|
225
|
+
default=None, alias="permissionDecision"
|
|
226
|
+
) # "allow", "deny", "ask"
|
|
227
|
+
permission_decision_reason: Optional[str] = Field(
|
|
228
|
+
default=None, alias="permissionDecisionReason"
|
|
229
|
+
)
|
|
230
|
+
updated_input: Optional[Dict[str, Any]] = Field(
|
|
231
|
+
default=None, alias="updatedInput"
|
|
232
|
+
) # Modified tool input
|
|
233
|
+
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
234
|
+
|
|
235
|
+
class Config:
|
|
236
|
+
populate_by_name = True
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class PermissionRequestDecision(BaseModel):
|
|
240
|
+
"""Decision object for PermissionRequest hooks."""
|
|
241
|
+
|
|
242
|
+
behavior: str = "" # "allow" or "deny"
|
|
243
|
+
updated_input: Optional[Dict[str, Any]] = Field(default=None, alias="updatedInput")
|
|
244
|
+
message: Optional[str] = None
|
|
245
|
+
interrupt: bool = False
|
|
246
|
+
|
|
247
|
+
class Config:
|
|
248
|
+
populate_by_name = True
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class PermissionRequestHookOutput(BaseModel):
|
|
252
|
+
"""Hook-specific output for PermissionRequest."""
|
|
253
|
+
|
|
254
|
+
hook_event_name: Literal["PermissionRequest"] = "PermissionRequest"
|
|
255
|
+
decision: Optional[PermissionRequestDecision] = None
|
|
256
|
+
|
|
257
|
+
class Config:
|
|
258
|
+
populate_by_name = True
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class PostToolUseHookOutput(BaseModel):
|
|
262
|
+
"""Hook-specific output for PostToolUse."""
|
|
263
|
+
|
|
264
|
+
hook_event_name: Literal["PostToolUse"] = "PostToolUse"
|
|
265
|
+
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
266
|
+
|
|
267
|
+
class Config:
|
|
268
|
+
populate_by_name = True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class UserPromptSubmitHookOutput(BaseModel):
|
|
272
|
+
"""Hook-specific output for UserPromptSubmit."""
|
|
273
|
+
|
|
274
|
+
hook_event_name: Literal["UserPromptSubmit"] = "UserPromptSubmit"
|
|
275
|
+
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
276
|
+
|
|
277
|
+
class Config:
|
|
278
|
+
populate_by_name = True
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class SessionStartHookOutput(BaseModel):
|
|
282
|
+
"""Hook-specific output for SessionStart."""
|
|
283
|
+
|
|
284
|
+
hook_event_name: Literal["SessionStart"] = "SessionStart"
|
|
285
|
+
additional_context: Optional[str] = Field(default=None, alias="additionalContext")
|
|
286
|
+
|
|
287
|
+
class Config:
|
|
288
|
+
populate_by_name = True
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
HookSpecificOutput = Union[
|
|
292
|
+
PreToolUseHookOutput,
|
|
293
|
+
PermissionRequestHookOutput,
|
|
294
|
+
PostToolUseHookOutput,
|
|
295
|
+
UserPromptSubmitHookOutput,
|
|
296
|
+
SessionStartHookOutput,
|
|
297
|
+
Dict[str, Any], # Fallback for unknown types
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class HookOutput(BaseModel):
|
|
302
|
+
"""Output from a hook execution.
|
|
303
|
+
|
|
304
|
+
Hooks can output:
|
|
305
|
+
- Plain text (treated as additional context or info)
|
|
306
|
+
- JSON with decision control
|
|
307
|
+
|
|
308
|
+
JSON output is only processed when hook exits with code 0.
|
|
309
|
+
Exit code 2 uses stderr directly as the error message.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
# Common JSON fields
|
|
313
|
+
continue_execution: bool = Field(default=True, alias="continue")
|
|
314
|
+
stop_reason: Optional[str] = Field(default=None, alias="stopReason")
|
|
315
|
+
suppress_output: bool = Field(default=False, alias="suppressOutput")
|
|
316
|
+
system_message: Optional[str] = Field(default=None, alias="systemMessage")
|
|
317
|
+
|
|
318
|
+
# Decision control (for backwards compatibility)
|
|
319
|
+
decision: Optional[HookDecision] = None
|
|
320
|
+
reason: Optional[str] = None
|
|
321
|
+
|
|
322
|
+
# Hook-specific output
|
|
323
|
+
hook_specific_output: Optional[HookSpecificOutput] = Field(
|
|
324
|
+
default=None, alias="hookSpecificOutput"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Additional context to inject
|
|
328
|
+
additional_context: Optional[str] = None
|
|
329
|
+
|
|
330
|
+
# Raw output (for non-JSON responses)
|
|
331
|
+
raw_output: Optional[str] = None
|
|
332
|
+
|
|
333
|
+
# Error info
|
|
334
|
+
error: Optional[str] = None
|
|
335
|
+
stderr: Optional[str] = None
|
|
336
|
+
exit_code: int = 0
|
|
337
|
+
timed_out: bool = False
|
|
338
|
+
|
|
339
|
+
class Config:
|
|
340
|
+
populate_by_name = True
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def from_raw(
|
|
344
|
+
cls, stdout: str, stderr: str, exit_code: int, timed_out: bool = False
|
|
345
|
+
) -> "HookOutput":
|
|
346
|
+
"""Parse hook output from raw command output.
|
|
347
|
+
|
|
348
|
+
Exit code behavior:
|
|
349
|
+
- 0: Success. stdout processed, JSON parsed if present.
|
|
350
|
+
- 2: Blocking error. stderr is used as error message, JSON ignored.
|
|
351
|
+
- Other: Non-blocking error. stderr shown to user.
|
|
352
|
+
"""
|
|
353
|
+
output = cls(exit_code=exit_code, stderr=stderr, timed_out=timed_out)
|
|
354
|
+
|
|
355
|
+
if timed_out:
|
|
356
|
+
output.error = "Hook timed out"
|
|
357
|
+
return output
|
|
358
|
+
|
|
359
|
+
# Exit code 2: Blocking error - use stderr directly
|
|
360
|
+
if exit_code == 2:
|
|
361
|
+
output.decision = HookDecision.DENY
|
|
362
|
+
output.reason = stderr.strip() if stderr.strip() else "Blocked by hook"
|
|
363
|
+
output.error = stderr.strip() if stderr.strip() else None
|
|
364
|
+
return output
|
|
365
|
+
|
|
366
|
+
# Other non-zero exit codes: non-blocking error
|
|
367
|
+
if exit_code != 0:
|
|
368
|
+
output.error = stderr or f"Hook exited with code {exit_code}"
|
|
369
|
+
return output
|
|
370
|
+
|
|
371
|
+
# Exit code 0: Process stdout
|
|
372
|
+
stdout = stdout.strip()
|
|
373
|
+
if not stdout:
|
|
374
|
+
return output
|
|
375
|
+
|
|
376
|
+
# Try to parse as JSON
|
|
377
|
+
try:
|
|
378
|
+
data = json.loads(stdout)
|
|
379
|
+
if isinstance(data, dict):
|
|
380
|
+
output = cls._parse_json_output(data, stderr)
|
|
381
|
+
return output
|
|
382
|
+
except json.JSONDecodeError:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
# Not JSON, treat as raw output / additional context
|
|
386
|
+
output.raw_output = stdout
|
|
387
|
+
output.additional_context = stdout
|
|
388
|
+
return output
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def _parse_json_output(cls, data: Dict[str, Any], stderr: str) -> "HookOutput":
|
|
392
|
+
"""Parse JSON output from hook."""
|
|
393
|
+
output = cls(stderr=stderr)
|
|
394
|
+
|
|
395
|
+
# Common fields
|
|
396
|
+
output.continue_execution = data.get("continue", True)
|
|
397
|
+
output.stop_reason = data.get("stopReason")
|
|
398
|
+
output.suppress_output = data.get("suppressOutput", False)
|
|
399
|
+
output.system_message = data.get("systemMessage")
|
|
400
|
+
|
|
401
|
+
# Legacy decision field (backwards compatibility)
|
|
402
|
+
if "decision" in data:
|
|
403
|
+
decision_str = str(data["decision"]).lower()
|
|
404
|
+
# Handle legacy aliases
|
|
405
|
+
if decision_str == "approve":
|
|
406
|
+
decision_str = "allow"
|
|
407
|
+
try:
|
|
408
|
+
output.decision = HookDecision(decision_str)
|
|
409
|
+
except ValueError:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
output.reason = data.get("reason")
|
|
413
|
+
|
|
414
|
+
# Parse hook-specific output
|
|
415
|
+
if "hookSpecificOutput" in data:
|
|
416
|
+
hso = data["hookSpecificOutput"]
|
|
417
|
+
if isinstance(hso, dict):
|
|
418
|
+
event_name = hso.get("hookEventName")
|
|
419
|
+
|
|
420
|
+
# Handle PreToolUse specific fields
|
|
421
|
+
if event_name == "PreToolUse":
|
|
422
|
+
output.hook_specific_output = PreToolUseHookOutput(
|
|
423
|
+
permission_decision=hso.get("permissionDecision"),
|
|
424
|
+
permission_decision_reason=hso.get("permissionDecisionReason"),
|
|
425
|
+
updated_input=hso.get("updatedInput"),
|
|
426
|
+
additional_context=hso.get("additionalContext"),
|
|
427
|
+
)
|
|
428
|
+
# Map permissionDecision to decision
|
|
429
|
+
perm_decision = hso.get("permissionDecision")
|
|
430
|
+
if perm_decision:
|
|
431
|
+
perm_decision = perm_decision.lower()
|
|
432
|
+
if perm_decision == "approve":
|
|
433
|
+
perm_decision = "allow"
|
|
434
|
+
try:
|
|
435
|
+
output.decision = HookDecision(perm_decision)
|
|
436
|
+
except ValueError:
|
|
437
|
+
pass
|
|
438
|
+
output.reason = hso.get("permissionDecisionReason")
|
|
439
|
+
|
|
440
|
+
# Handle PermissionRequest specific fields
|
|
441
|
+
elif event_name == "PermissionRequest":
|
|
442
|
+
decision_obj = hso.get("decision", {})
|
|
443
|
+
decision_data = None
|
|
444
|
+
if isinstance(decision_obj, dict):
|
|
445
|
+
decision_data = PermissionRequestDecision(
|
|
446
|
+
behavior=decision_obj.get("behavior", ""),
|
|
447
|
+
updated_input=decision_obj.get("updatedInput"),
|
|
448
|
+
message=decision_obj.get("message"),
|
|
449
|
+
interrupt=decision_obj.get("interrupt", False),
|
|
450
|
+
)
|
|
451
|
+
behavior = decision_obj.get("behavior")
|
|
452
|
+
if behavior == "allow":
|
|
453
|
+
output.decision = HookDecision.ALLOW
|
|
454
|
+
elif behavior == "deny":
|
|
455
|
+
output.decision = HookDecision.DENY
|
|
456
|
+
if decision_obj.get("message"):
|
|
457
|
+
output.reason = decision_obj.get("message")
|
|
458
|
+
output.hook_specific_output = PermissionRequestHookOutput(
|
|
459
|
+
decision=decision_data,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Handle PostToolUse specific fields
|
|
463
|
+
elif event_name == "PostToolUse":
|
|
464
|
+
output.hook_specific_output = PostToolUseHookOutput(
|
|
465
|
+
additional_context=hso.get("additionalContext"),
|
|
466
|
+
)
|
|
467
|
+
if hso.get("additionalContext"):
|
|
468
|
+
output.additional_context = hso["additionalContext"]
|
|
469
|
+
|
|
470
|
+
# Handle UserPromptSubmit specific fields
|
|
471
|
+
elif event_name == "UserPromptSubmit":
|
|
472
|
+
output.hook_specific_output = UserPromptSubmitHookOutput(
|
|
473
|
+
additional_context=hso.get("additionalContext"),
|
|
474
|
+
)
|
|
475
|
+
if hso.get("additionalContext"):
|
|
476
|
+
output.additional_context = hso["additionalContext"]
|
|
477
|
+
|
|
478
|
+
# Handle SessionStart specific fields
|
|
479
|
+
elif event_name == "SessionStart":
|
|
480
|
+
output.hook_specific_output = SessionStartHookOutput(
|
|
481
|
+
additional_context=hso.get("additionalContext"),
|
|
482
|
+
)
|
|
483
|
+
if hso.get("additionalContext"):
|
|
484
|
+
output.additional_context = hso["additionalContext"]
|
|
485
|
+
|
|
486
|
+
# Fallback for unknown types
|
|
487
|
+
else:
|
|
488
|
+
output.hook_specific_output = hso
|
|
489
|
+
if hso.get("additionalContext"):
|
|
490
|
+
output.additional_context = hso["additionalContext"]
|
|
491
|
+
|
|
492
|
+
# Direct additional context
|
|
493
|
+
if "additionalContext" in data:
|
|
494
|
+
output.additional_context = data["additionalContext"]
|
|
495
|
+
|
|
496
|
+
return output
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def should_block(self) -> bool:
|
|
500
|
+
"""Check if hook requests blocking."""
|
|
501
|
+
return self.decision in (HookDecision.DENY, HookDecision.BLOCK)
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def should_allow(self) -> bool:
|
|
505
|
+
"""Check if hook requests allowing."""
|
|
506
|
+
return self.decision in (HookDecision.ALLOW, HookDecision.APPROVE)
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def should_ask(self) -> bool:
|
|
510
|
+
"""Check if hook requests user confirmation."""
|
|
511
|
+
return self.decision == HookDecision.ASK
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def should_continue(self) -> bool:
|
|
515
|
+
"""Check if execution should continue."""
|
|
516
|
+
return self.continue_execution
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def updated_input(self) -> Optional[Dict[str, Any]]:
|
|
520
|
+
"""Get updated input from PreToolUse hook."""
|
|
521
|
+
if isinstance(self.hook_specific_output, PreToolUseHookOutput):
|
|
522
|
+
return self.hook_specific_output.updated_input
|
|
523
|
+
if isinstance(self.hook_specific_output, dict):
|
|
524
|
+
return self.hook_specific_output.get("updatedInput")
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# Type alias for any hook input
|
|
529
|
+
AnyHookInput = Union[
|
|
530
|
+
PreToolUseInput,
|
|
531
|
+
PermissionRequestInput,
|
|
532
|
+
PostToolUseInput,
|
|
533
|
+
UserPromptSubmitInput,
|
|
534
|
+
NotificationInput,
|
|
535
|
+
StopInput,
|
|
536
|
+
SubagentStopInput,
|
|
537
|
+
PreCompactInput,
|
|
538
|
+
SessionStartInput,
|
|
539
|
+
SessionEndInput,
|
|
540
|
+
]
|