@theokit/sdk-tools 0.14.0 → 0.15.1
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/CHANGELOG.md +24 -0
- package/dist/index.cjs +245 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +245 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4c5bd35: M15 review fixes (injected fs path only; local path unaffected): (1) the backend directory walk in
|
|
8
|
+
`glob_files`/`search_text` decides entry type via `stat` (which follows symlinks), so an in-boundary
|
|
9
|
+
symlink cycle could recurse until PATH_MAX — now depth-capped so it terminates; (2) `edit_file`'s
|
|
10
|
+
backend read mapped every failure to `not_found` — now only a genuinely missing file (`FileNotFoundError`)
|
|
11
|
+
maps to `not_found`; any other read error (e.g. a directory, a permission error) propagates (fail-loud),
|
|
12
|
+
matching the local path's ENOENT-only classification.
|
|
13
|
+
|
|
14
|
+
## 0.15.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 324835f: M15 — complete the surface-agnostic tool injection. `search_text`, `glob_files`, and `edit_file` now
|
|
19
|
+
accept an optional `filesystem` (`FilesystemProvider`), joining `shell_exec`/`git_diff` (`sandbox`) and
|
|
20
|
+
`interactive_shell`/`write_stdin` (`interactive`). When a backend is injected the recursive walk / read
|
|
21
|
+
/ backup / write go through it in project-relative path space (so the tool runs unchanged on a local
|
|
22
|
+
disk, a cluster container, or a Tauri desktop); when omitted the local `fs` path is byte-identical to
|
|
23
|
+
before. Backward compatibility is proven by conformance tests that run each tool through the real
|
|
24
|
+
`LocalFilesystem`/`LocalSandbox` backends and assert identical output to the local path. Additive — no
|
|
25
|
+
breaking change.
|
|
26
|
+
|
|
3
27
|
## 0.11.1
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -7,11 +7,12 @@ var zod = require('zod');
|
|
|
7
7
|
var fs = require('fs');
|
|
8
8
|
var pathSafety = require('@theokit/sdk/path-safety');
|
|
9
9
|
var persistence = require('@theokit/sdk/persistence');
|
|
10
|
+
var filesystem = require('@theokit/sdk/filesystem');
|
|
10
11
|
var child_process = require('child_process');
|
|
12
|
+
var sandbox = require('@theokit/sdk/sandbox');
|
|
11
13
|
var interactive = require('@theokit/sdk/interactive');
|
|
12
14
|
var promises$1 = require('dns/promises');
|
|
13
15
|
var net = require('net');
|
|
14
|
-
var filesystem = require('@theokit/sdk/filesystem');
|
|
15
16
|
|
|
16
17
|
// src/apply-patch.ts
|
|
17
18
|
var PathTraversalError = class extends sdk.ConfigurationError {
|
|
@@ -350,8 +351,82 @@ ${find}`);
|
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
// src/edit-file.ts
|
|
354
|
+
function computeEdit(content, old_string, new_string) {
|
|
355
|
+
const exactIdx = content.indexOf(old_string);
|
|
356
|
+
if (exactIdx !== -1) {
|
|
357
|
+
return {
|
|
358
|
+
ok: true,
|
|
359
|
+
result: content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const normalizedContent = normalizeWhitespace(content);
|
|
363
|
+
const normalizedOld = normalizeWhitespace(old_string);
|
|
364
|
+
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
365
|
+
if (normalizedIdx !== -1) {
|
|
366
|
+
const span = findOriginalSpan(content, normalizedContent, normalizedIdx, normalizedOld.length);
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
result: content.slice(0, span.start) + new_string + content.slice(span.end)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
return { ok: true, result: replaceUnique(content, old_string, new_string) };
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err instanceof ContextMatchError) return { ok: false, error: "no_match" };
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function editViaBackend(filesystem$1, ctx, path, old_string, new_string) {
|
|
380
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
381
|
+
let content;
|
|
382
|
+
try {
|
|
383
|
+
content = await backend.readFile(path);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (err instanceof filesystem.FileNotFoundError) {
|
|
386
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
387
|
+
}
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
391
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
392
|
+
await backend.writeFile(`${path}.bak`, content);
|
|
393
|
+
await backend.writeFile(path, outcome.result);
|
|
394
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
395
|
+
}
|
|
396
|
+
async function editViaLocal(projectRoot, path, old_string, new_string) {
|
|
397
|
+
const absolutePath = safePathJoin(projectRoot, path);
|
|
398
|
+
let content;
|
|
399
|
+
try {
|
|
400
|
+
content = await promises.readFile(absolutePath, "utf-8");
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const e = err;
|
|
403
|
+
if (e.code === "ENOENT") {
|
|
404
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
405
|
+
}
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
409
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
410
|
+
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
411
|
+
await promises.writeFile(absolutePath, outcome.result, "utf-8");
|
|
412
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
413
|
+
}
|
|
414
|
+
function editScopeError(path, projectRoot) {
|
|
415
|
+
if (isForbiddenPath(path)) {
|
|
416
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, path), projectRoot);
|
|
420
|
+
return null;
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
423
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
424
|
+
}
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
353
428
|
function createEditFileTool(opts) {
|
|
354
|
-
const { projectRoot } = opts;
|
|
429
|
+
const { projectRoot, filesystem } = opts;
|
|
355
430
|
return sdk.Tool.create({
|
|
356
431
|
name: "edit_file",
|
|
357
432
|
description: "Make an exact string replacement in a project-relative file. Replaces the FIRST occurrence of old_string with new_string (a whitespace-normalized fallback is attempted if the exact match fails) and writes a .bak backup first. Read the file first so old_string matches the on-disk text exactly; include enough surrounding context to make it unique \u2014 only the first match is replaced, so a too-short old_string can edit the wrong location. old_string must be non-empty and differ from new_string; to change every occurrence, call edit_file repeatedly. Returns { ok, replacements } or { ok: false, error }.",
|
|
@@ -360,67 +435,13 @@ function createEditFileTool(opts) {
|
|
|
360
435
|
old_string: zod.z.string().min(1).describe("String to find in the file."),
|
|
361
436
|
new_string: zod.z.string().describe("Replacement string.")
|
|
362
437
|
}),
|
|
363
|
-
|
|
364
|
-
handler: async ({ path, old_string, new_string }) => {
|
|
438
|
+
handler: async ({ path, old_string, new_string }, ctx) => {
|
|
365
439
|
if (old_string === new_string) {
|
|
366
440
|
return JSON.stringify({ ok: false, error: "no_change", path });
|
|
367
441
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
let absolutePath;
|
|
372
|
-
try {
|
|
373
|
-
absolutePath = safePathJoin(projectRoot, path);
|
|
374
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
375
|
-
} catch (err) {
|
|
376
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
377
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
378
|
-
}
|
|
379
|
-
throw err;
|
|
380
|
-
}
|
|
381
|
-
let content;
|
|
382
|
-
try {
|
|
383
|
-
content = await promises.readFile(absolutePath, "utf-8");
|
|
384
|
-
} catch (err) {
|
|
385
|
-
const e = err;
|
|
386
|
-
if (e.code === "ENOENT") {
|
|
387
|
-
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
388
|
-
}
|
|
389
|
-
throw err;
|
|
390
|
-
}
|
|
391
|
-
const exactIdx = content.indexOf(old_string);
|
|
392
|
-
if (exactIdx !== -1) {
|
|
393
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
394
|
-
const result = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
|
|
395
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
396
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
397
|
-
}
|
|
398
|
-
const normalizedContent = normalizeWhitespace(content);
|
|
399
|
-
const normalizedOld = normalizeWhitespace(old_string);
|
|
400
|
-
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
401
|
-
if (normalizedIdx !== -1) {
|
|
402
|
-
const span = findOriginalSpan(
|
|
403
|
-
content,
|
|
404
|
-
normalizedContent,
|
|
405
|
-
normalizedIdx,
|
|
406
|
-
normalizedOld.length
|
|
407
|
-
);
|
|
408
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
409
|
-
const result = content.slice(0, span.start) + new_string + content.slice(span.end);
|
|
410
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
411
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
412
|
-
}
|
|
413
|
-
try {
|
|
414
|
-
const result = replaceUnique(content, old_string, new_string);
|
|
415
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
416
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
417
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
418
|
-
} catch (err) {
|
|
419
|
-
if (err instanceof ContextMatchError) {
|
|
420
|
-
return JSON.stringify({ ok: false, error: "no_match", path });
|
|
421
|
-
}
|
|
422
|
-
throw err;
|
|
423
|
-
}
|
|
442
|
+
const scopeErr = editScopeError(path, projectRoot);
|
|
443
|
+
if (scopeErr !== null) return scopeErr;
|
|
444
|
+
return filesystem !== void 0 ? editViaBackend(filesystem, ctx, path, old_string, new_string) : editViaLocal(projectRoot, path, old_string, new_string);
|
|
424
445
|
}
|
|
425
446
|
});
|
|
426
447
|
}
|
|
@@ -533,11 +554,27 @@ function attachChildSettlers(child, gate, onClose, onError, resolve2) {
|
|
|
533
554
|
// src/git-diff.ts
|
|
534
555
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
535
556
|
var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
|
|
557
|
+
function shq(arg) {
|
|
558
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
559
|
+
}
|
|
560
|
+
async function diffViaSandbox(sandbox$1, ctx, cached, path, projectRoot, timeoutMs) {
|
|
561
|
+
const scopeCheck = checkPathScope(path, projectRoot);
|
|
562
|
+
if (scopeCheck !== null) return scopeCheck;
|
|
563
|
+
const command = ["git", ...buildDiffArgs(cached, path)].map(shq).join(" ");
|
|
564
|
+
const backend = await sandbox.resolveSandbox(sandbox$1, ctx ?? {});
|
|
565
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
566
|
+
if (r.timedOut) return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
|
|
567
|
+
if (r.exitCode !== 0) {
|
|
568
|
+
return /not a git repository/i.test(r.stderr) ? JSON.stringify({ ok: false, error: "not_a_repo" }) : JSON.stringify({ ok: false, error: "git_failed", stderr: r.stderr });
|
|
569
|
+
}
|
|
570
|
+
return JSON.stringify({ ok: true, diff: r.stdout, truncated: false });
|
|
571
|
+
}
|
|
536
572
|
function createGitDiffTool(opts) {
|
|
537
573
|
const {
|
|
538
574
|
projectRoot,
|
|
539
575
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
540
|
-
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES
|
|
576
|
+
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES,
|
|
577
|
+
sandbox
|
|
541
578
|
} = opts;
|
|
542
579
|
return sdk.Tool.create({
|
|
543
580
|
name: "git_diff",
|
|
@@ -546,7 +583,10 @@ function createGitDiffTool(opts) {
|
|
|
546
583
|
path: zod.z.string().optional().describe("Optional project-relative file or dir scope."),
|
|
547
584
|
cached: zod.z.boolean().optional().describe("If true, show staged changes (git diff --cached). Default false.")
|
|
548
585
|
}),
|
|
549
|
-
handler: async ({ path: path$1, cached }) => {
|
|
586
|
+
handler: async ({ path: path$1, cached }, ctx) => {
|
|
587
|
+
if (sandbox !== void 0) {
|
|
588
|
+
return diffViaSandbox(sandbox, ctx, cached, path$1, projectRoot, timeoutMs);
|
|
589
|
+
}
|
|
550
590
|
if (!fs.existsSync(path.join(projectRoot, ".git"))) {
|
|
551
591
|
return JSON.stringify({ ok: false, error: "not_a_repo" });
|
|
552
592
|
}
|
|
@@ -619,8 +659,9 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
619
659
|
});
|
|
620
660
|
}
|
|
621
661
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
662
|
+
var MAX_BACKEND_WALK_DEPTH = 64;
|
|
622
663
|
function createGlobTool(opts) {
|
|
623
|
-
const { projectRoot } = opts;
|
|
664
|
+
const { projectRoot, filesystem: filesystem$1 } = opts;
|
|
624
665
|
return sdk.Tool.create({
|
|
625
666
|
name: "glob_files",
|
|
626
667
|
description: "Find files by glob pattern across the project \u2014 fast at any repo size. Use glob_files when you know the filename SHAPE; use search_text when you know the file CONTENT; use read_file when you know the exact path. The pattern supports * and ** wildcards (e.g. '**/*.ts', 'src/**/*.json'); node_modules/.git/dist/.theo are excluded and results are relative paths. Returns { ok, files } or { ok: false, error }.",
|
|
@@ -628,20 +669,18 @@ function createGlobTool(opts) {
|
|
|
628
669
|
pattern: zod.z.string().min(1).describe("Glob pattern (e.g. '**/*.ts', 'src/**/*.json')."),
|
|
629
670
|
cwd: zod.z.string().optional().describe("Project-relative subdirectory to search from.")
|
|
630
671
|
}),
|
|
631
|
-
handler: async ({ pattern, cwd }) => {
|
|
632
|
-
|
|
633
|
-
if (
|
|
634
|
-
try {
|
|
635
|
-
searchRoot = safePathJoin(projectRoot, cwd);
|
|
636
|
-
assertNoSymlinkEscape(searchRoot, projectRoot);
|
|
637
|
-
} catch (err) {
|
|
638
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
639
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
640
|
-
}
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
672
|
+
handler: async ({ pattern, cwd }, ctx) => {
|
|
673
|
+
const scopeErr = globScopeError(cwd, projectRoot);
|
|
674
|
+
if (scopeErr !== null) return scopeErr;
|
|
644
675
|
const regex = globToRegex(pattern);
|
|
676
|
+
if (filesystem$1 !== void 0) {
|
|
677
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
678
|
+
const searchRel = cwd ?? "";
|
|
679
|
+
const found = [];
|
|
680
|
+
await walkDirBackend(backend, searchRel, searchRel, regex, found, 0);
|
|
681
|
+
return JSON.stringify({ ok: true, files: found.sort(), count: found.length });
|
|
682
|
+
}
|
|
683
|
+
const searchRoot = cwd ? safePathJoin(projectRoot, cwd) : projectRoot;
|
|
645
684
|
const files = [];
|
|
646
685
|
await walkDir(searchRoot, searchRoot, regex, files);
|
|
647
686
|
const relativePaths = files.map((f) => path.relative(projectRoot, f)).sort();
|
|
@@ -649,6 +688,18 @@ function createGlobTool(opts) {
|
|
|
649
688
|
}
|
|
650
689
|
});
|
|
651
690
|
}
|
|
691
|
+
function globScopeError(cwd, projectRoot) {
|
|
692
|
+
if (!cwd) return null;
|
|
693
|
+
try {
|
|
694
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, cwd), projectRoot);
|
|
695
|
+
return null;
|
|
696
|
+
} catch (err) {
|
|
697
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
698
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
699
|
+
}
|
|
700
|
+
throw err;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
652
703
|
async function walkDir(base, dir, pattern, results) {
|
|
653
704
|
let entries;
|
|
654
705
|
try {
|
|
@@ -667,6 +718,34 @@ async function walkDir(base, dir, pattern, results) {
|
|
|
667
718
|
}
|
|
668
719
|
}
|
|
669
720
|
}
|
|
721
|
+
async function walkDirBackend(backend, base, dir, pattern, results, depth) {
|
|
722
|
+
if (depth > MAX_BACKEND_WALK_DEPTH) return;
|
|
723
|
+
let names;
|
|
724
|
+
try {
|
|
725
|
+
names = await backend.list(dir);
|
|
726
|
+
} catch {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
for (const name of names) {
|
|
730
|
+
await walkBackendEntry(backend, base, dir, name, pattern, results, depth);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function walkBackendEntry(backend, base, dir, name, pattern, results, depth) {
|
|
734
|
+
if (DEFAULT_EXCLUDES.has(name)) return;
|
|
735
|
+
const fullRel = dir === "" ? name : `${dir}/${name}`;
|
|
736
|
+
const relPath = base === "" ? fullRel : path.relative(base, fullRel);
|
|
737
|
+
let st;
|
|
738
|
+
try {
|
|
739
|
+
st = await backend.stat(fullRel);
|
|
740
|
+
} catch {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (st.isDirectory) {
|
|
744
|
+
await walkDirBackend(backend, base, fullRel, pattern, results, depth + 1);
|
|
745
|
+
} else if (st.isFile && pattern.test(relPath)) {
|
|
746
|
+
results.push(fullRel);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
670
749
|
function globToRegex(pattern) {
|
|
671
750
|
let regexStr = "";
|
|
672
751
|
let i = 0;
|
|
@@ -1734,11 +1813,13 @@ var DEFAULT_MAX_MATCHES = 100;
|
|
|
1734
1813
|
var DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
1735
1814
|
var BINARY_PROBE_BYTES2 = 8 * 1024;
|
|
1736
1815
|
var PREVIEW_MAX = 200;
|
|
1816
|
+
var MAX_BACKEND_WALK_DEPTH2 = 64;
|
|
1737
1817
|
function createSearchTextTool(opts) {
|
|
1738
1818
|
const {
|
|
1739
1819
|
projectRoot,
|
|
1740
1820
|
maxMatches = DEFAULT_MAX_MATCHES,
|
|
1741
|
-
maxFileSize = DEFAULT_MAX_FILE_SIZE
|
|
1821
|
+
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
1822
|
+
filesystem: filesystem$1
|
|
1742
1823
|
} = opts;
|
|
1743
1824
|
return sdk.Tool.create({
|
|
1744
1825
|
name: "search_text",
|
|
@@ -1747,9 +1828,7 @@ function createSearchTextTool(opts) {
|
|
|
1747
1828
|
query: zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
1748
1829
|
path: zod.z.string().optional().describe("Optional project-relative directory to scope the search.")
|
|
1749
1830
|
}),
|
|
1750
|
-
handler: async ({ query, path }) => {
|
|
1751
|
-
const scope = resolveSearchScope(path, projectRoot);
|
|
1752
|
-
if ("error" in scope) return scope.error;
|
|
1831
|
+
handler: async ({ query, path }, ctx) => {
|
|
1753
1832
|
const state = {
|
|
1754
1833
|
matches: [],
|
|
1755
1834
|
totalMatches: 0,
|
|
@@ -1759,6 +1838,20 @@ function createSearchTextTool(opts) {
|
|
|
1759
1838
|
maxFileSize,
|
|
1760
1839
|
projectRoot
|
|
1761
1840
|
};
|
|
1841
|
+
if (filesystem$1 !== void 0) {
|
|
1842
|
+
const scopeRel = resolveScopeRel(path, projectRoot);
|
|
1843
|
+
if ("error" in scopeRel) return scopeRel.error;
|
|
1844
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
1845
|
+
await walkBackend(backend, scopeRel.rel, state, 0);
|
|
1846
|
+
return JSON.stringify({
|
|
1847
|
+
ok: true,
|
|
1848
|
+
matches: state.matches,
|
|
1849
|
+
truncated: state.truncated,
|
|
1850
|
+
totalMatches: state.totalMatches
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
const scope = resolveSearchScope(path, projectRoot);
|
|
1854
|
+
if ("error" in scope) return scope.error;
|
|
1762
1855
|
await walk(scope.scopeAbs, state);
|
|
1763
1856
|
return JSON.stringify({
|
|
1764
1857
|
ok: true,
|
|
@@ -1846,11 +1939,79 @@ async function scanFile(absPath, relPath, state) {
|
|
|
1846
1939
|
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1847
1940
|
}
|
|
1848
1941
|
}
|
|
1942
|
+
function resolveScopeRel(path, projectRoot) {
|
|
1943
|
+
const scopeRel = path === void 0 || path === "" || path === "." ? "" : path;
|
|
1944
|
+
if (scopeRel === "") return { rel: "" };
|
|
1945
|
+
try {
|
|
1946
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
1947
|
+
return { rel: scopeRel };
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1950
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
1951
|
+
}
|
|
1952
|
+
throw err;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
async function walkBackend(backend, dirRel, state, depth) {
|
|
1956
|
+
if (state.truncated) return;
|
|
1957
|
+
if (depth > MAX_BACKEND_WALK_DEPTH2) return;
|
|
1958
|
+
let names;
|
|
1959
|
+
try {
|
|
1960
|
+
names = await backend.list(dirRel);
|
|
1961
|
+
} catch {
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
for (const name of names) {
|
|
1965
|
+
if (state.truncated) return;
|
|
1966
|
+
await handleBackendEntry(backend, dirRel, name, state, depth);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
async function handleBackendEntry(backend, dirRel, name, state, depth) {
|
|
1970
|
+
const entryRel = dirRel === "" ? name : `${dirRel}/${name}`;
|
|
1971
|
+
if (isForbiddenPath(entryRel)) return;
|
|
1972
|
+
let st;
|
|
1973
|
+
try {
|
|
1974
|
+
st = await backend.stat(entryRel);
|
|
1975
|
+
} catch {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (st.isDirectory) {
|
|
1979
|
+
await walkBackend(backend, entryRel, state, depth + 1);
|
|
1980
|
+
} else if (st.isFile) {
|
|
1981
|
+
await scanFileBackend(backend, entryRel, st.size, state);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
async function scanFileBackend(backend, relPath, size, state) {
|
|
1985
|
+
if (size > state.maxFileSize) return;
|
|
1986
|
+
let content;
|
|
1987
|
+
try {
|
|
1988
|
+
content = await backend.readFile(relPath);
|
|
1989
|
+
} catch {
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
if (content.slice(0, BINARY_PROBE_BYTES2).includes("\0")) return;
|
|
1993
|
+
const lines = content.split("\n");
|
|
1994
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1995
|
+
const line = lines[i];
|
|
1996
|
+
if (!line.includes(state.query)) continue;
|
|
1997
|
+
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1849
2000
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
1850
2001
|
var MAX_TIMEOUT_MS = 3e5;
|
|
1851
2002
|
var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
2003
|
+
async function execViaSandbox(sandbox$1, ctx, command, timeoutMs) {
|
|
2004
|
+
const backend = await sandbox.resolveSandbox(sandbox$1, ctx ?? {});
|
|
2005
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
2006
|
+
return r.timedOut ? JSON.stringify({ ok: false, error: "timeout", timeout_ms: timeoutMs }) : JSON.stringify({ ok: true, stdout: r.stdout, stderr: r.stderr, exit_code: r.exitCode });
|
|
2007
|
+
}
|
|
1852
2008
|
function createShellTool(opts) {
|
|
1853
|
-
const {
|
|
2009
|
+
const {
|
|
2010
|
+
projectRoot,
|
|
2011
|
+
defaultTimeoutMs = DEFAULT_TIMEOUT_MS3,
|
|
2012
|
+
allowCatastrophic = false,
|
|
2013
|
+
sandbox
|
|
2014
|
+
} = opts;
|
|
1854
2015
|
return sdk.Tool.create({
|
|
1855
2016
|
name: "shell_exec",
|
|
1856
2017
|
description: "Execute a shell command in the project directory. Use this for terminal operations \u2014 running tests, git, package managers, build tools. Do NOT use it for file operations (reading, writing, editing, finding files): prefer the specialized read_file/write_file/edit_file/glob_files/search_text tools, which are path-checked and safer. Only commit, push, or change git state when the user explicitly asks. timeout_ms defaults to 30000 (max 300000); stdout/stderr are capped (~5 MB). Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
|
|
@@ -1858,7 +2019,7 @@ function createShellTool(opts) {
|
|
|
1858
2019
|
command: zod.z.string().min(1).describe("Shell command to execute."),
|
|
1859
2020
|
timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
1860
2021
|
}),
|
|
1861
|
-
handler: async ({ command, timeout_ms }) => {
|
|
2022
|
+
handler: async ({ command, timeout_ms }, ctx) => {
|
|
1862
2023
|
if (!allowCatastrophic) {
|
|
1863
2024
|
const reason = catastrophicShellReason(command);
|
|
1864
2025
|
if (reason) {
|
|
@@ -1867,8 +2028,8 @@ function createShellTool(opts) {
|
|
|
1867
2028
|
}
|
|
1868
2029
|
}
|
|
1869
2030
|
const timeoutMs = Math.min(timeout_ms ?? defaultTimeoutMs, MAX_TIMEOUT_MS);
|
|
1870
|
-
|
|
1871
|
-
return
|
|
2031
|
+
if (sandbox !== void 0) return execViaSandbox(sandbox, ctx, command, timeoutMs);
|
|
2032
|
+
return runShell(projectRoot, command, timeoutMs);
|
|
1872
2033
|
}
|
|
1873
2034
|
});
|
|
1874
2035
|
}
|