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 +3 -3
- package/dist/index.cjs +237 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -103
- package/dist/index.d.ts +72 -103
- package/dist/index.js +237 -264
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
3240
|
-
*
|
|
3288
|
+
* Shell-quote a string using single quotes (POSIX).
|
|
3289
|
+
* Escapes embedded single quotes with the '\'' technique.
|
|
3241
3290
|
*/
|
|
3242
|
-
function
|
|
3243
|
-
return
|
|
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
|
-
*
|
|
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
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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
|
-
*
|
|
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
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
const
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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
|
-
*
|
|
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
|
|
3354
|
-
|
|
3355
|
-
|
|
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
|
-
*
|
|
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
|
|
3377
|
-
|
|
3378
|
-
|
|
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
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
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
|
-
*
|
|
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
|
|
3415
|
-
const
|
|
3416
|
-
|
|
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
|
|
3418
|
+
* only need to implement execute(), uploadFiles(), and downloadFiles().
|
|
3482
3419
|
*
|
|
3483
|
-
*
|
|
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)
|
|
3499
|
-
const parsed =
|
|
3439
|
+
for (const line of lines) {
|
|
3440
|
+
const parsed = parseStatLine(line);
|
|
3441
|
+
if (!parsed) continue;
|
|
3500
3442
|
infos.push({
|
|
3501
|
-
path: parsed.
|
|
3443
|
+
path: parsed.isDir ? parsed.fullPath + "/" : parsed.fullPath,
|
|
3502
3444
|
is_dir: parsed.isDir,
|
|
3503
3445
|
size: parsed.size,
|
|
3504
|
-
modified_at:
|
|
3446
|
+
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
3505
3447
|
});
|
|
3506
|
-
}
|
|
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
|
|
3531
|
-
|
|
3532
|
-
|
|
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
|
|
3498
|
+
const output = (await this.execute(command)).output.trim();
|
|
3499
|
+
if (!output) return [];
|
|
3556
3500
|
const matches = [];
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
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 =
|
|
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
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
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:
|
|
3542
|
+
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
3583
3543
|
});
|
|
3584
|
-
}
|
|
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
|
-
|
|
3592
|
-
|
|
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
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
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;
|