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
ripperdoc/core/query.py CHANGED
@@ -29,6 +29,7 @@ from pydantic import ValidationError
29
29
  from ripperdoc.core.config import provider_protocol
30
30
  from ripperdoc.core.providers import ProviderClient, get_provider_client
31
31
  from ripperdoc.core.permissions import PermissionResult
32
+ from ripperdoc.core.hooks.manager import hook_manager
32
33
  from ripperdoc.core.query_utils import (
33
34
  build_full_system_prompt,
34
35
  determine_tool_mode,
@@ -154,6 +155,52 @@ async def _run_tool_use_generator(
154
155
  tool_context: ToolUseContext,
155
156
  ) -> AsyncGenerator[Union[UserMessage, ProgressMessage], None]:
156
157
  """Execute a single tool_use and yield progress/results."""
158
+ # Get tool input as dict for hooks
159
+ tool_input_dict = (
160
+ parsed_input.model_dump()
161
+ if hasattr(parsed_input, "model_dump")
162
+ else dict(parsed_input) if isinstance(parsed_input, dict) else {}
163
+ )
164
+
165
+ # Run PreToolUse hooks
166
+ pre_result = await hook_manager.run_pre_tool_use_async(
167
+ tool_name, tool_input_dict, tool_use_id=tool_use_id
168
+ )
169
+ if pre_result.should_block:
170
+ block_reason = pre_result.block_reason or f"Blocked by hook: {tool_name}"
171
+ logger.info(
172
+ f"[query] Tool {tool_name} blocked by PreToolUse hook",
173
+ extra={"tool_use_id": tool_use_id, "reason": block_reason},
174
+ )
175
+ yield tool_result_message(tool_use_id, f"Hook blocked: {block_reason}", is_error=True)
176
+ return
177
+
178
+ # Handle updated input from hooks
179
+ if pre_result.updated_input:
180
+ logger.debug(
181
+ f"[query] PreToolUse hook modified input for {tool_name}",
182
+ extra={"tool_use_id": tool_use_id},
183
+ )
184
+ # Re-parse the input with the updated values
185
+ try:
186
+ parsed_input = tool.input_schema(**pre_result.updated_input)
187
+ tool_input_dict = pre_result.updated_input
188
+ except (ValueError, TypeError) as exc:
189
+ logger.warning(
190
+ f"[query] Failed to apply updated input from hook: {exc}",
191
+ extra={"tool_use_id": tool_use_id},
192
+ )
193
+
194
+ # Add hook context if provided
195
+ if pre_result.additional_context:
196
+ logger.debug(
197
+ f"[query] PreToolUse hook added context for {tool_name}",
198
+ extra={"context": pre_result.additional_context[:100]},
199
+ )
200
+
201
+ tool_output = None
202
+ tool_error = None
203
+
157
204
  try:
158
205
  async for output in tool.call(parsed_input, tool_context):
159
206
  if isinstance(output, ToolProgress):
@@ -164,6 +211,7 @@ async def _run_tool_use_generator(
164
211
  )
165
212
  logger.debug(f"[query] Progress from tool_use_id={tool_use_id}: {output.content}")
166
213
  elif isinstance(output, ToolResult):
214
+ tool_output = output.data
167
215
  result_content = output.result_for_assistant or str(output.data)
168
216
  result_msg = tool_result_message(
169
217
  tool_use_id, result_content, tool_use_result=output.data
@@ -176,6 +224,7 @@ async def _run_tool_use_generator(
176
224
  except CancelledError:
177
225
  raise # Don't suppress task cancellation
178
226
  except (RuntimeError, ValueError, TypeError, OSError, IOError, AttributeError, KeyError) as exc:
227
+ tool_error = str(exc)
179
228
  logger.warning(
180
229
  "Error executing tool '%s': %s: %s",
181
230
  tool_name, type(exc).__name__, exc,
@@ -183,6 +232,11 @@ async def _run_tool_use_generator(
183
232
  )
184
233
  yield tool_result_message(tool_use_id, f"Error executing tool: {str(exc)}", is_error=True)
185
234
 
235
+ # Run PostToolUse hooks
236
+ await hook_manager.run_post_tool_use_async(
237
+ tool_name, tool_input_dict, tool_response=tool_output, tool_use_id=tool_use_id
238
+ )
239
+
186
240
 
187
241
  def _group_tool_calls_by_concurrency(prepared_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
188
242
  """Group consecutive tool calls by their concurrency safety."""
@@ -624,12 +678,12 @@ async def query_llm(
624
678
  )
625
679
  duration_ms = (time.time() - start_time) * 1000
626
680
  context_error = detect_context_length_error(e)
627
- metadata = None
681
+ error_metadata: Optional[Dict[str, Any]] = None
628
682
  content = f"Error querying AI model: {str(e)}"
629
683
 
630
684
  if context_error:
631
685
  content = f"The request exceeded the model's context window. {context_error.message}"
632
- metadata = {
686
+ error_metadata = {
633
687
  "context_length_exceeded": True,
634
688
  "context_length_provider": context_error.provider,
635
689
  "context_length_error_code": context_error.error_code,
@@ -645,7 +699,7 @@ async def query_llm(
645
699
  )
646
700
 
647
701
  error_msg = create_assistant_message(
648
- content=content, duration_ms=duration_ms, metadata=metadata
702
+ content=content, duration_ms=duration_ms, metadata=error_metadata
649
703
  )
650
704
  error_msg.is_api_error_message = True
651
705
  return error_msg
@@ -1042,7 +1096,10 @@ async def query(
1042
1096
  return
1043
1097
 
1044
1098
  # Update messages for next iteration
1045
- messages = messages + [result.assistant_message] + result.tool_results
1099
+ if result.assistant_message is not None:
1100
+ messages = messages + [result.assistant_message] + result.tool_results # type: ignore[operator]
1101
+ else:
1102
+ messages = messages + result.tool_results # type: ignore[operator]
1046
1103
  logger.debug(
1047
1104
  f"[query] Continuing loop with {len(messages)} messages after tools; "
1048
1105
  f"tool_results_count={len(result.tool_results)}"
@@ -276,7 +276,7 @@ def _tool_prompt_for_text_mode(tools: List[Tool[Any, Any]]) -> str:
276
276
  {
277
277
  "type": "tool_use",
278
278
  "tool_use_id": "tool_id_000001",
279
- "tool": "View",
279
+ "tool": "Read",
280
280
  "input": {"file_path": "README.md"},
281
281
  },
282
282
  ]
ripperdoc/core/tool.py CHANGED
@@ -42,7 +42,7 @@ class ToolUseContext(BaseModel):
42
42
  permission_checker: Optional[Any] = None
43
43
  read_file_timestamps: Dict[str, float] = Field(default_factory=dict)
44
44
  # SkipValidation prevents Pydantic from copying the dict during validation,
45
- # ensuring View/Read and Edit tools share the same cache instance
45
+ # ensuring Read and Edit tools share the same cache instance
46
46
  file_state_cache: Annotated[Dict[str, FileSnapshot], SkipValidation] = Field(default_factory=dict)
47
47
  tool_registry: Optional[Any] = None
48
48
  abort_signal: Optional[Any] = None
@@ -270,7 +270,7 @@ build projects, run tests, and interact with the file system."""
270
270
  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
271
271
  - If the output exceeds {MAX_OUTPUT_CHARS} characters, output will be truncated before being returned to you.
272
272
  - You can use the `run_in_background` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the BashOutput tool as it becomes available. Never use `run_in_background` to run 'sleep' as it will return immediately. You do not need to use '&' at the end of the command when using this parameter.
273
- - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use the Grep, Glob, or Task tools to search. Prefer the View and LS tools instead of shell commands like `cat`, `head`, `tail`, or `ls` when reading files and directories.
273
+ - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use the Grep, Glob, or Task tools to search. Prefer the Read and LS tools instead of shell commands like `cat`, `head`, `tail`, or `ls` when reading files and directories.
274
274
  - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
275
275
  - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the user explicitly requests it.
276
276
  <good-example>
@@ -652,7 +652,7 @@ build projects, run tests, and interact with the file system."""
652
652
  # Emit progress updates for newly received output chunks
653
653
  while not queue.empty():
654
654
  label, text = queue.get_nowait()
655
- yield ToolProgress(content=f"{label}: {text}")
655
+ yield ToolProgress(content=f"{label}: {text}") # type: ignore[misc]
656
656
 
657
657
  # Report progress at intervals
658
658
  if now - last_progress_time >= PROGRESS_INTERVAL_SECONDS:
@@ -660,7 +660,7 @@ build projects, run tests, and interact with the file system."""
660
660
  if combined_output:
661
661
  preview = get_last_n_lines(combined_output, 5)
662
662
  elapsed = format_duration((now - start_time) * 1000)
663
- yield ToolProgress(content=f"Running... ({elapsed})\n{preview}")
663
+ yield ToolProgress(content=f"Running... ({elapsed})\n{preview}") # type: ignore[misc]
664
664
  last_progress_time = now
665
665
 
666
666
  # Check timeout
@@ -907,14 +907,14 @@ build projects, run tests, and interact with the file system."""
907
907
 
908
908
  while not queue.empty():
909
909
  label, text = queue.get_nowait()
910
- yield ToolProgress(content=f"{label}: {text}")
910
+ yield ToolProgress(content=f"{label}: {text}") # type: ignore[misc]
911
911
 
912
912
  if now - last_progress_time >= PROGRESS_INTERVAL_SECONDS:
913
913
  combined_output = "".join(stdout_lines + stderr_lines)
914
914
  if combined_output:
915
915
  preview = get_last_n_lines(combined_output, 5)
916
916
  elapsed = format_duration((now - start) * 1000)
917
- yield ToolProgress(content=f"Running... ({elapsed})\n{preview}")
917
+ yield ToolProgress(content=f"Running... ({elapsed})\n{preview}") # type: ignore[misc]
918
918
  last_progress_time = now
919
919
 
920
920
  if deadline is not None and now >= deadline:
@@ -88,8 +88,8 @@ match exactly (including whitespace and indentation)."""
88
88
  return (
89
89
  "Performs exact string replacements in files.\n\n"
90
90
  "Usage:\n"
91
- "- You must use your `View` tool at least once in the conversation to read the file before editing; edits will fail if you skip reading.\n"
92
- "- When editing text from View output, preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix is formatted as spaces + line number + tab. Never include any part of the prefix in old_string or new_string.\n"
91
+ "- You must use your `Read` tool at least once in the conversation to read the file before editing; edits will fail if you skip reading.\n"
92
+ "- When editing text from Read output, preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix is formatted as spaces + line number + tab. Never include any part of the prefix in old_string or new_string.\n"
93
93
  "- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n"
94
94
  "- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n"
95
95
  "- The edit will FAIL if `old_string` is not unique in the file. Provide more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.\n"
@@ -18,7 +18,7 @@ from ripperdoc.core.tool import (
18
18
  )
19
19
  from ripperdoc.utils.log import get_logger
20
20
  from ripperdoc.utils.file_watch import record_snapshot
21
- from ripperdoc.utils.path_ignore import check_path_for_tool, is_path_ignored
21
+ from ripperdoc.utils.path_ignore import check_path_for_tool
22
22
 
23
23
  logger = get_logger()
24
24
 
@@ -48,7 +48,7 @@ class FileReadTool(Tool[FileReadToolInput, FileReadToolOutput]):
48
48
 
49
49
  @property
50
50
  def name(self) -> str:
51
- return "View"
51
+ return "Read"
52
52
 
53
53
  async def description(self) -> str:
54
54
  return """Read the contents of a file. You can optionally specify an offset
@@ -25,7 +25,7 @@ logger = get_logger()
25
25
 
26
26
 
27
27
  DEFAULT_ACTION = "Edit"
28
- TOOL_NAME_READ = "View"
28
+ TOOL_NAME_READ = "Read"
29
29
  NOTEBOOK_EDIT_TOOL_NAME = "NotebookEdit"
30
30
 
31
31
  MULTI_EDIT_DESCRIPTION = dedent(