acontext 0.1.3__py3-none-any.whl → 0.1.5__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.
@@ -0,0 +1,449 @@
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
+ }
acontext/async_client.py CHANGED
@@ -13,10 +13,8 @@ from .errors import APIError, TransportError
13
13
  from .messages import MessagePart as MessagePart
14
14
  from .uploads import FileUpload as FileUpload
15
15
  from .resources.async_disks import AsyncDisksAPI as AsyncDisksAPI
16
- from .resources.async_blocks import AsyncBlocksAPI as AsyncBlocksAPI
17
16
  from .resources.async_sandboxes import AsyncSandboxesAPI as AsyncSandboxesAPI
18
17
  from .resources.async_sessions import AsyncSessionsAPI as AsyncSessionsAPI
19
- from .resources.async_spaces import AsyncSpacesAPI as AsyncSpacesAPI
20
18
  from .resources.async_tools import AsyncToolsAPI as AsyncToolsAPI
21
19
  from .resources.async_skills import AsyncSkillsAPI as AsyncSkillsAPI
22
20
  from .resources.async_users import AsyncUsersAPI as AsyncUsersAPI
@@ -106,11 +104,9 @@ class AcontextAsyncClient:
106
104
 
107
105
  self._timeout = actual_timeout
108
106
 
109
- self.spaces = AsyncSpacesAPI(self)
110
107
  self.sessions = AsyncSessionsAPI(self)
111
108
  self.disks = AsyncDisksAPI(self)
112
109
  self.artifacts = self.disks.artifacts
113
- self.blocks = AsyncBlocksAPI(self)
114
110
  self.tools = AsyncToolsAPI(self)
115
111
  self.skills = AsyncSkillsAPI(self)
116
112
  self.users = AsyncUsersAPI(self)
@@ -160,7 +156,10 @@ class AcontextAsyncClient:
160
156
  data: Mapping[str, Any] | None = None,
161
157
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
162
158
  unwrap: bool = True,
159
+ timeout: float | None = None,
163
160
  ) -> Any:
161
+ # Use per-request timeout if provided, otherwise use client default
162
+ effective_timeout = timeout if timeout is not None else self._timeout
164
163
  try:
165
164
  response = await self._client.request(
166
165
  method=method,
@@ -169,7 +168,7 @@ class AcontextAsyncClient:
169
168
  json=json_data,
170
169
  data=data,
171
170
  files=files,
172
- timeout=self._timeout,
171
+ timeout=effective_timeout,
173
172
  )
174
173
  except httpx.HTTPError as exc: # pragma: no cover - passthrough to caller
175
174
  raise TransportError(str(exc)) from exc
acontext/client.py CHANGED
@@ -13,10 +13,8 @@ from .errors import APIError, TransportError
13
13
  from .messages import MessagePart as MessagePart
14
14
  from .uploads import FileUpload as FileUpload
15
15
  from .resources.disks import DisksAPI as DisksAPI
16
- from .resources.blocks import BlocksAPI as BlocksAPI
17
16
  from .resources.sandboxes import SandboxesAPI as SandboxesAPI
18
17
  from .resources.sessions import SessionsAPI as SessionsAPI
19
- from .resources.spaces import SpacesAPI as SpacesAPI
20
18
  from .resources.tools import ToolsAPI as ToolsAPI
21
19
  from .resources.skills import SkillsAPI as SkillsAPI
22
20
  from .resources.users import UsersAPI as UsersAPI
@@ -106,11 +104,9 @@ class AcontextClient:
106
104
 
107
105
  self._timeout = actual_timeout
108
106
 
109
- self.spaces = SpacesAPI(self)
110
107
  self.sessions = SessionsAPI(self)
111
108
  self.disks = DisksAPI(self)
112
109
  self.artifacts = self.disks.artifacts
113
- self.blocks = BlocksAPI(self)
114
110
  self.tools = ToolsAPI(self)
115
111
  self.skills = SkillsAPI(self)
116
112
  self.users = UsersAPI(self)
@@ -159,7 +155,10 @@ class AcontextClient:
159
155
  data: Mapping[str, Any] | None = None,
160
156
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
161
157
  unwrap: bool = True,
158
+ timeout: float | None = None,
162
159
  ) -> Any:
160
+ # Use per-request timeout if provided, otherwise use client default
161
+ effective_timeout = timeout if timeout is not None else self._timeout
163
162
  try:
164
163
  response = self._client.request(
165
164
  method=method,
@@ -168,7 +167,7 @@ class AcontextClient:
168
167
  json=json_data,
169
168
  data=data,
170
169
  files=files,
171
- timeout=self._timeout,
170
+ timeout=effective_timeout,
172
171
  )
173
172
  except httpx.HTTPError as exc: # pragma: no cover - passthrough to caller
174
173
  raise TransportError(str(exc)) from exc
acontext/client_types.py CHANGED
@@ -17,6 +17,7 @@ class RequesterProtocol(Protocol):
17
17
  data: Mapping[str, Any] | None = None,
18
18
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
19
19
  unwrap: bool = True,
20
+ timeout: float | None = None,
20
21
  ) -> Any:
21
22
  ...
22
23
 
@@ -32,5 +33,6 @@ class AsyncRequesterProtocol(Protocol):
32
33
  data: Mapping[str, Any] | None = None,
33
34
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
34
35
  unwrap: bool = True,
36
+ timeout: float | None = None,
35
37
  ) -> Awaitable[Any]:
36
38
  ...
@@ -1,18 +1,14 @@
1
1
  """Resource-specific API helpers for the Acontext client."""
2
2
 
3
- from .async_blocks import AsyncBlocksAPI
4
3
  from .async_disks import AsyncDisksAPI, AsyncDiskArtifactsAPI
5
4
  from .async_sandboxes import AsyncSandboxesAPI
6
5
  from .async_sessions import AsyncSessionsAPI
7
- from .async_spaces import AsyncSpacesAPI
8
6
  from .async_tools import AsyncToolsAPI
9
7
  from .async_skills import AsyncSkillsAPI
10
8
  from .async_users import AsyncUsersAPI
11
- from .blocks import BlocksAPI
12
9
  from .disks import DisksAPI, DiskArtifactsAPI
13
10
  from .sandboxes import SandboxesAPI
14
11
  from .sessions import SessionsAPI
15
- from .spaces import SpacesAPI
16
12
  from .tools import ToolsAPI
17
13
  from .skills import SkillsAPI
18
14
  from .users import UsersAPI
@@ -20,19 +16,15 @@ from .users import UsersAPI
20
16
  __all__ = [
21
17
  "DisksAPI",
22
18
  "DiskArtifactsAPI",
23
- "BlocksAPI",
24
19
  "SandboxesAPI",
25
20
  "SessionsAPI",
26
- "SpacesAPI",
27
21
  "ToolsAPI",
28
22
  "SkillsAPI",
29
23
  "UsersAPI",
30
24
  "AsyncDisksAPI",
31
25
  "AsyncDiskArtifactsAPI",
32
- "AsyncBlocksAPI",
33
26
  "AsyncSandboxesAPI",
34
27
  "AsyncSessionsAPI",
35
- "AsyncSpacesAPI",
36
28
  "AsyncToolsAPI",
37
29
  "AsyncSkillsAPI",
38
30
  "AsyncUsersAPI",
@@ -2,8 +2,10 @@
2
2
  Sandboxes endpoints (async).
3
3
  """
4
4
 
5
+ from .._utils import build_params
5
6
  from ..client_types import AsyncRequesterProtocol
6
7
  from ..types.sandbox import (
8
+ GetSandboxLogsOutput,
7
9
  SandboxCommandOutput,
8
10
  SandboxRuntimeInfo,
9
11
  )
@@ -28,12 +30,15 @@ class AsyncSandboxesAPI:
28
30
  *,
29
31
  sandbox_id: str,
30
32
  command: str,
33
+ timeout: float | None = None,
31
34
  ) -> SandboxCommandOutput:
32
35
  """Execute a shell command in the sandbox.
33
36
 
34
37
  Args:
35
38
  sandbox_id: The UUID of the sandbox.
36
39
  command: The shell command to execute.
40
+ timeout: Optional timeout in seconds for this command.
41
+ If not provided, uses the client's default timeout.
37
42
 
38
43
  Returns:
39
44
  SandboxCommandOutput containing stdout, stderr, and exit code.
@@ -42,6 +47,7 @@ class AsyncSandboxesAPI:
42
47
  "POST",
43
48
  f"/sandbox/{sandbox_id}/exec",
44
49
  json_data={"command": command},
50
+ timeout=timeout,
45
51
  )
46
52
  return SandboxCommandOutput.model_validate(data)
47
53
 
@@ -56,3 +62,24 @@ class AsyncSandboxesAPI:
56
62
  """
57
63
  data = await self._requester.request("DELETE", f"/sandbox/{sandbox_id}")
58
64
  return FlagResponse.model_validate(data)
65
+
66
+ async def get_logs(
67
+ self,
68
+ *,
69
+ limit: int | None = None,
70
+ cursor: str | None = None,
71
+ time_desc: bool | None = None,
72
+ ) -> GetSandboxLogsOutput:
73
+ """Get sandbox logs for the project with cursor-based pagination.
74
+
75
+ Args:
76
+ limit: Maximum number of logs to return (default 20, max 200).
77
+ cursor: Cursor for pagination. Use the cursor from the previous response to get the next page.
78
+ time_desc: Order by created_at descending if True, ascending if False (default False).
79
+
80
+ Returns:
81
+ GetSandboxLogsOutput containing the list of sandbox logs and pagination information.
82
+ """
83
+ params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
84
+ data = await self._requester.request("GET", "/sandbox/logs", params=params or None)
85
+ return GetSandboxLogsOutput.model_validate(data)