ripperdoc 0.1.0__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 (81) hide show
  1. ripperdoc/__init__.py +3 -0
  2. ripperdoc/__main__.py +25 -0
  3. ripperdoc/cli/__init__.py +1 -0
  4. ripperdoc/cli/cli.py +317 -0
  5. ripperdoc/cli/commands/__init__.py +76 -0
  6. ripperdoc/cli/commands/agents_cmd.py +234 -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 +19 -0
  10. ripperdoc/cli/commands/config_cmd.py +31 -0
  11. ripperdoc/cli/commands/context_cmd.py +114 -0
  12. ripperdoc/cli/commands/cost_cmd.py +77 -0
  13. ripperdoc/cli/commands/exit_cmd.py +19 -0
  14. ripperdoc/cli/commands/help_cmd.py +20 -0
  15. ripperdoc/cli/commands/mcp_cmd.py +65 -0
  16. ripperdoc/cli/commands/models_cmd.py +327 -0
  17. ripperdoc/cli/commands/resume_cmd.py +97 -0
  18. ripperdoc/cli/commands/status_cmd.py +167 -0
  19. ripperdoc/cli/commands/tasks_cmd.py +240 -0
  20. ripperdoc/cli/commands/todos_cmd.py +69 -0
  21. ripperdoc/cli/commands/tools_cmd.py +19 -0
  22. ripperdoc/cli/ui/__init__.py +1 -0
  23. ripperdoc/cli/ui/context_display.py +297 -0
  24. ripperdoc/cli/ui/helpers.py +22 -0
  25. ripperdoc/cli/ui/rich_ui.py +1010 -0
  26. ripperdoc/cli/ui/spinner.py +50 -0
  27. ripperdoc/core/__init__.py +1 -0
  28. ripperdoc/core/agents.py +306 -0
  29. ripperdoc/core/commands.py +33 -0
  30. ripperdoc/core/config.py +382 -0
  31. ripperdoc/core/default_tools.py +57 -0
  32. ripperdoc/core/permissions.py +227 -0
  33. ripperdoc/core/query.py +682 -0
  34. ripperdoc/core/system_prompt.py +418 -0
  35. ripperdoc/core/tool.py +214 -0
  36. ripperdoc/sdk/__init__.py +9 -0
  37. ripperdoc/sdk/client.py +309 -0
  38. ripperdoc/tools/__init__.py +1 -0
  39. ripperdoc/tools/background_shell.py +291 -0
  40. ripperdoc/tools/bash_output_tool.py +98 -0
  41. ripperdoc/tools/bash_tool.py +822 -0
  42. ripperdoc/tools/file_edit_tool.py +281 -0
  43. ripperdoc/tools/file_read_tool.py +168 -0
  44. ripperdoc/tools/file_write_tool.py +141 -0
  45. ripperdoc/tools/glob_tool.py +134 -0
  46. ripperdoc/tools/grep_tool.py +232 -0
  47. ripperdoc/tools/kill_bash_tool.py +136 -0
  48. ripperdoc/tools/ls_tool.py +298 -0
  49. ripperdoc/tools/mcp_tools.py +804 -0
  50. ripperdoc/tools/multi_edit_tool.py +393 -0
  51. ripperdoc/tools/notebook_edit_tool.py +325 -0
  52. ripperdoc/tools/task_tool.py +282 -0
  53. ripperdoc/tools/todo_tool.py +362 -0
  54. ripperdoc/tools/tool_search_tool.py +366 -0
  55. ripperdoc/utils/__init__.py +1 -0
  56. ripperdoc/utils/bash_constants.py +51 -0
  57. ripperdoc/utils/bash_output_utils.py +43 -0
  58. ripperdoc/utils/exit_code_handlers.py +241 -0
  59. ripperdoc/utils/log.py +76 -0
  60. ripperdoc/utils/mcp.py +427 -0
  61. ripperdoc/utils/memory.py +239 -0
  62. ripperdoc/utils/message_compaction.py +640 -0
  63. ripperdoc/utils/messages.py +399 -0
  64. ripperdoc/utils/output_utils.py +233 -0
  65. ripperdoc/utils/path_utils.py +46 -0
  66. ripperdoc/utils/permissions/__init__.py +21 -0
  67. ripperdoc/utils/permissions/path_validation_utils.py +165 -0
  68. ripperdoc/utils/permissions/shell_command_validation.py +74 -0
  69. ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
  70. ripperdoc/utils/safe_get_cwd.py +24 -0
  71. ripperdoc/utils/sandbox_utils.py +38 -0
  72. ripperdoc/utils/session_history.py +223 -0
  73. ripperdoc/utils/session_usage.py +110 -0
  74. ripperdoc/utils/shell_token_utils.py +95 -0
  75. ripperdoc/utils/todo.py +199 -0
  76. ripperdoc-0.1.0.dist-info/METADATA +178 -0
  77. ripperdoc-0.1.0.dist-info/RECORD +81 -0
  78. ripperdoc-0.1.0.dist-info/WHEEL +5 -0
  79. ripperdoc-0.1.0.dist-info/entry_points.txt +3 -0
  80. ripperdoc-0.1.0.dist-info/licenses/LICENSE +53 -0
  81. ripperdoc-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,134 @@
1
+ """Glob pattern matching tool.
2
+
3
+ Allows the AI to find files using glob patterns.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import AsyncGenerator, Optional, List
8
+ from pydantic import BaseModel, Field
9
+
10
+ from ripperdoc.core.tool import (
11
+ Tool,
12
+ ToolUseContext,
13
+ ToolResult,
14
+ ToolOutput,
15
+ ToolUseExample,
16
+ ValidationResult,
17
+ )
18
+
19
+
20
+ GLOB_USAGE = (
21
+ "- Fast file pattern matching tool for any codebase size\n"
22
+ '- Supports glob patterns like "**/*.js" or "src/**/*.ts"\n'
23
+ "- Returns matching file paths sorted by modification time (newest first)\n"
24
+ "- Use this when you need to find files by name patterns\n"
25
+ "- For open-ended searches that need multiple rounds of globbing and grepping, run the searches iteratively with these tools\n"
26
+ "- You can call multiple tools in a single response; speculatively batch useful searches together"
27
+ )
28
+
29
+
30
+ class GlobToolInput(BaseModel):
31
+ """Input schema for GlobTool."""
32
+
33
+ pattern: str = Field(description="Glob pattern to match files (e.g., '**/*.py')")
34
+ path: Optional[str] = Field(
35
+ default=None, description="Directory to search in (default: current working directory)"
36
+ )
37
+
38
+
39
+ class GlobToolOutput(BaseModel):
40
+ """Output from glob pattern matching."""
41
+
42
+ matches: List[str]
43
+ pattern: str
44
+ count: int
45
+
46
+
47
+ class GlobTool(Tool[GlobToolInput, GlobToolOutput]):
48
+ """Tool for finding files using glob patterns."""
49
+
50
+ @property
51
+ def name(self) -> str:
52
+ return "Glob"
53
+
54
+ async def description(self) -> str:
55
+ return GLOB_USAGE
56
+
57
+ @property
58
+ def input_schema(self) -> type[GlobToolInput]:
59
+ return GlobToolInput
60
+
61
+ def input_examples(self) -> List[ToolUseExample]:
62
+ return [
63
+ ToolUseExample(
64
+ description="Find Python sources inside src",
65
+ input={"pattern": "src/**/*.py"},
66
+ ),
67
+ ToolUseExample(
68
+ description="Locate snapshot files within tests",
69
+ input={"pattern": "tests/**/__snapshots__/*.snap", "path": "/repo"},
70
+ ),
71
+ ]
72
+
73
+ async def prompt(self, safe_mode: bool = False) -> str:
74
+ return GLOB_USAGE
75
+
76
+ def is_read_only(self) -> bool:
77
+ return True
78
+
79
+ def is_concurrency_safe(self) -> bool:
80
+ return True
81
+
82
+ def needs_permissions(self, input_data: Optional[GlobToolInput] = None) -> bool:
83
+ return False
84
+
85
+ async def validate_input(
86
+ self, input_data: GlobToolInput, context: Optional[ToolUseContext] = None
87
+ ) -> ValidationResult:
88
+ return ValidationResult(result=True)
89
+
90
+ def render_result_for_assistant(self, output: GlobToolOutput) -> str:
91
+ """Format output for the AI."""
92
+ if not output.matches:
93
+ return f"No files found matching pattern: {output.pattern}"
94
+
95
+ result = f"Found {output.count} file(s) matching '{output.pattern}':\n\n"
96
+ result += "\n".join(output.matches)
97
+
98
+ return result
99
+
100
+ def render_tool_use_message(self, input_data: GlobToolInput, verbose: bool = False) -> str:
101
+ """Format the tool use for display."""
102
+ return f"Glob: {input_data.pattern}"
103
+
104
+ async def call(
105
+ self, input_data: GlobToolInput, context: ToolUseContext
106
+ ) -> AsyncGenerator[ToolOutput, None]:
107
+ """Find files matching the pattern."""
108
+
109
+ try:
110
+ search_path = Path(input_data.path) if input_data.path else Path.cwd()
111
+
112
+ # Use glob to find matches, sorted by modification time (newest first)
113
+ paths = list(search_path.glob(input_data.pattern))
114
+
115
+ def _mtime(path: Path) -> float:
116
+ try:
117
+ return path.stat().st_mtime
118
+ except OSError:
119
+ return float("-inf")
120
+
121
+ matches = [str(p) for p in sorted(paths, key=_mtime, reverse=True)]
122
+
123
+ output = GlobToolOutput(matches=matches, pattern=input_data.pattern, count=len(matches))
124
+
125
+ yield ToolResult(
126
+ data=output, result_for_assistant=self.render_result_for_assistant(output)
127
+ )
128
+
129
+ except Exception as e:
130
+ error_output = GlobToolOutput(matches=[], pattern=input_data.pattern, count=0)
131
+
132
+ yield ToolResult(
133
+ data=error_output, result_for_assistant=f"Error executing glob: {str(e)}"
134
+ )
@@ -0,0 +1,232 @@
1
+ """Grep tool for searching code.
2
+
3
+ Allows the AI to search for patterns in files.
4
+ """
5
+
6
+ import asyncio
7
+ from typing import AsyncGenerator, Optional, List
8
+ from pydantic import BaseModel, Field
9
+
10
+ from ripperdoc.core.tool import (
11
+ Tool,
12
+ ToolUseContext,
13
+ ToolResult,
14
+ ToolOutput,
15
+ ToolUseExample,
16
+ ValidationResult,
17
+ )
18
+
19
+
20
+ GREP_USAGE = (
21
+ "A powerful search tool built on ripgrep.\n\n"
22
+ "Usage:\n"
23
+ "- ALWAYS use the Grep tool for search tasks. NEVER invoke `grep` or `rg` as a Bash command; this tool is optimized for permissions and access.\n"
24
+ '- Supports regex patterns (e.g., "log.*Error", "function\\s+\\w+")\n'
25
+ '- Filter files with the glob parameter (e.g., "*.js", "**/*.tsx")\n'
26
+ '- Output modes: "content" shows matching lines, "files_with_matches" (default) shows only file paths, "count" shows match counts\n'
27
+ "- For open-ended searches that need multiple rounds, iterate with Glob and Grep rather than shell commands\n"
28
+ "- Patterns are line-based; craft patterns accordingly and escape braces if needed (e.g., use `interface\\{\\}` to find `interface{}`)"
29
+ )
30
+
31
+
32
+ class GrepToolInput(BaseModel):
33
+ """Input schema for GrepTool."""
34
+
35
+ pattern: str = Field(description="Regular expression pattern to search for")
36
+ path: Optional[str] = Field(
37
+ default=None, description="Directory or file to search in (default: current directory)"
38
+ )
39
+ glob: Optional[str] = Field(default=None, description="File pattern to filter (e.g., '*.py')")
40
+ case_insensitive: bool = Field(default=False, description="Case insensitive search")
41
+ output_mode: str = Field(
42
+ default="files_with_matches",
43
+ description="Output mode: 'files_with_matches', 'content', or 'count'",
44
+ )
45
+
46
+
47
+ class GrepMatch(BaseModel):
48
+ """A single grep match."""
49
+
50
+ file: str
51
+ line_number: Optional[int] = None
52
+ content: Optional[str] = None
53
+ count: Optional[int] = None
54
+
55
+
56
+ class GrepToolOutput(BaseModel):
57
+ """Output from grep search."""
58
+
59
+ matches: List[GrepMatch]
60
+ pattern: str
61
+ total_files: int
62
+ total_matches: int
63
+
64
+
65
+ class GrepTool(Tool[GrepToolInput, GrepToolOutput]):
66
+ """Tool for searching code with grep."""
67
+
68
+ @property
69
+ def name(self) -> str:
70
+ return "Grep"
71
+
72
+ async def description(self) -> str:
73
+ return GREP_USAGE
74
+
75
+ @property
76
+ def input_schema(self) -> type[GrepToolInput]:
77
+ return GrepToolInput
78
+
79
+ def input_examples(self) -> List[ToolUseExample]:
80
+ return [
81
+ ToolUseExample(
82
+ description="Find TODO comments in TypeScript files",
83
+ input={"pattern": "TODO", "glob": "**/*.ts", "output_mode": "content"},
84
+ ),
85
+ ToolUseExample(
86
+ description="List files referencing a function name",
87
+ input={
88
+ "pattern": "fetchUserData",
89
+ "output_mode": "files_with_matches",
90
+ "path": "/repo/src",
91
+ },
92
+ ),
93
+ ]
94
+
95
+ async def prompt(self, safe_mode: bool = False) -> str:
96
+ return GREP_USAGE
97
+
98
+ def is_read_only(self) -> bool:
99
+ return True
100
+
101
+ def is_concurrency_safe(self) -> bool:
102
+ return True
103
+
104
+ def needs_permissions(self, input_data: Optional[GrepToolInput] = None) -> bool:
105
+ return False
106
+
107
+ async def validate_input(
108
+ self, input_data: GrepToolInput, context: Optional[ToolUseContext] = None
109
+ ) -> ValidationResult:
110
+ valid_modes = ["files_with_matches", "content", "count"]
111
+ if input_data.output_mode not in valid_modes:
112
+ return ValidationResult(
113
+ result=False, message=f"Invalid output_mode. Must be one of: {valid_modes}"
114
+ )
115
+ return ValidationResult(result=True)
116
+
117
+ def render_result_for_assistant(self, output: GrepToolOutput) -> str:
118
+ """Format output for the AI."""
119
+ if output.total_files == 0:
120
+ return f"No matches found for pattern: {output.pattern}"
121
+
122
+ result = f"Found {output.total_matches} match(es) in {output.total_files} file(s) for '{output.pattern}':\n\n"
123
+
124
+ for match in output.matches[:100]: # Limit to first 100
125
+ if match.content:
126
+ result += f"{match.file}:{match.line_number}: {match.content}\n"
127
+ elif match.count:
128
+ result += f"{match.file}: {match.count} matches\n"
129
+ else:
130
+ result += f"{match.file}\n"
131
+
132
+ if len(output.matches) > 100:
133
+ result += f"\n... and {len(output.matches) - 100} more matches"
134
+
135
+ return result
136
+
137
+ def render_tool_use_message(self, input_data: GrepToolInput, verbose: bool = False) -> str:
138
+ """Format the tool use for display."""
139
+ msg = f"Grep: {input_data.pattern}"
140
+ if input_data.glob:
141
+ msg += f" in {input_data.glob}"
142
+ return msg
143
+
144
+ async def call(
145
+ self, input_data: GrepToolInput, context: ToolUseContext
146
+ ) -> AsyncGenerator[ToolOutput, None]:
147
+ """Search for the pattern."""
148
+
149
+ try:
150
+ search_path = input_data.path or "."
151
+
152
+ # Build grep command
153
+ cmd = ["grep", "-r"]
154
+
155
+ if input_data.case_insensitive:
156
+ cmd.append("-i")
157
+
158
+ if input_data.output_mode == "files_with_matches":
159
+ cmd.extend(["-l"]) # Files with matches
160
+ elif input_data.output_mode == "count":
161
+ cmd.extend(["-c"]) # Count per file
162
+ else:
163
+ cmd.extend(["-n"]) # Line numbers
164
+
165
+ cmd.append(input_data.pattern)
166
+ cmd.append(search_path)
167
+
168
+ if input_data.glob:
169
+ cmd.extend(["--include", input_data.glob])
170
+
171
+ # Run grep asynchronously
172
+ process = await asyncio.create_subprocess_exec(
173
+ *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
174
+ )
175
+
176
+ stdout, stderr = await process.communicate()
177
+ returncode = process.returncode
178
+
179
+ # Parse output
180
+ matches: List[GrepMatch] = []
181
+
182
+ if returncode == 0:
183
+ lines = stdout.decode("utf-8").strip().split("\n")
184
+
185
+ for line in lines:
186
+ if not line:
187
+ continue
188
+
189
+ if input_data.output_mode == "files_with_matches":
190
+ matches.append(GrepMatch(file=line))
191
+
192
+ elif input_data.output_mode == "count":
193
+ # Format: file:count
194
+ parts = line.rsplit(":", 1)
195
+ if len(parts) == 2:
196
+ matches.append(
197
+ GrepMatch(
198
+ file=parts[0], count=int(parts[1]) if parts[1].isdigit() else 0
199
+ )
200
+ )
201
+
202
+ else: # content mode
203
+ # Format: file:line:content
204
+ parts = line.split(":", 2)
205
+ if len(parts) >= 3:
206
+ matches.append(
207
+ GrepMatch(
208
+ file=parts[0],
209
+ line_number=int(parts[1]) if parts[1].isdigit() else None,
210
+ content=parts[2] if len(parts) > 2 else "",
211
+ )
212
+ )
213
+
214
+ output = GrepToolOutput(
215
+ matches=matches,
216
+ pattern=input_data.pattern,
217
+ total_files=len(set(m.file for m in matches)),
218
+ total_matches=len(matches),
219
+ )
220
+
221
+ yield ToolResult(
222
+ data=output, result_for_assistant=self.render_result_for_assistant(output)
223
+ )
224
+
225
+ except Exception as e:
226
+ error_output = GrepToolOutput(
227
+ matches=[], pattern=input_data.pattern, total_files=0, total_matches=0
228
+ )
229
+
230
+ yield ToolResult(
231
+ data=error_output, result_for_assistant=f"Error executing grep: {str(e)}"
232
+ )
@@ -0,0 +1,136 @@
1
+ """Tool to terminate a running background bash task."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import AsyncGenerator, Optional
6
+
7
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field
8
+
9
+ from ripperdoc.core.tool import Tool, ToolResult, ToolUseContext, ValidationResult
10
+ from ripperdoc.tools.background_shell import (
11
+ get_background_status,
12
+ kill_background_task,
13
+ )
14
+ from ripperdoc.utils.permissions import PermissionDecision
15
+
16
+
17
+ KILL_BASH_PROMPT = """
18
+ - Kills a running background bash shell by its ID
19
+ - Takes a shell_id parameter identifying the shell to kill
20
+ - Returns a success or failure status
21
+ - Use this tool when you need to terminate a long-running shell
22
+ - Shell IDs can be found using the Bash tool (run_in_background) and BashOutput
23
+ """.strip()
24
+
25
+
26
+ class KillBashInput(BaseModel):
27
+ """Input schema for KillBash."""
28
+
29
+ task_id: str = Field(
30
+ description="Background task id to kill",
31
+ validation_alias=AliasChoices("task_id", "shell_id"),
32
+ serialization_alias="task_id",
33
+ )
34
+ model_config = ConfigDict(validate_by_alias=True, validate_by_name=True, extra="ignore")
35
+
36
+
37
+ class KillBashOutput(BaseModel):
38
+ """Result of attempting to kill a background task."""
39
+
40
+ task_id: str
41
+ success: bool
42
+ message: str
43
+ status: Optional[str] = None
44
+
45
+
46
+ class KillBashTool(Tool[KillBashInput, KillBashOutput]):
47
+ """Terminate a background bash command."""
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ return "KillBash"
52
+
53
+ async def description(self) -> str:
54
+ return "Kill a background bash shell by ID"
55
+
56
+ async def prompt(self, safe_mode: bool = False) -> str:
57
+ return KILL_BASH_PROMPT
58
+
59
+ @property
60
+ def input_schema(self) -> type[KillBashInput]:
61
+ return KillBashInput
62
+
63
+ def is_read_only(self) -> bool:
64
+ return False
65
+
66
+ def is_concurrency_safe(self) -> bool:
67
+ return True
68
+
69
+ def needs_permissions(self, input_data: Optional[KillBashInput] = None) -> bool:
70
+ return True
71
+
72
+ async def check_permissions(
73
+ self, input_data: KillBashInput, permission_context: object
74
+ ) -> PermissionDecision:
75
+ # Killing is destructive; require explicit confirmation upstream.
76
+ return PermissionDecision(behavior="allow", updated_input=input_data)
77
+
78
+ async def validate_input(
79
+ self, input_data: KillBashInput, context: Optional[ToolUseContext] = None
80
+ ) -> ValidationResult:
81
+ try:
82
+ get_background_status(input_data.task_id, consume=False)
83
+ except KeyError:
84
+ return ValidationResult(
85
+ result=False, message=f"No background task found with id '{input_data.task_id}'"
86
+ )
87
+ return ValidationResult(result=True)
88
+
89
+ def render_result_for_assistant(self, output: KillBashOutput) -> str:
90
+ return output.message
91
+
92
+ def render_tool_use_message(self, input_data: KillBashInput, verbose: bool = False) -> str:
93
+ return f"$ kill-background {input_data.task_id}"
94
+
95
+ async def call(
96
+ self, input_data: KillBashInput, context: ToolUseContext
97
+ ) -> AsyncGenerator[ToolResult, None]:
98
+ try:
99
+ status = get_background_status(input_data.task_id, consume=False)
100
+ except KeyError:
101
+ output = KillBashOutput(
102
+ task_id=input_data.task_id,
103
+ success=False,
104
+ message=f"No shell found with ID: {input_data.task_id}",
105
+ status=None,
106
+ )
107
+ yield ToolResult(
108
+ data=output, result_for_assistant=self.render_result_for_assistant(output)
109
+ )
110
+ return
111
+
112
+ if status["status"] != "running":
113
+ output = KillBashOutput(
114
+ task_id=input_data.task_id,
115
+ success=False,
116
+ message=f"Shell {input_data.task_id} is not running (status: {status['status']}).",
117
+ status=status["status"],
118
+ )
119
+ yield ToolResult(
120
+ data=output, result_for_assistant=self.render_result_for_assistant(output)
121
+ )
122
+ return
123
+
124
+ killed = await kill_background_task(input_data.task_id)
125
+ message = (
126
+ f"Successfully killed shell: {input_data.task_id} ({status['command']})"
127
+ if killed
128
+ else f"Failed to kill shell: {input_data.task_id}"
129
+ )
130
+ output = KillBashOutput(
131
+ task_id=input_data.task_id,
132
+ success=killed,
133
+ message=message,
134
+ status="killed" if killed else status["status"],
135
+ )
136
+ yield ToolResult(data=output, result_for_assistant=self.render_result_for_assistant(output))