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,412 @@
1
+ """Custom Command loading and execution for Ripperdoc.
2
+
3
+ Custom commands are defined in .md files under:
4
+ - `~/.ripperdoc/commands/` (user-level commands)
5
+ - `.ripperdoc/commands/` (project-level commands)
6
+
7
+ Features:
8
+ - Frontmatter support (YAML) for metadata: allowed-tools, description, argument-hint, model, thinking-mode
9
+ - Parameter substitution: $ARGUMENTS, $1, $2, etc.
10
+ - File references: @filename (resolved relative to project root)
11
+ - Bash command execution: !`command` syntax
12
+ - Nested commands via subdirectories (e.g., git/commit.md -> /git:commit)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import subprocess
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ from pathlib import Path
22
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
23
+
24
+ import yaml
25
+
26
+ from ripperdoc.utils.log import get_logger
27
+
28
+ logger = get_logger()
29
+
30
+ COMMAND_DIR_NAME = "commands"
31
+ COMMAND_FILE_SUFFIX = ".md"
32
+ _COMMAND_NAME_RE = re.compile(r"^[a-z0-9_-]{1,64}$")
33
+
34
+ # Pattern for bash command execution: !`command`
35
+ _BASH_COMMAND_PATTERN = re.compile(r"!\`([^`]+)\`")
36
+
37
+ # Pattern for file references: @filename
38
+ _FILE_REFERENCE_PATTERN = re.compile(r"@([^\s\]]+)")
39
+
40
+ # Pattern for positional arguments: $1, $2, etc.
41
+ _POSITIONAL_ARG_PATTERN = re.compile(r"\$(\d+)")
42
+
43
+ # Pattern for all arguments: $ARGUMENTS
44
+ _ALL_ARGS_PATTERN = re.compile(r"\$ARGUMENTS", re.IGNORECASE)
45
+
46
+ # Thinking mode keywords
47
+ THINKING_MODES = ["think", "think hard", "think harder", "ultrathink"]
48
+
49
+
50
+ class CommandLocation(str, Enum):
51
+ """Where a custom command is sourced from."""
52
+
53
+ USER = "user"
54
+ PROJECT = "project"
55
+ OTHER = "other"
56
+
57
+
58
+ @dataclass
59
+ class CustomCommandDefinition:
60
+ """Parsed representation of a custom command."""
61
+
62
+ name: str
63
+ description: str
64
+ content: str
65
+ path: Path
66
+ base_dir: Path
67
+ location: CommandLocation
68
+ allowed_tools: List[str] = field(default_factory=list)
69
+ argument_hint: Optional[str] = None
70
+ model: Optional[str] = None
71
+ thinking_mode: Optional[str] = None
72
+
73
+
74
+ @dataclass
75
+ class CustomCommandLoadError:
76
+ """Error encountered while loading a custom command file."""
77
+
78
+ path: Path
79
+ reason: str
80
+
81
+
82
+ @dataclass
83
+ class CustomCommandLoadResult:
84
+ """Aggregated result of loading custom commands."""
85
+
86
+ commands: List[CustomCommandDefinition]
87
+ errors: List[CustomCommandLoadError]
88
+
89
+
90
+ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
91
+ """Extract YAML frontmatter and body content from a markdown file."""
92
+ lines = raw_text.splitlines()
93
+ if len(lines) >= 3 and lines[0].strip() == "---":
94
+ for idx in range(1, len(lines)):
95
+ if lines[idx].strip() == "---":
96
+ frontmatter_text = "\n".join(lines[1:idx])
97
+ body = "\n".join(lines[idx + 1 :])
98
+ try:
99
+ frontmatter = yaml.safe_load(frontmatter_text) or {}
100
+ except (yaml.YAMLError, ValueError, TypeError) as exc:
101
+ logger.warning(
102
+ "[custom_commands] Invalid frontmatter: %s: %s",
103
+ type(exc).__name__,
104
+ exc,
105
+ )
106
+ return {"__error__": f"Invalid frontmatter: {exc}"}, body
107
+ return frontmatter, body
108
+ return {}, raw_text
109
+
110
+
111
+ def _normalize_allowed_tools(value: object) -> List[str]:
112
+ """Normalize allowed-tools values to a clean list of tool names."""
113
+ if value is None:
114
+ return []
115
+ if isinstance(value, str):
116
+ return [item.strip() for item in value.split(",") if item.strip()]
117
+ if isinstance(value, Iterable):
118
+ tools: List[str] = []
119
+ for item in value:
120
+ if isinstance(item, str) and item.strip():
121
+ tools.append(item.strip())
122
+ return tools
123
+ return []
124
+
125
+
126
+ def _extract_thinking_mode(content: str) -> Optional[str]:
127
+ """Extract thinking mode from content if present."""
128
+ content_lower = content.lower()
129
+ for mode in reversed(THINKING_MODES): # Check longer modes first
130
+ if mode in content_lower:
131
+ return mode
132
+ return None
133
+
134
+
135
+ def _derive_command_name(path: Path, base_commands_dir: Path) -> str:
136
+ """Derive command name from file path, supporting nested commands.
137
+
138
+ Examples:
139
+ commands/project-info.md -> project-info
140
+ commands/git/commit.md -> git:commit
141
+ commands/git/git-commit.md -> git:git-commit
142
+ """
143
+ relative = path.relative_to(base_commands_dir)
144
+ parts = list(relative.parts)
145
+
146
+ # Remove the .md suffix from the last part
147
+ if parts:
148
+ parts[-1] = parts[-1].removesuffix(COMMAND_FILE_SUFFIX)
149
+
150
+ # Join with : for nested commands
151
+ if len(parts) > 1:
152
+ return ":".join(parts)
153
+ return parts[0] if parts else path.stem
154
+
155
+
156
+ def _load_command_file(
157
+ path: Path, location: CommandLocation, base_commands_dir: Path
158
+ ) -> Tuple[Optional[CustomCommandDefinition], Optional[CustomCommandLoadError]]:
159
+ """Parse a single command .md file."""
160
+ try:
161
+ text = path.read_text(encoding="utf-8")
162
+ except (OSError, IOError, UnicodeDecodeError) as exc:
163
+ logger.warning(
164
+ "[custom_commands] Failed to read command file: %s: %s",
165
+ type(exc).__name__,
166
+ exc,
167
+ extra={"path": str(path)},
168
+ )
169
+ return None, CustomCommandLoadError(path=path, reason=f"Failed to read file: {exc}")
170
+
171
+ frontmatter, body = _split_frontmatter(text)
172
+ if "__error__" in frontmatter:
173
+ return None, CustomCommandLoadError(path=path, reason=str(frontmatter["__error__"]))
174
+
175
+ # Derive command name from path (supports nested commands)
176
+ command_name = _derive_command_name(path, base_commands_dir)
177
+
178
+ # Get description from frontmatter or use first line of content
179
+ raw_description = frontmatter.get("description")
180
+ if not isinstance(raw_description, str) or not raw_description.strip():
181
+ # Use first line of content as description (truncated)
182
+ first_line = body.strip().split("\n")[0] if body.strip() else ""
183
+ raw_description = first_line[:60] + "..." if len(first_line) > 60 else first_line
184
+ if not raw_description:
185
+ raw_description = f"Custom command: {command_name}"
186
+
187
+ allowed_tools = _normalize_allowed_tools(
188
+ frontmatter.get("allowed-tools") or frontmatter.get("allowed_tools")
189
+ )
190
+
191
+ argument_hint = frontmatter.get("argument-hint") or frontmatter.get("argument_hint")
192
+ if argument_hint and not isinstance(argument_hint, str):
193
+ argument_hint = str(argument_hint)
194
+
195
+ model_value = frontmatter.get("model")
196
+ model = model_value.strip() if isinstance(model_value, str) and model_value.strip() else None
197
+
198
+ # Get thinking mode from frontmatter or content
199
+ thinking_mode = frontmatter.get("thinking-mode") or frontmatter.get("thinking_mode")
200
+ if not thinking_mode:
201
+ thinking_mode = _extract_thinking_mode(body)
202
+
203
+ command = CustomCommandDefinition(
204
+ name=command_name,
205
+ description=raw_description.strip(),
206
+ content=body.strip(),
207
+ path=path,
208
+ base_dir=path.parent,
209
+ location=location,
210
+ allowed_tools=allowed_tools,
211
+ argument_hint=argument_hint.strip() if argument_hint else None,
212
+ model=model,
213
+ thinking_mode=thinking_mode.strip() if isinstance(thinking_mode, str) else None,
214
+ )
215
+ return command, None
216
+
217
+
218
+ def _load_commands_from_dir(
219
+ commands_dir: Path, location: CommandLocation
220
+ ) -> Tuple[List[CustomCommandDefinition], List[CustomCommandLoadError]]:
221
+ """Load all custom commands from a directory, including nested subdirectories."""
222
+ commands: List[CustomCommandDefinition] = []
223
+ errors: List[CustomCommandLoadError] = []
224
+
225
+ if not commands_dir.exists() or not commands_dir.is_dir():
226
+ return commands, errors
227
+
228
+ # Recursively find all .md files
229
+ try:
230
+ for md_file in commands_dir.rglob(f"*{COMMAND_FILE_SUFFIX}"):
231
+ if not md_file.is_file():
232
+ continue
233
+
234
+ command, error = _load_command_file(md_file, location, commands_dir)
235
+ if command:
236
+ commands.append(command)
237
+ elif error:
238
+ errors.append(error)
239
+ except OSError as exc:
240
+ logger.warning(
241
+ "[custom_commands] Failed to scan command directory: %s: %s",
242
+ type(exc).__name__,
243
+ exc,
244
+ extra={"path": str(commands_dir)},
245
+ )
246
+
247
+ return commands, errors
248
+
249
+
250
+ def command_directories(
251
+ project_path: Optional[Path] = None, home: Optional[Path] = None
252
+ ) -> List[Tuple[Path, CommandLocation]]:
253
+ """Return the standard command directories for user and project scopes."""
254
+ home_dir = (home or Path.home()).expanduser()
255
+ project_dir = (project_path or Path.cwd()).resolve()
256
+ return [
257
+ (home_dir / ".ripperdoc" / COMMAND_DIR_NAME, CommandLocation.USER),
258
+ (project_dir / ".ripperdoc" / COMMAND_DIR_NAME, CommandLocation.PROJECT),
259
+ ]
260
+
261
+
262
+ def load_all_custom_commands(
263
+ project_path: Optional[Path] = None, home: Optional[Path] = None
264
+ ) -> CustomCommandLoadResult:
265
+ """Load custom commands from user and project directories.
266
+
267
+ Project commands override user commands with the same name.
268
+ """
269
+ commands_by_name: Dict[str, CustomCommandDefinition] = {}
270
+ errors: List[CustomCommandLoadError] = []
271
+
272
+ # Load user commands first so project commands take precedence
273
+ for directory, location in command_directories(project_path=project_path, home=home):
274
+ loaded, dir_errors = _load_commands_from_dir(directory, location)
275
+ errors.extend(dir_errors)
276
+ for cmd in loaded:
277
+ if cmd.name in commands_by_name:
278
+ logger.debug(
279
+ "[custom_commands] Overriding command",
280
+ extra={
281
+ "command_name": cmd.name,
282
+ "previous_location": str(commands_by_name[cmd.name].location),
283
+ "new_location": str(location),
284
+ },
285
+ )
286
+ commands_by_name[cmd.name] = cmd
287
+
288
+ return CustomCommandLoadResult(commands=list(commands_by_name.values()), errors=errors)
289
+
290
+
291
+ def find_custom_command(
292
+ command_name: str, project_path: Optional[Path] = None, home: Optional[Path] = None
293
+ ) -> Optional[CustomCommandDefinition]:
294
+ """Find a custom command by name (case-sensitive match)."""
295
+ normalized = command_name.strip().lstrip("/")
296
+ if not normalized:
297
+ return None
298
+ result = load_all_custom_commands(project_path=project_path, home=home)
299
+ return next((cmd for cmd in result.commands if cmd.name == normalized), None)
300
+
301
+
302
+ def _execute_bash_command(command: str, cwd: Optional[Path] = None) -> str:
303
+ """Execute a bash command and return its output."""
304
+ try:
305
+ result = subprocess.run(
306
+ command,
307
+ shell=True,
308
+ capture_output=True,
309
+ text=True,
310
+ timeout=30,
311
+ cwd=cwd,
312
+ )
313
+ output = result.stdout.strip()
314
+ if result.returncode != 0 and result.stderr:
315
+ output = f"{output}\n{result.stderr}".strip()
316
+ return output if output else "(no output)"
317
+ except subprocess.TimeoutExpired:
318
+ return "(command timed out)"
319
+ except (OSError, subprocess.SubprocessError) as exc:
320
+ return f"(error: {exc})"
321
+
322
+
323
+ def _resolve_file_reference(filename: str, project_path: Path) -> str:
324
+ """Resolve a file reference and return its content."""
325
+ try:
326
+ file_path = project_path / filename
327
+ if file_path.exists() and file_path.is_file():
328
+ content = file_path.read_text(encoding="utf-8")
329
+ return content.strip()
330
+ return f"(file not found: {filename})"
331
+ except (OSError, IOError) as exc:
332
+ return f"(error reading {filename}: {exc})"
333
+
334
+
335
+ def expand_command_content(
336
+ command: CustomCommandDefinition,
337
+ arguments: str,
338
+ project_path: Optional[Path] = None,
339
+ ) -> str:
340
+ """Expand a custom command's content with arguments, bash commands, and file references.
341
+
342
+ Supports:
343
+ - $ARGUMENTS: All arguments as a single string
344
+ - $1, $2, etc.: Positional arguments
345
+ - !`command`: Execute bash command and include output
346
+ - @filename: Include file content
347
+ """
348
+ project_dir = (project_path or Path.cwd()).resolve()
349
+ content = command.content
350
+
351
+ # Split arguments for positional access
352
+ arg_parts = arguments.split() if arguments else []
353
+
354
+ # Replace $ARGUMENTS with all arguments
355
+ content = _ALL_ARGS_PATTERN.sub(arguments, content)
356
+
357
+ # Replace positional arguments $1, $2, etc.
358
+ def replace_positional(match: re.Match) -> str:
359
+ idx = int(match.group(1)) - 1 # $1 -> index 0
360
+ if 0 <= idx < len(arg_parts):
361
+ return arg_parts[idx]
362
+ return ""
363
+
364
+ content = _POSITIONAL_ARG_PATTERN.sub(replace_positional, content)
365
+
366
+ # Execute bash commands: !`command`
367
+ def replace_bash(match: re.Match) -> str:
368
+ bash_cmd = match.group(1)
369
+ return _execute_bash_command(bash_cmd, cwd=project_dir)
370
+
371
+ content = _BASH_COMMAND_PATTERN.sub(replace_bash, content)
372
+
373
+ # Resolve file references: @filename
374
+ def replace_file_ref(match: re.Match) -> str:
375
+ filename = match.group(1)
376
+ return _resolve_file_reference(filename, project_dir)
377
+
378
+ content = _FILE_REFERENCE_PATTERN.sub(replace_file_ref, content)
379
+
380
+ return content.strip()
381
+
382
+
383
+ def build_custom_command_summary(commands: Sequence[CustomCommandDefinition]) -> str:
384
+ """Render a concise instruction block listing available custom commands."""
385
+ if not commands:
386
+ return ""
387
+
388
+ lines = [
389
+ "# Custom Commands",
390
+ "The following custom commands are available:",
391
+ ]
392
+
393
+ for cmd in sorted(commands, key=lambda c: c.name):
394
+ location = f" ({cmd.location.value})" if cmd.location else ""
395
+ hint = f" {cmd.argument_hint}" if cmd.argument_hint else ""
396
+ lines.append(f"- /{cmd.name}{hint}{location}: {cmd.description}")
397
+
398
+ return "\n".join(lines)
399
+
400
+
401
+ __all__ = [
402
+ "CustomCommandDefinition",
403
+ "CustomCommandLoadError",
404
+ "CustomCommandLoadResult",
405
+ "CommandLocation",
406
+ "COMMAND_DIR_NAME",
407
+ "load_all_custom_commands",
408
+ "find_custom_command",
409
+ "expand_command_content",
410
+ "build_custom_command_summary",
411
+ "command_directories",
412
+ ]
@@ -68,11 +68,20 @@ def get_default_tools() -> List[Tool[Any, Any]]:
68
68
  if isinstance(tool, Tool):
69
69
  base_tools.append(tool)
70
70
  dynamic_tools.append(tool)
71
- except (ImportError, ModuleNotFoundError, OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
71
+ except (
72
+ ImportError,
73
+ ModuleNotFoundError,
74
+ OSError,
75
+ RuntimeError,
76
+ ConnectionError,
77
+ ValueError,
78
+ TypeError,
79
+ ) as exc:
72
80
  # If MCP runtime is not available, continue with base tools only.
73
81
  logger.warning(
74
82
  "[default_tools] Failed to load dynamic MCP tools: %s: %s",
75
- type(exc).__name__, exc,
83
+ type(exc).__name__,
84
+ exc,
76
85
  )
77
86
 
78
87
  task_tool = TaskTool(lambda: base_tools)
@@ -0,0 +1,99 @@
1
+ """Hooks system for Ripperdoc.
2
+
3
+ This module provides a hook system similar to Claude Code CLI's hooks,
4
+ allowing users to execute custom scripts at various points in the workflow.
5
+
6
+ Hook events:
7
+ - PreToolUse: Before a tool is called (can block/allow/ask)
8
+ - PermissionRequest: When user is shown permission dialog (can allow/deny)
9
+ - PostToolUse: After a tool completes
10
+ - UserPromptSubmit: When user submits a prompt (can block)
11
+ - Notification: When a notification is sent
12
+ - Stop: When the agent stops responding (can block to continue)
13
+ - SubagentStop: When a subagent task completes (can block to continue)
14
+ - PreCompact: Before conversation compaction
15
+ - SessionStart: When a session starts or resumes
16
+ - SessionEnd: When a session ends
17
+
18
+ Configuration is stored in:
19
+ - ~/.ripperdoc/hooks.json (global)
20
+ - .ripperdoc/hooks.json (project)
21
+ - .ripperdoc/hooks.local.json (local, git-ignored)
22
+ """
23
+
24
+ from ripperdoc.core.hooks.events import (
25
+ HookEvent,
26
+ HookDecision,
27
+ HookInput,
28
+ HookOutput,
29
+ PreToolUseInput,
30
+ PermissionRequestInput,
31
+ PostToolUseInput,
32
+ UserPromptSubmitInput,
33
+ NotificationInput,
34
+ StopInput,
35
+ SubagentStopInput,
36
+ PreCompactInput,
37
+ SessionStartInput,
38
+ SessionEndInput,
39
+ PreToolUseHookOutput,
40
+ PermissionRequestHookOutput,
41
+ PermissionRequestDecision,
42
+ PostToolUseHookOutput,
43
+ UserPromptSubmitHookOutput,
44
+ SessionStartHookOutput,
45
+ )
46
+ from ripperdoc.core.hooks.config import (
47
+ HookDefinition,
48
+ HookMatcher,
49
+ HooksConfig,
50
+ load_hooks_config,
51
+ get_merged_hooks_config,
52
+ get_global_hooks_path,
53
+ get_project_hooks_path,
54
+ get_project_local_hooks_path,
55
+ )
56
+ from ripperdoc.core.hooks.executor import HookExecutor, LLMCallback
57
+ from ripperdoc.core.hooks.manager import HookManager, HookResult, hook_manager, init_hook_manager
58
+
59
+ __all__ = [
60
+ # Events
61
+ "HookEvent",
62
+ "HookDecision",
63
+ "HookInput",
64
+ "HookOutput",
65
+ "PreToolUseInput",
66
+ "PermissionRequestInput",
67
+ "PostToolUseInput",
68
+ "UserPromptSubmitInput",
69
+ "NotificationInput",
70
+ "StopInput",
71
+ "SubagentStopInput",
72
+ "PreCompactInput",
73
+ "SessionStartInput",
74
+ "SessionEndInput",
75
+ # Hook-specific outputs
76
+ "PreToolUseHookOutput",
77
+ "PermissionRequestHookOutput",
78
+ "PermissionRequestDecision",
79
+ "PostToolUseHookOutput",
80
+ "UserPromptSubmitHookOutput",
81
+ "SessionStartHookOutput",
82
+ # Config
83
+ "HookDefinition",
84
+ "HookMatcher",
85
+ "HooksConfig",
86
+ "load_hooks_config",
87
+ "get_merged_hooks_config",
88
+ "get_global_hooks_path",
89
+ "get_project_hooks_path",
90
+ "get_project_local_hooks_path",
91
+ # Executor
92
+ "HookExecutor",
93
+ "LLMCallback",
94
+ # Manager
95
+ "HookManager",
96
+ "HookResult",
97
+ "hook_manager",
98
+ "init_hook_manager",
99
+ ]