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