tunacode-cli 0.0.12__py3-none-any.whl → 0.0.13__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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/constants.py +1 -1
- tunacode/core/agents/main.py +2 -0
- tunacode/tools/__init__.py +1 -0
- tunacode/tools/bash.py +252 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.13.dist-info}/METADATA +1 -1
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.13.dist-info}/RECORD +10 -9
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.13.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.13.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.13.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.13.dist-info}/top_level.txt +0 -0
tunacode/constants.py
CHANGED
tunacode/core/agents/main.py
CHANGED
|
@@ -12,6 +12,7 @@ from pydantic_ai.messages import ModelRequest, ToolReturnPart
|
|
|
12
12
|
|
|
13
13
|
from tunacode.core.state import StateManager
|
|
14
14
|
from tunacode.services.mcp import get_mcp_servers
|
|
15
|
+
from tunacode.tools.bash import bash
|
|
15
16
|
from tunacode.tools.read_file import read_file
|
|
16
17
|
from tunacode.tools.run_command import run_command
|
|
17
18
|
from tunacode.tools.update_file import update_file
|
|
@@ -37,6 +38,7 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
37
38
|
state_manager.session.agents[model] = Agent(
|
|
38
39
|
model=model,
|
|
39
40
|
tools=[
|
|
41
|
+
Tool(bash, max_retries=max_retries),
|
|
40
42
|
Tool(read_file, max_retries=max_retries),
|
|
41
43
|
Tool(run_command, max_retries=max_retries),
|
|
42
44
|
Tool(update_file, max_retries=max_retries),
|
tunacode/tools/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TunaCode tools package."""
|
tunacode/tools/bash.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.tools.bash
|
|
3
|
+
|
|
4
|
+
Enhanced bash execution tool for agent operations in the TunaCode application.
|
|
5
|
+
Provides advanced shell command execution with working directory support,
|
|
6
|
+
environment variables, timeouts, and improved output handling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import Dict, Optional
|
|
13
|
+
|
|
14
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
15
|
+
|
|
16
|
+
from tunacode.constants import MAX_COMMAND_OUTPUT
|
|
17
|
+
from tunacode.exceptions import ToolExecutionError
|
|
18
|
+
from tunacode.tools.base import BaseTool
|
|
19
|
+
from tunacode.types import ToolResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BashTool(BaseTool):
|
|
23
|
+
"""Enhanced shell command execution tool with advanced features."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def tool_name(self) -> str:
|
|
27
|
+
return "Bash"
|
|
28
|
+
|
|
29
|
+
async def _execute(
|
|
30
|
+
self,
|
|
31
|
+
command: str,
|
|
32
|
+
cwd: Optional[str] = None,
|
|
33
|
+
env: Optional[Dict[str, str]] = None,
|
|
34
|
+
timeout: Optional[int] = 30,
|
|
35
|
+
capture_output: bool = True,
|
|
36
|
+
) -> ToolResult:
|
|
37
|
+
"""Execute a bash command with enhanced features.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
command: The bash command to execute
|
|
41
|
+
cwd: Working directory for the command (defaults to current)
|
|
42
|
+
env: Additional environment variables to set
|
|
43
|
+
timeout: Command timeout in seconds (default 30, max 300)
|
|
44
|
+
capture_output: Whether to capture stdout/stderr (default True)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ToolResult: Formatted output with exit code, stdout, and stderr
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ModelRetry: For guidance on command failures
|
|
51
|
+
Exception: Any command execution errors
|
|
52
|
+
"""
|
|
53
|
+
# Validate and sanitize inputs
|
|
54
|
+
if timeout and (timeout < 1 or timeout > 300):
|
|
55
|
+
raise ModelRetry(
|
|
56
|
+
"Timeout must be between 1 and 300 seconds. "
|
|
57
|
+
"Use shorter timeouts for quick commands, longer for builds/tests."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Validate working directory if specified
|
|
61
|
+
if cwd and not os.path.isdir(cwd):
|
|
62
|
+
raise ModelRetry(
|
|
63
|
+
f"Working directory '{cwd}' does not exist. "
|
|
64
|
+
"Please verify the path or create the directory first."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Check for potentially destructive commands
|
|
68
|
+
destructive_patterns = ["rm -rf", "rm -r", "rm /", "dd if=", "mkfs", "fdisk"]
|
|
69
|
+
if any(pattern in command for pattern in destructive_patterns):
|
|
70
|
+
raise ModelRetry(
|
|
71
|
+
f"Command contains potentially destructive operations: {command}\n"
|
|
72
|
+
"Please confirm this is intentional and safe for your system."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Prepare environment
|
|
76
|
+
exec_env = os.environ.copy()
|
|
77
|
+
if env:
|
|
78
|
+
# Sanitize environment variables
|
|
79
|
+
for key, value in env.items():
|
|
80
|
+
if isinstance(key, str) and isinstance(value, str):
|
|
81
|
+
exec_env[key] = value
|
|
82
|
+
|
|
83
|
+
# Set working directory
|
|
84
|
+
exec_cwd = cwd or os.getcwd()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Execute command with timeout
|
|
88
|
+
process = await asyncio.create_subprocess_shell(
|
|
89
|
+
command,
|
|
90
|
+
stdout=subprocess.PIPE if capture_output else None,
|
|
91
|
+
stderr=subprocess.PIPE if capture_output else None,
|
|
92
|
+
cwd=exec_cwd,
|
|
93
|
+
env=exec_env,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
stdout, stderr = await asyncio.wait_for(
|
|
98
|
+
process.communicate(), timeout=timeout
|
|
99
|
+
)
|
|
100
|
+
except asyncio.TimeoutError:
|
|
101
|
+
# Kill the process if it times out
|
|
102
|
+
process.kill()
|
|
103
|
+
await process.wait()
|
|
104
|
+
raise ModelRetry(
|
|
105
|
+
f"Command timed out after {timeout} seconds: {command}\n"
|
|
106
|
+
"Consider using a longer timeout or breaking the command into smaller parts."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Decode output
|
|
110
|
+
stdout_text = stdout.decode("utf-8", errors="replace").strip() if stdout else ""
|
|
111
|
+
stderr_text = stderr.decode("utf-8", errors="replace").strip() if stderr else ""
|
|
112
|
+
|
|
113
|
+
# Format output
|
|
114
|
+
result = self._format_output(
|
|
115
|
+
command=command,
|
|
116
|
+
exit_code=process.returncode,
|
|
117
|
+
stdout=stdout_text,
|
|
118
|
+
stderr=stderr_text,
|
|
119
|
+
cwd=exec_cwd,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Handle non-zero exit codes as guidance, not failures
|
|
123
|
+
if process.returncode != 0 and stderr_text:
|
|
124
|
+
# Provide guidance for common error patterns
|
|
125
|
+
if "command not found" in stderr_text.lower():
|
|
126
|
+
raise ModelRetry(
|
|
127
|
+
f"Command '{command}' not found. "
|
|
128
|
+
"Check if the command is installed or use the full path."
|
|
129
|
+
)
|
|
130
|
+
elif "permission denied" in stderr_text.lower():
|
|
131
|
+
raise ModelRetry(
|
|
132
|
+
f"Permission denied for command '{command}'. "
|
|
133
|
+
"You may need elevated privileges or different file permissions."
|
|
134
|
+
)
|
|
135
|
+
elif "no such file or directory" in stderr_text.lower():
|
|
136
|
+
raise ModelRetry(
|
|
137
|
+
f"File or directory not found when running '{command}'. "
|
|
138
|
+
"Verify the path exists or create it first."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
raise ModelRetry(
|
|
145
|
+
f"Shell not found. Cannot execute command: {command}\n"
|
|
146
|
+
"This typically indicates a system configuration issue."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _format_output(
|
|
150
|
+
self,
|
|
151
|
+
command: str,
|
|
152
|
+
exit_code: int,
|
|
153
|
+
stdout: str,
|
|
154
|
+
stderr: str,
|
|
155
|
+
cwd: str,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Format command output in a consistent way.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
command: The executed command
|
|
161
|
+
exit_code: The process exit code
|
|
162
|
+
stdout: Standard output content
|
|
163
|
+
stderr: Standard error content
|
|
164
|
+
cwd: Working directory where command was executed
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
str: Formatted output string
|
|
168
|
+
"""
|
|
169
|
+
# Build the result
|
|
170
|
+
lines = [
|
|
171
|
+
f"Command: {command}",
|
|
172
|
+
f"Exit Code: {exit_code}",
|
|
173
|
+
f"Working Directory: {cwd}",
|
|
174
|
+
"",
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
# Add stdout if present
|
|
178
|
+
if stdout:
|
|
179
|
+
lines.extend(["STDOUT:", stdout, ""])
|
|
180
|
+
else:
|
|
181
|
+
lines.extend(["STDOUT:", "(no output)", ""])
|
|
182
|
+
|
|
183
|
+
# Add stderr if present
|
|
184
|
+
if stderr:
|
|
185
|
+
lines.extend(["STDERR:", stderr])
|
|
186
|
+
else:
|
|
187
|
+
lines.extend(["STDERR:", "(no errors)"])
|
|
188
|
+
|
|
189
|
+
result = "\n".join(lines)
|
|
190
|
+
|
|
191
|
+
# Truncate if too long
|
|
192
|
+
if len(result) > MAX_COMMAND_OUTPUT:
|
|
193
|
+
truncate_point = MAX_COMMAND_OUTPUT - 100 # Leave room for truncation message
|
|
194
|
+
result = result[:truncate_point] + "\n\n[... output truncated ...]"
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def _format_args(
|
|
199
|
+
self,
|
|
200
|
+
command: str,
|
|
201
|
+
cwd: Optional[str] = None,
|
|
202
|
+
env: Optional[Dict[str, str]] = None,
|
|
203
|
+
timeout: Optional[int] = None,
|
|
204
|
+
**kwargs,
|
|
205
|
+
) -> str:
|
|
206
|
+
"""Format arguments for display in UI logging."""
|
|
207
|
+
args = [repr(command)]
|
|
208
|
+
|
|
209
|
+
if cwd:
|
|
210
|
+
args.append(f"cwd={repr(cwd)}")
|
|
211
|
+
if timeout:
|
|
212
|
+
args.append(f"timeout={timeout}")
|
|
213
|
+
if env:
|
|
214
|
+
env_summary = f"{len(env)} vars" if len(env) > 3 else str(env)
|
|
215
|
+
args.append(f"env={env_summary}")
|
|
216
|
+
|
|
217
|
+
return ", ".join(args)
|
|
218
|
+
|
|
219
|
+
def _get_error_context(self, command: str = None, **kwargs) -> str:
|
|
220
|
+
"""Get error context for bash execution."""
|
|
221
|
+
if command:
|
|
222
|
+
return f"executing bash command '{command}'"
|
|
223
|
+
return super()._get_error_context()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Create the function that maintains the existing interface
|
|
227
|
+
async def bash(
|
|
228
|
+
command: str,
|
|
229
|
+
cwd: Optional[str] = None,
|
|
230
|
+
env: Optional[Dict[str, str]] = None,
|
|
231
|
+
timeout: Optional[int] = 30,
|
|
232
|
+
capture_output: bool = True,
|
|
233
|
+
) -> ToolResult:
|
|
234
|
+
"""
|
|
235
|
+
Execute a bash command with enhanced features.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
command (str): The bash command to execute
|
|
239
|
+
cwd (Optional[str]): Working directory for the command
|
|
240
|
+
env (Optional[Dict[str, str]]): Additional environment variables
|
|
241
|
+
timeout (Optional[int]): Command timeout in seconds (default 30, max 300)
|
|
242
|
+
capture_output (bool): Whether to capture stdout/stderr
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
ToolResult: Formatted output with exit code, stdout, and stderr
|
|
246
|
+
"""
|
|
247
|
+
tool = BashTool()
|
|
248
|
+
try:
|
|
249
|
+
return await tool.execute(command, cwd=cwd, env=env, timeout=timeout, capture_output=capture_output)
|
|
250
|
+
except ToolExecutionError as e:
|
|
251
|
+
# Return error message for pydantic-ai compatibility
|
|
252
|
+
return str(e)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
tunacode/constants.py,sha256=
|
|
2
|
+
tunacode/constants.py,sha256=gBGd50EIBnIUeflCZXGpyWYXjEOgymDTmVwETS1fDFE,3807
|
|
3
3
|
tunacode/context.py,sha256=0ttsxxLAyD4pSoxw7S-pyzor0JUkhOFZh96aAf4Kqsg,2634
|
|
4
4
|
tunacode/exceptions.py,sha256=RFUH8wOsWEvSPGIYM2exr4t47YkEyZt4Fr-DfTo6_JY,2647
|
|
5
5
|
tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -19,7 +19,7 @@ tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
19
19
|
tunacode/core/state.py,sha256=sf3xvc9NBnz98tkHiSi-mi1GgxuN-r5kERwjmuIKjq8,1344
|
|
20
20
|
tunacode/core/tool_handler.py,sha256=OKx7jM8pml6pSEnoARu33_uBY8awJBqvhbVeBn6T4ow,1804
|
|
21
21
|
tunacode/core/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
tunacode/core/agents/main.py,sha256=
|
|
22
|
+
tunacode/core/agents/main.py,sha256=DVwm0T3NbmY1NmA41EWHiLry_OE_f-CUoV6tyn3Y9bo,4751
|
|
23
23
|
tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
|
|
24
24
|
tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
|
|
25
25
|
tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
|
|
@@ -30,8 +30,9 @@ tunacode/core/setup/git_safety_setup.py,sha256=6sAQ0L74rRpayR7hWpnPyGKxCzW1Mk-2g
|
|
|
30
30
|
tunacode/prompts/system.txt,sha256=6ecousLK6KvRj6wzIVyzRE7OPQVC5n7P6ceSbtmmaZQ,3207
|
|
31
31
|
tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
|
|
32
32
|
tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
|
|
33
|
-
tunacode/tools/__init__.py,sha256=
|
|
33
|
+
tunacode/tools/__init__.py,sha256=l5O018T-bUjCSy7JSupJoKojZp07uAz3913qDFVdCzM,29
|
|
34
34
|
tunacode/tools/base.py,sha256=UioygDbXy0JY67EIcyEoYt2TBfX9-n9Usf_1huQ4zOE,7046
|
|
35
|
+
tunacode/tools/bash.py,sha256=h8pUzfp7n2aVtAJcUICJOJV0FBTphifZdYrzHLvrZjA,8781
|
|
35
36
|
tunacode/tools/read_file.py,sha256=ZNX9PvaYcI2Hw_GeSpQ3_wEy26DQ3LqLwYlaCVYDXO0,3051
|
|
36
37
|
tunacode/tools/run_command.py,sha256=jI5TWi4SKukfUkCdcFpSWDULsAM4RQN2Du0VTttIWvs,3796
|
|
37
38
|
tunacode/tools/update_file.py,sha256=QQwdTHUbtfwjCa2sbbrf-d2uIfZY1SQH1wkvjKU9OlQ,4137
|
|
@@ -57,9 +58,9 @@ tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
|
|
|
57
58
|
tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
|
|
58
59
|
tunacode/utils/text_utils.py,sha256=B9M1cuLTm_SSsr15WNHF6j7WdLWPvWzKZV0Lvfgdbjg,2458
|
|
59
60
|
tunacode/utils/user_configuration.py,sha256=uFrpSRTUf0CijZjw1rOp1sovqy1uyr0UNcn85S6jvdk,1790
|
|
60
|
-
tunacode_cli-0.0.
|
|
61
|
-
tunacode_cli-0.0.
|
|
62
|
-
tunacode_cli-0.0.
|
|
63
|
-
tunacode_cli-0.0.
|
|
64
|
-
tunacode_cli-0.0.
|
|
65
|
-
tunacode_cli-0.0.
|
|
61
|
+
tunacode_cli-0.0.13.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
62
|
+
tunacode_cli-0.0.13.dist-info/METADATA,sha256=sJNahxSaLVB_QK6WoXW3SkDD0Wmh67MPSTmI0g_gub8,12837
|
|
63
|
+
tunacode_cli-0.0.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
64
|
+
tunacode_cli-0.0.13.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
|
|
65
|
+
tunacode_cli-0.0.13.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
|
|
66
|
+
tunacode_cli-0.0.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|