deepagents 1.7.0 → 1.7.2

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.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
2
  <a href="https://docs.langchain.com/oss/python/deepagents/overview#deep-agents-overview">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: light)" srcset=".github/images/logo-dark.svg">
5
- <source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-light.svg">
6
- <img alt="Deep Agents Logo" src=".github/images/logo-dark.svg" width="80%">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg">
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-light.svg">
6
+ <img alt="Deep Agents Logo" src="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg" width="80%">
7
7
  </picture>
8
8
  </a>
9
9
  </div>
package/dist/index.cjs CHANGED
@@ -61,6 +61,55 @@ node_os = __toESM(node_os);
61
61
  function isSandboxBackend(backend) {
62
62
  return typeof backend.execute === "function" && typeof backend.id === "string";
63
63
  }
64
+ const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
65
+ /**
66
+ * Custom error class for sandbox operations.
67
+ *
68
+ * @param message - Human-readable error description
69
+ * @param code - Structured error code for programmatic handling
70
+ * @returns SandboxError with message and code
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * try {
75
+ * await sandbox.execute("some command");
76
+ * } catch (error) {
77
+ * if (error instanceof SandboxError) {
78
+ * switch (error.code) {
79
+ * case "NOT_INITIALIZED":
80
+ * await sandbox.initialize();
81
+ * break;
82
+ * case "COMMAND_TIMEOUT":
83
+ * console.error("Command took too long");
84
+ * break;
85
+ * default:
86
+ * throw error;
87
+ * }
88
+ * }
89
+ * }
90
+ * ```
91
+ */
92
+ var SandboxError = class SandboxError extends Error {
93
+ /** Symbol for identifying sandbox error instances */
94
+ [SANDBOX_ERROR_SYMBOL] = true;
95
+ /** Error name for instanceof checks and logging */
96
+ name = "SandboxError";
97
+ /**
98
+ * Creates a new SandboxError.
99
+ *
100
+ * @param message - Human-readable error description
101
+ * @param code - Structured error code for programmatic handling
102
+ */
103
+ constructor(message, code, cause) {
104
+ super(message);
105
+ this.code = code;
106
+ this.cause = cause;
107
+ Object.setPrototypeOf(this, SandboxError.prototype);
108
+ }
109
+ static isInstance(error) {
110
+ return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
111
+ }
112
+ };
64
113
 
65
114
  //#endregion
66
115
  //#region src/backends/utils.ts
@@ -3236,279 +3285,176 @@ var CompositeBackend = class {
3236
3285
  //#endregion
3237
3286
  //#region src/backends/sandbox.ts
3238
3287
  /**
3239
- * Node.js command template for glob operations.
3240
- * Uses web-standard atob() for base64 decoding.
3288
+ * Shell-quote a string using single quotes (POSIX).
3289
+ * Escapes embedded single quotes with the '\'' technique.
3241
3290
  */
3242
- function buildGlobCommand(searchPath, pattern) {
3243
- return `node -e "
3244
- const fs = require('fs');
3245
- const path = require('path');
3246
-
3247
- const searchPath = atob('${btoa(searchPath)}');
3248
- const pattern = atob('${btoa(pattern)}');
3249
-
3250
- function globMatch(relativePath, pattern) {
3251
- const regexPattern = pattern
3252
- .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')
3253
- .replace(/\\*/g, '[^/]*')
3254
- .replace(/\\?/g, '.')
3255
- .replace(/<<<GLOBSTAR>>>/g, '.*');
3256
- return new RegExp('^' + regexPattern + '$').test(relativePath);
3257
- }
3258
-
3259
- function walkDir(dir, baseDir, results) {
3260
- try {
3261
- const entries = fs.readdirSync(dir, { withFileTypes: true });
3262
- for (const entry of entries) {
3263
- const fullPath = path.join(dir, entry.name);
3264
- const relativePath = path.relative(baseDir, fullPath);
3265
- if (entry.isDirectory()) {
3266
- walkDir(fullPath, baseDir, results);
3267
- } else if (globMatch(relativePath, pattern)) {
3268
- const stat = fs.statSync(fullPath);
3269
- console.log(JSON.stringify({
3270
- path: relativePath,
3271
- size: stat.size,
3272
- mtime: stat.mtimeMs,
3273
- isDir: false
3274
- }));
3275
- }
3276
- }
3277
- } catch (e) {
3278
- // Silent failure for non-existent paths
3279
- }
3280
- }
3281
-
3282
- try {
3283
- process.chdir(searchPath);
3284
- walkDir('.', '.', []);
3285
- } catch (e) {
3286
- // Silent failure for non-existent paths
3287
- }
3288
- "`;
3291
+ function shellQuote(s) {
3292
+ return "'" + s.replace(/'/g, "'\\''") + "'";
3289
3293
  }
3290
3294
  /**
3291
- * Node.js command template for listing directory contents.
3295
+ * Convert a glob pattern to a path-aware RegExp.
3296
+ *
3297
+ * Inspired by the just-bash project's glob utilities:
3298
+ * - `*` matches any characters except `/`
3299
+ * - `**` matches any characters including `/` (recursive)
3300
+ * - `?` matches a single character except `/`
3301
+ * - `[...]` character classes
3292
3302
  */
3293
- function buildLsCommand(dirPath) {
3294
- return `node -e "
3295
- const fs = require('fs');
3296
- const path = require('path');
3297
-
3298
- const dirPath = atob('${btoa(dirPath)}');
3299
-
3300
- try {
3301
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
3302
- for (const entry of entries) {
3303
- const fullPath = path.join(dirPath, entry.name);
3304
- const stat = fs.statSync(fullPath);
3305
- console.log(JSON.stringify({
3306
- path: entry.isDirectory() ? fullPath + '/' : fullPath,
3307
- size: stat.size,
3308
- mtime: stat.mtimeMs,
3309
- isDir: entry.isDirectory()
3310
- }));
3311
- }
3312
- } catch (e) {
3313
- console.error('Error: ' + e.message);
3314
- process.exit(1);
3315
- }
3316
- "`;
3303
+ function globToPathRegex(pattern) {
3304
+ let regex = "^";
3305
+ let i = 0;
3306
+ while (i < pattern.length) {
3307
+ const c = pattern[i];
3308
+ if (c === "*") if (i + 1 < pattern.length && pattern[i + 1] === "*") {
3309
+ i += 2;
3310
+ if (i < pattern.length && pattern[i] === "/") {
3311
+ regex += "(.*/)?";
3312
+ i++;
3313
+ } else regex += ".*";
3314
+ } else {
3315
+ regex += "[^/]*";
3316
+ i++;
3317
+ }
3318
+ else if (c === "?") {
3319
+ regex += "[^/]";
3320
+ i++;
3321
+ } else if (c === "[") {
3322
+ let j = i + 1;
3323
+ while (j < pattern.length && pattern[j] !== "]") j++;
3324
+ regex += pattern.slice(i, j + 1);
3325
+ i = j + 1;
3326
+ } else if (c === "." || c === "+" || c === "^" || c === "$" || c === "{" || c === "}" || c === "(" || c === ")" || c === "|" || c === "\\") {
3327
+ regex += `\\${c}`;
3328
+ i++;
3329
+ } else {
3330
+ regex += c;
3331
+ i++;
3332
+ }
3333
+ }
3334
+ regex += "$";
3335
+ return new RegExp(regex);
3317
3336
  }
3318
3337
  /**
3319
- * Node.js command template for reading files.
3338
+ * Parse a single line of stat output in the format: size\tmtime\ttype\tpath
3339
+ *
3340
+ * The first three tab-delimited fields are always fixed (number, number, string),
3341
+ * so we safely take everything after the third tab as the file path — even if the
3342
+ * path itself contains tabs.
3320
3343
  */
3321
- function buildReadCommand(filePath, offset, limit) {
3322
- return `node -e "
3323
- const fs = require('fs');
3324
-
3325
- const filePath = atob('${btoa(filePath)}');
3326
- const offset = ${Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0};
3327
- const limit = ${Number.isFinite(limit) && limit > 0 && limit < Number.MAX_SAFE_INTEGER ? Math.floor(limit) : 0};
3328
-
3329
- if (!fs.existsSync(filePath)) {
3330
- console.log('Error: File not found');
3331
- process.exit(1);
3332
- }
3333
-
3334
- const stat = fs.statSync(filePath);
3335
- if (stat.size === 0) {
3336
- console.log('System reminder: File exists but has empty contents');
3337
- process.exit(0);
3338
- }
3339
-
3340
- const content = fs.readFileSync(filePath, 'utf-8');
3341
- const lines = content.split('\\n');
3342
- const selected = lines.slice(offset, offset + limit);
3343
-
3344
- for (let i = 0; i < selected.length; i++) {
3345
- const lineNum = offset + i + 1;
3346
- console.log(String(lineNum).padStart(6) + '\\t' + selected[i]);
3347
- }
3348
- "`;
3344
+ function parseStatLine(line) {
3345
+ const firstTab = line.indexOf(" ");
3346
+ if (firstTab === -1) return null;
3347
+ const secondTab = line.indexOf(" ", firstTab + 1);
3348
+ if (secondTab === -1) return null;
3349
+ const thirdTab = line.indexOf(" ", secondTab + 1);
3350
+ if (thirdTab === -1) return null;
3351
+ const size = parseInt(line.slice(0, firstTab), 10);
3352
+ const mtime = parseInt(line.slice(firstTab + 1, secondTab), 10);
3353
+ const fileType = line.slice(secondTab + 1, thirdTab);
3354
+ const fullPath = line.slice(thirdTab + 1);
3355
+ if (isNaN(size) || isNaN(mtime)) return null;
3356
+ return {
3357
+ size,
3358
+ mtime,
3359
+ isDir: fileType === "directory",
3360
+ fullPath
3361
+ };
3349
3362
  }
3350
3363
  /**
3351
- * Node.js command template for writing files.
3364
+ * Pure POSIX shell command for listing directory contents with metadata.
3365
+ * Uses find -maxdepth 1 + stat — works on any Linux including Alpine (busybox).
3366
+ *
3367
+ * Output format per line: size\tmtime\ttype\tpath
3352
3368
  */
3353
- function buildWriteCommand(filePath, content) {
3354
- return `node -e "
3355
- const fs = require('fs');
3356
- const path = require('path');
3357
-
3358
- const filePath = atob('${btoa(filePath)}');
3359
- const content = atob('${btoa(content)}');
3360
-
3361
- if (fs.existsSync(filePath)) {
3362
- console.error('Error: File already exists');
3363
- process.exit(1);
3364
- }
3365
-
3366
- const parentDir = path.dirname(filePath) || '.';
3367
- fs.mkdirSync(parentDir, { recursive: true });
3368
-
3369
- fs.writeFileSync(filePath, content, 'utf-8');
3370
- console.log('OK');
3371
- "`;
3369
+ function buildLsCommand(dirPath) {
3370
+ const quotedPath = shellQuote(dirPath);
3371
+ return `find ${quotedPath} -maxdepth 1 -not -path ${quotedPath} -exec stat -c '%s\\t%Y\\t%F\\t%n' {} + 2>/dev/null || true`;
3372
3372
  }
3373
3373
  /**
3374
- * Node.js command template for editing files.
3374
+ * Pure POSIX shell command for listing files recursively with metadata.
3375
+ * Uses find + stat — works on any Linux including Alpine (busybox).
3376
+ *
3377
+ * Output format per line: size\tmtime\ttype\tpath
3375
3378
  */
3376
- function buildEditCommand(filePath, oldStr, newStr, replaceAll) {
3377
- return `node -e "
3378
- const fs = require('fs');
3379
-
3380
- const filePath = atob('${btoa(filePath)}');
3381
- const oldStr = atob('${btoa(oldStr)}');
3382
- const newStr = atob('${btoa(newStr)}');
3383
- const replaceAll = ${Boolean(replaceAll)};
3384
-
3385
- let text;
3386
- try {
3387
- text = fs.readFileSync(filePath, 'utf-8');
3388
- } catch (e) {
3389
- process.exit(3);
3379
+ function buildFindCommand(searchPath) {
3380
+ const quotedPath = shellQuote(searchPath);
3381
+ return `find ${quotedPath} -not -path ${quotedPath} -exec stat -c '%s\\t%Y\\t%F\\t%n' {} + 2>/dev/null || true`;
3390
3382
  }
3391
-
3392
- const count = text.split(oldStr).length - 1;
3393
-
3394
- if (count === 0) {
3395
- process.exit(1);
3396
- }
3397
- if (count > 1 && !replaceAll) {
3398
- process.exit(2);
3399
- }
3400
-
3401
- const result = text.split(oldStr).join(newStr);
3402
- fs.writeFileSync(filePath, result, 'utf-8');
3403
- console.log(count);
3404
- "`;
3383
+ /**
3384
+ * Pure POSIX shell command for reading files with line numbers.
3385
+ * Uses awk for line numbering with offset/limit — works on any Linux including Alpine.
3386
+ */
3387
+ function buildReadCommand(filePath, offset, limit) {
3388
+ const quotedPath = shellQuote(filePath);
3389
+ const safeOffset = Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0;
3390
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(Math.floor(limit), 999999999) : 999999999;
3391
+ const start = safeOffset + 1;
3392
+ const end = safeOffset + safeLimit;
3393
+ return [
3394
+ `if [ ! -f ${quotedPath} ]; then echo "Error: File not found"; exit 1; fi`,
3395
+ `if [ ! -s ${quotedPath} ]; then echo "System reminder: File exists but has empty contents"; exit 0; fi`,
3396
+ `awk 'NR >= ${start} && NR <= ${end} { printf "%6d\\t%s\\n", NR, $0 }' ${quotedPath}`
3397
+ ].join("; ");
3405
3398
  }
3406
3399
  /**
3407
- * Node.js command template for grep operations with literal (fixed-string) search.
3400
+ * Build a grep command for literal (fixed-string) search.
3401
+ * Uses grep -rHnF for recursive, with-filename, with-line-number, fixed-string search.
3402
+ * Pure POSIX — works on any Linux including Alpine.
3408
3403
  *
3409
3404
  * @param pattern - Literal string to search for (NOT regex).
3410
3405
  * @param searchPath - Base path to search in.
3411
3406
  * @param globPattern - Optional glob pattern to filter files.
3412
3407
  */
3413
3408
  function buildGrepCommand(pattern, searchPath, globPattern) {
3414
- const patternB64 = btoa(pattern);
3415
- const pathB64 = btoa(searchPath);
3416
- const globB64 = globPattern ? btoa(globPattern) : "";
3417
- return `node -e "
3418
- const fs = require('fs');
3419
- const path = require('path');
3420
-
3421
- const pattern = atob('${patternB64}');
3422
- const searchPath = atob('${pathB64}');
3423
- const globPattern = ${globPattern ? `atob('${globB64}')` : "null"};
3424
-
3425
- function globMatch(filePath, pattern) {
3426
- if (!pattern) return true;
3427
- const regexPattern = pattern
3428
- .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')
3429
- .replace(/\\*/g, '[^/]*')
3430
- .replace(/\\?/g, '.')
3431
- .replace(/<<<GLOBSTAR>>>/g, '.*');
3432
- return new RegExp('^' + regexPattern + '$').test(filePath);
3433
- }
3434
-
3435
- function walkDir(dir, results) {
3436
- try {
3437
- const entries = fs.readdirSync(dir, { withFileTypes: true });
3438
- for (const entry of entries) {
3439
- const fullPath = path.join(dir, entry.name);
3440
- if (entry.isDirectory()) {
3441
- walkDir(fullPath, results);
3442
- } else {
3443
- const relativePath = path.relative(searchPath, fullPath);
3444
- if (globMatch(relativePath, globPattern)) {
3445
- try {
3446
- const content = fs.readFileSync(fullPath, 'utf-8');
3447
- const lines = content.split('\\n');
3448
- for (let i = 0; i < lines.length; i++) {
3449
- // Simple substring search for literal matching
3450
- if (lines[i].includes(pattern)) {
3451
- console.log(JSON.stringify({
3452
- path: fullPath,
3453
- line: i + 1,
3454
- text: lines[i]
3455
- }));
3456
- }
3457
- }
3458
- } catch (e) {
3459
- // Skip unreadable files
3460
- }
3461
- }
3462
- }
3463
- }
3464
- } catch (e) {
3465
- // Skip unreadable directories
3466
- }
3467
- }
3468
-
3469
- try {
3470
- walkDir(searchPath, []);
3471
- } catch (e) {
3472
- // Silent failure
3473
- }
3474
- "`;
3409
+ const patternEscaped = shellQuote(pattern);
3410
+ const searchPathQuoted = shellQuote(searchPath);
3411
+ return `grep -rHnF ${globPattern ? `--include=${shellQuote(globPattern)}` : ""} -e ${patternEscaped} ${searchPathQuoted} 2>/dev/null || true`;
3475
3412
  }
3476
3413
  /**
3477
3414
  * Base sandbox implementation with execute() as the only abstract method.
3478
3415
  *
3479
3416
  * This class provides default implementations for all SandboxBackendProtocol
3480
3417
  * methods using shell commands executed via execute(). Concrete implementations
3481
- * only need to implement the execute() method.
3418
+ * only need to implement execute(), uploadFiles(), and downloadFiles().
3482
3419
  *
3483
- * Requires Node.js 20+ on the sandbox host.
3420
+ * All shell commands use pure POSIX utilities (awk, grep, find, stat) that are
3421
+ * available on any Linux including Alpine/busybox. No Python, Node.js, or
3422
+ * other runtime is required on the sandbox host.
3484
3423
  */
3485
3424
  var BaseSandbox = class {
3486
3425
  /**
3487
3426
  * List files and directories in the specified directory (non-recursive).
3488
3427
  *
3428
+ * Uses pure POSIX shell (find + stat) via execute() — works on any Linux
3429
+ * including Alpine. No Python or Node.js needed.
3430
+ *
3489
3431
  * @param path - Absolute path to directory
3490
3432
  * @returns List of FileInfo objects for files and directories directly in the directory.
3491
3433
  */
3492
3434
  async lsInfo(path) {
3493
3435
  const command = buildLsCommand(path);
3494
3436
  const result = await this.execute(command);
3495
- if (result.exitCode !== 0) return [];
3496
3437
  const infos = [];
3497
3438
  const lines = result.output.trim().split("\n").filter(Boolean);
3498
- for (const line of lines) try {
3499
- const parsed = JSON.parse(line);
3439
+ for (const line of lines) {
3440
+ const parsed = parseStatLine(line);
3441
+ if (!parsed) continue;
3500
3442
  infos.push({
3501
- path: parsed.path,
3443
+ path: parsed.isDir ? parsed.fullPath + "/" : parsed.fullPath,
3502
3444
  is_dir: parsed.isDir,
3503
3445
  size: parsed.size,
3504
- modified_at: parsed.mtime ? new Date(parsed.mtime).toISOString() : void 0
3446
+ modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
3505
3447
  });
3506
- } catch {}
3448
+ }
3507
3449
  return infos;
3508
3450
  }
3509
3451
  /**
3510
3452
  * Read file content with line numbers.
3511
3453
  *
3454
+ * Uses pure POSIX shell (awk) via execute() — only the requested slice
3455
+ * is returned over the wire, making this efficient for large files.
3456
+ * Works on any Linux including Alpine (no Python or Node.js needed).
3457
+ *
3512
3458
  * @param filePath - Absolute file path
3513
3459
  * @param offset - Line offset to start reading from (0-indexed)
3514
3460
  * @param limit - Maximum number of lines to read
@@ -3523,18 +3469,15 @@ var BaseSandbox = class {
3523
3469
  /**
3524
3470
  * Read file content as raw FileData.
3525
3471
  *
3472
+ * Uses downloadFiles() directly — no runtime needed on the sandbox host.
3473
+ *
3526
3474
  * @param filePath - Absolute file path
3527
3475
  * @returns Raw file content as FileData
3528
3476
  */
3529
3477
  async readRaw(filePath) {
3530
- const command = buildReadCommand(filePath, 0, Number.MAX_SAFE_INTEGER);
3531
- const result = await this.execute(command);
3532
- if (result.exitCode !== 0) throw new Error(`File '${filePath}' not found`);
3533
- const lines = [];
3534
- for (const line of result.output.split("\n")) {
3535
- const tabIndex = line.indexOf(" ");
3536
- if (tabIndex !== -1) lines.push(line.substring(tabIndex + 1));
3537
- }
3478
+ const results = await this.downloadFiles([filePath]);
3479
+ if (results[0].error || !results[0].content) throw new Error(`File '${filePath}' not found`);
3480
+ const lines = new TextDecoder().decode(results[0].content).split("\n");
3538
3481
  const now = (/* @__PURE__ */ new Date()).toISOString();
3539
3482
  return {
3540
3483
  content: lines,
@@ -3543,7 +3486,7 @@ var BaseSandbox = class {
3543
3486
  };
3544
3487
  }
3545
3488
  /**
3546
- * Search for a literal text pattern in files.
3489
+ * Search for a literal text pattern in files using grep.
3547
3490
  *
3548
3491
  * @param pattern - Literal string to search for (NOT regex).
3549
3492
  * @param path - Directory or file path to search in.
@@ -3552,44 +3495,69 @@ var BaseSandbox = class {
3552
3495
  */
3553
3496
  async grepRaw(pattern, path = "/", glob = null) {
3554
3497
  const command = buildGrepCommand(pattern, path, glob);
3555
- const result = await this.execute(command);
3498
+ const output = (await this.execute(command)).output.trim();
3499
+ if (!output) return [];
3556
3500
  const matches = [];
3557
- const lines = result.output.trim().split("\n").filter(Boolean);
3558
- for (const line of lines) try {
3559
- const parsed = JSON.parse(line);
3560
- matches.push({
3561
- path: parsed.path,
3562
- line: parsed.line,
3563
- text: parsed.text
3564
- });
3565
- } catch {}
3501
+ for (const line of output.split("\n")) {
3502
+ const parts = line.split(":");
3503
+ if (parts.length >= 3) {
3504
+ const lineNum = parseInt(parts[1], 10);
3505
+ if (!isNaN(lineNum)) matches.push({
3506
+ path: parts[0],
3507
+ line: lineNum,
3508
+ text: parts.slice(2).join(":")
3509
+ });
3510
+ }
3511
+ }
3566
3512
  return matches;
3567
3513
  }
3568
3514
  /**
3569
3515
  * Structured glob matching returning FileInfo objects.
3516
+ *
3517
+ * Uses pure POSIX shell (find + stat) via execute() to list all files,
3518
+ * then applies glob-to-regex matching in TypeScript. No Python or Node.js
3519
+ * needed on the sandbox host.
3520
+ *
3521
+ * Glob patterns are matched against paths relative to the search base:
3522
+ * - `*` matches any characters except `/`
3523
+ * - `**` matches any characters including `/` (recursive)
3524
+ * - `?` matches a single character except `/`
3525
+ * - `[...]` character classes
3570
3526
  */
3571
3527
  async globInfo(pattern, path = "/") {
3572
- const command = buildGlobCommand(path, pattern);
3528
+ const command = buildFindCommand(path);
3573
3529
  const result = await this.execute(command);
3530
+ const regex = globToPathRegex(pattern);
3574
3531
  const infos = [];
3575
3532
  const lines = result.output.trim().split("\n").filter(Boolean);
3576
- for (const line of lines) try {
3577
- const parsed = JSON.parse(line);
3578
- infos.push({
3579
- path: parsed.path,
3533
+ const basePath = path.endsWith("/") ? path.slice(0, -1) : path;
3534
+ for (const line of lines) {
3535
+ const parsed = parseStatLine(line);
3536
+ if (!parsed) continue;
3537
+ const relPath = parsed.fullPath.startsWith(basePath + "/") ? parsed.fullPath.slice(basePath.length + 1) : parsed.fullPath;
3538
+ if (regex.test(relPath)) infos.push({
3539
+ path: relPath,
3580
3540
  is_dir: parsed.isDir,
3581
3541
  size: parsed.size,
3582
- modified_at: parsed.mtime ? new Date(parsed.mtime).toISOString() : void 0
3542
+ modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
3583
3543
  });
3584
- } catch {}
3544
+ }
3585
3545
  return infos;
3586
3546
  }
3587
3547
  /**
3588
3548
  * Create a new file with content.
3549
+ *
3550
+ * Uses downloadFiles() to check existence and uploadFiles() to write.
3551
+ * No runtime needed on the sandbox host.
3589
3552
  */
3590
3553
  async write(filePath, content) {
3591
- const command = buildWriteCommand(filePath, content);
3592
- if ((await this.execute(command)).exitCode !== 0) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
3554
+ try {
3555
+ const existCheck = await this.downloadFiles([filePath]);
3556
+ if (existCheck[0].content !== null && existCheck[0].error === null) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
3557
+ } catch {}
3558
+ const encoder = new TextEncoder();
3559
+ const results = await this.uploadFiles([[filePath, encoder.encode(content)]]);
3560
+ if (results[0].error) return { error: `Failed to write to ${filePath}: ${results[0].error}` };
3593
3561
  return {
3594
3562
  path: filePath,
3595
3563
  filesUpdate: null
@@ -3597,21 +3565,26 @@ var BaseSandbox = class {
3597
3565
  }
3598
3566
  /**
3599
3567
  * Edit a file by replacing string occurrences.
3568
+ *
3569
+ * Uses downloadFiles() to read, performs string replacement in TypeScript,
3570
+ * then uploadFiles() to write back. No runtime needed on the sandbox host.
3600
3571
  */
3601
3572
  async edit(filePath, oldString, newString, replaceAll = false) {
3602
- const command = buildEditCommand(filePath, oldString, newString, replaceAll);
3603
- const result = await this.execute(command);
3604
- switch (result.exitCode) {
3605
- case 0: return {
3606
- path: filePath,
3607
- filesUpdate: null,
3608
- occurrences: parseInt(result.output.trim(), 10) || 1
3609
- };
3610
- case 1: return { error: `String not found in file '${filePath}'` };
3611
- case 2: return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
3612
- case 3: return { error: `Error: File '${filePath}' not found` };
3613
- default: return { error: `Unknown error editing file '${filePath}'` };
3614
- }
3573
+ const results = await this.downloadFiles([filePath]);
3574
+ if (results[0].error || !results[0].content) return { error: `Error: File '${filePath}' not found` };
3575
+ const text = new TextDecoder().decode(results[0].content);
3576
+ const count = text.split(oldString).length - 1;
3577
+ if (count === 0) return { error: `String not found in file '${filePath}'` };
3578
+ if (count > 1 && !replaceAll) return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
3579
+ const newText = replaceAll ? text.split(oldString).join(newString) : text.replace(oldString, newString);
3580
+ const encoder = new TextEncoder();
3581
+ const uploadResults = await this.uploadFiles([[filePath, encoder.encode(newText)]]);
3582
+ if (uploadResults[0].error) return { error: `Failed to write edited file '${filePath}': ${uploadResults[0].error}` };
3583
+ return {
3584
+ path: filePath,
3585
+ filesUpdate: null,
3586
+ occurrences: count
3587
+ };
3615
3588
  }
3616
3589
  };
3617
3590
 
@@ -4326,6 +4299,7 @@ exports.GENERAL_PURPOSE_SUBAGENT = GENERAL_PURPOSE_SUBAGENT;
4326
4299
  exports.MAX_SKILL_DESCRIPTION_LENGTH = MAX_SKILL_DESCRIPTION_LENGTH;
4327
4300
  exports.MAX_SKILL_FILE_SIZE = MAX_SKILL_FILE_SIZE;
4328
4301
  exports.MAX_SKILL_NAME_LENGTH = MAX_SKILL_NAME_LENGTH;
4302
+ exports.SandboxError = SandboxError;
4329
4303
  exports.StateBackend = StateBackend;
4330
4304
  exports.StoreBackend = StoreBackend;
4331
4305
  exports.TASK_SYSTEM_PROMPT = TASK_SYSTEM_PROMPT;