ripperdoc 0.2.6__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 +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Utilities for processing and truncating command output."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Maximum output length to prevent token overflow
|
|
8
|
+
MAX_OUTPUT_CHARS = 30000
|
|
9
|
+
|
|
10
|
+
# Threshold for considering output "large"
|
|
11
|
+
LARGE_OUTPUT_THRESHOLD = 5000
|
|
12
|
+
|
|
13
|
+
# When truncating, keep this many chars from start and end
|
|
14
|
+
TRUNCATE_KEEP_START = 15000
|
|
15
|
+
TRUNCATE_KEEP_END = 10000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def trim_blank_lines(text: str) -> str:
|
|
19
|
+
"""Remove leading and trailing blank lines while preserving internal spacing.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: Input text
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Text with leading/trailing blank lines removed
|
|
26
|
+
"""
|
|
27
|
+
lines = text.split("\n")
|
|
28
|
+
|
|
29
|
+
# Remove leading blank lines
|
|
30
|
+
start = 0
|
|
31
|
+
while start < len(lines) and not lines[start].strip():
|
|
32
|
+
start += 1
|
|
33
|
+
|
|
34
|
+
# Remove trailing blank lines
|
|
35
|
+
end = len(lines)
|
|
36
|
+
while end > start and not lines[end - 1].strip():
|
|
37
|
+
end -= 1
|
|
38
|
+
|
|
39
|
+
return "\n".join(lines[start:end])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_image_data(text: str) -> bool:
|
|
43
|
+
"""Check if text appears to be base64 encoded image data.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
text: Text to check
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if text looks like image data
|
|
50
|
+
"""
|
|
51
|
+
if not text:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
stripped = text.strip()
|
|
55
|
+
|
|
56
|
+
# Check for data URI scheme (most reliable indicator)
|
|
57
|
+
if stripped.startswith("data:image/"):
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
# Don't treat arbitrary long text as base64 unless it has image indicators
|
|
61
|
+
# Base64 images are typically very long AND have specific characteristics
|
|
62
|
+
if len(stripped) < 1000:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
# Check for common image base64 patterns
|
|
66
|
+
# Real base64 images usually have variety of characters and padding
|
|
67
|
+
base64_chars = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
|
|
68
|
+
text_chars = set(stripped)
|
|
69
|
+
|
|
70
|
+
# If text only uses a small subset of base64 chars, it's probably not base64
|
|
71
|
+
# Real base64 uses a variety of characters
|
|
72
|
+
if len(text_chars) < 10:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Must be valid base64 characters
|
|
76
|
+
if not text_chars.issubset(base64_chars):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Must end with proper base64 padding or no padding
|
|
80
|
+
if not (
|
|
81
|
+
stripped.endswith("==")
|
|
82
|
+
or stripped.endswith("=")
|
|
83
|
+
or stripped[-1] in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
84
|
+
):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# If all checks pass and it's very long, might be base64 image
|
|
88
|
+
return len(stripped) > 10000
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def truncate_output(text: str, max_chars: int = MAX_OUTPUT_CHARS) -> dict[str, Any]:
|
|
92
|
+
"""Truncate output if it exceeds max length.
|
|
93
|
+
|
|
94
|
+
Keeps both the beginning and end of output to preserve context.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
text: Output text to truncate
|
|
98
|
+
max_chars: Maximum character limit
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict with:
|
|
102
|
+
- truncated_content: Potentially truncated text
|
|
103
|
+
- is_truncated: Whether truncation occurred
|
|
104
|
+
- original_length: Original text length
|
|
105
|
+
- is_image: Whether content appears to be image data
|
|
106
|
+
"""
|
|
107
|
+
if not text:
|
|
108
|
+
return {
|
|
109
|
+
"truncated_content": text,
|
|
110
|
+
"is_truncated": False,
|
|
111
|
+
"original_length": 0,
|
|
112
|
+
"is_image": False,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Check if it's image data
|
|
116
|
+
if is_image_data(text):
|
|
117
|
+
return {
|
|
118
|
+
"truncated_content": text,
|
|
119
|
+
"is_truncated": False,
|
|
120
|
+
"original_length": len(text),
|
|
121
|
+
"is_image": True,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
original_length = len(text)
|
|
125
|
+
|
|
126
|
+
if original_length <= max_chars:
|
|
127
|
+
return {
|
|
128
|
+
"truncated_content": text,
|
|
129
|
+
"is_truncated": False,
|
|
130
|
+
"original_length": original_length,
|
|
131
|
+
"is_image": False,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
marker_template = "\n\n... [Output truncated: {omitted} characters omitted] ...\n\n"
|
|
135
|
+
short_marker = "... [truncated] ..."
|
|
136
|
+
|
|
137
|
+
def _choose_marker(omitted: int, budget: int) -> str:
|
|
138
|
+
"""Pick the most informative marker that fits within the budget."""
|
|
139
|
+
full_marker = marker_template.format(omitted=omitted)
|
|
140
|
+
if len(full_marker) <= budget:
|
|
141
|
+
return full_marker
|
|
142
|
+
if len(short_marker) <= budget:
|
|
143
|
+
return short_marker
|
|
144
|
+
# Last resort: squeeze an ellipsis into the budget (may be empty for tiny budgets)
|
|
145
|
+
return "..."[: max(budget, 0)]
|
|
146
|
+
|
|
147
|
+
# Iteratively balance how much of the start/end to keep while ensuring we never exceed max_chars.
|
|
148
|
+
marker = _choose_marker(original_length - max_chars, max_chars)
|
|
149
|
+
keep_start = keep_end = 0
|
|
150
|
+
for _ in range(2):
|
|
151
|
+
available = max(0, max_chars - len(marker))
|
|
152
|
+
keep_start = min(TRUNCATE_KEEP_START, available // 2)
|
|
153
|
+
keep_end = min(TRUNCATE_KEEP_END, available - keep_start)
|
|
154
|
+
marker = _choose_marker(max(0, original_length - (keep_start + keep_end)), max_chars)
|
|
155
|
+
|
|
156
|
+
available = max(0, max_chars - len(marker))
|
|
157
|
+
# Ensure kept sections fit the final budget; trim end first, then start if needed.
|
|
158
|
+
if keep_start + keep_end > available:
|
|
159
|
+
overflow = keep_start + keep_end - available
|
|
160
|
+
trim_end = min(overflow, keep_end)
|
|
161
|
+
keep_end -= trim_end
|
|
162
|
+
overflow -= trim_end
|
|
163
|
+
keep_start = max(0, keep_start - overflow)
|
|
164
|
+
|
|
165
|
+
truncated = text[:keep_start] + marker + (text[-keep_end:] if keep_end else "")
|
|
166
|
+
if len(truncated) > max_chars:
|
|
167
|
+
truncated = truncated[:max_chars]
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"truncated_content": truncated,
|
|
171
|
+
"is_truncated": True,
|
|
172
|
+
"original_length": original_length,
|
|
173
|
+
"is_image": False,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def format_duration(duration_ms: float) -> str:
|
|
178
|
+
"""Format duration in milliseconds to human-readable string.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
duration_ms: Duration in milliseconds
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Formatted duration string (e.g., "1.23s", "45.6ms")
|
|
185
|
+
"""
|
|
186
|
+
if duration_ms < 1000:
|
|
187
|
+
return f"{duration_ms:.0f}ms"
|
|
188
|
+
else:
|
|
189
|
+
return f"{duration_ms / 1000:.2f}s"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def is_output_large(text: str) -> bool:
|
|
193
|
+
"""Check if output is considered large.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
text: Output text
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if output exceeds large threshold
|
|
200
|
+
"""
|
|
201
|
+
return len(text) > LARGE_OUTPUT_THRESHOLD
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def count_lines(text: str) -> int:
|
|
205
|
+
"""Count number of lines in text.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
text: Text to count
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Number of lines
|
|
212
|
+
"""
|
|
213
|
+
if not text:
|
|
214
|
+
return 0
|
|
215
|
+
return text.count("\n") + 1
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_last_n_lines(text: str, n: int) -> str:
|
|
219
|
+
"""Get the last N lines from text.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
text: Input text
|
|
223
|
+
n: Number of lines to keep
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Last N lines
|
|
227
|
+
"""
|
|
228
|
+
if not text:
|
|
229
|
+
return text
|
|
230
|
+
|
|
231
|
+
lines = text.split("\n")
|
|
232
|
+
if len(lines) <= n:
|
|
233
|
+
return text
|
|
234
|
+
|
|
235
|
+
return "\n".join(lines[-n:])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def sanitize_output(text: str) -> str:
|
|
239
|
+
"""Sanitize output by removing control/escape sequences and ensuring UTF-8."""
|
|
240
|
+
# ANSI/VT escape patterns, including charset selection (e.g., ESC(B) and OSC)
|
|
241
|
+
ansi_escape = re.compile(
|
|
242
|
+
r"""
|
|
243
|
+
\x1B
|
|
244
|
+
(?:
|
|
245
|
+
[@-Z\\-_] # 7-bit C1 control
|
|
246
|
+
| \[ [0-?]* [ -/]* [@-~] # CSI (colors, cursor moves, etc.)
|
|
247
|
+
| [()][0-9A-Za-z] # Charset selection like ESC(B
|
|
248
|
+
| \] (?: [^\x07\x1B]* \x07 | [^\x1B]* \x1B\\ ) # OSC to BEL or ST
|
|
249
|
+
)
|
|
250
|
+
""",
|
|
251
|
+
re.VERBOSE,
|
|
252
|
+
)
|
|
253
|
+
text = ansi_escape.sub("", text)
|
|
254
|
+
|
|
255
|
+
# Remove remaining control characters except newline, tab, carriage return
|
|
256
|
+
text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", text)
|
|
257
|
+
|
|
258
|
+
return text
|