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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +5 -0
- ripperdoc/cli/commands/__init__.py +71 -6
- ripperdoc/cli/commands/clear_cmd.py +1 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -1
- ripperdoc/cli/commands/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +636 -0
- ripperdoc/cli/commands/permissions_cmd.py +36 -34
- ripperdoc/cli/commands/resume_cmd.py +71 -37
- ripperdoc/cli/ui/file_mention_completer.py +276 -0
- ripperdoc/cli/ui/helpers.py +100 -3
- ripperdoc/cli/ui/interrupt_handler.py +175 -0
- ripperdoc/cli/ui/message_display.py +249 -0
- ripperdoc/cli/ui/panels.py +63 -0
- ripperdoc/cli/ui/rich_ui.py +233 -648
- ripperdoc/cli/ui/tool_renderers.py +2 -2
- ripperdoc/core/agents.py +4 -4
- ripperdoc/core/custom_commands.py +411 -0
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +303 -0
- ripperdoc/core/hooks/events.py +540 -0
- ripperdoc/core/hooks/executor.py +498 -0
- ripperdoc/core/hooks/integration.py +353 -0
- ripperdoc/core/hooks/manager.py +720 -0
- ripperdoc/core/providers/anthropic.py +476 -69
- ripperdoc/core/query.py +61 -4
- ripperdoc/core/query_utils.py +1 -1
- ripperdoc/core/tool.py +1 -1
- ripperdoc/tools/bash_tool.py +5 -5
- ripperdoc/tools/file_edit_tool.py +2 -2
- ripperdoc/tools/file_read_tool.py +2 -2
- ripperdoc/tools/multi_edit_tool.py +1 -1
- ripperdoc/utils/conversation_compaction.py +476 -0
- ripperdoc/utils/message_compaction.py +109 -154
- ripperdoc/utils/message_formatting.py +216 -0
- ripperdoc/utils/messages.py +31 -9
- ripperdoc/utils/path_ignore.py +3 -4
- ripperdoc/utils/session_history.py +19 -7
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/METADATA +24 -3
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/RECORD +44 -30
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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)}"
|
ripperdoc/core/query_utils.py
CHANGED
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
|
|
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
|
ripperdoc/tools/bash_tool.py
CHANGED
|
@@ -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
|
|
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 `
|
|
92
|
-
"- When editing text from
|
|
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
|
|
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 "
|
|
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
|