acontext 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {acontext-0.1.4 → acontext-0.1.6}/PKG-INFO +1 -1
- {acontext-0.1.4 → acontext-0.1.6}/pyproject.toml +1 -1
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/agent/prompts.py +4 -1
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/agent/sandbox.py +26 -10
- acontext-0.1.6/src/acontext/agent/text_editor.py +420 -0
- acontext-0.1.4/src/acontext/agent/text_editor.py +0 -436
- {acontext-0.1.4 → acontext-0.1.6}/README.md +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/__init__.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/_constants.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/_utils.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/agent/__init__.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/agent/base.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/agent/disk.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/agent/skill.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/async_client.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/client.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/client_types.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/errors.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/messages.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/py.typed +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/__init__.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/async_disks.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/async_sandboxes.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/async_sessions.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/async_skills.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/async_tools.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/async_users.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/disks.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/sandboxes.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/sessions.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/skills.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/tools.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/resources/users.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/__init__.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/common.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/disk.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/sandbox.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/session.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/skill.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/tool.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/types/user.py +0 -0
- {acontext-0.1.4 → acontext-0.1.6}/src/acontext/uploads.py +0 -0
|
@@ -92,5 +92,8 @@ Container environment:
|
|
|
92
92
|
- Standard Unix utilities available (grep, sed, awk, etc.)
|
|
93
93
|
- Archive tools: tar, unzip, zip
|
|
94
94
|
- Additional tools: ripgrep, fd, sqlite3, jq, imagemagick
|
|
95
|
-
-
|
|
95
|
+
- You can install new packages with pip if needed (internet access is available)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
Remember to always export your artifacts at the end of your task so that the user can view them.
|
|
96
99
|
"""
|
|
@@ -10,6 +10,15 @@ from .prompts import SANDBOX_TEXT_EDITOR_REMINDER, SANDBOX_BASH_REMINDER, SKILL_
|
|
|
10
10
|
from ..client import AcontextClient
|
|
11
11
|
from ..async_client import AcontextAsyncClient
|
|
12
12
|
|
|
13
|
+
MAX_OUTPUT_CHARS = 20000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def truncate_output(text: str, max_chars: int = MAX_OUTPUT_CHARS) -> str:
|
|
17
|
+
"""Truncate text to max_chars, appending a truncation flag if needed."""
|
|
18
|
+
if len(text) > max_chars:
|
|
19
|
+
return text[:max_chars] + "...[truncated]"
|
|
20
|
+
return text
|
|
21
|
+
|
|
13
22
|
|
|
14
23
|
class MountedSkill(TypedDict):
|
|
15
24
|
name: str
|
|
@@ -191,8 +200,8 @@ class BashTool(BaseTool):
|
|
|
191
200
|
|
|
192
201
|
return json.dumps(
|
|
193
202
|
{
|
|
194
|
-
"stdout": result.stdout,
|
|
195
|
-
"stderr": result.stderr,
|
|
203
|
+
"stdout": truncate_output(result.stdout),
|
|
204
|
+
"stderr": truncate_output(result.stderr),
|
|
196
205
|
"exit_code": result.exit_code,
|
|
197
206
|
}
|
|
198
207
|
)
|
|
@@ -213,8 +222,8 @@ class BashTool(BaseTool):
|
|
|
213
222
|
|
|
214
223
|
return json.dumps(
|
|
215
224
|
{
|
|
216
|
-
"stdout": result.stdout,
|
|
217
|
-
"stderr": result.stderr,
|
|
225
|
+
"stdout": truncate_output(result.stdout),
|
|
226
|
+
"stderr": truncate_output(result.stderr),
|
|
218
227
|
"exit_code": result.exit_code,
|
|
219
228
|
}
|
|
220
229
|
)
|
|
@@ -248,27 +257,34 @@ class TextEditorTool(BaseTool):
|
|
|
248
257
|
"command": {
|
|
249
258
|
"type": "string",
|
|
250
259
|
"enum": ["view", "create", "str_replace"],
|
|
251
|
-
"description":
|
|
260
|
+
"description": (
|
|
261
|
+
"Perform only text operations: 'view', 'create', or 'str_replace'. "
|
|
262
|
+
"Required parameters per command: "
|
|
263
|
+
"'view' requires path (view_range is optional); "
|
|
264
|
+
"'create' requires path and file_text; "
|
|
265
|
+
"'str_replace' requires path, old_str, and new_str."
|
|
266
|
+
),
|
|
252
267
|
},
|
|
253
268
|
"path": {
|
|
254
269
|
"type": "string",
|
|
255
|
-
"description": "The file path in the sandbox (e.g., '/workspace/script.py')",
|
|
270
|
+
"description": "Required for all commands. The file path in the sandbox (e.g., '/workspace/script.py')",
|
|
256
271
|
},
|
|
257
272
|
"file_text": {
|
|
258
273
|
"type": ["string", "null"],
|
|
259
|
-
"description": "
|
|
274
|
+
"description": "Required for 'create' command. The content to write to the file.",
|
|
260
275
|
},
|
|
261
276
|
"old_str": {
|
|
262
277
|
"type": ["string", "null"],
|
|
263
|
-
"description": "
|
|
278
|
+
"description": "Required for 'str_replace' command. The exact string to find and replace.",
|
|
264
279
|
},
|
|
265
280
|
"new_str": {
|
|
266
281
|
"type": ["string", "null"],
|
|
267
|
-
"description": "
|
|
282
|
+
"description": "Required for 'str_replace' command. The string to replace old_str with.",
|
|
268
283
|
},
|
|
269
284
|
"view_range": {
|
|
270
285
|
"type": ["array", "null"],
|
|
271
|
-
"
|
|
286
|
+
"items": {"type": "integer"},
|
|
287
|
+
"description": "Optional for 'view' command. An array [start_line, end_line] to view specific lines. If not provided, shows the first 200 lines.",
|
|
272
288
|
},
|
|
273
289
|
}
|
|
274
290
|
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Text editor file operations for sandbox environments."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import posixpath
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .sandbox import AsyncSandboxContext, SandboxContext
|
|
9
|
+
|
|
10
|
+
MAX_CONTENT_CHARS = 20000
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def truncate_content(text: str, max_chars: int = MAX_CONTENT_CHARS) -> str:
|
|
14
|
+
"""Truncate text to max_chars, appending a truncation flag if needed."""
|
|
15
|
+
if len(text) > max_chars:
|
|
16
|
+
return text[:max_chars] + "...[truncated]"
|
|
17
|
+
return text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def escape_for_shell(s: str) -> str:
|
|
21
|
+
"""Escape a string for safe use in shell commands."""
|
|
22
|
+
# Use single quotes and escape any single quotes in the string
|
|
23
|
+
return "'" + s.replace("'", "'\"'\"'") + "'"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Sync Operations
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def view_file(
|
|
32
|
+
ctx: "SandboxContext", path: str, view_range: list | None, timeout: float | None
|
|
33
|
+
) -> dict:
|
|
34
|
+
"""View file content with line numbers.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
ctx: The sandbox context.
|
|
38
|
+
path: The file path to view.
|
|
39
|
+
view_range: Optional [start_line, end_line] to view specific lines.
|
|
40
|
+
timeout: Optional timeout for command execution.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A dict with file content and metadata, or error information.
|
|
44
|
+
"""
|
|
45
|
+
escaped_path = escape_for_shell(path)
|
|
46
|
+
|
|
47
|
+
# Build combined command: check existence, get total lines, and view content in one exec
|
|
48
|
+
if view_range and len(view_range) == 2:
|
|
49
|
+
start_line, end_line = view_range
|
|
50
|
+
view_cmd = (
|
|
51
|
+
f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
max_lines = 200
|
|
55
|
+
view_cmd = f"head -n {max_lines} {escaped_path} | nl -ba"
|
|
56
|
+
start_line = 1
|
|
57
|
+
|
|
58
|
+
# Single combined command: outputs "TOTAL:<n>" on first line, then file content
|
|
59
|
+
cmd = (
|
|
60
|
+
f"if [ ! -f {escaped_path} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; "
|
|
61
|
+
f'echo "TOTAL:$(wc -l < {escaped_path})"; {view_cmd}'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
result = ctx.client.sandboxes.exec_command(
|
|
65
|
+
sandbox_id=ctx.sandbox_id,
|
|
66
|
+
command=cmd,
|
|
67
|
+
timeout=timeout,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if result.exit_code != 0 or "FILE_NOT_FOUND" in result.stdout:
|
|
71
|
+
return {
|
|
72
|
+
"error": f"File not found: {path}",
|
|
73
|
+
"stderr": result.stderr,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Parse output: first line is "TOTAL:<n>", rest is content
|
|
77
|
+
lines = result.stdout.split("\n", 1)
|
|
78
|
+
total_lines = 0
|
|
79
|
+
content = ""
|
|
80
|
+
|
|
81
|
+
if lines and lines[0].startswith("TOTAL:"):
|
|
82
|
+
total_str = lines[0][6:].strip()
|
|
83
|
+
total_lines = int(total_str) if total_str.isdigit() else 0
|
|
84
|
+
content = lines[1] if len(lines) > 1 else ""
|
|
85
|
+
|
|
86
|
+
content_lines = content.rstrip("\n").split("\n") if content.strip() else []
|
|
87
|
+
num_lines = len(content_lines)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"file_type": "text",
|
|
91
|
+
"content": truncate_content(content),
|
|
92
|
+
"numLines": num_lines,
|
|
93
|
+
"startLine": start_line if view_range else 1,
|
|
94
|
+
"totalLines": total_lines + 1, # wc -l doesn't count last line without newline
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_file(
|
|
99
|
+
ctx: "SandboxContext", path: str, file_text: str, timeout: float | None
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Create a new file with content.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
ctx: The sandbox context.
|
|
105
|
+
path: The file path to create.
|
|
106
|
+
file_text: The content to write to the file.
|
|
107
|
+
timeout: Optional timeout for command execution.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A dict with creation status or error information.
|
|
111
|
+
"""
|
|
112
|
+
escaped_path = escape_for_shell(path)
|
|
113
|
+
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
114
|
+
|
|
115
|
+
# Get directory path for mkdir
|
|
116
|
+
dir_path = posixpath.dirname(path)
|
|
117
|
+
mkdir_part = f"mkdir -p {escape_for_shell(dir_path)} && " if dir_path else ""
|
|
118
|
+
|
|
119
|
+
# Single combined command: check existence, create dir, write file
|
|
120
|
+
cmd = (
|
|
121
|
+
f"is_update=$(test -f {escaped_path} && echo 1 || echo 0); "
|
|
122
|
+
f"{mkdir_part}"
|
|
123
|
+
f"echo {escape_for_shell(encoded_content)} | base64 -d > {escaped_path} && "
|
|
124
|
+
f'echo "STATUS:$is_update"'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
result = ctx.client.sandboxes.exec_command(
|
|
128
|
+
sandbox_id=ctx.sandbox_id,
|
|
129
|
+
command=cmd,
|
|
130
|
+
timeout=timeout,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result.exit_code != 0 or "STATUS:" not in result.stdout:
|
|
134
|
+
return {
|
|
135
|
+
"error": f"Failed to create file: {path}",
|
|
136
|
+
"stderr": result.stderr,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
is_update = "STATUS:1" in result.stdout
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"is_file_update": is_update,
|
|
143
|
+
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def str_replace(
|
|
148
|
+
ctx: "SandboxContext", path: str, old_str: str, new_str: str, timeout: float | None
|
|
149
|
+
) -> dict:
|
|
150
|
+
"""Replace a string in a file.
|
|
151
|
+
|
|
152
|
+
Uses a Python script on the sandbox to avoid transferring the entire file.
|
|
153
|
+
Only the base64-encoded old_str and new_str are sent.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
ctx: The sandbox context.
|
|
157
|
+
path: The file path to modify.
|
|
158
|
+
old_str: The exact string to find and replace.
|
|
159
|
+
new_str: The string to replace old_str with.
|
|
160
|
+
timeout: Optional timeout for command execution.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
A dict with success message or error details.
|
|
164
|
+
"""
|
|
165
|
+
old_b64 = base64.b64encode(old_str.encode()).decode()
|
|
166
|
+
new_b64 = base64.b64encode(new_str.encode()).decode()
|
|
167
|
+
|
|
168
|
+
# Write Python script to a temp file and execute it
|
|
169
|
+
# This avoids shell escaping issues with inline python -c
|
|
170
|
+
py_script = f'''import sys, base64, os
|
|
171
|
+
old = base64.b64decode("{old_b64}").decode()
|
|
172
|
+
new = base64.b64decode("{new_b64}").decode()
|
|
173
|
+
path = "{path}"
|
|
174
|
+
if not os.path.exists(path):
|
|
175
|
+
print("FILE_NOT_FOUND")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
with open(path, "r") as f:
|
|
178
|
+
content = f.read()
|
|
179
|
+
count = content.count(old)
|
|
180
|
+
if count == 0:
|
|
181
|
+
print("NOT_FOUND")
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
if count > 1:
|
|
184
|
+
print(f"MULTIPLE:{{count}}")
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
with open(path, "w") as f:
|
|
187
|
+
f.write(content.replace(old, new, 1))
|
|
188
|
+
print("SUCCESS")
|
|
189
|
+
'''
|
|
190
|
+
# Base64 encode the script itself to avoid any escaping issues
|
|
191
|
+
script_b64 = base64.b64encode(py_script.encode()).decode()
|
|
192
|
+
cmd = f"echo {escape_for_shell(script_b64)} | base64 -d | python3"
|
|
193
|
+
|
|
194
|
+
result = ctx.client.sandboxes.exec_command(
|
|
195
|
+
sandbox_id=ctx.sandbox_id,
|
|
196
|
+
command=cmd,
|
|
197
|
+
timeout=timeout,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
output = result.stdout.strip()
|
|
201
|
+
|
|
202
|
+
if result.exit_code != 0 or output == "FILE_NOT_FOUND":
|
|
203
|
+
return {"error": f"File not found: {path}", "stderr": result.stderr}
|
|
204
|
+
|
|
205
|
+
if output == "NOT_FOUND":
|
|
206
|
+
return {"error": f"String not found in file: {old_str[:50]}..."}
|
|
207
|
+
|
|
208
|
+
if output.startswith("MULTIPLE:"):
|
|
209
|
+
count = output.split(":")[1]
|
|
210
|
+
return {
|
|
211
|
+
"error": f"Multiple occurrences ({count}) of the string found. "
|
|
212
|
+
"Please provide more context to make the match unique."
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if output == "SUCCESS":
|
|
216
|
+
return {"msg": "Successfully replaced text at exactly one location."}
|
|
217
|
+
|
|
218
|
+
return {"error": f"Unexpected response: {output}", "stderr": result.stderr}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ============================================================================
|
|
222
|
+
# Async Operations
|
|
223
|
+
# ============================================================================
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def async_view_file(
|
|
227
|
+
ctx: "AsyncSandboxContext",
|
|
228
|
+
path: str,
|
|
229
|
+
view_range: list | None,
|
|
230
|
+
timeout: float | None,
|
|
231
|
+
) -> dict:
|
|
232
|
+
"""View file content with line numbers (async).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
ctx: The async sandbox context.
|
|
236
|
+
path: The file path to view.
|
|
237
|
+
view_range: Optional [start_line, end_line] to view specific lines.
|
|
238
|
+
timeout: Optional timeout for command execution.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A dict with file content and metadata, or error information.
|
|
242
|
+
"""
|
|
243
|
+
escaped_path = escape_for_shell(path)
|
|
244
|
+
|
|
245
|
+
# Build combined command: check existence, get total lines, and view content in one exec
|
|
246
|
+
if view_range and len(view_range) == 2:
|
|
247
|
+
start_line, end_line = view_range
|
|
248
|
+
view_cmd = (
|
|
249
|
+
f"sed -n '{start_line},{end_line}p' {escaped_path} | nl -ba -v {start_line}"
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
max_lines = 200
|
|
253
|
+
view_cmd = f"head -n {max_lines} {escaped_path} | nl -ba"
|
|
254
|
+
start_line = 1
|
|
255
|
+
|
|
256
|
+
# Single combined command: outputs "TOTAL:<n>" on first line, then file content
|
|
257
|
+
cmd = (
|
|
258
|
+
f"if [ ! -f {escaped_path} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; "
|
|
259
|
+
f'echo "TOTAL:$(wc -l < {escaped_path})"; {view_cmd}'
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
result = await ctx.client.sandboxes.exec_command(
|
|
263
|
+
sandbox_id=ctx.sandbox_id,
|
|
264
|
+
command=cmd,
|
|
265
|
+
timeout=timeout,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if result.exit_code != 0 or "FILE_NOT_FOUND" in result.stdout:
|
|
269
|
+
return {
|
|
270
|
+
"error": f"File not found: {path}",
|
|
271
|
+
"stderr": result.stderr,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Parse output: first line is "TOTAL:<n>", rest is content
|
|
275
|
+
lines = result.stdout.split("\n", 1)
|
|
276
|
+
total_lines = 0
|
|
277
|
+
content = ""
|
|
278
|
+
|
|
279
|
+
if lines and lines[0].startswith("TOTAL:"):
|
|
280
|
+
total_str = lines[0][6:].strip()
|
|
281
|
+
total_lines = int(total_str) if total_str.isdigit() else 0
|
|
282
|
+
content = lines[1] if len(lines) > 1 else ""
|
|
283
|
+
|
|
284
|
+
content_lines = content.rstrip("\n").split("\n") if content.strip() else []
|
|
285
|
+
num_lines = len(content_lines)
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
"file_type": "text",
|
|
289
|
+
"content": truncate_content(content),
|
|
290
|
+
"numLines": num_lines,
|
|
291
|
+
"startLine": start_line if view_range else 1,
|
|
292
|
+
"totalLines": total_lines + 1,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
async def async_create_file(
|
|
297
|
+
ctx: "AsyncSandboxContext", path: str, file_text: str, timeout: float | None
|
|
298
|
+
) -> dict:
|
|
299
|
+
"""Create a new file with content (async).
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
ctx: The async sandbox context.
|
|
303
|
+
path: The file path to create.
|
|
304
|
+
file_text: The content to write to the file.
|
|
305
|
+
timeout: Optional timeout for command execution.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
A dict with creation status or error information.
|
|
309
|
+
"""
|
|
310
|
+
escaped_path = escape_for_shell(path)
|
|
311
|
+
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
312
|
+
|
|
313
|
+
# Get directory path for mkdir
|
|
314
|
+
dir_path = posixpath.dirname(path)
|
|
315
|
+
mkdir_part = f"mkdir -p {escape_for_shell(dir_path)} && " if dir_path else ""
|
|
316
|
+
|
|
317
|
+
# Single combined command: check existence, create dir, write file
|
|
318
|
+
cmd = (
|
|
319
|
+
f"is_update=$(test -f {escaped_path} && echo 1 || echo 0); "
|
|
320
|
+
f"{mkdir_part}"
|
|
321
|
+
f"echo {escape_for_shell(encoded_content)} | base64 -d > {escaped_path} && "
|
|
322
|
+
f'echo "STATUS:$is_update"'
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
result = await ctx.client.sandboxes.exec_command(
|
|
326
|
+
sandbox_id=ctx.sandbox_id,
|
|
327
|
+
command=cmd,
|
|
328
|
+
timeout=timeout,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if result.exit_code != 0 or "STATUS:" not in result.stdout:
|
|
332
|
+
return {
|
|
333
|
+
"error": f"Failed to create file: {path}",
|
|
334
|
+
"stderr": result.stderr,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
is_update = "STATUS:1" in result.stdout
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
"is_file_update": is_update,
|
|
341
|
+
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def async_str_replace(
|
|
346
|
+
ctx: "AsyncSandboxContext",
|
|
347
|
+
path: str,
|
|
348
|
+
old_str: str,
|
|
349
|
+
new_str: str,
|
|
350
|
+
timeout: float | None,
|
|
351
|
+
) -> dict:
|
|
352
|
+
"""Replace a string in a file (async).
|
|
353
|
+
|
|
354
|
+
Uses a Python script on the sandbox to avoid transferring the entire file.
|
|
355
|
+
Only the base64-encoded old_str and new_str are sent.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
ctx: The async sandbox context.
|
|
359
|
+
path: The file path to modify.
|
|
360
|
+
old_str: The exact string to find and replace.
|
|
361
|
+
new_str: The string to replace old_str with.
|
|
362
|
+
timeout: Optional timeout for command execution.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
A dict with success message or error details.
|
|
366
|
+
"""
|
|
367
|
+
old_b64 = base64.b64encode(old_str.encode()).decode()
|
|
368
|
+
new_b64 = base64.b64encode(new_str.encode()).decode()
|
|
369
|
+
|
|
370
|
+
# Write Python script to a temp file and execute it
|
|
371
|
+
# This avoids shell escaping issues with inline python -c
|
|
372
|
+
py_script = f'''import sys, base64, os
|
|
373
|
+
old = base64.b64decode("{old_b64}").decode()
|
|
374
|
+
new = base64.b64decode("{new_b64}").decode()
|
|
375
|
+
path = "{path}"
|
|
376
|
+
if not os.path.exists(path):
|
|
377
|
+
print("FILE_NOT_FOUND")
|
|
378
|
+
sys.exit(1)
|
|
379
|
+
with open(path, "r") as f:
|
|
380
|
+
content = f.read()
|
|
381
|
+
count = content.count(old)
|
|
382
|
+
if count == 0:
|
|
383
|
+
print("NOT_FOUND")
|
|
384
|
+
sys.exit(0)
|
|
385
|
+
if count > 1:
|
|
386
|
+
print(f"MULTIPLE:{{count}}")
|
|
387
|
+
sys.exit(0)
|
|
388
|
+
with open(path, "w") as f:
|
|
389
|
+
f.write(content.replace(old, new, 1))
|
|
390
|
+
print("SUCCESS")
|
|
391
|
+
'''
|
|
392
|
+
# Base64 encode the script itself to avoid any escaping issues
|
|
393
|
+
script_b64 = base64.b64encode(py_script.encode()).decode()
|
|
394
|
+
cmd = f"echo {escape_for_shell(script_b64)} | base64 -d | python3"
|
|
395
|
+
|
|
396
|
+
result = await ctx.client.sandboxes.exec_command(
|
|
397
|
+
sandbox_id=ctx.sandbox_id,
|
|
398
|
+
command=cmd,
|
|
399
|
+
timeout=timeout,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
output = result.stdout.strip()
|
|
403
|
+
|
|
404
|
+
if result.exit_code != 0 or output == "FILE_NOT_FOUND":
|
|
405
|
+
return {"error": f"File not found: {path}", "stderr": result.stderr}
|
|
406
|
+
|
|
407
|
+
if output == "NOT_FOUND":
|
|
408
|
+
return {"error": f"String not found in file: {old_str[:50]}..."}
|
|
409
|
+
|
|
410
|
+
if output.startswith("MULTIPLE:"):
|
|
411
|
+
count = output.split(":")[1]
|
|
412
|
+
return {
|
|
413
|
+
"error": f"Multiple occurrences ({count}) of the string found. "
|
|
414
|
+
"Please provide more context to make the match unique."
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if output == "SUCCESS":
|
|
418
|
+
return {"msg": "Successfully replaced text at exactly one location."}
|
|
419
|
+
|
|
420
|
+
return {"error": f"Unexpected response: {output}", "stderr": result.stderr}
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
"""Text editor file operations for sandbox environments."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from .sandbox import AsyncSandboxContext, SandboxContext
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def escape_for_shell(s: str) -> str:
|
|
11
|
-
"""Escape a string for safe use in shell commands."""
|
|
12
|
-
# Use single quotes and escape any single quotes in the string
|
|
13
|
-
return "'" + s.replace("'", "'\"'\"'") + "'"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# ============================================================================
|
|
17
|
-
# Sync Operations
|
|
18
|
-
# ============================================================================
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def view_file(
|
|
22
|
-
ctx: "SandboxContext", path: str, view_range: list | None, timeout: float | None
|
|
23
|
-
) -> dict:
|
|
24
|
-
"""View file content with line numbers.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
ctx: The sandbox context.
|
|
28
|
-
path: The file path to view.
|
|
29
|
-
view_range: Optional [start_line, end_line] to view specific lines.
|
|
30
|
-
timeout: Optional timeout for command execution.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
A dict with file content and metadata, or error information.
|
|
34
|
-
"""
|
|
35
|
-
# First check if file exists and get total lines
|
|
36
|
-
check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
|
|
37
|
-
result = ctx.client.sandboxes.exec_command(
|
|
38
|
-
sandbox_id=ctx.sandbox_id,
|
|
39
|
-
command=check_cmd,
|
|
40
|
-
timeout=timeout,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
|
|
44
|
-
return {
|
|
45
|
-
"error": f"File not found: {path}",
|
|
46
|
-
"stderr": result.stderr,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
50
|
-
|
|
51
|
-
# Build the view command with line numbers
|
|
52
|
-
if view_range and len(view_range) == 2:
|
|
53
|
-
start_line, end_line = view_range
|
|
54
|
-
cmd = f"sed -n '{start_line},{end_line}p' {escape_for_shell(path)} | nl -ba -v {start_line}"
|
|
55
|
-
else:
|
|
56
|
-
cmd = f"nl -ba {escape_for_shell(path)}"
|
|
57
|
-
start_line = 1
|
|
58
|
-
|
|
59
|
-
result = ctx.client.sandboxes.exec_command(
|
|
60
|
-
sandbox_id=ctx.sandbox_id,
|
|
61
|
-
command=cmd,
|
|
62
|
-
timeout=timeout,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
if result.exit_code != 0:
|
|
66
|
-
return {
|
|
67
|
-
"error": f"Failed to view file: {path}",
|
|
68
|
-
"stderr": result.stderr,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# Count lines in output
|
|
72
|
-
content_lines = (
|
|
73
|
-
result.stdout.rstrip("\n").split("\n") if result.stdout.strip() else []
|
|
74
|
-
)
|
|
75
|
-
num_lines = len(content_lines)
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
"file_type": "text",
|
|
79
|
-
"content": result.stdout,
|
|
80
|
-
"numLines": num_lines,
|
|
81
|
-
"startLine": start_line if view_range else 1,
|
|
82
|
-
"totalLines": total_lines + 1, # wc -l doesn't count last line without newline
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def create_file(
|
|
87
|
-
ctx: "SandboxContext", path: str, file_text: str, timeout: float | None
|
|
88
|
-
) -> dict:
|
|
89
|
-
"""Create a new file with content.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
ctx: The sandbox context.
|
|
93
|
-
path: The file path to create.
|
|
94
|
-
file_text: The content to write to the file.
|
|
95
|
-
timeout: Optional timeout for command execution.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
A dict with creation status or error information.
|
|
99
|
-
"""
|
|
100
|
-
# Check if file already exists
|
|
101
|
-
check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
|
|
102
|
-
check_result = ctx.client.sandboxes.exec_command(
|
|
103
|
-
sandbox_id=ctx.sandbox_id,
|
|
104
|
-
command=check_cmd,
|
|
105
|
-
timeout=timeout,
|
|
106
|
-
)
|
|
107
|
-
is_update = "EXISTS" in check_result.stdout
|
|
108
|
-
|
|
109
|
-
# Create directory if needed
|
|
110
|
-
dir_path = "/".join(path.split("/")[:-1])
|
|
111
|
-
if dir_path:
|
|
112
|
-
mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
|
|
113
|
-
ctx.client.sandboxes.exec_command(
|
|
114
|
-
sandbox_id=ctx.sandbox_id,
|
|
115
|
-
command=mkdir_cmd,
|
|
116
|
-
timeout=timeout,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Write file using base64 encoding to safely transfer content
|
|
120
|
-
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
121
|
-
write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
|
|
122
|
-
|
|
123
|
-
result = ctx.client.sandboxes.exec_command(
|
|
124
|
-
sandbox_id=ctx.sandbox_id,
|
|
125
|
-
command=write_cmd,
|
|
126
|
-
timeout=timeout,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
if result.exit_code != 0:
|
|
130
|
-
return {
|
|
131
|
-
"error": f"Failed to create file: {path}",
|
|
132
|
-
"stderr": result.stderr,
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
"is_file_update": is_update,
|
|
137
|
-
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def str_replace(
|
|
142
|
-
ctx: "SandboxContext", path: str, old_str: str, new_str: str, timeout: float | None
|
|
143
|
-
) -> dict:
|
|
144
|
-
"""Replace a string in a file.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
ctx: The sandbox context.
|
|
148
|
-
path: The file path to modify.
|
|
149
|
-
old_str: The exact string to find and replace.
|
|
150
|
-
new_str: The string to replace old_str with.
|
|
151
|
-
timeout: Optional timeout for command execution.
|
|
152
|
-
|
|
153
|
-
Returns:
|
|
154
|
-
A dict with diff information or error details.
|
|
155
|
-
"""
|
|
156
|
-
# First read the file content
|
|
157
|
-
read_cmd = f"cat {escape_for_shell(path)}"
|
|
158
|
-
result = ctx.client.sandboxes.exec_command(
|
|
159
|
-
sandbox_id=ctx.sandbox_id,
|
|
160
|
-
command=read_cmd,
|
|
161
|
-
timeout=timeout,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if result.exit_code != 0:
|
|
165
|
-
return {
|
|
166
|
-
"error": f"File not found: {path}",
|
|
167
|
-
"stderr": result.stderr,
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
original_content = result.stdout
|
|
171
|
-
|
|
172
|
-
# Check if old_str exists in the file
|
|
173
|
-
if old_str not in original_content:
|
|
174
|
-
return {
|
|
175
|
-
"error": f"String not found in file: {old_str[:50]}...",
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
# Count occurrences
|
|
179
|
-
occurrences = original_content.count(old_str)
|
|
180
|
-
if occurrences > 1:
|
|
181
|
-
return {
|
|
182
|
-
"error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
# Perform the replacement
|
|
186
|
-
new_content = original_content.replace(old_str, new_str, 1)
|
|
187
|
-
|
|
188
|
-
# Find the line numbers affected
|
|
189
|
-
old_lines = original_content.split("\n")
|
|
190
|
-
new_lines = new_content.split("\n")
|
|
191
|
-
|
|
192
|
-
# Find where the change starts
|
|
193
|
-
old_start = 1
|
|
194
|
-
for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
|
|
195
|
-
if old_line != new_line:
|
|
196
|
-
old_start = i + 1
|
|
197
|
-
break
|
|
198
|
-
|
|
199
|
-
# Write the new content
|
|
200
|
-
encoded_content = base64.b64encode(new_content.encode()).decode()
|
|
201
|
-
write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
|
|
202
|
-
|
|
203
|
-
result = ctx.client.sandboxes.exec_command(
|
|
204
|
-
sandbox_id=ctx.sandbox_id,
|
|
205
|
-
command=write_cmd,
|
|
206
|
-
timeout=timeout,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if result.exit_code != 0:
|
|
210
|
-
return {
|
|
211
|
-
"error": f"Failed to write file: {path}",
|
|
212
|
-
"stderr": result.stderr,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
# Calculate diff info
|
|
216
|
-
old_str_lines = old_str.count("\n") + 1
|
|
217
|
-
new_str_lines = new_str.count("\n") + 1
|
|
218
|
-
|
|
219
|
-
# Build diff lines
|
|
220
|
-
diff_lines = []
|
|
221
|
-
for line in old_str.split("\n"):
|
|
222
|
-
diff_lines.append(f"-{line}")
|
|
223
|
-
for line in new_str.split("\n"):
|
|
224
|
-
diff_lines.append(f"+{line}")
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
"oldStart": old_start,
|
|
228
|
-
"oldLines": old_str_lines,
|
|
229
|
-
"newStart": old_start,
|
|
230
|
-
"newLines": new_str_lines,
|
|
231
|
-
"lines": diff_lines,
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# ============================================================================
|
|
236
|
-
# Async Operations
|
|
237
|
-
# ============================================================================
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
async def async_view_file(
|
|
241
|
-
ctx: "AsyncSandboxContext", path: str, view_range: list | None, timeout: float | None
|
|
242
|
-
) -> dict:
|
|
243
|
-
"""View file content with line numbers (async).
|
|
244
|
-
|
|
245
|
-
Args:
|
|
246
|
-
ctx: The async sandbox context.
|
|
247
|
-
path: The file path to view.
|
|
248
|
-
view_range: Optional [start_line, end_line] to view specific lines.
|
|
249
|
-
timeout: Optional timeout for command execution.
|
|
250
|
-
|
|
251
|
-
Returns:
|
|
252
|
-
A dict with file content and metadata, or error information.
|
|
253
|
-
"""
|
|
254
|
-
check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
|
|
255
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
256
|
-
sandbox_id=ctx.sandbox_id,
|
|
257
|
-
command=check_cmd,
|
|
258
|
-
timeout=timeout,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
|
|
262
|
-
return {
|
|
263
|
-
"error": f"File not found: {path}",
|
|
264
|
-
"stderr": result.stderr,
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
268
|
-
|
|
269
|
-
if view_range and len(view_range) == 2:
|
|
270
|
-
start_line, end_line = view_range
|
|
271
|
-
cmd = f"sed -n '{start_line},{end_line}p' {escape_for_shell(path)} | nl -ba -v {start_line}"
|
|
272
|
-
else:
|
|
273
|
-
cmd = f"nl -ba {escape_for_shell(path)}"
|
|
274
|
-
start_line = 1
|
|
275
|
-
|
|
276
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
277
|
-
sandbox_id=ctx.sandbox_id,
|
|
278
|
-
command=cmd,
|
|
279
|
-
timeout=timeout,
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
if result.exit_code != 0:
|
|
283
|
-
return {
|
|
284
|
-
"error": f"Failed to view file: {path}",
|
|
285
|
-
"stderr": result.stderr,
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
content_lines = (
|
|
289
|
-
result.stdout.rstrip("\n").split("\n") if result.stdout.strip() else []
|
|
290
|
-
)
|
|
291
|
-
num_lines = len(content_lines)
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
"file_type": "text",
|
|
295
|
-
"content": result.stdout,
|
|
296
|
-
"numLines": num_lines,
|
|
297
|
-
"startLine": start_line if view_range else 1,
|
|
298
|
-
"totalLines": total_lines + 1,
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
async def async_create_file(
|
|
303
|
-
ctx: "AsyncSandboxContext", path: str, file_text: str, timeout: float | None
|
|
304
|
-
) -> dict:
|
|
305
|
-
"""Create a new file with content (async).
|
|
306
|
-
|
|
307
|
-
Args:
|
|
308
|
-
ctx: The async sandbox context.
|
|
309
|
-
path: The file path to create.
|
|
310
|
-
file_text: The content to write to the file.
|
|
311
|
-
timeout: Optional timeout for command execution.
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
A dict with creation status or error information.
|
|
315
|
-
"""
|
|
316
|
-
check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
|
|
317
|
-
check_result = await ctx.client.sandboxes.exec_command(
|
|
318
|
-
sandbox_id=ctx.sandbox_id,
|
|
319
|
-
command=check_cmd,
|
|
320
|
-
timeout=timeout,
|
|
321
|
-
)
|
|
322
|
-
is_update = "EXISTS" in check_result.stdout
|
|
323
|
-
|
|
324
|
-
dir_path = "/".join(path.split("/")[:-1])
|
|
325
|
-
if dir_path:
|
|
326
|
-
mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
|
|
327
|
-
await ctx.client.sandboxes.exec_command(
|
|
328
|
-
sandbox_id=ctx.sandbox_id,
|
|
329
|
-
command=mkdir_cmd,
|
|
330
|
-
timeout=timeout,
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
encoded_content = base64.b64encode(file_text.encode()).decode()
|
|
334
|
-
write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
|
|
335
|
-
|
|
336
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
337
|
-
sandbox_id=ctx.sandbox_id,
|
|
338
|
-
command=write_cmd,
|
|
339
|
-
timeout=timeout,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
if result.exit_code != 0:
|
|
343
|
-
return {
|
|
344
|
-
"error": f"Failed to create file: {path}",
|
|
345
|
-
"stderr": result.stderr,
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
"is_file_update": is_update,
|
|
350
|
-
"message": f"File {'updated' if is_update else 'created'}: {path}",
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
async def async_str_replace(
|
|
355
|
-
ctx: "AsyncSandboxContext", path: str, old_str: str, new_str: str, timeout: float | None
|
|
356
|
-
) -> dict:
|
|
357
|
-
"""Replace a string in a file (async).
|
|
358
|
-
|
|
359
|
-
Args:
|
|
360
|
-
ctx: The async sandbox context.
|
|
361
|
-
path: The file path to modify.
|
|
362
|
-
old_str: The exact string to find and replace.
|
|
363
|
-
new_str: The string to replace old_str with.
|
|
364
|
-
timeout: Optional timeout for command execution.
|
|
365
|
-
|
|
366
|
-
Returns:
|
|
367
|
-
A dict with diff information or error details.
|
|
368
|
-
"""
|
|
369
|
-
read_cmd = f"cat {escape_for_shell(path)}"
|
|
370
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
371
|
-
sandbox_id=ctx.sandbox_id,
|
|
372
|
-
command=read_cmd,
|
|
373
|
-
timeout=timeout,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
if result.exit_code != 0:
|
|
377
|
-
return {
|
|
378
|
-
"error": f"File not found: {path}",
|
|
379
|
-
"stderr": result.stderr,
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
original_content = result.stdout
|
|
383
|
-
|
|
384
|
-
if old_str not in original_content:
|
|
385
|
-
return {
|
|
386
|
-
"error": f"String not found in file: {old_str[:50]}...",
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
occurrences = original_content.count(old_str)
|
|
390
|
-
if occurrences > 1:
|
|
391
|
-
return {
|
|
392
|
-
"error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
new_content = original_content.replace(old_str, new_str, 1)
|
|
396
|
-
|
|
397
|
-
old_lines = original_content.split("\n")
|
|
398
|
-
new_lines = new_content.split("\n")
|
|
399
|
-
|
|
400
|
-
old_start = 1
|
|
401
|
-
for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
|
|
402
|
-
if old_line != new_line:
|
|
403
|
-
old_start = i + 1
|
|
404
|
-
break
|
|
405
|
-
|
|
406
|
-
encoded_content = base64.b64encode(new_content.encode()).decode()
|
|
407
|
-
write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
|
|
408
|
-
|
|
409
|
-
result = await ctx.client.sandboxes.exec_command(
|
|
410
|
-
sandbox_id=ctx.sandbox_id,
|
|
411
|
-
command=write_cmd,
|
|
412
|
-
timeout=timeout,
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
if result.exit_code != 0:
|
|
416
|
-
return {
|
|
417
|
-
"error": f"Failed to write file: {path}",
|
|
418
|
-
"stderr": result.stderr,
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
old_str_lines = old_str.count("\n") + 1
|
|
422
|
-
new_str_lines = new_str.count("\n") + 1
|
|
423
|
-
|
|
424
|
-
diff_lines = []
|
|
425
|
-
for line in old_str.split("\n"):
|
|
426
|
-
diff_lines.append(f"-{line}")
|
|
427
|
-
for line in new_str.split("\n"):
|
|
428
|
-
diff_lines.append(f"+{line}")
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
"oldStart": old_start,
|
|
432
|
-
"oldLines": old_str_lines,
|
|
433
|
-
"newStart": old_start,
|
|
434
|
-
"newLines": new_str_lines,
|
|
435
|
-
"lines": diff_lines,
|
|
436
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|