tunacode-cli 0.0.12__py3-none-any.whl → 0.0.14__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/cli/commands.py +205 -14
- tunacode/cli/repl.py +41 -2
- tunacode/configuration/defaults.py +1 -0
- tunacode/constants.py +1 -1
- tunacode/core/agents/main.py +220 -2
- tunacode/core/state.py +10 -2
- tunacode/prompts/system.txt +22 -0
- tunacode/tools/__init__.py +1 -0
- tunacode/tools/bash.py +252 -0
- tunacode/tools/grep.py +760 -0
- tunacode/tools/read_file.py +15 -10
- tunacode/tools/run_command.py +13 -7
- tunacode/tools/update_file.py +9 -10
- tunacode/tools/write_file.py +8 -9
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.14.dist-info}/METADATA +50 -14
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.14.dist-info}/RECORD +20 -18
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.14.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.14.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.14.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.12.dist-info → tunacode_cli-0.0.14.dist-info}/top_level.txt +0 -0
tunacode/core/state.py
CHANGED
|
@@ -8,8 +8,15 @@ import uuid
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any, Optional
|
|
10
10
|
|
|
11
|
-
from tunacode.types import (
|
|
12
|
-
|
|
11
|
+
from tunacode.types import (
|
|
12
|
+
DeviceId,
|
|
13
|
+
InputSessions,
|
|
14
|
+
MessageHistory,
|
|
15
|
+
ModelName,
|
|
16
|
+
SessionId,
|
|
17
|
+
ToolName,
|
|
18
|
+
UserConfig,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
@dataclass
|
|
@@ -25,6 +32,7 @@ class SessionState:
|
|
|
25
32
|
tool_ignore: list[ToolName] = field(default_factory=list)
|
|
26
33
|
yolo: bool = False
|
|
27
34
|
undo_initialized: bool = False
|
|
35
|
+
show_thoughts: bool = False
|
|
28
36
|
session_id: SessionId = field(default_factory=lambda: str(uuid.uuid4()))
|
|
29
37
|
device_id: Optional[DeviceId] = None
|
|
30
38
|
input_sessions: InputSessions = field(default_factory=dict)
|
tunacode/prompts/system.txt
CHANGED
|
@@ -66,6 +66,28 @@ CORRECT: First `read_file("tools/base.py")` to see the base class, then `write_f
|
|
|
66
66
|
- `run_command("pwd")` - Show current directory
|
|
67
67
|
- `run_command("cat pyproject.toml | grep -A5 dependencies")` - Check dependencies
|
|
68
68
|
|
|
69
|
+
## ReAct Pattern: Reasoning and Acting
|
|
70
|
+
|
|
71
|
+
Follow this pattern for complex tasks:
|
|
72
|
+
|
|
73
|
+
1. **THINK**: Output {"thought": "I need to understand the task..."} to reason about what to do
|
|
74
|
+
2. **ACT**: Use tools to gather information or make changes
|
|
75
|
+
3. **OBSERVE**: Analyze tool outputs with {"thought": "The output shows..."}
|
|
76
|
+
4. **ITERATE**: Continue thinking and acting until the task is complete
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
- {"thought": "User wants me to analyze a file. I should first read it to understand its contents."}
|
|
80
|
+
- Use read_file tool
|
|
81
|
+
- {"thought": "The file contains Python code. I can see it needs optimization in the loop section."}
|
|
82
|
+
- Use update_file tool
|
|
83
|
+
|
|
84
|
+
**Key principles:**
|
|
85
|
+
- Always think before acting
|
|
86
|
+
- Use tools immediately after thinking
|
|
87
|
+
- Reason about tool outputs before continuing
|
|
88
|
+
- Break complex tasks into logical steps
|
|
89
|
+
|
|
69
90
|
USE YOUR TOOLS NOW!
|
|
70
91
|
|
|
71
92
|
If asked, you were created by the grifter tunahors
|
|
93
|
+
|
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)
|