ripperdoc 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +33 -115
  3. ripperdoc/cli/commands/__init__.py +70 -6
  4. ripperdoc/cli/commands/agents_cmd.py +6 -3
  5. ripperdoc/cli/commands/clear_cmd.py +1 -4
  6. ripperdoc/cli/commands/config_cmd.py +1 -1
  7. ripperdoc/cli/commands/context_cmd.py +3 -2
  8. ripperdoc/cli/commands/doctor_cmd.py +18 -4
  9. ripperdoc/cli/commands/help_cmd.py +11 -1
  10. ripperdoc/cli/commands/hooks_cmd.py +610 -0
  11. ripperdoc/cli/commands/models_cmd.py +26 -9
  12. ripperdoc/cli/commands/permissions_cmd.py +57 -37
  13. ripperdoc/cli/commands/resume_cmd.py +6 -4
  14. ripperdoc/cli/commands/status_cmd.py +4 -4
  15. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  16. ripperdoc/cli/ui/file_mention_completer.py +64 -8
  17. ripperdoc/cli/ui/interrupt_handler.py +3 -4
  18. ripperdoc/cli/ui/message_display.py +5 -3
  19. ripperdoc/cli/ui/panels.py +13 -10
  20. ripperdoc/cli/ui/provider_options.py +247 -0
  21. ripperdoc/cli/ui/rich_ui.py +196 -77
  22. ripperdoc/cli/ui/spinner.py +25 -1
  23. ripperdoc/cli/ui/tool_renderers.py +8 -2
  24. ripperdoc/cli/ui/wizard.py +215 -0
  25. ripperdoc/core/agents.py +9 -3
  26. ripperdoc/core/config.py +49 -12
  27. ripperdoc/core/custom_commands.py +412 -0
  28. ripperdoc/core/default_tools.py +11 -2
  29. ripperdoc/core/hooks/__init__.py +99 -0
  30. ripperdoc/core/hooks/config.py +301 -0
  31. ripperdoc/core/hooks/events.py +535 -0
  32. ripperdoc/core/hooks/executor.py +496 -0
  33. ripperdoc/core/hooks/integration.py +344 -0
  34. ripperdoc/core/hooks/manager.py +745 -0
  35. ripperdoc/core/permissions.py +40 -8
  36. ripperdoc/core/providers/anthropic.py +548 -68
  37. ripperdoc/core/providers/gemini.py +70 -5
  38. ripperdoc/core/providers/openai.py +60 -5
  39. ripperdoc/core/query.py +140 -39
  40. ripperdoc/core/query_utils.py +2 -0
  41. ripperdoc/core/skills.py +9 -3
  42. ripperdoc/core/system_prompt.py +4 -2
  43. ripperdoc/core/tool.py +9 -5
  44. ripperdoc/sdk/client.py +2 -2
  45. ripperdoc/tools/ask_user_question_tool.py +5 -3
  46. ripperdoc/tools/background_shell.py +2 -1
  47. ripperdoc/tools/bash_output_tool.py +1 -1
  48. ripperdoc/tools/bash_tool.py +30 -20
  49. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  50. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  51. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  52. ripperdoc/tools/file_edit_tool.py +8 -4
  53. ripperdoc/tools/file_read_tool.py +9 -5
  54. ripperdoc/tools/file_write_tool.py +9 -5
  55. ripperdoc/tools/glob_tool.py +3 -2
  56. ripperdoc/tools/grep_tool.py +3 -2
  57. ripperdoc/tools/kill_bash_tool.py +1 -1
  58. ripperdoc/tools/ls_tool.py +1 -1
  59. ripperdoc/tools/mcp_tools.py +13 -10
  60. ripperdoc/tools/multi_edit_tool.py +8 -7
  61. ripperdoc/tools/notebook_edit_tool.py +7 -4
  62. ripperdoc/tools/skill_tool.py +1 -1
  63. ripperdoc/tools/task_tool.py +5 -4
  64. ripperdoc/tools/todo_tool.py +2 -2
  65. ripperdoc/tools/tool_search_tool.py +3 -2
  66. ripperdoc/utils/conversation_compaction.py +11 -7
  67. ripperdoc/utils/file_watch.py +8 -2
  68. ripperdoc/utils/json_utils.py +2 -1
  69. ripperdoc/utils/mcp.py +11 -3
  70. ripperdoc/utils/memory.py +4 -2
  71. ripperdoc/utils/message_compaction.py +21 -7
  72. ripperdoc/utils/message_formatting.py +11 -7
  73. ripperdoc/utils/messages.py +105 -66
  74. ripperdoc/utils/path_ignore.py +38 -12
  75. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  76. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  77. ripperdoc/utils/safe_get_cwd.py +2 -1
  78. ripperdoc/utils/session_history.py +13 -6
  79. ripperdoc/utils/todo.py +2 -1
  80. ripperdoc/utils/token_estimation.py +6 -1
  81. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
  82. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  83. ripperdoc-0.2.7.dist-info/RECORD +0 -113
  84. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  85. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  86. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  87. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,535 @@
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
+ from enum import Enum
9
+ from typing import Any, Dict, Literal, Optional, Union
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+
13
+ class HookEvent(str, Enum):
14
+ """Hook event types that can trigger user-defined hooks."""
15
+
16
+ # Tool lifecycle events
17
+ PRE_TOOL_USE = "PreToolUse"
18
+ PERMISSION_REQUEST = "PermissionRequest"
19
+ POST_TOOL_USE = "PostToolUse"
20
+
21
+ # User interaction events
22
+ USER_PROMPT_SUBMIT = "UserPromptSubmit"
23
+ NOTIFICATION = "Notification"
24
+
25
+ # Completion events
26
+ STOP = "Stop"
27
+ SUBAGENT_STOP = "SubagentStop"
28
+
29
+ # Session lifecycle events
30
+ PRE_COMPACT = "PreCompact"
31
+ SESSION_START = "SessionStart"
32
+ SESSION_END = "SessionEnd"
33
+
34
+
35
+ class HookDecision(str, Enum):
36
+ """Decision values that hooks can return to control flow.
37
+
38
+ PreToolUse/PermissionRequest decisions:
39
+ - allow: Bypass permission system, auto-approve the tool call
40
+ - deny: Block the tool call, inform the model
41
+ - ask: Prompt user for confirmation (PreToolUse only)
42
+
43
+ PostToolUse decisions:
44
+ - block: Auto-prompt the model about the issue
45
+
46
+ UserPromptSubmit decisions:
47
+ - block: Reject the prompt, show reason to user
48
+
49
+ Stop/SubagentStop decisions:
50
+ - block: Prevent stopping, reason tells model how to continue
51
+ """
52
+
53
+ ALLOW = "allow"
54
+ DENY = "deny"
55
+ ASK = "ask"
56
+ BLOCK = "block"
57
+
58
+ # Legacy aliases (deprecated but supported)
59
+ APPROVE = "approve" # Use 'allow' instead
60
+
61
+
62
+ class HookInput(BaseModel):
63
+ """Base class for hook input data.
64
+
65
+ Common fields present in all hook inputs.
66
+ """
67
+
68
+ # Common fields for all hooks
69
+ session_id: Optional[str] = None
70
+ transcript_path: Optional[str] = None # Path to conversation JSON
71
+ cwd: Optional[str] = None # Current working directory
72
+ permission_mode: str = "default" # "default", "plan", "acceptEdits", "bypassPermissions"
73
+ hook_event_name: str = ""
74
+
75
+ model_config = ConfigDict(populate_by_name=True)
76
+
77
+
78
+ class PreToolUseInput(HookInput):
79
+ """Input data for PreToolUse hooks.
80
+
81
+ Runs after Agent creates tool parameters and before processing the tool call.
82
+ """
83
+
84
+ hook_event_name: str = "PreToolUse"
85
+ tool_name: str = ""
86
+ tool_input: Dict[str, Any] = Field(default_factory=dict)
87
+ tool_use_id: Optional[str] = None
88
+
89
+
90
+ class PermissionRequestInput(HookInput):
91
+ """Input data for PermissionRequest hooks.
92
+
93
+ Runs when the user is shown a permission dialog.
94
+ """
95
+
96
+ hook_event_name: str = "PermissionRequest"
97
+ tool_name: str = ""
98
+ tool_input: Dict[str, Any] = Field(default_factory=dict)
99
+ tool_use_id: Optional[str] = None
100
+
101
+
102
+ class PostToolUseInput(HookInput):
103
+ """Input data for PostToolUse hooks.
104
+
105
+ Runs immediately after a tool completes successfully.
106
+ """
107
+
108
+ hook_event_name: str = "PostToolUse"
109
+ tool_name: str = ""
110
+ tool_input: Dict[str, Any] = Field(default_factory=dict)
111
+ tool_response: Any = None # Tool's response/output
112
+ tool_use_id: Optional[str] = None
113
+
114
+
115
+ class UserPromptSubmitInput(HookInput):
116
+ """Input data for UserPromptSubmit hooks.
117
+
118
+ Runs when the user submits a prompt, before Agent processes it.
119
+ """
120
+
121
+ hook_event_name: str = "UserPromptSubmit"
122
+ prompt: str = ""
123
+
124
+
125
+ class NotificationInput(HookInput):
126
+ """Input data for Notification hooks.
127
+
128
+ Runs when Ripperdoc sends notifications.
129
+
130
+ notification_type values:
131
+ - permission_prompt: Permission requests
132
+ - idle_prompt: Waiting for user input (after 60+ seconds idle)
133
+ - auth_success: Authentication success
134
+ - elicitation_dialog: MCP tool elicitation
135
+ """
136
+
137
+ hook_event_name: str = "Notification"
138
+ message: str = ""
139
+ notification_type: str = "" # Used as matcher
140
+
141
+
142
+ class StopInput(HookInput):
143
+ """Input data for Stop hooks.
144
+
145
+ Runs when the main agent has finished responding.
146
+ Does not run if stoppage occurred due to user interrupt.
147
+ """
148
+
149
+ hook_event_name: str = "Stop"
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
+
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
+ duration_seconds: Optional[float] = None
214
+ message_count: Optional[int] = None
215
+
216
+
217
+ # ─────────────────────────────────────────────────────────────────────────────
218
+ # Hook Output Types
219
+ # ─────────────────────────────────────────────────────────────────────────────
220
+
221
+
222
+ class PreToolUseHookOutput(BaseModel):
223
+ """Hook-specific output for PreToolUse."""
224
+
225
+ hook_event_name: Literal["PreToolUse"] = "PreToolUse"
226
+ permission_decision: Optional[str] = Field(
227
+ default=None, alias="permissionDecision"
228
+ ) # "allow", "deny", "ask"
229
+ permission_decision_reason: Optional[str] = Field(
230
+ default=None, alias="permissionDecisionReason"
231
+ )
232
+ updated_input: Optional[Dict[str, Any]] = Field(
233
+ default=None, alias="updatedInput"
234
+ ) # Modified tool input
235
+ additional_context: Optional[str] = Field(default=None, alias="additionalContext")
236
+
237
+ model_config = ConfigDict(populate_by_name=True)
238
+
239
+
240
+ class PermissionRequestDecision(BaseModel):
241
+ """Decision object for PermissionRequest hooks."""
242
+
243
+ behavior: str = "" # "allow" or "deny"
244
+ updated_input: Optional[Dict[str, Any]] = Field(default=None, alias="updatedInput")
245
+ message: Optional[str] = None
246
+ interrupt: bool = False
247
+
248
+ model_config = ConfigDict(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
+ model_config = ConfigDict(populate_by_name=True)
258
+
259
+
260
+ class PostToolUseHookOutput(BaseModel):
261
+ """Hook-specific output for PostToolUse."""
262
+
263
+ hook_event_name: Literal["PostToolUse"] = "PostToolUse"
264
+ additional_context: Optional[str] = Field(default=None, alias="additionalContext")
265
+
266
+ model_config = ConfigDict(populate_by_name=True)
267
+
268
+
269
+ class UserPromptSubmitHookOutput(BaseModel):
270
+ """Hook-specific output for UserPromptSubmit."""
271
+
272
+ hook_event_name: Literal["UserPromptSubmit"] = "UserPromptSubmit"
273
+ additional_context: Optional[str] = Field(default=None, alias="additionalContext")
274
+
275
+ model_config = ConfigDict(populate_by_name=True)
276
+
277
+
278
+ class SessionStartHookOutput(BaseModel):
279
+ """Hook-specific output for SessionStart."""
280
+
281
+ hook_event_name: Literal["SessionStart"] = "SessionStart"
282
+ additional_context: Optional[str] = Field(default=None, alias="additionalContext")
283
+
284
+ model_config = ConfigDict(populate_by_name=True)
285
+
286
+
287
+ HookSpecificOutput = Union[
288
+ PreToolUseHookOutput,
289
+ PermissionRequestHookOutput,
290
+ PostToolUseHookOutput,
291
+ UserPromptSubmitHookOutput,
292
+ SessionStartHookOutput,
293
+ Dict[str, Any], # Fallback for unknown types
294
+ ]
295
+
296
+
297
+ class HookOutput(BaseModel):
298
+ """Output from a hook execution.
299
+
300
+ Hooks can output:
301
+ - Plain text (treated as additional context or info)
302
+ - JSON with decision control
303
+
304
+ JSON output is only processed when hook exits with code 0.
305
+ Exit code 2 uses stderr directly as the error message.
306
+ """
307
+
308
+ # Common JSON fields
309
+ continue_execution: bool = Field(default=True, alias="continue")
310
+ stop_reason: Optional[str] = Field(default=None, alias="stopReason")
311
+ suppress_output: bool = Field(default=False, alias="suppressOutput")
312
+ system_message: Optional[str] = Field(default=None, alias="systemMessage")
313
+
314
+ # Decision control (for backwards compatibility)
315
+ decision: Optional[HookDecision] = None
316
+ reason: Optional[str] = None
317
+
318
+ # Hook-specific output
319
+ hook_specific_output: Optional[HookSpecificOutput] = Field(
320
+ default=None, alias="hookSpecificOutput"
321
+ )
322
+
323
+ # Additional context to inject
324
+ additional_context: Optional[str] = Field(default=None, alias="additionalContext")
325
+
326
+ # Raw output (for non-JSON responses)
327
+ raw_output: Optional[str] = None
328
+
329
+ # Error info
330
+ error: Optional[str] = None
331
+ stderr: Optional[str] = None
332
+ exit_code: int = 0
333
+ timed_out: bool = False
334
+
335
+ model_config = ConfigDict(populate_by_name=True)
336
+
337
+ @classmethod
338
+ def from_raw(
339
+ cls, stdout: str, stderr: str, exit_code: int, timed_out: bool = False
340
+ ) -> "HookOutput":
341
+ """Parse hook output from raw command output.
342
+
343
+ Exit code behavior:
344
+ - 0: Success. stdout processed, JSON parsed if present.
345
+ - 2: Blocking error. stderr is used as error message, JSON ignored.
346
+ - Other: Non-blocking error. stderr shown to user.
347
+ """
348
+ output = cls(exit_code=exit_code, stderr=stderr, timed_out=timed_out)
349
+
350
+ if timed_out:
351
+ output.error = "Hook timed out"
352
+ return output
353
+
354
+ # Exit code 2: Blocking error - use stderr directly
355
+ if exit_code == 2:
356
+ output.decision = HookDecision.DENY
357
+ output.reason = stderr.strip() if stderr.strip() else "Blocked by hook"
358
+ output.error = stderr.strip() if stderr.strip() else None
359
+ return output
360
+
361
+ # Other non-zero exit codes: non-blocking error
362
+ if exit_code != 0:
363
+ output.error = stderr or f"Hook exited with code {exit_code}"
364
+ return output
365
+
366
+ # Exit code 0: Process stdout
367
+ stdout = stdout.strip()
368
+ if not stdout:
369
+ return output
370
+
371
+ # Try to parse as JSON
372
+ try:
373
+ data = json.loads(stdout)
374
+ if isinstance(data, dict):
375
+ output = cls._parse_json_output(data, stderr)
376
+ return output
377
+ except json.JSONDecodeError:
378
+ pass
379
+
380
+ # Not JSON, treat as raw output / additional context
381
+ output.raw_output = stdout
382
+ output.additional_context = stdout
383
+ return output
384
+
385
+ @classmethod
386
+ def _parse_json_output(cls, data: Dict[str, Any], stderr: str) -> "HookOutput":
387
+ """Parse JSON output from hook."""
388
+ output = cls(stderr=stderr)
389
+
390
+ # Common fields
391
+ output.continue_execution = data.get("continue", True)
392
+ output.stop_reason = data.get("stopReason")
393
+ output.suppress_output = data.get("suppressOutput", False)
394
+ output.system_message = data.get("systemMessage")
395
+
396
+ # Legacy decision field (backwards compatibility)
397
+ if "decision" in data:
398
+ decision_str = str(data["decision"]).lower()
399
+ # Handle legacy aliases
400
+ if decision_str == "approve":
401
+ decision_str = "allow"
402
+ try:
403
+ output.decision = HookDecision(decision_str)
404
+ except ValueError:
405
+ pass
406
+
407
+ output.reason = data.get("reason")
408
+
409
+ # Parse hook-specific output
410
+ if "hookSpecificOutput" in data:
411
+ hso = data["hookSpecificOutput"]
412
+ if isinstance(hso, dict):
413
+ event_name = hso.get("hookEventName")
414
+
415
+ # Handle PreToolUse specific fields
416
+ if event_name == "PreToolUse":
417
+ output.hook_specific_output = PreToolUseHookOutput(
418
+ permissionDecision=hso.get("permissionDecision"),
419
+ permissionDecisionReason=hso.get("permissionDecisionReason"),
420
+ updatedInput=hso.get("updatedInput"),
421
+ additionalContext=hso.get("additionalContext"),
422
+ )
423
+ # Map permissionDecision to decision
424
+ perm_decision = hso.get("permissionDecision")
425
+ if perm_decision:
426
+ perm_decision = perm_decision.lower()
427
+ if perm_decision == "approve":
428
+ perm_decision = "allow"
429
+ try:
430
+ output.decision = HookDecision(perm_decision)
431
+ except ValueError:
432
+ pass
433
+ output.reason = hso.get("permissionDecisionReason")
434
+
435
+ # Handle PermissionRequest specific fields
436
+ elif event_name == "PermissionRequest":
437
+ decision_obj = hso.get("decision", {})
438
+ decision_data = None
439
+ if isinstance(decision_obj, dict):
440
+ decision_data = PermissionRequestDecision(
441
+ behavior=decision_obj.get("behavior", ""),
442
+ updatedInput=decision_obj.get("updatedInput"),
443
+ message=decision_obj.get("message"),
444
+ interrupt=decision_obj.get("interrupt", False),
445
+ )
446
+ behavior = decision_obj.get("behavior")
447
+ if behavior == "allow":
448
+ output.decision = HookDecision.ALLOW
449
+ elif behavior == "deny":
450
+ output.decision = HookDecision.DENY
451
+ if decision_obj.get("message"):
452
+ output.reason = decision_obj.get("message")
453
+ output.hook_specific_output = PermissionRequestHookOutput(
454
+ decision=decision_data,
455
+ )
456
+
457
+ # Handle PostToolUse specific fields
458
+ elif event_name == "PostToolUse":
459
+ output.hook_specific_output = PostToolUseHookOutput(
460
+ additionalContext=hso.get("additionalContext"),
461
+ )
462
+ if hso.get("additionalContext"):
463
+ output.additional_context = hso["additionalContext"]
464
+
465
+ # Handle UserPromptSubmit specific fields
466
+ elif event_name == "UserPromptSubmit":
467
+ output.hook_specific_output = UserPromptSubmitHookOutput(
468
+ additionalContext=hso.get("additionalContext"),
469
+ )
470
+ if hso.get("additionalContext"):
471
+ output.additional_context = hso["additionalContext"]
472
+
473
+ # Handle SessionStart specific fields
474
+ elif event_name == "SessionStart":
475
+ output.hook_specific_output = SessionStartHookOutput(
476
+ additionalContext=hso.get("additionalContext"),
477
+ )
478
+ if hso.get("additionalContext"):
479
+ output.additional_context = hso["additionalContext"]
480
+
481
+ # Fallback for unknown types
482
+ else:
483
+ output.hook_specific_output = hso
484
+ if hso.get("additionalContext"):
485
+ output.additional_context = hso["additionalContext"]
486
+
487
+ # Direct additional context
488
+ if "additionalContext" in data:
489
+ output.additional_context = data["additionalContext"]
490
+
491
+ return output
492
+
493
+ @property
494
+ def should_block(self) -> bool:
495
+ """Check if hook requests blocking."""
496
+ return self.decision in (HookDecision.DENY, HookDecision.BLOCK)
497
+
498
+ @property
499
+ def should_allow(self) -> bool:
500
+ """Check if hook requests allowing."""
501
+ return self.decision in (HookDecision.ALLOW, HookDecision.APPROVE)
502
+
503
+ @property
504
+ def should_ask(self) -> bool:
505
+ """Check if hook requests user confirmation."""
506
+ return self.decision == HookDecision.ASK
507
+
508
+ @property
509
+ def should_continue(self) -> bool:
510
+ """Check if execution should continue."""
511
+ return self.continue_execution
512
+
513
+ @property
514
+ def updated_input(self) -> Optional[Dict[str, Any]]:
515
+ """Get updated input from PreToolUse hook."""
516
+ if isinstance(self.hook_specific_output, PreToolUseHookOutput):
517
+ return self.hook_specific_output.updated_input
518
+ if isinstance(self.hook_specific_output, dict):
519
+ return self.hook_specific_output.get("updatedInput")
520
+ return None
521
+
522
+
523
+ # Type alias for any hook input
524
+ AnyHookInput = Union[
525
+ PreToolUseInput,
526
+ PermissionRequestInput,
527
+ PostToolUseInput,
528
+ UserPromptSubmitInput,
529
+ NotificationInput,
530
+ StopInput,
531
+ SubagentStopInput,
532
+ PreCompactInput,
533
+ SessionStartInput,
534
+ SessionEndInput,
535
+ ]