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 +215 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +215 -265
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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}
|
|
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
|
-
*
|
|
3289
|
-
*
|
|
3289
|
+
* Shell-quote a string using single quotes (POSIX).
|
|
3290
|
+
* Escapes embedded single quotes with the '\'' technique.
|
|
3290
3291
|
*/
|
|
3291
|
-
function
|
|
3292
|
-
return
|
|
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
|
-
*
|
|
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
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
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
|
-
*
|
|
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
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
const
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
3426
|
-
|
|
3427
|
-
const
|
|
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
|
-
|
|
3447
|
-
|
|
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
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
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
|
-
*
|
|
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
|
|
3464
|
-
const
|
|
3465
|
-
|
|
3466
|
-
return `
|
|
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
|
|
3443
|
+
* only need to implement execute(), uploadFiles(), and downloadFiles().
|
|
3531
3444
|
*
|
|
3532
|
-
*
|
|
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)
|
|
3548
|
-
const parsed =
|
|
3464
|
+
for (const line of lines) {
|
|
3465
|
+
const parsed = parseStatLine(line);
|
|
3466
|
+
if (!parsed) continue;
|
|
3549
3467
|
infos.push({
|
|
3550
|
-
path: parsed.
|
|
3468
|
+
path: parsed.isDir ? parsed.fullPath + "/" : parsed.fullPath,
|
|
3551
3469
|
is_dir: parsed.isDir,
|
|
3552
3470
|
size: parsed.size,
|
|
3553
|
-
modified_at:
|
|
3471
|
+
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
3554
3472
|
});
|
|
3555
|
-
}
|
|
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
|
|
3580
|
-
|
|
3581
|
-
|
|
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
|
|
3524
|
+
const output = (await this.execute(command)).output.trim();
|
|
3525
|
+
if (!output) return [];
|
|
3605
3526
|
const matches = [];
|
|
3606
|
-
const
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
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 =
|
|
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
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
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:
|
|
3568
|
+
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
3632
3569
|
});
|
|
3633
|
-
}
|
|
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
|
-
|
|
3641
|
-
|
|
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
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
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
|
|