ripperdoc 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl

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