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.
Files changed (107) hide show
  1. ripperdoc/__init__.py +3 -0
  2. ripperdoc/__main__.py +20 -0
  3. ripperdoc/cli/__init__.py +1 -0
  4. ripperdoc/cli/cli.py +405 -0
  5. ripperdoc/cli/commands/__init__.py +82 -0
  6. ripperdoc/cli/commands/agents_cmd.py +263 -0
  7. ripperdoc/cli/commands/base.py +19 -0
  8. ripperdoc/cli/commands/clear_cmd.py +18 -0
  9. ripperdoc/cli/commands/compact_cmd.py +23 -0
  10. ripperdoc/cli/commands/config_cmd.py +31 -0
  11. ripperdoc/cli/commands/context_cmd.py +144 -0
  12. ripperdoc/cli/commands/cost_cmd.py +82 -0
  13. ripperdoc/cli/commands/doctor_cmd.py +221 -0
  14. ripperdoc/cli/commands/exit_cmd.py +19 -0
  15. ripperdoc/cli/commands/help_cmd.py +20 -0
  16. ripperdoc/cli/commands/mcp_cmd.py +70 -0
  17. ripperdoc/cli/commands/memory_cmd.py +202 -0
  18. ripperdoc/cli/commands/models_cmd.py +413 -0
  19. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  20. ripperdoc/cli/commands/resume_cmd.py +98 -0
  21. ripperdoc/cli/commands/status_cmd.py +167 -0
  22. ripperdoc/cli/commands/tasks_cmd.py +278 -0
  23. ripperdoc/cli/commands/todos_cmd.py +69 -0
  24. ripperdoc/cli/commands/tools_cmd.py +19 -0
  25. ripperdoc/cli/ui/__init__.py +1 -0
  26. ripperdoc/cli/ui/context_display.py +298 -0
  27. ripperdoc/cli/ui/helpers.py +22 -0
  28. ripperdoc/cli/ui/rich_ui.py +1557 -0
  29. ripperdoc/cli/ui/spinner.py +49 -0
  30. ripperdoc/cli/ui/thinking_spinner.py +128 -0
  31. ripperdoc/cli/ui/tool_renderers.py +298 -0
  32. ripperdoc/core/__init__.py +1 -0
  33. ripperdoc/core/agents.py +486 -0
  34. ripperdoc/core/commands.py +33 -0
  35. ripperdoc/core/config.py +559 -0
  36. ripperdoc/core/default_tools.py +88 -0
  37. ripperdoc/core/permissions.py +252 -0
  38. ripperdoc/core/providers/__init__.py +47 -0
  39. ripperdoc/core/providers/anthropic.py +250 -0
  40. ripperdoc/core/providers/base.py +265 -0
  41. ripperdoc/core/providers/gemini.py +615 -0
  42. ripperdoc/core/providers/openai.py +487 -0
  43. ripperdoc/core/query.py +1058 -0
  44. ripperdoc/core/query_utils.py +622 -0
  45. ripperdoc/core/skills.py +295 -0
  46. ripperdoc/core/system_prompt.py +431 -0
  47. ripperdoc/core/tool.py +240 -0
  48. ripperdoc/sdk/__init__.py +9 -0
  49. ripperdoc/sdk/client.py +333 -0
  50. ripperdoc/tools/__init__.py +1 -0
  51. ripperdoc/tools/ask_user_question_tool.py +431 -0
  52. ripperdoc/tools/background_shell.py +389 -0
  53. ripperdoc/tools/bash_output_tool.py +98 -0
  54. ripperdoc/tools/bash_tool.py +1016 -0
  55. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  56. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  57. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  58. ripperdoc/tools/file_edit_tool.py +346 -0
  59. ripperdoc/tools/file_read_tool.py +203 -0
  60. ripperdoc/tools/file_write_tool.py +205 -0
  61. ripperdoc/tools/glob_tool.py +179 -0
  62. ripperdoc/tools/grep_tool.py +370 -0
  63. ripperdoc/tools/kill_bash_tool.py +136 -0
  64. ripperdoc/tools/ls_tool.py +471 -0
  65. ripperdoc/tools/mcp_tools.py +591 -0
  66. ripperdoc/tools/multi_edit_tool.py +456 -0
  67. ripperdoc/tools/notebook_edit_tool.py +386 -0
  68. ripperdoc/tools/skill_tool.py +205 -0
  69. ripperdoc/tools/task_tool.py +379 -0
  70. ripperdoc/tools/todo_tool.py +494 -0
  71. ripperdoc/tools/tool_search_tool.py +380 -0
  72. ripperdoc/utils/__init__.py +1 -0
  73. ripperdoc/utils/bash_constants.py +51 -0
  74. ripperdoc/utils/bash_output_utils.py +43 -0
  75. ripperdoc/utils/coerce.py +34 -0
  76. ripperdoc/utils/context_length_errors.py +252 -0
  77. ripperdoc/utils/exit_code_handlers.py +241 -0
  78. ripperdoc/utils/file_watch.py +135 -0
  79. ripperdoc/utils/git_utils.py +274 -0
  80. ripperdoc/utils/json_utils.py +27 -0
  81. ripperdoc/utils/log.py +176 -0
  82. ripperdoc/utils/mcp.py +560 -0
  83. ripperdoc/utils/memory.py +253 -0
  84. ripperdoc/utils/message_compaction.py +676 -0
  85. ripperdoc/utils/messages.py +519 -0
  86. ripperdoc/utils/output_utils.py +258 -0
  87. ripperdoc/utils/path_ignore.py +677 -0
  88. ripperdoc/utils/path_utils.py +46 -0
  89. ripperdoc/utils/permissions/__init__.py +27 -0
  90. ripperdoc/utils/permissions/path_validation_utils.py +174 -0
  91. ripperdoc/utils/permissions/shell_command_validation.py +552 -0
  92. ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
  93. ripperdoc/utils/prompt.py +17 -0
  94. ripperdoc/utils/safe_get_cwd.py +31 -0
  95. ripperdoc/utils/sandbox_utils.py +38 -0
  96. ripperdoc/utils/session_history.py +260 -0
  97. ripperdoc/utils/session_usage.py +117 -0
  98. ripperdoc/utils/shell_token_utils.py +95 -0
  99. ripperdoc/utils/shell_utils.py +159 -0
  100. ripperdoc/utils/todo.py +203 -0
  101. ripperdoc/utils/token_estimation.py +34 -0
  102. ripperdoc-0.2.6.dist-info/METADATA +193 -0
  103. ripperdoc-0.2.6.dist-info/RECORD +107 -0
  104. ripperdoc-0.2.6.dist-info/WHEEL +5 -0
  105. ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
  106. ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
  107. 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