ripperdoc 0.2.8__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 (84) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +28 -115
  3. ripperdoc/cli/commands/__init__.py +0 -1
  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/hooks_cmd.py +27 -53
  10. ripperdoc/cli/commands/models_cmd.py +26 -9
  11. ripperdoc/cli/commands/permissions_cmd.py +27 -9
  12. ripperdoc/cli/commands/resume_cmd.py +5 -3
  13. ripperdoc/cli/commands/status_cmd.py +4 -4
  14. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  15. ripperdoc/cli/ui/file_mention_completer.py +2 -1
  16. ripperdoc/cli/ui/interrupt_handler.py +2 -3
  17. ripperdoc/cli/ui/message_display.py +4 -2
  18. ripperdoc/cli/ui/provider_options.py +247 -0
  19. ripperdoc/cli/ui/rich_ui.py +110 -59
  20. ripperdoc/cli/ui/spinner.py +25 -1
  21. ripperdoc/cli/ui/tool_renderers.py +8 -2
  22. ripperdoc/cli/ui/wizard.py +215 -0
  23. ripperdoc/core/agents.py +9 -3
  24. ripperdoc/core/config.py +49 -12
  25. ripperdoc/core/custom_commands.py +7 -6
  26. ripperdoc/core/default_tools.py +11 -2
  27. ripperdoc/core/hooks/config.py +1 -3
  28. ripperdoc/core/hooks/events.py +23 -28
  29. ripperdoc/core/hooks/executor.py +4 -6
  30. ripperdoc/core/hooks/integration.py +12 -21
  31. ripperdoc/core/hooks/manager.py +40 -15
  32. ripperdoc/core/permissions.py +40 -8
  33. ripperdoc/core/providers/anthropic.py +109 -36
  34. ripperdoc/core/providers/gemini.py +70 -5
  35. ripperdoc/core/providers/openai.py +60 -5
  36. ripperdoc/core/query.py +82 -38
  37. ripperdoc/core/query_utils.py +2 -0
  38. ripperdoc/core/skills.py +9 -3
  39. ripperdoc/core/system_prompt.py +4 -2
  40. ripperdoc/core/tool.py +9 -5
  41. ripperdoc/sdk/client.py +2 -2
  42. ripperdoc/tools/ask_user_question_tool.py +5 -3
  43. ripperdoc/tools/background_shell.py +2 -1
  44. ripperdoc/tools/bash_output_tool.py +1 -1
  45. ripperdoc/tools/bash_tool.py +26 -16
  46. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  47. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  48. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  49. ripperdoc/tools/file_edit_tool.py +8 -4
  50. ripperdoc/tools/file_read_tool.py +8 -4
  51. ripperdoc/tools/file_write_tool.py +9 -5
  52. ripperdoc/tools/glob_tool.py +3 -2
  53. ripperdoc/tools/grep_tool.py +3 -2
  54. ripperdoc/tools/kill_bash_tool.py +1 -1
  55. ripperdoc/tools/ls_tool.py +1 -1
  56. ripperdoc/tools/mcp_tools.py +13 -10
  57. ripperdoc/tools/multi_edit_tool.py +8 -7
  58. ripperdoc/tools/notebook_edit_tool.py +7 -4
  59. ripperdoc/tools/skill_tool.py +1 -1
  60. ripperdoc/tools/task_tool.py +5 -4
  61. ripperdoc/tools/todo_tool.py +2 -2
  62. ripperdoc/tools/tool_search_tool.py +3 -2
  63. ripperdoc/utils/conversation_compaction.py +8 -4
  64. ripperdoc/utils/file_watch.py +8 -2
  65. ripperdoc/utils/json_utils.py +2 -1
  66. ripperdoc/utils/mcp.py +11 -3
  67. ripperdoc/utils/memory.py +4 -2
  68. ripperdoc/utils/message_compaction.py +21 -7
  69. ripperdoc/utils/message_formatting.py +11 -7
  70. ripperdoc/utils/messages.py +105 -66
  71. ripperdoc/utils/path_ignore.py +35 -8
  72. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  73. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  74. ripperdoc/utils/safe_get_cwd.py +2 -1
  75. ripperdoc/utils/session_history.py +13 -6
  76. ripperdoc/utils/todo.py +2 -1
  77. ripperdoc/utils/token_estimation.py +6 -1
  78. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
  79. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  80. ripperdoc-0.2.8.dist-info/RECORD +0 -121
  81. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  82. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  83. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  84. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
ripperdoc/core/agents.py CHANGED
@@ -278,10 +278,15 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
278
278
  body = "\n".join(lines[idx + 1 :])
279
279
  try:
280
280
  frontmatter = yaml.safe_load(frontmatter_text) or {}
281
- except (yaml.YAMLError, ValueError, TypeError) as exc: # pragma: no cover - defensive
281
+ except (
282
+ yaml.YAMLError,
283
+ ValueError,
284
+ TypeError,
285
+ ) as exc: # pragma: no cover - defensive
282
286
  logger.warning(
283
287
  "Invalid frontmatter in agent file: %s: %s",
284
- type(exc).__name__, exc,
288
+ type(exc).__name__,
289
+ exc,
285
290
  extra={"error": str(exc)},
286
291
  )
287
292
  return {"__error__": f"Invalid frontmatter: {exc}"}, body
@@ -312,7 +317,8 @@ def _parse_agent_file(
312
317
  except (OSError, IOError, UnicodeDecodeError) as exc:
313
318
  logger.warning(
314
319
  "Failed to read agent file: %s: %s",
315
- type(exc).__name__, exc,
320
+ type(exc).__name__,
321
+ exc,
316
322
  extra={"error": str(exc), "path": str(path)},
317
323
  )
318
324
  return None, f"Failed to read agent file {path}: {exc}"
ripperdoc/core/config.py CHANGED
@@ -7,8 +7,8 @@ including API keys, model settings, and user preferences.
7
7
  import json
8
8
  import os
9
9
  from pathlib import Path
10
- from typing import Dict, Optional, Literal
11
- from pydantic import BaseModel, Field
10
+ from typing import Any, Dict, Optional, Literal
11
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
12
  from enum import Enum
13
13
 
14
14
  from ripperdoc.utils.log import get_logger
@@ -111,7 +111,7 @@ class ModelProfile(BaseModel):
111
111
  # interactions into plain text to support providers that reject tool roles.
112
112
  openai_tool_mode: Literal["native", "text"] = "native"
113
113
  # Optional override for thinking protocol handling (e.g., "deepseek", "openrouter",
114
- # "qwen", "gemini_openai", "openai_reasoning"). When unset, provider heuristics are used.
114
+ # "qwen", "gemini_openai", "openai"). When unset, provider heuristics are used.
115
115
  thinking_mode: Optional[str] = None
116
116
  # Pricing (USD per 1M tokens). Leave as 0 to skip cost calculation.
117
117
  input_cost_per_million_tokens: float = 0.0
@@ -130,7 +130,7 @@ class ModelPointers(BaseModel):
130
130
  class GlobalConfig(BaseModel):
131
131
  """Global configuration stored in ~/.ripperdoc.json"""
132
132
 
133
- model_config = {"protected_namespaces": ()}
133
+ model_config = {"protected_namespaces": (), "populate_by_name": True}
134
134
 
135
135
  # Model configuration
136
136
  model_profiles: Dict[str, ModelProfile] = Field(default_factory=dict)
@@ -139,7 +139,8 @@ class GlobalConfig(BaseModel):
139
139
  # User preferences
140
140
  theme: str = "dark"
141
141
  verbose: bool = False
142
- safe_mode: bool = True
142
+ yolo_mode: bool = Field(default=False)
143
+ show_full_thinking: bool = Field(default=False)
143
144
  auto_compact_enabled: bool = True
144
145
  context_token_limit: Optional[int] = None
145
146
 
@@ -154,6 +155,18 @@ class GlobalConfig(BaseModel):
154
155
  # Statistics
155
156
  num_startups: int = 0
156
157
 
158
+ @model_validator(mode="before")
159
+ @classmethod
160
+ def _migrate_safe_mode(cls, data: Any) -> Any:
161
+ """Translate legacy safe_mode to the new yolo_mode flag."""
162
+ if isinstance(data, dict) and "safe_mode" in data and "yolo_mode" not in data:
163
+ data = dict(data)
164
+ try:
165
+ data["yolo_mode"] = not bool(data.pop("safe_mode"))
166
+ except Exception:
167
+ data["yolo_mode"] = False
168
+ return data
169
+
157
170
 
158
171
  class ProjectConfig(BaseModel):
159
172
  """Project-specific configuration stored in .ripperdoc/config.json"""
@@ -167,7 +180,7 @@ class ProjectConfig(BaseModel):
167
180
  # Path ignore patterns (gitignore-style)
168
181
  ignore_patterns: list[str] = Field(
169
182
  default_factory=list,
170
- description="Gitignore-style patterns for paths to ignore in file operations"
183
+ description="Gitignore-style patterns for paths to ignore in file operations",
171
184
  )
172
185
 
173
186
  # Context
@@ -222,10 +235,18 @@ class ConfigManager:
222
235
  "profile_count": len(self._global_config.model_profiles),
223
236
  },
224
237
  )
225
- except (json.JSONDecodeError, OSError, IOError, UnicodeDecodeError, ValueError, TypeError) as e:
238
+ except (
239
+ json.JSONDecodeError,
240
+ OSError,
241
+ IOError,
242
+ UnicodeDecodeError,
243
+ ValueError,
244
+ TypeError,
245
+ ) as e:
226
246
  logger.warning(
227
247
  "Error loading global config: %s: %s",
228
- type(e).__name__, e,
248
+ type(e).__name__,
249
+ e,
229
250
  extra={"error": str(e)},
230
251
  )
231
252
  self._global_config = GlobalConfig()
@@ -276,10 +297,18 @@ class ConfigManager:
276
297
  "allowed_tools": len(self._project_config.allowed_tools),
277
298
  },
278
299
  )
279
- except (json.JSONDecodeError, OSError, IOError, UnicodeDecodeError, ValueError, TypeError) as e:
300
+ except (
301
+ json.JSONDecodeError,
302
+ OSError,
303
+ IOError,
304
+ UnicodeDecodeError,
305
+ ValueError,
306
+ TypeError,
307
+ ) as e:
280
308
  logger.warning(
281
309
  "Error loading project config: %s: %s",
282
- type(e).__name__, e,
310
+ type(e).__name__,
311
+ e,
283
312
  extra={"error": str(e), "path": str(config_path)},
284
313
  )
285
314
  self._project_config = ProjectConfig()
@@ -344,10 +373,18 @@ class ConfigManager:
344
373
  "project_path": str(self.current_project_path),
345
374
  },
346
375
  )
347
- except (json.JSONDecodeError, OSError, IOError, UnicodeDecodeError, ValueError, TypeError) as e:
376
+ except (
377
+ json.JSONDecodeError,
378
+ OSError,
379
+ IOError,
380
+ UnicodeDecodeError,
381
+ ValueError,
382
+ TypeError,
383
+ ) as e:
348
384
  logger.warning(
349
385
  "Error loading project-local config: %s: %s",
350
- type(e).__name__, e,
386
+ type(e).__name__,
387
+ e,
351
388
  extra={"error": str(e), "path": str(config_path)},
352
389
  )
353
390
  self._project_local_config = ProjectLocalConfig()
@@ -14,7 +14,6 @@ Features:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import asyncio
18
17
  import re
19
18
  import subprocess
20
19
  from dataclasses import dataclass, field
@@ -24,7 +23,6 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
24
23
 
25
24
  import yaml
26
25
 
27
- from ripperdoc.utils.coerce import parse_boolish, parse_optional_int
28
26
  from ripperdoc.utils.log import get_logger
29
27
 
30
28
  logger = get_logger()
@@ -96,13 +94,14 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
96
94
  for idx in range(1, len(lines)):
97
95
  if lines[idx].strip() == "---":
98
96
  frontmatter_text = "\n".join(lines[1:idx])
99
- body = "\n".join(lines[idx + 1:])
97
+ body = "\n".join(lines[idx + 1 :])
100
98
  try:
101
99
  frontmatter = yaml.safe_load(frontmatter_text) or {}
102
100
  except (yaml.YAMLError, ValueError, TypeError) as exc:
103
101
  logger.warning(
104
102
  "[custom_commands] Invalid frontmatter: %s: %s",
105
- type(exc).__name__, exc,
103
+ type(exc).__name__,
104
+ exc,
106
105
  )
107
106
  return {"__error__": f"Invalid frontmatter: {exc}"}, body
108
107
  return frontmatter, body
@@ -163,7 +162,8 @@ def _load_command_file(
163
162
  except (OSError, IOError, UnicodeDecodeError) as exc:
164
163
  logger.warning(
165
164
  "[custom_commands] Failed to read command file: %s: %s",
166
- type(exc).__name__, exc,
165
+ type(exc).__name__,
166
+ exc,
167
167
  extra={"path": str(path)},
168
168
  )
169
169
  return None, CustomCommandLoadError(path=path, reason=f"Failed to read file: {exc}")
@@ -239,7 +239,8 @@ def _load_commands_from_dir(
239
239
  except OSError as exc:
240
240
  logger.warning(
241
241
  "[custom_commands] Failed to scan command directory: %s: %s",
242
- type(exc).__name__, exc,
242
+ type(exc).__name__,
243
+ exc,
243
244
  extra={"path": str(commands_dir)},
244
245
  )
245
246
 
@@ -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)
@@ -234,9 +234,7 @@ def _parse_hooks_file(data: Dict[str, Any]) -> HooksConfig:
234
234
  hook_definitions.append(hook_def)
235
235
 
236
236
  if hook_definitions:
237
- parsed_matchers.append(
238
- HookMatcher(matcher=matcher_pattern, hooks=hook_definitions)
239
- )
237
+ parsed_matchers.append(HookMatcher(matcher=matcher_pattern, hooks=hook_definitions))
240
238
 
241
239
  if parsed_matchers:
242
240
  parsed_hooks[event_name] = parsed_matchers
@@ -5,10 +5,9 @@ as well as the input/output data structures for each event type.
5
5
  """
6
6
 
7
7
  import json
8
- import os
9
8
  from enum import Enum
10
- from typing import Any, Dict, List, Literal, Optional, Union
11
- from pydantic import BaseModel, Field
9
+ from typing import Any, Dict, Literal, Optional, Union
10
+ from pydantic import BaseModel, ConfigDict, Field
12
11
 
13
12
 
14
13
  class HookEvent(str, Enum):
@@ -73,8 +72,7 @@ class HookInput(BaseModel):
73
72
  permission_mode: str = "default" # "default", "plan", "acceptEdits", "bypassPermissions"
74
73
  hook_event_name: str = ""
75
74
 
76
- class Config:
77
- populate_by_name = True
75
+ model_config = ConfigDict(populate_by_name=True)
78
76
 
79
77
 
80
78
  class PreToolUseInput(HookInput):
@@ -150,6 +148,8 @@ class StopInput(HookInput):
150
148
 
151
149
  hook_event_name: str = "Stop"
152
150
  stop_hook_active: bool = False # True if already continuing from a stop hook
151
+ reason: Optional[str] = None
152
+ stop_sequence: Optional[str] = None
153
153
 
154
154
 
155
155
  class SubagentStopInput(HookInput):
@@ -210,6 +210,8 @@ class SessionEndInput(HookInput):
210
210
 
211
211
  hook_event_name: str = "SessionEnd"
212
212
  reason: str = "" # "clear", "logout", "prompt_input_exit", "other"
213
+ duration_seconds: Optional[float] = None
214
+ message_count: Optional[int] = None
213
215
 
214
216
 
215
217
  # ─────────────────────────────────────────────────────────────────────────────
@@ -232,8 +234,7 @@ class PreToolUseHookOutput(BaseModel):
232
234
  ) # Modified tool input
233
235
  additional_context: Optional[str] = Field(default=None, alias="additionalContext")
234
236
 
235
- class Config:
236
- populate_by_name = True
237
+ model_config = ConfigDict(populate_by_name=True)
237
238
 
238
239
 
239
240
  class PermissionRequestDecision(BaseModel):
@@ -244,8 +245,7 @@ class PermissionRequestDecision(BaseModel):
244
245
  message: Optional[str] = None
245
246
  interrupt: bool = False
246
247
 
247
- class Config:
248
- populate_by_name = True
248
+ model_config = ConfigDict(populate_by_name=True)
249
249
 
250
250
 
251
251
  class PermissionRequestHookOutput(BaseModel):
@@ -254,8 +254,7 @@ class PermissionRequestHookOutput(BaseModel):
254
254
  hook_event_name: Literal["PermissionRequest"] = "PermissionRequest"
255
255
  decision: Optional[PermissionRequestDecision] = None
256
256
 
257
- class Config:
258
- populate_by_name = True
257
+ model_config = ConfigDict(populate_by_name=True)
259
258
 
260
259
 
261
260
  class PostToolUseHookOutput(BaseModel):
@@ -264,8 +263,7 @@ class PostToolUseHookOutput(BaseModel):
264
263
  hook_event_name: Literal["PostToolUse"] = "PostToolUse"
265
264
  additional_context: Optional[str] = Field(default=None, alias="additionalContext")
266
265
 
267
- class Config:
268
- populate_by_name = True
266
+ model_config = ConfigDict(populate_by_name=True)
269
267
 
270
268
 
271
269
  class UserPromptSubmitHookOutput(BaseModel):
@@ -274,8 +272,7 @@ class UserPromptSubmitHookOutput(BaseModel):
274
272
  hook_event_name: Literal["UserPromptSubmit"] = "UserPromptSubmit"
275
273
  additional_context: Optional[str] = Field(default=None, alias="additionalContext")
276
274
 
277
- class Config:
278
- populate_by_name = True
275
+ model_config = ConfigDict(populate_by_name=True)
279
276
 
280
277
 
281
278
  class SessionStartHookOutput(BaseModel):
@@ -284,8 +281,7 @@ class SessionStartHookOutput(BaseModel):
284
281
  hook_event_name: Literal["SessionStart"] = "SessionStart"
285
282
  additional_context: Optional[str] = Field(default=None, alias="additionalContext")
286
283
 
287
- class Config:
288
- populate_by_name = True
284
+ model_config = ConfigDict(populate_by_name=True)
289
285
 
290
286
 
291
287
  HookSpecificOutput = Union[
@@ -325,7 +321,7 @@ class HookOutput(BaseModel):
325
321
  )
326
322
 
327
323
  # Additional context to inject
328
- additional_context: Optional[str] = None
324
+ additional_context: Optional[str] = Field(default=None, alias="additionalContext")
329
325
 
330
326
  # Raw output (for non-JSON responses)
331
327
  raw_output: Optional[str] = None
@@ -336,8 +332,7 @@ class HookOutput(BaseModel):
336
332
  exit_code: int = 0
337
333
  timed_out: bool = False
338
334
 
339
- class Config:
340
- populate_by_name = True
335
+ model_config = ConfigDict(populate_by_name=True)
341
336
 
342
337
  @classmethod
343
338
  def from_raw(
@@ -420,10 +415,10 @@ class HookOutput(BaseModel):
420
415
  # Handle PreToolUse specific fields
421
416
  if event_name == "PreToolUse":
422
417
  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"),
418
+ permissionDecision=hso.get("permissionDecision"),
419
+ permissionDecisionReason=hso.get("permissionDecisionReason"),
420
+ updatedInput=hso.get("updatedInput"),
421
+ additionalContext=hso.get("additionalContext"),
427
422
  )
428
423
  # Map permissionDecision to decision
429
424
  perm_decision = hso.get("permissionDecision")
@@ -444,7 +439,7 @@ class HookOutput(BaseModel):
444
439
  if isinstance(decision_obj, dict):
445
440
  decision_data = PermissionRequestDecision(
446
441
  behavior=decision_obj.get("behavior", ""),
447
- updated_input=decision_obj.get("updatedInput"),
442
+ updatedInput=decision_obj.get("updatedInput"),
448
443
  message=decision_obj.get("message"),
449
444
  interrupt=decision_obj.get("interrupt", False),
450
445
  )
@@ -462,7 +457,7 @@ class HookOutput(BaseModel):
462
457
  # Handle PostToolUse specific fields
463
458
  elif event_name == "PostToolUse":
464
459
  output.hook_specific_output = PostToolUseHookOutput(
465
- additional_context=hso.get("additionalContext"),
460
+ additionalContext=hso.get("additionalContext"),
466
461
  )
467
462
  if hso.get("additionalContext"):
468
463
  output.additional_context = hso["additionalContext"]
@@ -470,7 +465,7 @@ class HookOutput(BaseModel):
470
465
  # Handle UserPromptSubmit specific fields
471
466
  elif event_name == "UserPromptSubmit":
472
467
  output.hook_specific_output = UserPromptSubmitHookOutput(
473
- additional_context=hso.get("additionalContext"),
468
+ additionalContext=hso.get("additionalContext"),
474
469
  )
475
470
  if hso.get("additionalContext"):
476
471
  output.additional_context = hso["additionalContext"]
@@ -478,7 +473,7 @@ class HookOutput(BaseModel):
478
473
  # Handle SessionStart specific fields
479
474
  elif event_name == "SessionStart":
480
475
  output.hook_specific_output = SessionStartHookOutput(
481
- additional_context=hso.get("additionalContext"),
476
+ additionalContext=hso.get("additionalContext"),
482
477
  )
483
478
  if hso.get("additionalContext"):
484
479
  output.additional_context = hso["additionalContext"]
@@ -14,7 +14,7 @@ import os
14
14
  import subprocess
15
15
  import tempfile
16
16
  from pathlib import Path
17
- from typing import Any, Callable, Dict, Optional, Awaitable
17
+ from typing import Callable, Dict, Optional, Awaitable
18
18
 
19
19
  from ripperdoc.core.hooks.config import HookDefinition
20
20
  from ripperdoc.core.hooks.events import AnyHookInput, HookOutput, HookDecision, SessionStartInput
@@ -191,7 +191,7 @@ class HookExecutor:
191
191
  pass
192
192
 
193
193
  # Not JSON, treat as additional context
194
- return HookOutput(raw_output=response, additional_context=response)
194
+ return HookOutput(raw_output=response, additionalContext=response)
195
195
 
196
196
  async def execute_prompt_async(
197
197
  self,
@@ -224,7 +224,7 @@ class HookExecutor:
224
224
  prompt = self._expand_prompt(hook.prompt, input_data)
225
225
 
226
226
  logger.debug(
227
- f"Executing prompt hook",
227
+ "Executing prompt hook",
228
228
  extra={
229
229
  "event": input_data.hook_event_name,
230
230
  "timeout": hook.timeout,
@@ -277,9 +277,7 @@ class HookExecutor:
277
277
  """
278
278
  # Prompt hooks require async - skip in sync mode
279
279
  if hook.is_prompt_hook():
280
- logger.warning(
281
- "Prompt hook skipped in sync mode. Use execute_async for prompt hooks."
282
- )
280
+ logger.warning("Prompt hook skipped in sync mode. Use execute_async for prompt hooks.")
283
281
  return HookOutput()
284
282
 
285
283
  return self._execute_command_sync(hook, input_data)
@@ -4,11 +4,9 @@ This module provides convenient integration points for running hooks
4
4
  as part of tool execution flows.
5
5
  """
6
6
 
7
- from pathlib import Path
8
7
  from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, Union
9
8
 
10
- from ripperdoc.core.hooks.events import HookDecision
11
- from ripperdoc.core.hooks.manager import HookManager, HookResult, hook_manager
9
+ from ripperdoc.core.hooks.manager import HookManager, hook_manager
12
10
  from ripperdoc.utils.log import get_logger
13
11
 
14
12
  logger = get_logger()
@@ -98,9 +96,7 @@ class HookInterceptor:
98
96
  Returns:
99
97
  Tuple of (should_continue, block_reason, additional_context)
100
98
  """
101
- result = self.manager.run_post_tool_use(
102
- tool_name, tool_input, tool_output, tool_error
103
- )
99
+ result = self.manager.run_post_tool_use(tool_name, tool_input, tool_output, tool_error)
104
100
 
105
101
  if result.should_block:
106
102
  return False, result.block_reason, result.additional_context
@@ -129,7 +125,7 @@ class HookInterceptor:
129
125
  tool_name: str,
130
126
  tool_input: Dict[str, Any],
131
127
  execute_fn: Callable[[], T],
132
- ) -> Tuple[bool, Union[T, str], Optional[str]]:
128
+ ) -> Tuple[bool, Union[T, str, None], Optional[str]]:
133
129
  """Wrap synchronous tool execution with pre/post hooks.
134
130
 
135
131
  Args:
@@ -141,9 +137,7 @@ class HookInterceptor:
141
137
  Tuple of (success, result_or_error, additional_context)
142
138
  """
143
139
  # Run pre-tool hooks
144
- should_proceed, block_reason, pre_context = self.check_pre_tool_use(
145
- tool_name, tool_input
146
- )
140
+ should_proceed, block_reason, pre_context = self.check_pre_tool_use(tool_name, tool_input)
147
141
 
148
142
  if not should_proceed:
149
143
  return False, block_reason or "Blocked by hook", pre_context
@@ -157,9 +151,7 @@ class HookInterceptor:
157
151
  tool_error = str(e)
158
152
 
159
153
  # Run post-tool hooks
160
- _, _, post_context = self.run_post_tool_use(
161
- tool_name, tool_input, result, tool_error
162
- )
154
+ _, _, post_context = self.run_post_tool_use(tool_name, tool_input, result, tool_error)
163
155
 
164
156
  # Combine contexts
165
157
  combined_context = None
@@ -168,7 +160,7 @@ class HookInterceptor:
168
160
  combined_context = "\n".join(parts) if parts else None
169
161
 
170
162
  if tool_error:
171
- return False, tool_error, combined_context
163
+ return False, tool_error or "", combined_context
172
164
 
173
165
  return True, result, combined_context
174
166
 
@@ -177,7 +169,7 @@ class HookInterceptor:
177
169
  tool_name: str,
178
170
  tool_input: Dict[str, Any],
179
171
  execute_fn: Callable[[], T],
180
- ) -> Tuple[bool, Union[T, str], Optional[str]]:
172
+ ) -> Tuple[bool, Union[T, str, None], Optional[str]]:
181
173
  """Wrap async tool execution with pre/post hooks."""
182
174
  # Run pre-tool hooks
183
175
  should_proceed, block_reason, pre_context = await self.check_pre_tool_use_async(
@@ -190,6 +182,7 @@ class HookInterceptor:
190
182
  # Execute the tool
191
183
  try:
192
184
  import asyncio
185
+
193
186
  if asyncio.iscoroutinefunction(execute_fn):
194
187
  result = await execute_fn()
195
188
  else:
@@ -211,7 +204,7 @@ class HookInterceptor:
211
204
  combined_context = "\n".join(parts) if parts else None
212
205
 
213
206
  if tool_error:
214
- return False, tool_error, combined_context
207
+ return False, tool_error or "", combined_context
215
208
 
216
209
  return True, result, combined_context
217
210
 
@@ -241,9 +234,7 @@ def run_post_tool_use(
241
234
  tool_error: Optional[str] = None,
242
235
  ) -> Tuple[bool, Optional[str], Optional[str]]:
243
236
  """Convenience function to run post-tool hooks using global interceptor."""
244
- return hook_interceptor.run_post_tool_use(
245
- tool_name, tool_input, tool_output, tool_error
246
- )
237
+ return hook_interceptor.run_post_tool_use(tool_name, tool_input, tool_output, tool_error)
247
238
 
248
239
 
249
240
  async def run_post_tool_use_async(
@@ -333,7 +324,7 @@ def check_stop(
333
324
  - should_stop: True if agent should stop
334
325
  - continue_reason: Reason to continue if blocked
335
326
  """
336
- result = hook_manager.run_stop(reason, stop_sequence)
327
+ result = hook_manager.run_stop(False, reason, stop_sequence)
337
328
 
338
329
  if result.should_block:
339
330
  return False, result.block_reason
@@ -345,7 +336,7 @@ async def check_stop_async(
345
336
  reason: Optional[str] = None, stop_sequence: Optional[str] = None
346
337
  ) -> Tuple[bool, Optional[str]]:
347
338
  """Async version of check_stop."""
348
- result = await hook_manager.run_stop_async(reason, stop_sequence)
339
+ result = await hook_manager.run_stop_async(False, reason, stop_sequence)
349
340
 
350
341
  if result.should_block:
351
342
  return False, result.block_reason