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.
- ripperdoc/__init__.py +3 -0
- ripperdoc/__main__.py +25 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +317 -0
- ripperdoc/cli/commands/__init__.py +76 -0
- ripperdoc/cli/commands/agents_cmd.py +234 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +19 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +114 -0
- ripperdoc/cli/commands/cost_cmd.py +77 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +65 -0
- ripperdoc/cli/commands/models_cmd.py +327 -0
- ripperdoc/cli/commands/resume_cmd.py +97 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +240 -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 +297 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1010 -0
- ripperdoc/cli/ui/spinner.py +50 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +306 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +382 -0
- ripperdoc/core/default_tools.py +57 -0
- ripperdoc/core/permissions.py +227 -0
- ripperdoc/core/query.py +682 -0
- ripperdoc/core/system_prompt.py +418 -0
- ripperdoc/core/tool.py +214 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +309 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/background_shell.py +291 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +822 -0
- ripperdoc/tools/file_edit_tool.py +281 -0
- ripperdoc/tools/file_read_tool.py +168 -0
- ripperdoc/tools/file_write_tool.py +141 -0
- ripperdoc/tools/glob_tool.py +134 -0
- ripperdoc/tools/grep_tool.py +232 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +298 -0
- ripperdoc/tools/mcp_tools.py +804 -0
- ripperdoc/tools/multi_edit_tool.py +393 -0
- ripperdoc/tools/notebook_edit_tool.py +325 -0
- ripperdoc/tools/task_tool.py +282 -0
- ripperdoc/tools/todo_tool.py +362 -0
- ripperdoc/tools/tool_search_tool.py +366 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/log.py +76 -0
- ripperdoc/utils/mcp.py +427 -0
- ripperdoc/utils/memory.py +239 -0
- ripperdoc/utils/message_compaction.py +640 -0
- ripperdoc/utils/messages.py +399 -0
- ripperdoc/utils/output_utils.py +233 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +21 -0
- ripperdoc/utils/permissions/path_validation_utils.py +165 -0
- ripperdoc/utils/permissions/shell_command_validation.py +74 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/safe_get_cwd.py +24 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +223 -0
- ripperdoc/utils/session_usage.py +110 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/todo.py +199 -0
- ripperdoc-0.1.0.dist-info/METADATA +178 -0
- ripperdoc-0.1.0.dist-info/RECORD +81 -0
- ripperdoc-0.1.0.dist-info/WHEEL +5 -0
- ripperdoc-0.1.0.dist-info/entry_points.txt +3 -0
- ripperdoc-0.1.0.dist-info/licenses/LICENSE +53 -0
- 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))
|