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,253 @@
|
|
|
1
|
+
"""Helpers for loading AGENTS.md memory files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional, Set
|
|
9
|
+
from ripperdoc.utils.log import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger()
|
|
12
|
+
|
|
13
|
+
MEMORY_FILE_NAME = "AGENTS.md"
|
|
14
|
+
LOCAL_MEMORY_FILE_NAME = "AGENTS.local.md"
|
|
15
|
+
|
|
16
|
+
MEMORY_INSTRUCTIONS = (
|
|
17
|
+
"Codebase and user instructions are shown below. Be sure to adhere to these "
|
|
18
|
+
"instructions. IMPORTANT: These instructions OVERRIDE any default behavior "
|
|
19
|
+
"and you MUST follow them exactly as written."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
MAX_CONTENT_LENGTH = 40_000
|
|
23
|
+
MAX_INCLUDE_DEPTH = 5
|
|
24
|
+
|
|
25
|
+
_CODE_FENCE_RE = re.compile(r"```.*?```", flags=re.DOTALL)
|
|
26
|
+
_INLINE_CODE_RE = re.compile(r"`[^`]*`")
|
|
27
|
+
_MENTION_RE = re.compile(r"(?:^|\s)@((?:[^\s\\]|\\ )+)")
|
|
28
|
+
_PUNCT_START_RE = re.compile(r"^[#%^&*()]+")
|
|
29
|
+
_VALID_START_RE = re.compile(r"^[A-Za-z0-9._-]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class MemoryFile:
|
|
34
|
+
"""Representation of a loaded memory file."""
|
|
35
|
+
|
|
36
|
+
path: str
|
|
37
|
+
type: str
|
|
38
|
+
content: str
|
|
39
|
+
parent: Optional[str] = None
|
|
40
|
+
is_nested: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_path_under_directory(path: Path, directory: Path) -> bool:
|
|
44
|
+
"""Return True if path is inside directory (after resolving)."""
|
|
45
|
+
try:
|
|
46
|
+
path.resolve().relative_to(directory.resolve())
|
|
47
|
+
return True
|
|
48
|
+
except (ValueError, OSError) as exc:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"[memory] Failed to compare path containment: %s: %s",
|
|
51
|
+
type(exc).__name__, exc,
|
|
52
|
+
extra={"path": str(path), "directory": str(directory)},
|
|
53
|
+
)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _resolve_relative_path(raw_path: str, base_path: Path) -> Path:
|
|
58
|
+
"""Resolve a mention (./foo, ~/bar, /abs, or relative) against a base file."""
|
|
59
|
+
normalized = raw_path.replace("\\ ", " ")
|
|
60
|
+
if normalized.startswith("~/"):
|
|
61
|
+
return (Path.home() / normalized[2:]).resolve()
|
|
62
|
+
candidate = Path(normalized)
|
|
63
|
+
if not candidate.is_absolute():
|
|
64
|
+
return (base_path.parent / candidate).resolve()
|
|
65
|
+
return candidate.resolve()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _read_file_with_type(file_path: Path, file_type: str) -> Optional[MemoryFile]:
|
|
69
|
+
"""Read a file if it exists, returning a MemoryFile entry."""
|
|
70
|
+
try:
|
|
71
|
+
if not file_path.exists() or not file_path.is_file():
|
|
72
|
+
return None
|
|
73
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
74
|
+
return MemoryFile(path=str(file_path), type=file_type, content=content)
|
|
75
|
+
except PermissionError:
|
|
76
|
+
logger.exception("[memory] Permission error reading file", extra={"path": str(file_path)})
|
|
77
|
+
return None
|
|
78
|
+
except OSError:
|
|
79
|
+
logger.exception("[memory] OS error reading file", extra={"path": str(file_path)})
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _extract_relative_paths_from_markdown(markdown_content: str, base_path: Path) -> List[Path]:
|
|
84
|
+
"""Extract @-mentions that look like file paths from markdown content."""
|
|
85
|
+
if not markdown_content:
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
cleaned = _CODE_FENCE_RE.sub("", markdown_content)
|
|
89
|
+
cleaned = _INLINE_CODE_RE.sub("", cleaned)
|
|
90
|
+
|
|
91
|
+
relative_paths: Set[Path] = set()
|
|
92
|
+
for match in _MENTION_RE.finditer(cleaned):
|
|
93
|
+
mention = (match.group(1) or "").replace("\\ ", " ").strip()
|
|
94
|
+
if not mention or mention.startswith("@"):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if not (
|
|
98
|
+
mention.startswith("./")
|
|
99
|
+
or mention.startswith("~/")
|
|
100
|
+
or (mention.startswith("/") and mention != "/")
|
|
101
|
+
or (not _PUNCT_START_RE.match(mention) and _VALID_START_RE.match(mention))
|
|
102
|
+
):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
resolved = _resolve_relative_path(mention, base_path)
|
|
106
|
+
relative_paths.add(resolved)
|
|
107
|
+
|
|
108
|
+
return list(relative_paths)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _collect_files(
|
|
112
|
+
file_path: Path,
|
|
113
|
+
file_type: str,
|
|
114
|
+
visited: Set[str],
|
|
115
|
+
allow_outside_cwd: bool,
|
|
116
|
+
depth: int = 0,
|
|
117
|
+
parent_path: Optional[Path] = None,
|
|
118
|
+
) -> List[MemoryFile]:
|
|
119
|
+
"""Collect a memory file and any nested references."""
|
|
120
|
+
if depth >= MAX_INCLUDE_DEPTH:
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
resolved_path = file_path.expanduser()
|
|
124
|
+
try:
|
|
125
|
+
resolved_path = resolved_path.resolve()
|
|
126
|
+
except (OSError, ValueError) as exc:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"[memory] Failed to resolve memory file path: %s: %s",
|
|
129
|
+
type(exc).__name__, exc,
|
|
130
|
+
extra={"path": str(resolved_path)},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
resolved_key = str(resolved_path)
|
|
134
|
+
if resolved_key in visited:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
current_file = _read_file_with_type(resolved_path, file_type)
|
|
138
|
+
if not current_file or not current_file.content.strip():
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
if parent_path is not None:
|
|
142
|
+
current_file.parent = str(parent_path)
|
|
143
|
+
current_file.is_nested = depth > 0
|
|
144
|
+
|
|
145
|
+
visited.add(resolved_key)
|
|
146
|
+
|
|
147
|
+
collected: List[MemoryFile] = [current_file]
|
|
148
|
+
relative_paths = _extract_relative_paths_from_markdown(current_file.content, resolved_path)
|
|
149
|
+
for nested_path in relative_paths:
|
|
150
|
+
if not allow_outside_cwd and not _is_path_under_directory(nested_path, Path.cwd()):
|
|
151
|
+
continue
|
|
152
|
+
collected.extend(
|
|
153
|
+
_collect_files(
|
|
154
|
+
nested_path,
|
|
155
|
+
file_type,
|
|
156
|
+
visited,
|
|
157
|
+
allow_outside_cwd,
|
|
158
|
+
depth + 1,
|
|
159
|
+
resolved_path,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return collected
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def collect_all_memory_files(force_include_external: bool = False) -> List[MemoryFile]:
|
|
167
|
+
"""Collect all AGENTS memory files reachable from the working directory."""
|
|
168
|
+
visited: Set[str] = set()
|
|
169
|
+
files: List[MemoryFile] = []
|
|
170
|
+
|
|
171
|
+
# Global/user-level memories live in home and ~/.ripperdoc.
|
|
172
|
+
user_memory_paths = [
|
|
173
|
+
Path.home() / ".ripperdoc" / MEMORY_FILE_NAME,
|
|
174
|
+
Path.home() / MEMORY_FILE_NAME,
|
|
175
|
+
]
|
|
176
|
+
for user_memory_path in user_memory_paths:
|
|
177
|
+
files.extend(
|
|
178
|
+
_collect_files(
|
|
179
|
+
user_memory_path,
|
|
180
|
+
"User",
|
|
181
|
+
visited,
|
|
182
|
+
allow_outside_cwd=True,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Project memories from the current working directory up to the filesystem root.
|
|
187
|
+
ancestor_dirs: List[Path] = []
|
|
188
|
+
current_dir = Path.cwd()
|
|
189
|
+
while True:
|
|
190
|
+
ancestor_dirs.append(current_dir)
|
|
191
|
+
if current_dir.parent == current_dir:
|
|
192
|
+
break
|
|
193
|
+
current_dir = current_dir.parent
|
|
194
|
+
|
|
195
|
+
for directory in reversed(ancestor_dirs):
|
|
196
|
+
files.extend(
|
|
197
|
+
_collect_files(
|
|
198
|
+
directory / MEMORY_FILE_NAME,
|
|
199
|
+
"Project",
|
|
200
|
+
visited,
|
|
201
|
+
allow_outside_cwd=force_include_external,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
files.extend(
|
|
205
|
+
_collect_files(
|
|
206
|
+
directory / LOCAL_MEMORY_FILE_NAME,
|
|
207
|
+
"Local",
|
|
208
|
+
visited,
|
|
209
|
+
allow_outside_cwd=force_include_external,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return files
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_oversized_memory_files() -> List[MemoryFile]:
|
|
217
|
+
"""Return memory files that exceed the recommended length."""
|
|
218
|
+
return [file for file in collect_all_memory_files() if len(file.content) > MAX_CONTENT_LENGTH]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_memory_instructions() -> str:
|
|
222
|
+
"""Build the instruction block to append to the system prompt."""
|
|
223
|
+
memory_files = collect_all_memory_files()
|
|
224
|
+
snippets: List[str] = []
|
|
225
|
+
for memory_file in memory_files:
|
|
226
|
+
if not memory_file.content:
|
|
227
|
+
continue
|
|
228
|
+
type_description = (
|
|
229
|
+
" (project instructions, checked into the codebase)"
|
|
230
|
+
if memory_file.type == "Project"
|
|
231
|
+
else (
|
|
232
|
+
" (user's private project instructions, not checked in)"
|
|
233
|
+
if memory_file.type == "Local"
|
|
234
|
+
else " (user's private global instructions)"
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
snippets.append(
|
|
238
|
+
f"Contents of {memory_file.path}{type_description}:\n\n{memory_file.content}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not snippets:
|
|
242
|
+
return ""
|
|
243
|
+
|
|
244
|
+
return f"{MEMORY_INSTRUCTIONS}\n\n" + "\n\n".join(snippets)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
__all__ = [
|
|
248
|
+
"MemoryFile",
|
|
249
|
+
"collect_all_memory_files",
|
|
250
|
+
"build_memory_instructions",
|
|
251
|
+
"get_oversized_memory_files",
|
|
252
|
+
"MAX_CONTENT_LENGTH",
|
|
253
|
+
]
|