ripperdoc 0.2.7__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/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 +1 -1
- ripperdoc/cli/ui/file_mention_completer.py +62 -7
- ripperdoc/cli/ui/interrupt_handler.py +1 -1
- ripperdoc/cli/ui/message_display.py +1 -1
- ripperdoc/cli/ui/panels.py +13 -10
- ripperdoc/cli/ui/rich_ui.py +92 -24
- 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/tools/bash_tool.py +4 -4
- ripperdoc/tools/file_read_tool.py +1 -1
- ripperdoc/utils/conversation_compaction.py +3 -3
- ripperdoc/utils/path_ignore.py +3 -4
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.8.dist-info}/METADATA +24 -3
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.8.dist-info}/RECORD +31 -23
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.8.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.8.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
"""Hook manager for coordinating hook execution.
|
|
2
|
+
|
|
3
|
+
This module provides the main interface for triggering hooks
|
|
4
|
+
throughout the application lifecycle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ripperdoc.core.hooks.config import (
|
|
12
|
+
HooksConfig,
|
|
13
|
+
HookDefinition,
|
|
14
|
+
get_merged_hooks_config,
|
|
15
|
+
)
|
|
16
|
+
from ripperdoc.core.hooks.events import (
|
|
17
|
+
HookEvent,
|
|
18
|
+
HookDecision,
|
|
19
|
+
HookOutput,
|
|
20
|
+
PreToolUseInput,
|
|
21
|
+
PermissionRequestInput,
|
|
22
|
+
PostToolUseInput,
|
|
23
|
+
UserPromptSubmitInput,
|
|
24
|
+
NotificationInput,
|
|
25
|
+
StopInput,
|
|
26
|
+
SubagentStopInput,
|
|
27
|
+
PreCompactInput,
|
|
28
|
+
SessionStartInput,
|
|
29
|
+
SessionEndInput,
|
|
30
|
+
)
|
|
31
|
+
from ripperdoc.core.hooks.executor import HookExecutor, LLMCallback
|
|
32
|
+
from ripperdoc.utils.log import get_logger
|
|
33
|
+
|
|
34
|
+
logger = get_logger()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HookResult:
|
|
38
|
+
"""Result of running hooks for an event.
|
|
39
|
+
|
|
40
|
+
Aggregates results from all hooks and provides convenience methods
|
|
41
|
+
for checking the overall decision.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, outputs: List[HookOutput]):
|
|
45
|
+
self.outputs = outputs
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def should_block(self) -> bool:
|
|
49
|
+
"""Check if any hook returned a blocking decision."""
|
|
50
|
+
return any(
|
|
51
|
+
o.decision in (HookDecision.DENY, HookDecision.BLOCK)
|
|
52
|
+
for o in self.outputs
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def should_allow(self) -> bool:
|
|
57
|
+
"""Check if any hook returned an allow decision."""
|
|
58
|
+
return any(o.decision == HookDecision.ALLOW for o in self.outputs)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def should_ask(self) -> bool:
|
|
62
|
+
"""Check if any hook returned an ask decision."""
|
|
63
|
+
return any(o.decision == HookDecision.ASK for o in self.outputs)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def should_continue(self) -> bool:
|
|
67
|
+
"""Check if execution should continue (no hook set continue=false)."""
|
|
68
|
+
return all(o.should_continue for o in self.outputs)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def block_reason(self) -> Optional[str]:
|
|
72
|
+
"""Get the reason for blocking, if any."""
|
|
73
|
+
for o in self.outputs:
|
|
74
|
+
if o.decision in (HookDecision.DENY, HookDecision.BLOCK) and o.reason:
|
|
75
|
+
return o.reason
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def stop_reason(self) -> Optional[str]:
|
|
80
|
+
"""Get the stop reason from hooks, if any."""
|
|
81
|
+
for o in self.outputs:
|
|
82
|
+
if o.stop_reason:
|
|
83
|
+
return o.stop_reason
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def additional_context(self) -> Optional[str]:
|
|
88
|
+
"""Get combined additional context from all hooks."""
|
|
89
|
+
contexts = []
|
|
90
|
+
for o in self.outputs:
|
|
91
|
+
if o.additional_context:
|
|
92
|
+
contexts.append(o.additional_context)
|
|
93
|
+
return "\n".join(contexts) if contexts else None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def system_message(self) -> Optional[str]:
|
|
97
|
+
"""Get system message from hooks, if any."""
|
|
98
|
+
for o in self.outputs:
|
|
99
|
+
if o.system_message:
|
|
100
|
+
return o.system_message
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def updated_input(self) -> Optional[Dict[str, Any]]:
|
|
105
|
+
"""Get updated tool input from PreToolUse hooks, if any."""
|
|
106
|
+
for o in self.outputs:
|
|
107
|
+
if o.updated_input:
|
|
108
|
+
return o.updated_input
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def has_errors(self) -> bool:
|
|
113
|
+
"""Check if any hook had an error."""
|
|
114
|
+
return any(o.error for o in self.outputs)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def errors(self) -> List[str]:
|
|
118
|
+
"""Get all error messages."""
|
|
119
|
+
return [o.error for o in self.outputs if o.error]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class HookManager:
|
|
123
|
+
"""Manages hook configuration and execution.
|
|
124
|
+
|
|
125
|
+
This is the main interface for triggering hooks in the application.
|
|
126
|
+
It loads configuration, finds matching hooks, and executes them.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
project_dir: Optional[Path] = None,
|
|
132
|
+
session_id: Optional[str] = None,
|
|
133
|
+
transcript_path: Optional[str] = None,
|
|
134
|
+
permission_mode: str = "default",
|
|
135
|
+
llm_callback: Optional[LLMCallback] = None,
|
|
136
|
+
):
|
|
137
|
+
"""Initialize the hook manager.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
project_dir: The project directory
|
|
141
|
+
session_id: Current session ID for hook input
|
|
142
|
+
transcript_path: Path to the conversation transcript JSON
|
|
143
|
+
permission_mode: Current permission mode (default, plan, acceptEdits, bypassPermissions)
|
|
144
|
+
llm_callback: Async callback for prompt-based hooks. Takes prompt string,
|
|
145
|
+
returns LLM response string. If not set, prompt hooks will
|
|
146
|
+
be skipped with a warning.
|
|
147
|
+
"""
|
|
148
|
+
self.project_dir = project_dir
|
|
149
|
+
self.session_id = session_id
|
|
150
|
+
self.transcript_path = transcript_path
|
|
151
|
+
self.permission_mode = permission_mode
|
|
152
|
+
self.llm_callback = llm_callback
|
|
153
|
+
self._config: Optional[HooksConfig] = None
|
|
154
|
+
self._executor: Optional[HookExecutor] = None
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def config(self) -> HooksConfig:
|
|
158
|
+
"""Get the hooks configuration (lazy loaded)."""
|
|
159
|
+
if self._config is None:
|
|
160
|
+
self._config = get_merged_hooks_config(self.project_dir)
|
|
161
|
+
return self._config
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def executor(self) -> HookExecutor:
|
|
165
|
+
"""Get the hook executor (lazy created)."""
|
|
166
|
+
if self._executor is None:
|
|
167
|
+
self._executor = HookExecutor(
|
|
168
|
+
project_dir=self.project_dir,
|
|
169
|
+
session_id=self.session_id,
|
|
170
|
+
transcript_path=self.transcript_path,
|
|
171
|
+
llm_callback=self.llm_callback,
|
|
172
|
+
)
|
|
173
|
+
return self._executor
|
|
174
|
+
|
|
175
|
+
def reload_config(self) -> None:
|
|
176
|
+
"""Reload hooks configuration from files."""
|
|
177
|
+
self._config = None
|
|
178
|
+
logger.debug("Hooks configuration will be reloaded on next access")
|
|
179
|
+
|
|
180
|
+
def set_project_dir(self, project_dir: Optional[Path]) -> None:
|
|
181
|
+
"""Update the project directory and reload config."""
|
|
182
|
+
self.project_dir = project_dir
|
|
183
|
+
self._config = None
|
|
184
|
+
self._executor = None
|
|
185
|
+
|
|
186
|
+
def set_session_id(self, session_id: Optional[str]) -> None:
|
|
187
|
+
"""Update the session ID."""
|
|
188
|
+
self.session_id = session_id
|
|
189
|
+
if self._executor:
|
|
190
|
+
self._executor.session_id = session_id
|
|
191
|
+
|
|
192
|
+
def set_transcript_path(self, transcript_path: Optional[str]) -> None:
|
|
193
|
+
"""Update the transcript path."""
|
|
194
|
+
self.transcript_path = transcript_path
|
|
195
|
+
if self._executor:
|
|
196
|
+
self._executor.transcript_path = transcript_path
|
|
197
|
+
|
|
198
|
+
def set_permission_mode(self, mode: str) -> None:
|
|
199
|
+
"""Update the permission mode."""
|
|
200
|
+
self.permission_mode = mode
|
|
201
|
+
|
|
202
|
+
def set_llm_callback(self, callback: Optional[LLMCallback]) -> None:
|
|
203
|
+
"""Update the LLM callback for prompt hooks."""
|
|
204
|
+
self.llm_callback = callback
|
|
205
|
+
if self._executor:
|
|
206
|
+
self._executor.llm_callback = callback
|
|
207
|
+
|
|
208
|
+
def _get_cwd(self) -> Optional[str]:
|
|
209
|
+
"""Get current working directory."""
|
|
210
|
+
try:
|
|
211
|
+
return os.getcwd()
|
|
212
|
+
except OSError:
|
|
213
|
+
return str(self.project_dir) if self.project_dir else None
|
|
214
|
+
|
|
215
|
+
def _get_hooks(self, event: HookEvent, tool_name: Optional[str] = None) -> List[HookDefinition]:
|
|
216
|
+
"""Get hooks that should run for an event."""
|
|
217
|
+
return self.config.get_hooks_for_event(event, tool_name)
|
|
218
|
+
|
|
219
|
+
def cleanup(self) -> None:
|
|
220
|
+
"""Clean up resources (call on session end)."""
|
|
221
|
+
if self._executor:
|
|
222
|
+
self._executor.cleanup_env_file()
|
|
223
|
+
|
|
224
|
+
# --- Pre Tool Use ---
|
|
225
|
+
|
|
226
|
+
def run_pre_tool_use(
|
|
227
|
+
self,
|
|
228
|
+
tool_name: str,
|
|
229
|
+
tool_input: Dict[str, Any],
|
|
230
|
+
tool_use_id: Optional[str] = None,
|
|
231
|
+
) -> HookResult:
|
|
232
|
+
"""Run PreToolUse hooks synchronously.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
tool_name: Name of the tool being called
|
|
236
|
+
tool_input: Input parameters for the tool
|
|
237
|
+
tool_use_id: Unique ID for this tool use
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
HookResult with decision information
|
|
241
|
+
"""
|
|
242
|
+
hooks = self._get_hooks(HookEvent.PRE_TOOL_USE, tool_name)
|
|
243
|
+
if not hooks:
|
|
244
|
+
return HookResult([])
|
|
245
|
+
|
|
246
|
+
input_data = PreToolUseInput(
|
|
247
|
+
tool_name=tool_name,
|
|
248
|
+
tool_input=tool_input,
|
|
249
|
+
tool_use_id=tool_use_id,
|
|
250
|
+
session_id=self.session_id,
|
|
251
|
+
transcript_path=self.transcript_path,
|
|
252
|
+
cwd=self._get_cwd(),
|
|
253
|
+
permission_mode=self.permission_mode,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
257
|
+
return HookResult(outputs)
|
|
258
|
+
|
|
259
|
+
async def run_pre_tool_use_async(
|
|
260
|
+
self,
|
|
261
|
+
tool_name: str,
|
|
262
|
+
tool_input: Dict[str, Any],
|
|
263
|
+
tool_use_id: Optional[str] = None,
|
|
264
|
+
) -> HookResult:
|
|
265
|
+
"""Run PreToolUse hooks asynchronously."""
|
|
266
|
+
hooks = self._get_hooks(HookEvent.PRE_TOOL_USE, tool_name)
|
|
267
|
+
if not hooks:
|
|
268
|
+
return HookResult([])
|
|
269
|
+
|
|
270
|
+
input_data = PreToolUseInput(
|
|
271
|
+
tool_name=tool_name,
|
|
272
|
+
tool_input=tool_input,
|
|
273
|
+
tool_use_id=tool_use_id,
|
|
274
|
+
session_id=self.session_id,
|
|
275
|
+
transcript_path=self.transcript_path,
|
|
276
|
+
cwd=self._get_cwd(),
|
|
277
|
+
permission_mode=self.permission_mode,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
281
|
+
return HookResult(outputs)
|
|
282
|
+
|
|
283
|
+
# --- Permission Request ---
|
|
284
|
+
|
|
285
|
+
def run_permission_request(
|
|
286
|
+
self,
|
|
287
|
+
tool_name: str,
|
|
288
|
+
tool_input: Dict[str, Any],
|
|
289
|
+
tool_use_id: Optional[str] = None,
|
|
290
|
+
) -> HookResult:
|
|
291
|
+
"""Run PermissionRequest hooks synchronously.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
tool_name: Name of the tool requesting permission
|
|
295
|
+
tool_input: Input parameters for the tool
|
|
296
|
+
tool_use_id: Unique ID for this tool use
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
HookResult with decision information
|
|
300
|
+
"""
|
|
301
|
+
hooks = self._get_hooks(HookEvent.PERMISSION_REQUEST, tool_name)
|
|
302
|
+
if not hooks:
|
|
303
|
+
return HookResult([])
|
|
304
|
+
|
|
305
|
+
input_data = PermissionRequestInput(
|
|
306
|
+
tool_name=tool_name,
|
|
307
|
+
tool_input=tool_input,
|
|
308
|
+
tool_use_id=tool_use_id,
|
|
309
|
+
session_id=self.session_id,
|
|
310
|
+
transcript_path=self.transcript_path,
|
|
311
|
+
cwd=self._get_cwd(),
|
|
312
|
+
permission_mode=self.permission_mode,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
316
|
+
return HookResult(outputs)
|
|
317
|
+
|
|
318
|
+
async def run_permission_request_async(
|
|
319
|
+
self,
|
|
320
|
+
tool_name: str,
|
|
321
|
+
tool_input: Dict[str, Any],
|
|
322
|
+
tool_use_id: Optional[str] = None,
|
|
323
|
+
) -> HookResult:
|
|
324
|
+
"""Run PermissionRequest hooks asynchronously."""
|
|
325
|
+
hooks = self._get_hooks(HookEvent.PERMISSION_REQUEST, tool_name)
|
|
326
|
+
if not hooks:
|
|
327
|
+
return HookResult([])
|
|
328
|
+
|
|
329
|
+
input_data = PermissionRequestInput(
|
|
330
|
+
tool_name=tool_name,
|
|
331
|
+
tool_input=tool_input,
|
|
332
|
+
tool_use_id=tool_use_id,
|
|
333
|
+
session_id=self.session_id,
|
|
334
|
+
transcript_path=self.transcript_path,
|
|
335
|
+
cwd=self._get_cwd(),
|
|
336
|
+
permission_mode=self.permission_mode,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
340
|
+
return HookResult(outputs)
|
|
341
|
+
|
|
342
|
+
# --- Post Tool Use ---
|
|
343
|
+
|
|
344
|
+
def run_post_tool_use(
|
|
345
|
+
self,
|
|
346
|
+
tool_name: str,
|
|
347
|
+
tool_input: Dict[str, Any],
|
|
348
|
+
tool_response: Any = None,
|
|
349
|
+
tool_use_id: Optional[str] = None,
|
|
350
|
+
) -> HookResult:
|
|
351
|
+
"""Run PostToolUse hooks synchronously."""
|
|
352
|
+
hooks = self._get_hooks(HookEvent.POST_TOOL_USE, tool_name)
|
|
353
|
+
if not hooks:
|
|
354
|
+
return HookResult([])
|
|
355
|
+
|
|
356
|
+
input_data = PostToolUseInput(
|
|
357
|
+
tool_name=tool_name,
|
|
358
|
+
tool_input=tool_input,
|
|
359
|
+
tool_response=tool_response,
|
|
360
|
+
tool_use_id=tool_use_id,
|
|
361
|
+
session_id=self.session_id,
|
|
362
|
+
transcript_path=self.transcript_path,
|
|
363
|
+
cwd=self._get_cwd(),
|
|
364
|
+
permission_mode=self.permission_mode,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
368
|
+
return HookResult(outputs)
|
|
369
|
+
|
|
370
|
+
async def run_post_tool_use_async(
|
|
371
|
+
self,
|
|
372
|
+
tool_name: str,
|
|
373
|
+
tool_input: Dict[str, Any],
|
|
374
|
+
tool_response: Any = None,
|
|
375
|
+
tool_use_id: Optional[str] = None,
|
|
376
|
+
) -> HookResult:
|
|
377
|
+
"""Run PostToolUse hooks asynchronously."""
|
|
378
|
+
hooks = self._get_hooks(HookEvent.POST_TOOL_USE, tool_name)
|
|
379
|
+
if not hooks:
|
|
380
|
+
return HookResult([])
|
|
381
|
+
|
|
382
|
+
input_data = PostToolUseInput(
|
|
383
|
+
tool_name=tool_name,
|
|
384
|
+
tool_input=tool_input,
|
|
385
|
+
tool_response=tool_response,
|
|
386
|
+
tool_use_id=tool_use_id,
|
|
387
|
+
session_id=self.session_id,
|
|
388
|
+
transcript_path=self.transcript_path,
|
|
389
|
+
cwd=self._get_cwd(),
|
|
390
|
+
permission_mode=self.permission_mode,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
394
|
+
return HookResult(outputs)
|
|
395
|
+
|
|
396
|
+
# --- User Prompt Submit ---
|
|
397
|
+
|
|
398
|
+
def run_user_prompt_submit(self, prompt: str) -> HookResult:
|
|
399
|
+
"""Run UserPromptSubmit hooks synchronously."""
|
|
400
|
+
hooks = self._get_hooks(HookEvent.USER_PROMPT_SUBMIT)
|
|
401
|
+
if not hooks:
|
|
402
|
+
return HookResult([])
|
|
403
|
+
|
|
404
|
+
input_data = UserPromptSubmitInput(
|
|
405
|
+
prompt=prompt,
|
|
406
|
+
session_id=self.session_id,
|
|
407
|
+
transcript_path=self.transcript_path,
|
|
408
|
+
cwd=self._get_cwd(),
|
|
409
|
+
permission_mode=self.permission_mode,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
413
|
+
return HookResult(outputs)
|
|
414
|
+
|
|
415
|
+
async def run_user_prompt_submit_async(self, prompt: str) -> HookResult:
|
|
416
|
+
"""Run UserPromptSubmit hooks asynchronously."""
|
|
417
|
+
hooks = self._get_hooks(HookEvent.USER_PROMPT_SUBMIT)
|
|
418
|
+
if not hooks:
|
|
419
|
+
return HookResult([])
|
|
420
|
+
|
|
421
|
+
input_data = UserPromptSubmitInput(
|
|
422
|
+
prompt=prompt,
|
|
423
|
+
session_id=self.session_id,
|
|
424
|
+
transcript_path=self.transcript_path,
|
|
425
|
+
cwd=self._get_cwd(),
|
|
426
|
+
permission_mode=self.permission_mode,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
430
|
+
return HookResult(outputs)
|
|
431
|
+
|
|
432
|
+
# --- Notification ---
|
|
433
|
+
|
|
434
|
+
def run_notification(
|
|
435
|
+
self, message: str, notification_type: str = "info"
|
|
436
|
+
) -> HookResult:
|
|
437
|
+
"""Run Notification hooks synchronously.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
message: The notification message
|
|
441
|
+
notification_type: Type of notification (permission_prompt, idle_prompt, auth_success, elicitation_dialog)
|
|
442
|
+
"""
|
|
443
|
+
hooks = self._get_hooks(HookEvent.NOTIFICATION)
|
|
444
|
+
if not hooks:
|
|
445
|
+
return HookResult([])
|
|
446
|
+
|
|
447
|
+
input_data = NotificationInput(
|
|
448
|
+
message=message,
|
|
449
|
+
notification_type=notification_type,
|
|
450
|
+
session_id=self.session_id,
|
|
451
|
+
transcript_path=self.transcript_path,
|
|
452
|
+
cwd=self._get_cwd(),
|
|
453
|
+
permission_mode=self.permission_mode,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
457
|
+
return HookResult(outputs)
|
|
458
|
+
|
|
459
|
+
async def run_notification_async(
|
|
460
|
+
self, message: str, notification_type: str = "info"
|
|
461
|
+
) -> HookResult:
|
|
462
|
+
"""Run Notification hooks asynchronously."""
|
|
463
|
+
hooks = self._get_hooks(HookEvent.NOTIFICATION)
|
|
464
|
+
if not hooks:
|
|
465
|
+
return HookResult([])
|
|
466
|
+
|
|
467
|
+
input_data = NotificationInput(
|
|
468
|
+
message=message,
|
|
469
|
+
notification_type=notification_type,
|
|
470
|
+
session_id=self.session_id,
|
|
471
|
+
transcript_path=self.transcript_path,
|
|
472
|
+
cwd=self._get_cwd(),
|
|
473
|
+
permission_mode=self.permission_mode,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
477
|
+
return HookResult(outputs)
|
|
478
|
+
|
|
479
|
+
# --- Stop ---
|
|
480
|
+
|
|
481
|
+
def run_stop(self, stop_hook_active: bool = False) -> HookResult:
|
|
482
|
+
"""Run Stop hooks synchronously.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
stop_hook_active: True if already continuing from a stop hook
|
|
486
|
+
"""
|
|
487
|
+
hooks = self._get_hooks(HookEvent.STOP)
|
|
488
|
+
if not hooks:
|
|
489
|
+
return HookResult([])
|
|
490
|
+
|
|
491
|
+
input_data = StopInput(
|
|
492
|
+
stop_hook_active=stop_hook_active,
|
|
493
|
+
session_id=self.session_id,
|
|
494
|
+
transcript_path=self.transcript_path,
|
|
495
|
+
cwd=self._get_cwd(),
|
|
496
|
+
permission_mode=self.permission_mode,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
500
|
+
return HookResult(outputs)
|
|
501
|
+
|
|
502
|
+
async def run_stop_async(self, stop_hook_active: bool = False) -> HookResult:
|
|
503
|
+
"""Run Stop hooks asynchronously."""
|
|
504
|
+
hooks = self._get_hooks(HookEvent.STOP)
|
|
505
|
+
if not hooks:
|
|
506
|
+
return HookResult([])
|
|
507
|
+
|
|
508
|
+
input_data = StopInput(
|
|
509
|
+
stop_hook_active=stop_hook_active,
|
|
510
|
+
session_id=self.session_id,
|
|
511
|
+
transcript_path=self.transcript_path,
|
|
512
|
+
cwd=self._get_cwd(),
|
|
513
|
+
permission_mode=self.permission_mode,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
517
|
+
return HookResult(outputs)
|
|
518
|
+
|
|
519
|
+
# --- Subagent Stop ---
|
|
520
|
+
|
|
521
|
+
def run_subagent_stop(self, stop_hook_active: bool = False) -> HookResult:
|
|
522
|
+
"""Run SubagentStop hooks synchronously.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
stop_hook_active: True if already continuing from a stop hook
|
|
526
|
+
"""
|
|
527
|
+
hooks = self._get_hooks(HookEvent.SUBAGENT_STOP)
|
|
528
|
+
if not hooks:
|
|
529
|
+
return HookResult([])
|
|
530
|
+
|
|
531
|
+
input_data = SubagentStopInput(
|
|
532
|
+
stop_hook_active=stop_hook_active,
|
|
533
|
+
session_id=self.session_id,
|
|
534
|
+
transcript_path=self.transcript_path,
|
|
535
|
+
cwd=self._get_cwd(),
|
|
536
|
+
permission_mode=self.permission_mode,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
540
|
+
return HookResult(outputs)
|
|
541
|
+
|
|
542
|
+
async def run_subagent_stop_async(self, stop_hook_active: bool = False) -> HookResult:
|
|
543
|
+
"""Run SubagentStop hooks asynchronously."""
|
|
544
|
+
hooks = self._get_hooks(HookEvent.SUBAGENT_STOP)
|
|
545
|
+
if not hooks:
|
|
546
|
+
return HookResult([])
|
|
547
|
+
|
|
548
|
+
input_data = SubagentStopInput(
|
|
549
|
+
stop_hook_active=stop_hook_active,
|
|
550
|
+
session_id=self.session_id,
|
|
551
|
+
transcript_path=self.transcript_path,
|
|
552
|
+
cwd=self._get_cwd(),
|
|
553
|
+
permission_mode=self.permission_mode,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
557
|
+
return HookResult(outputs)
|
|
558
|
+
|
|
559
|
+
# --- Pre Compact ---
|
|
560
|
+
|
|
561
|
+
def run_pre_compact(
|
|
562
|
+
self, trigger: str, custom_instructions: str = ""
|
|
563
|
+
) -> HookResult:
|
|
564
|
+
"""Run PreCompact hooks synchronously.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
trigger: "manual" or "auto"
|
|
568
|
+
custom_instructions: Custom instructions passed to /compact
|
|
569
|
+
"""
|
|
570
|
+
hooks = self._get_hooks(HookEvent.PRE_COMPACT)
|
|
571
|
+
if not hooks:
|
|
572
|
+
return HookResult([])
|
|
573
|
+
|
|
574
|
+
input_data = PreCompactInput(
|
|
575
|
+
trigger=trigger,
|
|
576
|
+
custom_instructions=custom_instructions,
|
|
577
|
+
session_id=self.session_id,
|
|
578
|
+
transcript_path=self.transcript_path,
|
|
579
|
+
cwd=self._get_cwd(),
|
|
580
|
+
permission_mode=self.permission_mode,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
584
|
+
return HookResult(outputs)
|
|
585
|
+
|
|
586
|
+
async def run_pre_compact_async(
|
|
587
|
+
self, trigger: str, custom_instructions: str = ""
|
|
588
|
+
) -> HookResult:
|
|
589
|
+
"""Run PreCompact hooks asynchronously."""
|
|
590
|
+
hooks = self._get_hooks(HookEvent.PRE_COMPACT)
|
|
591
|
+
if not hooks:
|
|
592
|
+
return HookResult([])
|
|
593
|
+
|
|
594
|
+
input_data = PreCompactInput(
|
|
595
|
+
trigger=trigger,
|
|
596
|
+
custom_instructions=custom_instructions,
|
|
597
|
+
session_id=self.session_id,
|
|
598
|
+
transcript_path=self.transcript_path,
|
|
599
|
+
cwd=self._get_cwd(),
|
|
600
|
+
permission_mode=self.permission_mode,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
604
|
+
return HookResult(outputs)
|
|
605
|
+
|
|
606
|
+
# --- Session Start ---
|
|
607
|
+
|
|
608
|
+
def run_session_start(self, source: str) -> HookResult:
|
|
609
|
+
"""Run SessionStart hooks synchronously.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
source: "startup", "resume", "clear", or "compact"
|
|
613
|
+
"""
|
|
614
|
+
hooks = self._get_hooks(HookEvent.SESSION_START)
|
|
615
|
+
if not hooks:
|
|
616
|
+
return HookResult([])
|
|
617
|
+
|
|
618
|
+
input_data = SessionStartInput(
|
|
619
|
+
source=source,
|
|
620
|
+
session_id=self.session_id,
|
|
621
|
+
transcript_path=self.transcript_path,
|
|
622
|
+
cwd=self._get_cwd(),
|
|
623
|
+
permission_mode=self.permission_mode,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
627
|
+
return HookResult(outputs)
|
|
628
|
+
|
|
629
|
+
async def run_session_start_async(self, source: str) -> HookResult:
|
|
630
|
+
"""Run SessionStart hooks asynchronously."""
|
|
631
|
+
hooks = self._get_hooks(HookEvent.SESSION_START)
|
|
632
|
+
if not hooks:
|
|
633
|
+
return HookResult([])
|
|
634
|
+
|
|
635
|
+
input_data = SessionStartInput(
|
|
636
|
+
source=source,
|
|
637
|
+
session_id=self.session_id,
|
|
638
|
+
transcript_path=self.transcript_path,
|
|
639
|
+
cwd=self._get_cwd(),
|
|
640
|
+
permission_mode=self.permission_mode,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
644
|
+
return HookResult(outputs)
|
|
645
|
+
|
|
646
|
+
# --- Session End ---
|
|
647
|
+
|
|
648
|
+
def run_session_end(self, reason: str) -> HookResult:
|
|
649
|
+
"""Run SessionEnd hooks synchronously.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
reason: "clear", "logout", "prompt_input_exit", or "other"
|
|
653
|
+
"""
|
|
654
|
+
hooks = self._get_hooks(HookEvent.SESSION_END)
|
|
655
|
+
if not hooks:
|
|
656
|
+
return HookResult([])
|
|
657
|
+
|
|
658
|
+
input_data = SessionEndInput(
|
|
659
|
+
reason=reason,
|
|
660
|
+
session_id=self.session_id,
|
|
661
|
+
transcript_path=self.transcript_path,
|
|
662
|
+
cwd=self._get_cwd(),
|
|
663
|
+
permission_mode=self.permission_mode,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
667
|
+
return HookResult(outputs)
|
|
668
|
+
|
|
669
|
+
async def run_session_end_async(self, reason: str) -> HookResult:
|
|
670
|
+
"""Run SessionEnd hooks asynchronously."""
|
|
671
|
+
hooks = self._get_hooks(HookEvent.SESSION_END)
|
|
672
|
+
if not hooks:
|
|
673
|
+
return HookResult([])
|
|
674
|
+
|
|
675
|
+
input_data = SessionEndInput(
|
|
676
|
+
reason=reason,
|
|
677
|
+
session_id=self.session_id,
|
|
678
|
+
transcript_path=self.transcript_path,
|
|
679
|
+
cwd=self._get_cwd(),
|
|
680
|
+
permission_mode=self.permission_mode,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
outputs = await self.executor.execute_hooks_async(hooks, input_data)
|
|
684
|
+
return HookResult(outputs)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# Global instance for convenience
|
|
688
|
+
hook_manager = HookManager()
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def get_hook_manager() -> HookManager:
|
|
692
|
+
"""Get the global hook manager instance."""
|
|
693
|
+
return hook_manager
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def init_hook_manager(
|
|
697
|
+
project_dir: Optional[Path] = None,
|
|
698
|
+
session_id: Optional[str] = None,
|
|
699
|
+
transcript_path: Optional[str] = None,
|
|
700
|
+
permission_mode: str = "default",
|
|
701
|
+
llm_callback: Optional[LLMCallback] = None,
|
|
702
|
+
) -> HookManager:
|
|
703
|
+
"""Initialize the global hook manager with project context.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
project_dir: The project directory
|
|
707
|
+
session_id: Current session ID
|
|
708
|
+
transcript_path: Path to the conversation transcript JSON
|
|
709
|
+
permission_mode: Current permission mode
|
|
710
|
+
llm_callback: Async callback for prompt-based hooks
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
The initialized global hook manager
|
|
714
|
+
"""
|
|
715
|
+
hook_manager.set_project_dir(project_dir)
|
|
716
|
+
hook_manager.set_session_id(session_id)
|
|
717
|
+
hook_manager.set_transcript_path(transcript_path)
|
|
718
|
+
hook_manager.set_permission_mode(permission_mode)
|
|
719
|
+
hook_manager.set_llm_callback(llm_callback)
|
|
720
|
+
return hook_manager
|