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.
@@ -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