deepagents 1.7.1 → 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/dist/index.cjs CHANGED
@@ -3285,279 +3285,176 @@ var CompositeBackend = class {
3285
3285
  //#endregion
3286
3286
  //#region src/backends/sandbox.ts
3287
3287
  /**
3288
- * Node.js command template for glob operations.
3289
- * Uses web-standard atob() for base64 decoding.
3288
+ * Shell-quote a string using single quotes (POSIX).
3289
+ * Escapes embedded single quotes with the '\'' technique.
3290
3290
  */
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
- "`;
3291
+ function shellQuote(s) {
3292
+ return "'" + s.replace(/'/g, "'\\''") + "'";
3338
3293
  }
3339
3294
  /**
3340
- * 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
3341
3302
  */
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
- "`;
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);
3366
3336
  }
3367
3337
  /**
3368
- * 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.
3369
3343
  */
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
- "`;
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
+ };
3398
3362
  }
3399
3363
  /**
3400
- * 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
3401
3368
  */
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
- "`;
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`;
3421
3372
  }
3422
3373
  /**
3423
- * 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
3424
3378
  */
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);
3445
- }
3446
- if (count > 1 && !replaceAll) {
3447
- process.exit(2);
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`;
3448
3382
  }
3449
-
3450
- const result = text.split(oldStr).join(newStr);
3451
- fs.writeFileSync(filePath, result, 'utf-8');
3452
- console.log(count);
3453
- "`;
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("; ");
3454
3398
  }
3455
3399
  /**
3456
- * 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.
3457
3403
  *
3458
3404
  * @param pattern - Literal string to search for (NOT regex).
3459
3405
  * @param searchPath - Base path to search in.
3460
3406
  * @param globPattern - Optional glob pattern to filter files.
3461
3407
  */
3462
3408
  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
- "`;
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`;
3524
3412
  }
3525
3413
  /**
3526
3414
  * Base sandbox implementation with execute() as the only abstract method.
3527
3415
  *
3528
3416
  * This class provides default implementations for all SandboxBackendProtocol
3529
3417
  * methods using shell commands executed via execute(). Concrete implementations
3530
- * only need to implement the execute() method.
3418
+ * only need to implement execute(), uploadFiles(), and downloadFiles().
3531
3419
  *
3532
- * 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.
3533
3423
  */
3534
3424
  var BaseSandbox = class {
3535
3425
  /**
3536
3426
  * List files and directories in the specified directory (non-recursive).
3537
3427
  *
3428
+ * Uses pure POSIX shell (find + stat) via execute() — works on any Linux
3429
+ * including Alpine. No Python or Node.js needed.
3430
+ *
3538
3431
  * @param path - Absolute path to directory
3539
3432
  * @returns List of FileInfo objects for files and directories directly in the directory.
3540
3433
  */
3541
3434
  async lsInfo(path) {
3542
3435
  const command = buildLsCommand(path);
3543
3436
  const result = await this.execute(command);
3544
- if (result.exitCode !== 0) return [];
3545
3437
  const infos = [];
3546
3438
  const lines = result.output.trim().split("\n").filter(Boolean);
3547
- for (const line of lines) try {
3548
- const parsed = JSON.parse(line);
3439
+ for (const line of lines) {
3440
+ const parsed = parseStatLine(line);
3441
+ if (!parsed) continue;
3549
3442
  infos.push({
3550
- path: parsed.path,
3443
+ path: parsed.isDir ? parsed.fullPath + "/" : parsed.fullPath,
3551
3444
  is_dir: parsed.isDir,
3552
3445
  size: parsed.size,
3553
- modified_at: parsed.mtime ? new Date(parsed.mtime).toISOString() : void 0
3446
+ modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
3554
3447
  });
3555
- } catch {}
3448
+ }
3556
3449
  return infos;
3557
3450
  }
3558
3451
  /**
3559
3452
  * Read file content with line numbers.
3560
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
+ *
3561
3458
  * @param filePath - Absolute file path
3562
3459
  * @param offset - Line offset to start reading from (0-indexed)
3563
3460
  * @param limit - Maximum number of lines to read
@@ -3572,18 +3469,15 @@ var BaseSandbox = class {
3572
3469
  /**
3573
3470
  * Read file content as raw FileData.
3574
3471
  *
3472
+ * Uses downloadFiles() directly — no runtime needed on the sandbox host.
3473
+ *
3575
3474
  * @param filePath - Absolute file path
3576
3475
  * @returns Raw file content as FileData
3577
3476
  */
3578
3477
  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
- }
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");
3587
3481
  const now = (/* @__PURE__ */ new Date()).toISOString();
3588
3482
  return {
3589
3483
  content: lines,
@@ -3592,7 +3486,7 @@ var BaseSandbox = class {
3592
3486
  };
3593
3487
  }
3594
3488
  /**
3595
- * Search for a literal text pattern in files.
3489
+ * Search for a literal text pattern in files using grep.
3596
3490
  *
3597
3491
  * @param pattern - Literal string to search for (NOT regex).
3598
3492
  * @param path - Directory or file path to search in.
@@ -3601,44 +3495,69 @@ var BaseSandbox = class {
3601
3495
  */
3602
3496
  async grepRaw(pattern, path = "/", glob = null) {
3603
3497
  const command = buildGrepCommand(pattern, path, glob);
3604
- const result = await this.execute(command);
3498
+ const output = (await this.execute(command)).output.trim();
3499
+ if (!output) return [];
3605
3500
  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 {}
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
+ }
3615
3512
  return matches;
3616
3513
  }
3617
3514
  /**
3618
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
3619
3526
  */
3620
3527
  async globInfo(pattern, path = "/") {
3621
- const command = buildGlobCommand(path, pattern);
3528
+ const command = buildFindCommand(path);
3622
3529
  const result = await this.execute(command);
3530
+ const regex = globToPathRegex(pattern);
3623
3531
  const infos = [];
3624
3532
  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,
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,
3629
3540
  is_dir: parsed.isDir,
3630
3541
  size: parsed.size,
3631
- modified_at: parsed.mtime ? new Date(parsed.mtime).toISOString() : void 0
3542
+ modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
3632
3543
  });
3633
- } catch {}
3544
+ }
3634
3545
  return infos;
3635
3546
  }
3636
3547
  /**
3637
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.
3638
3552
  */
3639
3553
  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.` };
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}` };
3642
3561
  return {
3643
3562
  path: filePath,
3644
3563
  filesUpdate: null
@@ -3646,21 +3565,26 @@ var BaseSandbox = class {
3646
3565
  }
3647
3566
  /**
3648
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.
3649
3571
  */
3650
3572
  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
- }
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
+ };
3664
3588
  }
3665
3589
  };
3666
3590