@theokit/sdk-tools 0.13.0 → 0.15.0
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 +13 -0
- package/dist/index.cjs +295 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +294 -85
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,10 +5,12 @@ import { z } from 'zod';
|
|
|
5
5
|
import { existsSync, statSync, mkdirSync, writeFileSync, realpathSync, readFileSync, lstatSync, readlinkSync, readdirSync } from 'fs';
|
|
6
6
|
import { safeFilenameForId, safePathJoin as safePathJoin$1 } from '@theokit/sdk/path-safety';
|
|
7
7
|
import { replaceFileAtomic } from '@theokit/sdk/persistence';
|
|
8
|
+
import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
|
|
8
9
|
import { spawn } from 'child_process';
|
|
10
|
+
import { resolveSandbox } from '@theokit/sdk/sandbox';
|
|
11
|
+
import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
|
|
9
12
|
import { lookup } from 'dns/promises';
|
|
10
13
|
import { isIP } from 'net';
|
|
11
|
-
import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
|
|
12
14
|
|
|
13
15
|
// src/apply-patch.ts
|
|
14
16
|
var PathTraversalError = class extends ConfigurationError {
|
|
@@ -347,8 +349,79 @@ ${find}`);
|
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
// src/edit-file.ts
|
|
352
|
+
function computeEdit(content, old_string, new_string) {
|
|
353
|
+
const exactIdx = content.indexOf(old_string);
|
|
354
|
+
if (exactIdx !== -1) {
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
result: content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const normalizedContent = normalizeWhitespace(content);
|
|
361
|
+
const normalizedOld = normalizeWhitespace(old_string);
|
|
362
|
+
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
363
|
+
if (normalizedIdx !== -1) {
|
|
364
|
+
const span = findOriginalSpan(content, normalizedContent, normalizedIdx, normalizedOld.length);
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
result: content.slice(0, span.start) + new_string + content.slice(span.end)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
return { ok: true, result: replaceUnique(content, old_string, new_string) };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (err instanceof ContextMatchError) return { ok: false, error: "no_match" };
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function editViaBackend(filesystem, ctx, path, old_string, new_string) {
|
|
378
|
+
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
379
|
+
let content;
|
|
380
|
+
try {
|
|
381
|
+
content = await backend.readFile(path);
|
|
382
|
+
} catch {
|
|
383
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
384
|
+
}
|
|
385
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
386
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
387
|
+
await backend.writeFile(`${path}.bak`, content);
|
|
388
|
+
await backend.writeFile(path, outcome.result);
|
|
389
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
390
|
+
}
|
|
391
|
+
async function editViaLocal(projectRoot, path, old_string, new_string) {
|
|
392
|
+
const absolutePath = safePathJoin(projectRoot, path);
|
|
393
|
+
let content;
|
|
394
|
+
try {
|
|
395
|
+
content = await readFile(absolutePath, "utf-8");
|
|
396
|
+
} catch (err) {
|
|
397
|
+
const e = err;
|
|
398
|
+
if (e.code === "ENOENT") {
|
|
399
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
400
|
+
}
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
404
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
405
|
+
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
406
|
+
await writeFile(absolutePath, outcome.result, "utf-8");
|
|
407
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
408
|
+
}
|
|
409
|
+
function editScopeError(path, projectRoot) {
|
|
410
|
+
if (isForbiddenPath(path)) {
|
|
411
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, path), projectRoot);
|
|
415
|
+
return null;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
418
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
419
|
+
}
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
350
423
|
function createEditFileTool(opts) {
|
|
351
|
-
const { projectRoot } = opts;
|
|
424
|
+
const { projectRoot, filesystem } = opts;
|
|
352
425
|
return Tool.create({
|
|
353
426
|
name: "edit_file",
|
|
354
427
|
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 }.",
|
|
@@ -357,67 +430,13 @@ function createEditFileTool(opts) {
|
|
|
357
430
|
old_string: z.string().min(1).describe("String to find in the file."),
|
|
358
431
|
new_string: z.string().describe("Replacement string.")
|
|
359
432
|
}),
|
|
360
|
-
|
|
361
|
-
handler: async ({ path, old_string, new_string }) => {
|
|
433
|
+
handler: async ({ path, old_string, new_string }, ctx) => {
|
|
362
434
|
if (old_string === new_string) {
|
|
363
435
|
return JSON.stringify({ ok: false, error: "no_change", path });
|
|
364
436
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
let absolutePath;
|
|
369
|
-
try {
|
|
370
|
-
absolutePath = safePathJoin(projectRoot, path);
|
|
371
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
372
|
-
} catch (err) {
|
|
373
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
374
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
375
|
-
}
|
|
376
|
-
throw err;
|
|
377
|
-
}
|
|
378
|
-
let content;
|
|
379
|
-
try {
|
|
380
|
-
content = await readFile(absolutePath, "utf-8");
|
|
381
|
-
} catch (err) {
|
|
382
|
-
const e = err;
|
|
383
|
-
if (e.code === "ENOENT") {
|
|
384
|
-
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
385
|
-
}
|
|
386
|
-
throw err;
|
|
387
|
-
}
|
|
388
|
-
const exactIdx = content.indexOf(old_string);
|
|
389
|
-
if (exactIdx !== -1) {
|
|
390
|
-
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
391
|
-
const result = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
|
|
392
|
-
await writeFile(absolutePath, result, "utf-8");
|
|
393
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
394
|
-
}
|
|
395
|
-
const normalizedContent = normalizeWhitespace(content);
|
|
396
|
-
const normalizedOld = normalizeWhitespace(old_string);
|
|
397
|
-
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
398
|
-
if (normalizedIdx !== -1) {
|
|
399
|
-
const span = findOriginalSpan(
|
|
400
|
-
content,
|
|
401
|
-
normalizedContent,
|
|
402
|
-
normalizedIdx,
|
|
403
|
-
normalizedOld.length
|
|
404
|
-
);
|
|
405
|
-
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
406
|
-
const result = content.slice(0, span.start) + new_string + content.slice(span.end);
|
|
407
|
-
await writeFile(absolutePath, result, "utf-8");
|
|
408
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
409
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
const result = replaceUnique(content, old_string, new_string);
|
|
412
|
-
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
413
|
-
await writeFile(absolutePath, result, "utf-8");
|
|
414
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
415
|
-
} catch (err) {
|
|
416
|
-
if (err instanceof ContextMatchError) {
|
|
417
|
-
return JSON.stringify({ ok: false, error: "no_match", path });
|
|
418
|
-
}
|
|
419
|
-
throw err;
|
|
420
|
-
}
|
|
437
|
+
const scopeErr = editScopeError(path, projectRoot);
|
|
438
|
+
if (scopeErr !== null) return scopeErr;
|
|
439
|
+
return filesystem !== void 0 ? editViaBackend(filesystem, ctx, path, old_string, new_string) : editViaLocal(projectRoot, path, old_string, new_string);
|
|
421
440
|
}
|
|
422
441
|
});
|
|
423
442
|
}
|
|
@@ -530,11 +549,27 @@ function attachChildSettlers(child, gate, onClose, onError, resolve2) {
|
|
|
530
549
|
// src/git-diff.ts
|
|
531
550
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
532
551
|
var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
|
|
552
|
+
function shq(arg) {
|
|
553
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
554
|
+
}
|
|
555
|
+
async function diffViaSandbox(sandbox, ctx, cached, path, projectRoot, timeoutMs) {
|
|
556
|
+
const scopeCheck = checkPathScope(path, projectRoot);
|
|
557
|
+
if (scopeCheck !== null) return scopeCheck;
|
|
558
|
+
const command = ["git", ...buildDiffArgs(cached, path)].map(shq).join(" ");
|
|
559
|
+
const backend = await resolveSandbox(sandbox, ctx ?? {});
|
|
560
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
561
|
+
if (r.timedOut) return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
|
|
562
|
+
if (r.exitCode !== 0) {
|
|
563
|
+
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 });
|
|
564
|
+
}
|
|
565
|
+
return JSON.stringify({ ok: true, diff: r.stdout, truncated: false });
|
|
566
|
+
}
|
|
533
567
|
function createGitDiffTool(opts) {
|
|
534
568
|
const {
|
|
535
569
|
projectRoot,
|
|
536
570
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
537
|
-
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES
|
|
571
|
+
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES,
|
|
572
|
+
sandbox
|
|
538
573
|
} = opts;
|
|
539
574
|
return Tool.create({
|
|
540
575
|
name: "git_diff",
|
|
@@ -543,7 +578,10 @@ function createGitDiffTool(opts) {
|
|
|
543
578
|
path: z.string().optional().describe("Optional project-relative file or dir scope."),
|
|
544
579
|
cached: z.boolean().optional().describe("If true, show staged changes (git diff --cached). Default false.")
|
|
545
580
|
}),
|
|
546
|
-
handler: async ({ path, cached }) => {
|
|
581
|
+
handler: async ({ path, cached }, ctx) => {
|
|
582
|
+
if (sandbox !== void 0) {
|
|
583
|
+
return diffViaSandbox(sandbox, ctx, cached, path, projectRoot, timeoutMs);
|
|
584
|
+
}
|
|
547
585
|
if (!existsSync(join(projectRoot, ".git"))) {
|
|
548
586
|
return JSON.stringify({ ok: false, error: "not_a_repo" });
|
|
549
587
|
}
|
|
@@ -617,7 +655,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
617
655
|
}
|
|
618
656
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
619
657
|
function createGlobTool(opts) {
|
|
620
|
-
const { projectRoot } = opts;
|
|
658
|
+
const { projectRoot, filesystem } = opts;
|
|
621
659
|
return Tool.create({
|
|
622
660
|
name: "glob_files",
|
|
623
661
|
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 }.",
|
|
@@ -625,20 +663,18 @@ function createGlobTool(opts) {
|
|
|
625
663
|
pattern: z.string().min(1).describe("Glob pattern (e.g. '**/*.ts', 'src/**/*.json')."),
|
|
626
664
|
cwd: z.string().optional().describe("Project-relative subdirectory to search from.")
|
|
627
665
|
}),
|
|
628
|
-
handler: async ({ pattern, cwd }) => {
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
try {
|
|
632
|
-
searchRoot = safePathJoin(projectRoot, cwd);
|
|
633
|
-
assertNoSymlinkEscape(searchRoot, projectRoot);
|
|
634
|
-
} catch (err) {
|
|
635
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
636
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
637
|
-
}
|
|
638
|
-
throw err;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
666
|
+
handler: async ({ pattern, cwd }, ctx) => {
|
|
667
|
+
const scopeErr = globScopeError(cwd, projectRoot);
|
|
668
|
+
if (scopeErr !== null) return scopeErr;
|
|
641
669
|
const regex = globToRegex(pattern);
|
|
670
|
+
if (filesystem !== void 0) {
|
|
671
|
+
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
672
|
+
const searchRel = cwd ?? "";
|
|
673
|
+
const found = [];
|
|
674
|
+
await walkDirBackend(backend, searchRel, searchRel, regex, found);
|
|
675
|
+
return JSON.stringify({ ok: true, files: found.sort(), count: found.length });
|
|
676
|
+
}
|
|
677
|
+
const searchRoot = cwd ? safePathJoin(projectRoot, cwd) : projectRoot;
|
|
642
678
|
const files = [];
|
|
643
679
|
await walkDir(searchRoot, searchRoot, regex, files);
|
|
644
680
|
const relativePaths = files.map((f) => relative(projectRoot, f)).sort();
|
|
@@ -646,6 +682,18 @@ function createGlobTool(opts) {
|
|
|
646
682
|
}
|
|
647
683
|
});
|
|
648
684
|
}
|
|
685
|
+
function globScopeError(cwd, projectRoot) {
|
|
686
|
+
if (!cwd) return null;
|
|
687
|
+
try {
|
|
688
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, cwd), projectRoot);
|
|
689
|
+
return null;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
692
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
693
|
+
}
|
|
694
|
+
throw err;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
649
697
|
async function walkDir(base, dir, pattern, results) {
|
|
650
698
|
let entries;
|
|
651
699
|
try {
|
|
@@ -664,6 +712,33 @@ async function walkDir(base, dir, pattern, results) {
|
|
|
664
712
|
}
|
|
665
713
|
}
|
|
666
714
|
}
|
|
715
|
+
async function walkDirBackend(backend, base, dir, pattern, results) {
|
|
716
|
+
let names;
|
|
717
|
+
try {
|
|
718
|
+
names = await backend.list(dir);
|
|
719
|
+
} catch {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
for (const name of names) {
|
|
723
|
+
await walkBackendEntry(backend, base, dir, name, pattern, results);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function walkBackendEntry(backend, base, dir, name, pattern, results) {
|
|
727
|
+
if (DEFAULT_EXCLUDES.has(name)) return;
|
|
728
|
+
const fullRel = dir === "" ? name : `${dir}/${name}`;
|
|
729
|
+
const relPath = base === "" ? fullRel : relative(base, fullRel);
|
|
730
|
+
let st;
|
|
731
|
+
try {
|
|
732
|
+
st = await backend.stat(fullRel);
|
|
733
|
+
} catch {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (st.isDirectory) {
|
|
737
|
+
await walkDirBackend(backend, base, fullRel, pattern, results);
|
|
738
|
+
} else if (st.isFile && pattern.test(relPath)) {
|
|
739
|
+
results.push(fullRel);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
667
742
|
function globToRegex(pattern) {
|
|
668
743
|
let regexStr = "";
|
|
669
744
|
let i = 0;
|
|
@@ -689,6 +764,60 @@ function globToRegex(pattern) {
|
|
|
689
764
|
}
|
|
690
765
|
return new RegExp(`^${regexStr}$`);
|
|
691
766
|
}
|
|
767
|
+
function toErrorJson(err) {
|
|
768
|
+
if (err instanceof InteractiveUnavailableError) {
|
|
769
|
+
return JSON.stringify({ ok: false, error: "interactive_unavailable" });
|
|
770
|
+
}
|
|
771
|
+
if (err instanceof NoSuchSessionError) {
|
|
772
|
+
return JSON.stringify({ ok: false, error: "no_such_session" });
|
|
773
|
+
}
|
|
774
|
+
throw err;
|
|
775
|
+
}
|
|
776
|
+
function createInteractiveShellTool(opts) {
|
|
777
|
+
const { interactive } = opts;
|
|
778
|
+
return Tool.create({
|
|
779
|
+
name: "interactive_shell",
|
|
780
|
+
description: "Start an interactive shell session for a command that PROMPTS for input or is a REPL (python, node, `git rebase -i`, a `read` prompt) \u2014 NOT for one-shot commands (use shell_exec). Returns a session_id; drive it with write_stdin, reading the incremental output each step. Returns { ok, session_id, output } or { ok: false, error }.",
|
|
781
|
+
inputSchema: z.object({
|
|
782
|
+
command: z.string().min(1).describe("Command to run interactively, e.g. 'python3' or 'bash -i'."),
|
|
783
|
+
yield_time_ms: z.number().int().positive().optional().describe("How long to wait for startup output before returning (clamped by the backend).")
|
|
784
|
+
}),
|
|
785
|
+
handler: async ({ command, yield_time_ms }, ctx) => {
|
|
786
|
+
try {
|
|
787
|
+
const backend = await resolveInteractive(interactive, ctx ?? {});
|
|
788
|
+
const { sessionId, output } = await backend.startInteractive(command, {
|
|
789
|
+
yieldMs: yield_time_ms
|
|
790
|
+
});
|
|
791
|
+
return JSON.stringify({ ok: true, session_id: sessionId, output });
|
|
792
|
+
} catch (err) {
|
|
793
|
+
return toErrorJson(err);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function createWriteStdinTool(opts) {
|
|
799
|
+
const { interactive } = opts;
|
|
800
|
+
return Tool.create({
|
|
801
|
+
name: "write_stdin",
|
|
802
|
+
description: "Write input to a live interactive session (from interactive_shell) and read the output it produces during the wait window. Include a trailing newline to submit a line. Returns { ok, output, alive } (alive:false means the session exited) or { ok: false, error }.",
|
|
803
|
+
inputSchema: z.object({
|
|
804
|
+
session_id: z.string().min(1).describe("The session_id returned by interactive_shell."),
|
|
805
|
+
input: z.string().describe("Text to write to stdin (add a trailing '\\n' to submit a line)."),
|
|
806
|
+
yield_time_ms: z.number().int().positive().optional().describe("How long to wait for output before returning (clamped by the backend).")
|
|
807
|
+
}),
|
|
808
|
+
handler: async ({ session_id, input, yield_time_ms }, ctx) => {
|
|
809
|
+
try {
|
|
810
|
+
const backend = await resolveInteractive(interactive, ctx ?? {});
|
|
811
|
+
const { output, alive } = await backend.writeStdin(session_id, input, {
|
|
812
|
+
yieldMs: yield_time_ms
|
|
813
|
+
});
|
|
814
|
+
return JSON.stringify({ ok: true, output, alive });
|
|
815
|
+
} catch (err) {
|
|
816
|
+
return toErrorJson(err);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
692
821
|
var CatastrophicCommandError = class extends ConfigurationError {
|
|
693
822
|
name = "CatastrophicCommandError";
|
|
694
823
|
constructor(reason) {
|
|
@@ -1681,7 +1810,8 @@ function createSearchTextTool(opts) {
|
|
|
1681
1810
|
const {
|
|
1682
1811
|
projectRoot,
|
|
1683
1812
|
maxMatches = DEFAULT_MAX_MATCHES,
|
|
1684
|
-
maxFileSize = DEFAULT_MAX_FILE_SIZE
|
|
1813
|
+
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
1814
|
+
filesystem
|
|
1685
1815
|
} = opts;
|
|
1686
1816
|
return Tool.create({
|
|
1687
1817
|
name: "search_text",
|
|
@@ -1690,9 +1820,7 @@ function createSearchTextTool(opts) {
|
|
|
1690
1820
|
query: z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
1691
1821
|
path: z.string().optional().describe("Optional project-relative directory to scope the search.")
|
|
1692
1822
|
}),
|
|
1693
|
-
handler: async ({ query, path }) => {
|
|
1694
|
-
const scope = resolveSearchScope(path, projectRoot);
|
|
1695
|
-
if ("error" in scope) return scope.error;
|
|
1823
|
+
handler: async ({ query, path }, ctx) => {
|
|
1696
1824
|
const state = {
|
|
1697
1825
|
matches: [],
|
|
1698
1826
|
totalMatches: 0,
|
|
@@ -1702,6 +1830,20 @@ function createSearchTextTool(opts) {
|
|
|
1702
1830
|
maxFileSize,
|
|
1703
1831
|
projectRoot
|
|
1704
1832
|
};
|
|
1833
|
+
if (filesystem !== void 0) {
|
|
1834
|
+
const scopeRel = resolveScopeRel(path, projectRoot);
|
|
1835
|
+
if ("error" in scopeRel) return scopeRel.error;
|
|
1836
|
+
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
1837
|
+
await walkBackend(backend, scopeRel.rel, state);
|
|
1838
|
+
return JSON.stringify({
|
|
1839
|
+
ok: true,
|
|
1840
|
+
matches: state.matches,
|
|
1841
|
+
truncated: state.truncated,
|
|
1842
|
+
totalMatches: state.totalMatches
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
const scope = resolveSearchScope(path, projectRoot);
|
|
1846
|
+
if ("error" in scope) return scope.error;
|
|
1705
1847
|
await walk(scope.scopeAbs, state);
|
|
1706
1848
|
return JSON.stringify({
|
|
1707
1849
|
ok: true,
|
|
@@ -1789,11 +1931,78 @@ async function scanFile(absPath, relPath, state) {
|
|
|
1789
1931
|
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1790
1932
|
}
|
|
1791
1933
|
}
|
|
1934
|
+
function resolveScopeRel(path, projectRoot) {
|
|
1935
|
+
const scopeRel = path === void 0 || path === "" || path === "." ? "" : path;
|
|
1936
|
+
if (scopeRel === "") return { rel: "" };
|
|
1937
|
+
try {
|
|
1938
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
1939
|
+
return { rel: scopeRel };
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1942
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
1943
|
+
}
|
|
1944
|
+
throw err;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function walkBackend(backend, dirRel, state) {
|
|
1948
|
+
if (state.truncated) return;
|
|
1949
|
+
let names;
|
|
1950
|
+
try {
|
|
1951
|
+
names = await backend.list(dirRel);
|
|
1952
|
+
} catch {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
for (const name of names) {
|
|
1956
|
+
if (state.truncated) return;
|
|
1957
|
+
await handleBackendEntry(backend, dirRel, name, state);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
async function handleBackendEntry(backend, dirRel, name, state) {
|
|
1961
|
+
const entryRel = dirRel === "" ? name : `${dirRel}/${name}`;
|
|
1962
|
+
if (isForbiddenPath(entryRel)) return;
|
|
1963
|
+
let st;
|
|
1964
|
+
try {
|
|
1965
|
+
st = await backend.stat(entryRel);
|
|
1966
|
+
} catch {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (st.isDirectory) {
|
|
1970
|
+
await walkBackend(backend, entryRel, state);
|
|
1971
|
+
} else if (st.isFile) {
|
|
1972
|
+
await scanFileBackend(backend, entryRel, st.size, state);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
async function scanFileBackend(backend, relPath, size, state) {
|
|
1976
|
+
if (size > state.maxFileSize) return;
|
|
1977
|
+
let content;
|
|
1978
|
+
try {
|
|
1979
|
+
content = await backend.readFile(relPath);
|
|
1980
|
+
} catch {
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if (content.slice(0, BINARY_PROBE_BYTES2).includes("\0")) return;
|
|
1984
|
+
const lines = content.split("\n");
|
|
1985
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1986
|
+
const line = lines[i];
|
|
1987
|
+
if (!line.includes(state.query)) continue;
|
|
1988
|
+
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1792
1991
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
1793
1992
|
var MAX_TIMEOUT_MS = 3e5;
|
|
1794
1993
|
var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1994
|
+
async function execViaSandbox(sandbox, ctx, command, timeoutMs) {
|
|
1995
|
+
const backend = await resolveSandbox(sandbox, ctx ?? {});
|
|
1996
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
1997
|
+
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 });
|
|
1998
|
+
}
|
|
1795
1999
|
function createShellTool(opts) {
|
|
1796
|
-
const {
|
|
2000
|
+
const {
|
|
2001
|
+
projectRoot,
|
|
2002
|
+
defaultTimeoutMs = DEFAULT_TIMEOUT_MS3,
|
|
2003
|
+
allowCatastrophic = false,
|
|
2004
|
+
sandbox
|
|
2005
|
+
} = opts;
|
|
1797
2006
|
return Tool.create({
|
|
1798
2007
|
name: "shell_exec",
|
|
1799
2008
|
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 }.",
|
|
@@ -1801,7 +2010,7 @@ function createShellTool(opts) {
|
|
|
1801
2010
|
command: z.string().min(1).describe("Shell command to execute."),
|
|
1802
2011
|
timeout_ms: z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
1803
2012
|
}),
|
|
1804
|
-
handler: async ({ command, timeout_ms }) => {
|
|
2013
|
+
handler: async ({ command, timeout_ms }, ctx) => {
|
|
1805
2014
|
if (!allowCatastrophic) {
|
|
1806
2015
|
const reason = catastrophicShellReason(command);
|
|
1807
2016
|
if (reason) {
|
|
@@ -1810,8 +2019,8 @@ function createShellTool(opts) {
|
|
|
1810
2019
|
}
|
|
1811
2020
|
}
|
|
1812
2021
|
const timeoutMs = Math.min(timeout_ms ?? defaultTimeoutMs, MAX_TIMEOUT_MS);
|
|
1813
|
-
|
|
1814
|
-
return
|
|
2022
|
+
if (sandbox !== void 0) return execViaSandbox(sandbox, ctx, command, timeoutMs);
|
|
2023
|
+
return runShell(projectRoot, command, timeoutMs);
|
|
1815
2024
|
}
|
|
1816
2025
|
});
|
|
1817
2026
|
}
|
|
@@ -2341,6 +2550,6 @@ async function isBinaryFile(absolutePath) {
|
|
|
2341
2550
|
}
|
|
2342
2551
|
}
|
|
2343
2552
|
|
|
2344
|
-
export { CatastrophicCommandError, ContextMatchError, DEFAULT_TOOL_GUIDANCE, ReadTracker, ReasoningTools, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };
|
|
2553
|
+
export { CatastrophicCommandError, ContextMatchError, DEFAULT_TOOL_GUIDANCE, ReadTracker, ReasoningTools, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createInteractiveShellTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, createWriteStdinTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, replaceUnique, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withName, withShellExitGuidance, withToolResultGuidance };
|
|
2345
2554
|
//# sourceMappingURL=index.js.map
|
|
2346
2555
|
//# sourceMappingURL=index.js.map
|