deepagents 1.7.1 → 1.7.3

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/dist/index.cjs CHANGED
@@ -2021,7 +2021,8 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
2021
2021
  */
2022
2022
  async function listSkillsFromBackend(backend, sourcePath) {
2023
2023
  const skills = [];
2024
- const normalizedPath = sourcePath.endsWith("/") || sourcePath.endsWith("\\") ? sourcePath : `${sourcePath}/`;
2024
+ const pathSep = sourcePath.includes("\\") ? "\\" : "/";
2025
+ const normalizedPath = sourcePath.endsWith("/") || sourcePath.endsWith("\\") ? sourcePath : `${sourcePath}${pathSep}`;
2025
2026
  let fileInfos;
2026
2027
  try {
2027
2028
  fileInfos = await backend.lsInfo(normalizedPath);
@@ -2034,7 +2035,7 @@ async function listSkillsFromBackend(backend, sourcePath) {
2034
2035
  }));
2035
2036
  for (const entry of entries) {
2036
2037
  if (entry.type !== "directory") continue;
2037
- const skillMdPath = `${normalizedPath}${entry.name}/SKILL.md`;
2038
+ const skillMdPath = `${normalizedPath}${entry.name}${pathSep}SKILL.md`;
2038
2039
  let content;
2039
2040
  if (backend.downloadFiles) {
2040
2041
  const results = await backend.downloadFiles([skillMdPath]);
@@ -3285,285 +3286,207 @@ var CompositeBackend = class {
3285
3286
  //#endregion
3286
3287
  //#region src/backends/sandbox.ts
3287
3288
  /**
3288
- * Node.js command template for glob operations.
3289
- * Uses web-standard atob() for base64 decoding.
3289
+ * Shell-quote a string using single quotes (POSIX).
3290
+ * Escapes embedded single quotes with the '\'' technique.
3290
3291
  */
3291
- function buildGlobCommand(searchPath, pattern) {
3292
- return `node -e "
3293
- const fs = require('fs');
3294
- const path = require('path');
3295
-
3296
- const searchPath = atob('${btoa(searchPath)}');
3297
- const pattern = atob('${btoa(pattern)}');
3298
-
3299
- function globMatch(relativePath, pattern) {
3300
- const regexPattern = pattern
3301
- .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')
3302
- .replace(/\\*/g, '[^/]*')
3303
- .replace(/\\?/g, '.')
3304
- .replace(/<<<GLOBSTAR>>>/g, '.*');
3305
- return new RegExp('^' + regexPattern + '$').test(relativePath);
3306
- }
3307
-
3308
- function walkDir(dir, baseDir, results) {
3309
- try {
3310
- const entries = fs.readdirSync(dir, { withFileTypes: true });
3311
- for (const entry of entries) {
3312
- const fullPath = path.join(dir, entry.name);
3313
- const relativePath = path.relative(baseDir, fullPath);
3314
- if (entry.isDirectory()) {
3315
- walkDir(fullPath, baseDir, results);
3316
- } else if (globMatch(relativePath, pattern)) {
3317
- const stat = fs.statSync(fullPath);
3318
- console.log(JSON.stringify({
3319
- path: relativePath,
3320
- size: stat.size,
3321
- mtime: stat.mtimeMs,
3322
- isDir: false
3323
- }));
3324
- }
3325
- }
3326
- } catch (e) {
3327
- // Silent failure for non-existent paths
3328
- }
3329
- }
3330
-
3331
- try {
3332
- process.chdir(searchPath);
3333
- walkDir('.', '.', []);
3334
- } catch (e) {
3335
- // Silent failure for non-existent paths
3336
- }
3337
- "`;
3292
+ function shellQuote(s) {
3293
+ return "'" + s.replace(/'/g, "'\\''") + "'";
3338
3294
  }
3339
3295
  /**
3340
- * Node.js command template for listing directory contents.
3296
+ * Convert a glob pattern to a path-aware RegExp.
3297
+ *
3298
+ * Inspired by the just-bash project's glob utilities:
3299
+ * - `*` matches any characters except `/`
3300
+ * - `**` matches any characters including `/` (recursive)
3301
+ * - `?` matches a single character except `/`
3302
+ * - `[...]` character classes
3341
3303
  */
3342
- function buildLsCommand(dirPath) {
3343
- return `node -e "
3344
- const fs = require('fs');
3345
- const path = require('path');
3346
-
3347
- const dirPath = atob('${btoa(dirPath)}');
3348
-
3349
- try {
3350
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
3351
- for (const entry of entries) {
3352
- const fullPath = path.join(dirPath, entry.name);
3353
- const stat = fs.statSync(fullPath);
3354
- console.log(JSON.stringify({
3355
- path: entry.isDirectory() ? fullPath + '/' : fullPath,
3356
- size: stat.size,
3357
- mtime: stat.mtimeMs,
3358
- isDir: entry.isDirectory()
3359
- }));
3360
- }
3361
- } catch (e) {
3362
- console.error('Error: ' + e.message);
3363
- process.exit(1);
3364
- }
3365
- "`;
3304
+ function globToPathRegex(pattern) {
3305
+ let regex = "^";
3306
+ let i = 0;
3307
+ while (i < pattern.length) {
3308
+ const c = pattern[i];
3309
+ if (c === "*") if (i + 1 < pattern.length && pattern[i + 1] === "*") {
3310
+ i += 2;
3311
+ if (i < pattern.length && pattern[i] === "/") {
3312
+ regex += "(.*/)?";
3313
+ i++;
3314
+ } else regex += ".*";
3315
+ } else {
3316
+ regex += "[^/]*";
3317
+ i++;
3318
+ }
3319
+ else if (c === "?") {
3320
+ regex += "[^/]";
3321
+ i++;
3322
+ } else if (c === "[") {
3323
+ let j = i + 1;
3324
+ while (j < pattern.length && pattern[j] !== "]") j++;
3325
+ regex += pattern.slice(i, j + 1);
3326
+ i = j + 1;
3327
+ } else if (c === "." || c === "+" || c === "^" || c === "$" || c === "{" || c === "}" || c === "(" || c === ")" || c === "|" || c === "\\") {
3328
+ regex += `\\${c}`;
3329
+ i++;
3330
+ } else {
3331
+ regex += c;
3332
+ i++;
3333
+ }
3334
+ }
3335
+ regex += "$";
3336
+ return new RegExp(regex);
3366
3337
  }
3367
3338
  /**
3368
- * Node.js command template for reading files.
3339
+ * Parse a single line of stat/find output in the format: size\tmtime\ttype\tpath
3340
+ *
3341
+ * The first three tab-delimited fields are always fixed (number, number, string),
3342
+ * so we safely take everything after the third tab as the file path — even if the
3343
+ * path itself contains tabs.
3344
+ *
3345
+ * The type field varies by platform / tool:
3346
+ * - GNU find -printf %y: single letter "d", "f", "l"
3347
+ * - BSD stat -f %Sp: permission strings like "drwxr-xr-x", "-rw-r--r--"
3348
+ *
3349
+ * The mtime field may be a float (GNU find %T@ → "1234567890.0000000000")
3350
+ * or an integer (BSD stat %m → "1234567890"); parseInt handles both.
3369
3351
  */
3370
- function buildReadCommand(filePath, offset, limit) {
3371
- return `node -e "
3372
- const fs = require('fs');
3373
-
3374
- const filePath = atob('${btoa(filePath)}');
3375
- const offset = ${Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0};
3376
- const limit = ${Number.isFinite(limit) && limit > 0 && limit < Number.MAX_SAFE_INTEGER ? Math.floor(limit) : 0};
3377
-
3378
- if (!fs.existsSync(filePath)) {
3379
- console.log('Error: File not found');
3380
- process.exit(1);
3381
- }
3382
-
3383
- const stat = fs.statSync(filePath);
3384
- if (stat.size === 0) {
3385
- console.log('System reminder: File exists but has empty contents');
3386
- process.exit(0);
3387
- }
3388
-
3389
- const content = fs.readFileSync(filePath, 'utf-8');
3390
- const lines = content.split('\\n');
3391
- const selected = lines.slice(offset, offset + limit);
3392
-
3393
- for (let i = 0; i < selected.length; i++) {
3394
- const lineNum = offset + i + 1;
3395
- console.log(String(lineNum).padStart(6) + '\\t' + selected[i]);
3396
- }
3397
- "`;
3352
+ function parseStatLine(line) {
3353
+ const firstTab = line.indexOf(" ");
3354
+ if (firstTab === -1) return null;
3355
+ const secondTab = line.indexOf(" ", firstTab + 1);
3356
+ if (secondTab === -1) return null;
3357
+ const thirdTab = line.indexOf(" ", secondTab + 1);
3358
+ if (thirdTab === -1) return null;
3359
+ const size = parseInt(line.slice(0, firstTab), 10);
3360
+ const mtime = parseInt(line.slice(firstTab + 1, secondTab), 10);
3361
+ const fileType = line.slice(secondTab + 1, thirdTab);
3362
+ const fullPath = line.slice(thirdTab + 1);
3363
+ if (isNaN(size) || isNaN(mtime)) return null;
3364
+ return {
3365
+ size,
3366
+ mtime,
3367
+ isDir: fileType === "d" || fileType === "directory" || fileType.startsWith("d"),
3368
+ fullPath
3369
+ };
3398
3370
  }
3399
3371
  /**
3400
- * Node.js command template for writing files.
3372
+ * BusyBox/Alpine fallback script for stat -c.
3373
+ *
3374
+ * Determines file type with POSIX test builtins, then uses stat -c
3375
+ * (supported by both GNU coreutils and BusyBox) for size and mtime.
3376
+ * printf handles tab-delimited output formatting.
3401
3377
  */
3402
- function buildWriteCommand(filePath, content) {
3403
- return `node -e "
3404
- const fs = require('fs');
3405
- const path = require('path');
3406
-
3407
- const filePath = atob('${btoa(filePath)}');
3408
- const content = atob('${btoa(content)}');
3409
-
3410
- if (fs.existsSync(filePath)) {
3411
- console.error('Error: File already exists');
3412
- process.exit(1);
3413
- }
3414
-
3415
- const parentDir = path.dirname(filePath) || '.';
3416
- fs.mkdirSync(parentDir, { recursive: true });
3417
-
3418
- fs.writeFileSync(filePath, content, 'utf-8');
3419
- console.log('OK');
3420
- "`;
3421
- }
3378
+ const STAT_C_SCRIPT = "for f; do if [ -d \"$f\" ]; then t=d; elif [ -L \"$f\" ]; then t=l; else t=f; fi; sz=$(stat -c %s \"$f\" 2>/dev/null) || continue; mt=$(stat -c %Y \"$f\" 2>/dev/null) || continue; printf \"%s\\t%s\\t%s\\t%s\\n\" \"$sz\" \"$mt\" \"$t\" \"$f\"; done";
3422
3379
  /**
3423
- * Node.js command template for editing files.
3380
+ * Shell command for listing directory contents with metadata.
3381
+ *
3382
+ * Detects the environment at runtime with three-way probing:
3383
+ * 1. GNU find (full Linux): uses built-in `-printf` (most efficient)
3384
+ * 2. BusyBox / Alpine: uses `find -exec sh -c` with `stat -c` fallback
3385
+ * 3. BSD / macOS: uses `find -exec stat -f`
3386
+ *
3387
+ * Output format per line: size\tmtime\ttype\tpath
3424
3388
  */
3425
- function buildEditCommand(filePath, oldStr, newStr, replaceAll) {
3426
- return `node -e "
3427
- const fs = require('fs');
3428
-
3429
- const filePath = atob('${btoa(filePath)}');
3430
- const oldStr = atob('${btoa(oldStr)}');
3431
- const newStr = atob('${btoa(newStr)}');
3432
- const replaceAll = ${Boolean(replaceAll)};
3433
-
3434
- let text;
3435
- try {
3436
- text = fs.readFileSync(filePath, 'utf-8');
3437
- } catch (e) {
3438
- process.exit(3);
3439
- }
3440
-
3441
- const count = text.split(oldStr).length - 1;
3442
-
3443
- if (count === 0) {
3444
- process.exit(1);
3389
+ function buildLsCommand(dirPath) {
3390
+ const quotedPath = shellQuote(dirPath);
3391
+ const findBase = `find ${quotedPath} -maxdepth 1 -not -path ${quotedPath}`;
3392
+ return `if find /dev/null -maxdepth 0 -printf '' 2>/dev/null; then ${findBase} -printf '%s\\t%T@\\t%y\\t%p\\n' 2>/dev/null; elif stat -c %s /dev/null >/dev/null 2>&1; then ${findBase} -exec sh -c '${STAT_C_SCRIPT}' _ {} +; else ${findBase} -exec stat -f '%z\t%m\t%Sp\t%N' {} + 2>/dev/null; fi || true`;
3445
3393
  }
3446
- if (count > 1 && !replaceAll) {
3447
- process.exit(2);
3394
+ /**
3395
+ * Shell command for listing files recursively with metadata.
3396
+ * Same three-way detection as buildLsCommand (GNU -printf / stat -c / BSD stat -f).
3397
+ *
3398
+ * Output format per line: size\tmtime\ttype\tpath
3399
+ */
3400
+ function buildFindCommand(searchPath) {
3401
+ const quotedPath = shellQuote(searchPath);
3402
+ const findBase = `find ${quotedPath} -not -path ${quotedPath}`;
3403
+ return `if find /dev/null -maxdepth 0 -printf '' 2>/dev/null; then ${findBase} -printf '%s\\t%T@\\t%y\\t%p\\n' 2>/dev/null; elif stat -c %s /dev/null >/dev/null 2>&1; then ${findBase} -exec sh -c '${STAT_C_SCRIPT}' _ {} +; else ${findBase} -exec stat -f '%z\t%m\t%Sp\t%N' {} + 2>/dev/null; fi || true`;
3448
3404
  }
3449
-
3450
- const result = text.split(oldStr).join(newStr);
3451
- fs.writeFileSync(filePath, result, 'utf-8');
3452
- console.log(count);
3453
- "`;
3405
+ /**
3406
+ * Pure POSIX shell command for reading files with line numbers.
3407
+ * Uses awk for line numbering with offset/limit — works on any Linux including Alpine.
3408
+ */
3409
+ function buildReadCommand(filePath, offset, limit) {
3410
+ const quotedPath = shellQuote(filePath);
3411
+ const safeOffset = Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0;
3412
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(Math.floor(limit), 999999999) : 999999999;
3413
+ const start = safeOffset + 1;
3414
+ const end = safeOffset + safeLimit;
3415
+ return [
3416
+ `if [ ! -f ${quotedPath} ]; then echo "Error: File not found"; exit 1; fi`,
3417
+ `if [ ! -s ${quotedPath} ]; then echo "System reminder: File exists but has empty contents"; exit 0; fi`,
3418
+ `awk 'NR >= ${start} && NR <= ${end} { printf "%6d\\t%s\\n", NR, $0 }' ${quotedPath}`
3419
+ ].join("; ");
3454
3420
  }
3455
3421
  /**
3456
- * Node.js command template for grep operations with literal (fixed-string) search.
3422
+ * Build a grep command for literal (fixed-string) search.
3423
+ * Uses grep -rHnF for recursive, with-filename, with-line-number, fixed-string search.
3424
+ *
3425
+ * When a glob pattern is provided, uses `find -name GLOB -exec grep` instead of
3426
+ * `grep --include=GLOB` for universal compatibility (BusyBox grep lacks --include).
3457
3427
  *
3458
3428
  * @param pattern - Literal string to search for (NOT regex).
3459
3429
  * @param searchPath - Base path to search in.
3460
3430
  * @param globPattern - Optional glob pattern to filter files.
3461
3431
  */
3462
3432
  function buildGrepCommand(pattern, searchPath, globPattern) {
3463
- const patternB64 = btoa(pattern);
3464
- const pathB64 = btoa(searchPath);
3465
- const globB64 = globPattern ? btoa(globPattern) : "";
3466
- return `node -e "
3467
- const fs = require('fs');
3468
- const path = require('path');
3469
-
3470
- const pattern = atob('${patternB64}');
3471
- const searchPath = atob('${pathB64}');
3472
- const globPattern = ${globPattern ? `atob('${globB64}')` : "null"};
3473
-
3474
- function globMatch(filePath, pattern) {
3475
- if (!pattern) return true;
3476
- const regexPattern = pattern
3477
- .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')
3478
- .replace(/\\*/g, '[^/]*')
3479
- .replace(/\\?/g, '.')
3480
- .replace(/<<<GLOBSTAR>>>/g, '.*');
3481
- return new RegExp('^' + regexPattern + '$').test(filePath);
3482
- }
3483
-
3484
- function walkDir(dir, results) {
3485
- try {
3486
- const entries = fs.readdirSync(dir, { withFileTypes: true });
3487
- for (const entry of entries) {
3488
- const fullPath = path.join(dir, entry.name);
3489
- if (entry.isDirectory()) {
3490
- walkDir(fullPath, results);
3491
- } else {
3492
- const relativePath = path.relative(searchPath, fullPath);
3493
- if (globMatch(relativePath, globPattern)) {
3494
- try {
3495
- const content = fs.readFileSync(fullPath, 'utf-8');
3496
- const lines = content.split('\\n');
3497
- for (let i = 0; i < lines.length; i++) {
3498
- // Simple substring search for literal matching
3499
- if (lines[i].includes(pattern)) {
3500
- console.log(JSON.stringify({
3501
- path: fullPath,
3502
- line: i + 1,
3503
- text: lines[i]
3504
- }));
3505
- }
3506
- }
3507
- } catch (e) {
3508
- // Skip unreadable files
3509
- }
3510
- }
3511
- }
3512
- }
3513
- } catch (e) {
3514
- // Skip unreadable directories
3515
- }
3516
- }
3517
-
3518
- try {
3519
- walkDir(searchPath, []);
3520
- } catch (e) {
3521
- // Silent failure
3522
- }
3523
- "`;
3433
+ const patternEscaped = shellQuote(pattern);
3434
+ const searchPathQuoted = shellQuote(searchPath);
3435
+ if (globPattern) return `find ${searchPathQuoted} -type f -name ${shellQuote(globPattern)} -exec grep -HnF -e ${patternEscaped} {} + 2>/dev/null || true`;
3436
+ return `grep -rHnF -e ${patternEscaped} ${searchPathQuoted} 2>/dev/null || true`;
3524
3437
  }
3525
3438
  /**
3526
3439
  * Base sandbox implementation with execute() as the only abstract method.
3527
3440
  *
3528
3441
  * This class provides default implementations for all SandboxBackendProtocol
3529
3442
  * methods using shell commands executed via execute(). Concrete implementations
3530
- * only need to implement the execute() method.
3443
+ * only need to implement execute(), uploadFiles(), and downloadFiles().
3531
3444
  *
3532
- * Requires Node.js 20+ on the sandbox host.
3445
+ * All shell commands use pure POSIX utilities (awk, grep, find, stat) that are
3446
+ * available on any Linux including Alpine/busybox. No Python, Node.js, or
3447
+ * other runtime is required on the sandbox host.
3533
3448
  */
3534
3449
  var BaseSandbox = class {
3535
3450
  /**
3536
3451
  * List files and directories in the specified directory (non-recursive).
3537
3452
  *
3453
+ * Uses pure POSIX shell (find + stat) via execute() — works on any Linux
3454
+ * including Alpine. No Python or Node.js needed.
3455
+ *
3538
3456
  * @param path - Absolute path to directory
3539
3457
  * @returns List of FileInfo objects for files and directories directly in the directory.
3540
3458
  */
3541
3459
  async lsInfo(path) {
3542
3460
  const command = buildLsCommand(path);
3543
3461
  const result = await this.execute(command);
3544
- if (result.exitCode !== 0) return [];
3545
3462
  const infos = [];
3546
3463
  const lines = result.output.trim().split("\n").filter(Boolean);
3547
- for (const line of lines) try {
3548
- const parsed = JSON.parse(line);
3464
+ for (const line of lines) {
3465
+ const parsed = parseStatLine(line);
3466
+ if (!parsed) continue;
3549
3467
  infos.push({
3550
- path: parsed.path,
3468
+ path: parsed.isDir ? parsed.fullPath + "/" : parsed.fullPath,
3551
3469
  is_dir: parsed.isDir,
3552
3470
  size: parsed.size,
3553
- modified_at: parsed.mtime ? new Date(parsed.mtime).toISOString() : void 0
3471
+ modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
3554
3472
  });
3555
- } catch {}
3473
+ }
3556
3474
  return infos;
3557
3475
  }
3558
3476
  /**
3559
3477
  * Read file content with line numbers.
3560
3478
  *
3479
+ * Uses pure POSIX shell (awk) via execute() — only the requested slice
3480
+ * is returned over the wire, making this efficient for large files.
3481
+ * Works on any Linux including Alpine (no Python or Node.js needed).
3482
+ *
3561
3483
  * @param filePath - Absolute file path
3562
3484
  * @param offset - Line offset to start reading from (0-indexed)
3563
3485
  * @param limit - Maximum number of lines to read
3564
3486
  * @returns Formatted file content with line numbers, or error message
3565
3487
  */
3566
3488
  async read(filePath, offset = 0, limit = 500) {
3489
+ if (limit === 0) return "";
3567
3490
  const command = buildReadCommand(filePath, offset, limit);
3568
3491
  const result = await this.execute(command);
3569
3492
  if (result.exitCode !== 0) return `Error: File '${filePath}' not found`;
@@ -3572,18 +3495,15 @@ var BaseSandbox = class {
3572
3495
  /**
3573
3496
  * Read file content as raw FileData.
3574
3497
  *
3498
+ * Uses downloadFiles() directly — no runtime needed on the sandbox host.
3499
+ *
3575
3500
  * @param filePath - Absolute file path
3576
3501
  * @returns Raw file content as FileData
3577
3502
  */
3578
3503
  async readRaw(filePath) {
3579
- const command = buildReadCommand(filePath, 0, Number.MAX_SAFE_INTEGER);
3580
- const result = await this.execute(command);
3581
- if (result.exitCode !== 0) throw new Error(`File '${filePath}' not found`);
3582
- const lines = [];
3583
- for (const line of result.output.split("\n")) {
3584
- const tabIndex = line.indexOf(" ");
3585
- if (tabIndex !== -1) lines.push(line.substring(tabIndex + 1));
3586
- }
3504
+ const results = await this.downloadFiles([filePath]);
3505
+ if (results[0].error || !results[0].content) throw new Error(`File '${filePath}' not found`);
3506
+ const lines = new TextDecoder().decode(results[0].content).split("\n");
3587
3507
  const now = (/* @__PURE__ */ new Date()).toISOString();
3588
3508
  return {
3589
3509
  content: lines,
@@ -3592,7 +3512,7 @@ var BaseSandbox = class {
3592
3512
  };
3593
3513
  }
3594
3514
  /**
3595
- * Search for a literal text pattern in files.
3515
+ * Search for a literal text pattern in files using grep.
3596
3516
  *
3597
3517
  * @param pattern - Literal string to search for (NOT regex).
3598
3518
  * @param path - Directory or file path to search in.
@@ -3601,44 +3521,69 @@ var BaseSandbox = class {
3601
3521
  */
3602
3522
  async grepRaw(pattern, path = "/", glob = null) {
3603
3523
  const command = buildGrepCommand(pattern, path, glob);
3604
- const result = await this.execute(command);
3524
+ const output = (await this.execute(command)).output.trim();
3525
+ if (!output) return [];
3605
3526
  const matches = [];
3606
- const lines = result.output.trim().split("\n").filter(Boolean);
3607
- for (const line of lines) try {
3608
- const parsed = JSON.parse(line);
3609
- matches.push({
3610
- path: parsed.path,
3611
- line: parsed.line,
3612
- text: parsed.text
3613
- });
3614
- } catch {}
3527
+ for (const line of output.split("\n")) {
3528
+ const parts = line.split(":");
3529
+ if (parts.length >= 3) {
3530
+ const lineNum = parseInt(parts[1], 10);
3531
+ if (!isNaN(lineNum)) matches.push({
3532
+ path: parts[0],
3533
+ line: lineNum,
3534
+ text: parts.slice(2).join(":")
3535
+ });
3536
+ }
3537
+ }
3615
3538
  return matches;
3616
3539
  }
3617
3540
  /**
3618
3541
  * Structured glob matching returning FileInfo objects.
3542
+ *
3543
+ * Uses pure POSIX shell (find + stat) via execute() to list all files,
3544
+ * then applies glob-to-regex matching in TypeScript. No Python or Node.js
3545
+ * needed on the sandbox host.
3546
+ *
3547
+ * Glob patterns are matched against paths relative to the search base:
3548
+ * - `*` matches any characters except `/`
3549
+ * - `**` matches any characters including `/` (recursive)
3550
+ * - `?` matches a single character except `/`
3551
+ * - `[...]` character classes
3619
3552
  */
3620
3553
  async globInfo(pattern, path = "/") {
3621
- const command = buildGlobCommand(path, pattern);
3554
+ const command = buildFindCommand(path);
3622
3555
  const result = await this.execute(command);
3556
+ const regex = globToPathRegex(pattern);
3623
3557
  const infos = [];
3624
3558
  const lines = result.output.trim().split("\n").filter(Boolean);
3625
- for (const line of lines) try {
3626
- const parsed = JSON.parse(line);
3627
- infos.push({
3628
- path: parsed.path,
3559
+ const basePath = path.endsWith("/") ? path.slice(0, -1) : path;
3560
+ for (const line of lines) {
3561
+ const parsed = parseStatLine(line);
3562
+ if (!parsed) continue;
3563
+ const relPath = parsed.fullPath.startsWith(basePath + "/") ? parsed.fullPath.slice(basePath.length + 1) : parsed.fullPath;
3564
+ if (regex.test(relPath)) infos.push({
3565
+ path: relPath,
3629
3566
  is_dir: parsed.isDir,
3630
3567
  size: parsed.size,
3631
- modified_at: parsed.mtime ? new Date(parsed.mtime).toISOString() : void 0
3568
+ modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
3632
3569
  });
3633
- } catch {}
3570
+ }
3634
3571
  return infos;
3635
3572
  }
3636
3573
  /**
3637
3574
  * Create a new file with content.
3575
+ *
3576
+ * Uses downloadFiles() to check existence and uploadFiles() to write.
3577
+ * No runtime needed on the sandbox host.
3638
3578
  */
3639
3579
  async write(filePath, content) {
3640
- const command = buildWriteCommand(filePath, content);
3641
- 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.` };
3580
+ try {
3581
+ const existCheck = await this.downloadFiles([filePath]);
3582
+ 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.` };
3583
+ } catch {}
3584
+ const encoder = new TextEncoder();
3585
+ const results = await this.uploadFiles([[filePath, encoder.encode(content)]]);
3586
+ if (results[0].error) return { error: `Failed to write to ${filePath}: ${results[0].error}` };
3642
3587
  return {
3643
3588
  path: filePath,
3644
3589
  filesUpdate: null
@@ -3646,21 +3591,26 @@ var BaseSandbox = class {
3646
3591
  }
3647
3592
  /**
3648
3593
  * Edit a file by replacing string occurrences.
3594
+ *
3595
+ * Uses downloadFiles() to read, performs string replacement in TypeScript,
3596
+ * then uploadFiles() to write back. No runtime needed on the sandbox host.
3649
3597
  */
3650
3598
  async edit(filePath, oldString, newString, replaceAll = false) {
3651
- const command = buildEditCommand(filePath, oldString, newString, replaceAll);
3652
- const result = await this.execute(command);
3653
- switch (result.exitCode) {
3654
- case 0: return {
3655
- path: filePath,
3656
- filesUpdate: null,
3657
- occurrences: parseInt(result.output.trim(), 10) || 1
3658
- };
3659
- case 1: return { error: `String not found in file '${filePath}'` };
3660
- case 2: return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
3661
- case 3: return { error: `Error: File '${filePath}' not found` };
3662
- default: return { error: `Unknown error editing file '${filePath}'` };
3663
- }
3599
+ const results = await this.downloadFiles([filePath]);
3600
+ if (results[0].error || !results[0].content) return { error: `Error: File '${filePath}' not found` };
3601
+ const text = new TextDecoder().decode(results[0].content);
3602
+ const count = text.split(oldString).length - 1;
3603
+ if (count === 0) return { error: `String not found in file '${filePath}'` };
3604
+ if (count > 1 && !replaceAll) return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
3605
+ const newText = replaceAll ? text.split(oldString).join(newString) : text.replace(oldString, newString);
3606
+ const encoder = new TextEncoder();
3607
+ const uploadResults = await this.uploadFiles([[filePath, encoder.encode(newText)]]);
3608
+ if (uploadResults[0].error) return { error: `Failed to write edited file '${filePath}': ${uploadResults[0].error}` };
3609
+ return {
3610
+ path: filePath,
3611
+ filesUpdate: null,
3612
+ occurrences: count
3613
+ };
3664
3614
  }
3665
3615
  };
3666
3616