@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 324835f: M15 — complete the surface-agnostic tool injection. `search_text`, `glob_files`, and `edit_file` now
|
|
8
|
+
accept an optional `filesystem` (`FilesystemProvider`), joining `shell_exec`/`git_diff` (`sandbox`) and
|
|
9
|
+
`interactive_shell`/`write_stdin` (`interactive`). When a backend is injected the recursive walk / read
|
|
10
|
+
/ backup / write go through it in project-relative path space (so the tool runs unchanged on a local
|
|
11
|
+
disk, a cluster container, or a Tauri desktop); when omitted the local `fs` path is byte-identical to
|
|
12
|
+
before. Backward compatibility is proven by conformance tests that run each tool through the real
|
|
13
|
+
`LocalFilesystem`/`LocalSandbox` backends and assert identical output to the local path. Additive — no
|
|
14
|
+
breaking change.
|
|
15
|
+
|
|
3
16
|
## 0.11.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -7,10 +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');
|
|
13
|
+
var interactive = require('@theokit/sdk/interactive');
|
|
11
14
|
var promises$1 = require('dns/promises');
|
|
12
15
|
var net = require('net');
|
|
13
|
-
var filesystem = require('@theokit/sdk/filesystem');
|
|
14
16
|
|
|
15
17
|
// src/apply-patch.ts
|
|
16
18
|
var PathTraversalError = class extends sdk.ConfigurationError {
|
|
@@ -349,8 +351,79 @@ ${find}`);
|
|
|
349
351
|
}
|
|
350
352
|
|
|
351
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 {
|
|
385
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
386
|
+
}
|
|
387
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
388
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
389
|
+
await backend.writeFile(`${path}.bak`, content);
|
|
390
|
+
await backend.writeFile(path, outcome.result);
|
|
391
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
392
|
+
}
|
|
393
|
+
async function editViaLocal(projectRoot, path, old_string, new_string) {
|
|
394
|
+
const absolutePath = safePathJoin(projectRoot, path);
|
|
395
|
+
let content;
|
|
396
|
+
try {
|
|
397
|
+
content = await promises.readFile(absolutePath, "utf-8");
|
|
398
|
+
} catch (err) {
|
|
399
|
+
const e = err;
|
|
400
|
+
if (e.code === "ENOENT") {
|
|
401
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
406
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
407
|
+
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
408
|
+
await promises.writeFile(absolutePath, outcome.result, "utf-8");
|
|
409
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
410
|
+
}
|
|
411
|
+
function editScopeError(path, projectRoot) {
|
|
412
|
+
if (isForbiddenPath(path)) {
|
|
413
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, path), projectRoot);
|
|
417
|
+
return null;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
420
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
421
|
+
}
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
352
425
|
function createEditFileTool(opts) {
|
|
353
|
-
const { projectRoot } = opts;
|
|
426
|
+
const { projectRoot, filesystem } = opts;
|
|
354
427
|
return sdk.Tool.create({
|
|
355
428
|
name: "edit_file",
|
|
356
429
|
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 }.",
|
|
@@ -359,67 +432,13 @@ function createEditFileTool(opts) {
|
|
|
359
432
|
old_string: zod.z.string().min(1).describe("String to find in the file."),
|
|
360
433
|
new_string: zod.z.string().describe("Replacement string.")
|
|
361
434
|
}),
|
|
362
|
-
|
|
363
|
-
handler: async ({ path, old_string, new_string }) => {
|
|
435
|
+
handler: async ({ path, old_string, new_string }, ctx) => {
|
|
364
436
|
if (old_string === new_string) {
|
|
365
437
|
return JSON.stringify({ ok: false, error: "no_change", path });
|
|
366
438
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
let absolutePath;
|
|
371
|
-
try {
|
|
372
|
-
absolutePath = safePathJoin(projectRoot, path);
|
|
373
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
374
|
-
} catch (err) {
|
|
375
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
376
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
377
|
-
}
|
|
378
|
-
throw err;
|
|
379
|
-
}
|
|
380
|
-
let content;
|
|
381
|
-
try {
|
|
382
|
-
content = await promises.readFile(absolutePath, "utf-8");
|
|
383
|
-
} catch (err) {
|
|
384
|
-
const e = err;
|
|
385
|
-
if (e.code === "ENOENT") {
|
|
386
|
-
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
387
|
-
}
|
|
388
|
-
throw err;
|
|
389
|
-
}
|
|
390
|
-
const exactIdx = content.indexOf(old_string);
|
|
391
|
-
if (exactIdx !== -1) {
|
|
392
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
393
|
-
const result = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
|
|
394
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
395
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
396
|
-
}
|
|
397
|
-
const normalizedContent = normalizeWhitespace(content);
|
|
398
|
-
const normalizedOld = normalizeWhitespace(old_string);
|
|
399
|
-
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
400
|
-
if (normalizedIdx !== -1) {
|
|
401
|
-
const span = findOriginalSpan(
|
|
402
|
-
content,
|
|
403
|
-
normalizedContent,
|
|
404
|
-
normalizedIdx,
|
|
405
|
-
normalizedOld.length
|
|
406
|
-
);
|
|
407
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
408
|
-
const result = content.slice(0, span.start) + new_string + content.slice(span.end);
|
|
409
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
410
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
const result = replaceUnique(content, old_string, new_string);
|
|
414
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
415
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
416
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
417
|
-
} catch (err) {
|
|
418
|
-
if (err instanceof ContextMatchError) {
|
|
419
|
-
return JSON.stringify({ ok: false, error: "no_match", path });
|
|
420
|
-
}
|
|
421
|
-
throw err;
|
|
422
|
-
}
|
|
439
|
+
const scopeErr = editScopeError(path, projectRoot);
|
|
440
|
+
if (scopeErr !== null) return scopeErr;
|
|
441
|
+
return filesystem !== void 0 ? editViaBackend(filesystem, ctx, path, old_string, new_string) : editViaLocal(projectRoot, path, old_string, new_string);
|
|
423
442
|
}
|
|
424
443
|
});
|
|
425
444
|
}
|
|
@@ -532,11 +551,27 @@ function attachChildSettlers(child, gate, onClose, onError, resolve2) {
|
|
|
532
551
|
// src/git-diff.ts
|
|
533
552
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
534
553
|
var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
|
|
554
|
+
function shq(arg) {
|
|
555
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
556
|
+
}
|
|
557
|
+
async function diffViaSandbox(sandbox$1, ctx, cached, path, projectRoot, timeoutMs) {
|
|
558
|
+
const scopeCheck = checkPathScope(path, projectRoot);
|
|
559
|
+
if (scopeCheck !== null) return scopeCheck;
|
|
560
|
+
const command = ["git", ...buildDiffArgs(cached, path)].map(shq).join(" ");
|
|
561
|
+
const backend = await sandbox.resolveSandbox(sandbox$1, ctx ?? {});
|
|
562
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
563
|
+
if (r.timedOut) return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
|
|
564
|
+
if (r.exitCode !== 0) {
|
|
565
|
+
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 });
|
|
566
|
+
}
|
|
567
|
+
return JSON.stringify({ ok: true, diff: r.stdout, truncated: false });
|
|
568
|
+
}
|
|
535
569
|
function createGitDiffTool(opts) {
|
|
536
570
|
const {
|
|
537
571
|
projectRoot,
|
|
538
572
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
539
|
-
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES
|
|
573
|
+
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES,
|
|
574
|
+
sandbox
|
|
540
575
|
} = opts;
|
|
541
576
|
return sdk.Tool.create({
|
|
542
577
|
name: "git_diff",
|
|
@@ -545,7 +580,10 @@ function createGitDiffTool(opts) {
|
|
|
545
580
|
path: zod.z.string().optional().describe("Optional project-relative file or dir scope."),
|
|
546
581
|
cached: zod.z.boolean().optional().describe("If true, show staged changes (git diff --cached). Default false.")
|
|
547
582
|
}),
|
|
548
|
-
handler: async ({ path: path$1, cached }) => {
|
|
583
|
+
handler: async ({ path: path$1, cached }, ctx) => {
|
|
584
|
+
if (sandbox !== void 0) {
|
|
585
|
+
return diffViaSandbox(sandbox, ctx, cached, path$1, projectRoot, timeoutMs);
|
|
586
|
+
}
|
|
549
587
|
if (!fs.existsSync(path.join(projectRoot, ".git"))) {
|
|
550
588
|
return JSON.stringify({ ok: false, error: "not_a_repo" });
|
|
551
589
|
}
|
|
@@ -619,7 +657,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
619
657
|
}
|
|
620
658
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
621
659
|
function createGlobTool(opts) {
|
|
622
|
-
const { projectRoot } = opts;
|
|
660
|
+
const { projectRoot, filesystem: filesystem$1 } = opts;
|
|
623
661
|
return sdk.Tool.create({
|
|
624
662
|
name: "glob_files",
|
|
625
663
|
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 }.",
|
|
@@ -627,20 +665,18 @@ function createGlobTool(opts) {
|
|
|
627
665
|
pattern: zod.z.string().min(1).describe("Glob pattern (e.g. '**/*.ts', 'src/**/*.json')."),
|
|
628
666
|
cwd: zod.z.string().optional().describe("Project-relative subdirectory to search from.")
|
|
629
667
|
}),
|
|
630
|
-
handler: async ({ pattern, cwd }) => {
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
try {
|
|
634
|
-
searchRoot = safePathJoin(projectRoot, cwd);
|
|
635
|
-
assertNoSymlinkEscape(searchRoot, projectRoot);
|
|
636
|
-
} catch (err) {
|
|
637
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
638
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
639
|
-
}
|
|
640
|
-
throw err;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
668
|
+
handler: async ({ pattern, cwd }, ctx) => {
|
|
669
|
+
const scopeErr = globScopeError(cwd, projectRoot);
|
|
670
|
+
if (scopeErr !== null) return scopeErr;
|
|
643
671
|
const regex = globToRegex(pattern);
|
|
672
|
+
if (filesystem$1 !== void 0) {
|
|
673
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
674
|
+
const searchRel = cwd ?? "";
|
|
675
|
+
const found = [];
|
|
676
|
+
await walkDirBackend(backend, searchRel, searchRel, regex, found);
|
|
677
|
+
return JSON.stringify({ ok: true, files: found.sort(), count: found.length });
|
|
678
|
+
}
|
|
679
|
+
const searchRoot = cwd ? safePathJoin(projectRoot, cwd) : projectRoot;
|
|
644
680
|
const files = [];
|
|
645
681
|
await walkDir(searchRoot, searchRoot, regex, files);
|
|
646
682
|
const relativePaths = files.map((f) => path.relative(projectRoot, f)).sort();
|
|
@@ -648,6 +684,18 @@ function createGlobTool(opts) {
|
|
|
648
684
|
}
|
|
649
685
|
});
|
|
650
686
|
}
|
|
687
|
+
function globScopeError(cwd, projectRoot) {
|
|
688
|
+
if (!cwd) return null;
|
|
689
|
+
try {
|
|
690
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, cwd), projectRoot);
|
|
691
|
+
return null;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
694
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
695
|
+
}
|
|
696
|
+
throw err;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
651
699
|
async function walkDir(base, dir, pattern, results) {
|
|
652
700
|
let entries;
|
|
653
701
|
try {
|
|
@@ -666,6 +714,33 @@ async function walkDir(base, dir, pattern, results) {
|
|
|
666
714
|
}
|
|
667
715
|
}
|
|
668
716
|
}
|
|
717
|
+
async function walkDirBackend(backend, base, dir, pattern, results) {
|
|
718
|
+
let names;
|
|
719
|
+
try {
|
|
720
|
+
names = await backend.list(dir);
|
|
721
|
+
} catch {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
for (const name of names) {
|
|
725
|
+
await walkBackendEntry(backend, base, dir, name, pattern, results);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function walkBackendEntry(backend, base, dir, name, pattern, results) {
|
|
729
|
+
if (DEFAULT_EXCLUDES.has(name)) return;
|
|
730
|
+
const fullRel = dir === "" ? name : `${dir}/${name}`;
|
|
731
|
+
const relPath = base === "" ? fullRel : path.relative(base, fullRel);
|
|
732
|
+
let st;
|
|
733
|
+
try {
|
|
734
|
+
st = await backend.stat(fullRel);
|
|
735
|
+
} catch {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (st.isDirectory) {
|
|
739
|
+
await walkDirBackend(backend, base, fullRel, pattern, results);
|
|
740
|
+
} else if (st.isFile && pattern.test(relPath)) {
|
|
741
|
+
results.push(fullRel);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
669
744
|
function globToRegex(pattern) {
|
|
670
745
|
let regexStr = "";
|
|
671
746
|
let i = 0;
|
|
@@ -691,6 +766,60 @@ function globToRegex(pattern) {
|
|
|
691
766
|
}
|
|
692
767
|
return new RegExp(`^${regexStr}$`);
|
|
693
768
|
}
|
|
769
|
+
function toErrorJson(err) {
|
|
770
|
+
if (err instanceof interactive.InteractiveUnavailableError) {
|
|
771
|
+
return JSON.stringify({ ok: false, error: "interactive_unavailable" });
|
|
772
|
+
}
|
|
773
|
+
if (err instanceof interactive.NoSuchSessionError) {
|
|
774
|
+
return JSON.stringify({ ok: false, error: "no_such_session" });
|
|
775
|
+
}
|
|
776
|
+
throw err;
|
|
777
|
+
}
|
|
778
|
+
function createInteractiveShellTool(opts) {
|
|
779
|
+
const { interactive: interactive$1 } = opts;
|
|
780
|
+
return sdk.Tool.create({
|
|
781
|
+
name: "interactive_shell",
|
|
782
|
+
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 }.",
|
|
783
|
+
inputSchema: zod.z.object({
|
|
784
|
+
command: zod.z.string().min(1).describe("Command to run interactively, e.g. 'python3' or 'bash -i'."),
|
|
785
|
+
yield_time_ms: zod.z.number().int().positive().optional().describe("How long to wait for startup output before returning (clamped by the backend).")
|
|
786
|
+
}),
|
|
787
|
+
handler: async ({ command, yield_time_ms }, ctx) => {
|
|
788
|
+
try {
|
|
789
|
+
const backend = await interactive.resolveInteractive(interactive$1, ctx ?? {});
|
|
790
|
+
const { sessionId, output } = await backend.startInteractive(command, {
|
|
791
|
+
yieldMs: yield_time_ms
|
|
792
|
+
});
|
|
793
|
+
return JSON.stringify({ ok: true, session_id: sessionId, output });
|
|
794
|
+
} catch (err) {
|
|
795
|
+
return toErrorJson(err);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function createWriteStdinTool(opts) {
|
|
801
|
+
const { interactive: interactive$1 } = opts;
|
|
802
|
+
return sdk.Tool.create({
|
|
803
|
+
name: "write_stdin",
|
|
804
|
+
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 }.",
|
|
805
|
+
inputSchema: zod.z.object({
|
|
806
|
+
session_id: zod.z.string().min(1).describe("The session_id returned by interactive_shell."),
|
|
807
|
+
input: zod.z.string().describe("Text to write to stdin (add a trailing '\\n' to submit a line)."),
|
|
808
|
+
yield_time_ms: zod.z.number().int().positive().optional().describe("How long to wait for output before returning (clamped by the backend).")
|
|
809
|
+
}),
|
|
810
|
+
handler: async ({ session_id, input, yield_time_ms }, ctx) => {
|
|
811
|
+
try {
|
|
812
|
+
const backend = await interactive.resolveInteractive(interactive$1, ctx ?? {});
|
|
813
|
+
const { output, alive } = await backend.writeStdin(session_id, input, {
|
|
814
|
+
yieldMs: yield_time_ms
|
|
815
|
+
});
|
|
816
|
+
return JSON.stringify({ ok: true, output, alive });
|
|
817
|
+
} catch (err) {
|
|
818
|
+
return toErrorJson(err);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
694
823
|
var CatastrophicCommandError = class extends sdk.ConfigurationError {
|
|
695
824
|
name = "CatastrophicCommandError";
|
|
696
825
|
constructor(reason) {
|
|
@@ -1683,7 +1812,8 @@ function createSearchTextTool(opts) {
|
|
|
1683
1812
|
const {
|
|
1684
1813
|
projectRoot,
|
|
1685
1814
|
maxMatches = DEFAULT_MAX_MATCHES,
|
|
1686
|
-
maxFileSize = DEFAULT_MAX_FILE_SIZE
|
|
1815
|
+
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
1816
|
+
filesystem: filesystem$1
|
|
1687
1817
|
} = opts;
|
|
1688
1818
|
return sdk.Tool.create({
|
|
1689
1819
|
name: "search_text",
|
|
@@ -1692,9 +1822,7 @@ function createSearchTextTool(opts) {
|
|
|
1692
1822
|
query: zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
1693
1823
|
path: zod.z.string().optional().describe("Optional project-relative directory to scope the search.")
|
|
1694
1824
|
}),
|
|
1695
|
-
handler: async ({ query, path }) => {
|
|
1696
|
-
const scope = resolveSearchScope(path, projectRoot);
|
|
1697
|
-
if ("error" in scope) return scope.error;
|
|
1825
|
+
handler: async ({ query, path }, ctx) => {
|
|
1698
1826
|
const state = {
|
|
1699
1827
|
matches: [],
|
|
1700
1828
|
totalMatches: 0,
|
|
@@ -1704,6 +1832,20 @@ function createSearchTextTool(opts) {
|
|
|
1704
1832
|
maxFileSize,
|
|
1705
1833
|
projectRoot
|
|
1706
1834
|
};
|
|
1835
|
+
if (filesystem$1 !== void 0) {
|
|
1836
|
+
const scopeRel = resolveScopeRel(path, projectRoot);
|
|
1837
|
+
if ("error" in scopeRel) return scopeRel.error;
|
|
1838
|
+
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
1839
|
+
await walkBackend(backend, scopeRel.rel, state);
|
|
1840
|
+
return JSON.stringify({
|
|
1841
|
+
ok: true,
|
|
1842
|
+
matches: state.matches,
|
|
1843
|
+
truncated: state.truncated,
|
|
1844
|
+
totalMatches: state.totalMatches
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
const scope = resolveSearchScope(path, projectRoot);
|
|
1848
|
+
if ("error" in scope) return scope.error;
|
|
1707
1849
|
await walk(scope.scopeAbs, state);
|
|
1708
1850
|
return JSON.stringify({
|
|
1709
1851
|
ok: true,
|
|
@@ -1791,11 +1933,78 @@ async function scanFile(absPath, relPath, state) {
|
|
|
1791
1933
|
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1792
1934
|
}
|
|
1793
1935
|
}
|
|
1936
|
+
function resolveScopeRel(path, projectRoot) {
|
|
1937
|
+
const scopeRel = path === void 0 || path === "" || path === "." ? "" : path;
|
|
1938
|
+
if (scopeRel === "") return { rel: "" };
|
|
1939
|
+
try {
|
|
1940
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
1941
|
+
return { rel: scopeRel };
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1944
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
1945
|
+
}
|
|
1946
|
+
throw err;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
async function walkBackend(backend, dirRel, state) {
|
|
1950
|
+
if (state.truncated) return;
|
|
1951
|
+
let names;
|
|
1952
|
+
try {
|
|
1953
|
+
names = await backend.list(dirRel);
|
|
1954
|
+
} catch {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
for (const name of names) {
|
|
1958
|
+
if (state.truncated) return;
|
|
1959
|
+
await handleBackendEntry(backend, dirRel, name, state);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
async function handleBackendEntry(backend, dirRel, name, state) {
|
|
1963
|
+
const entryRel = dirRel === "" ? name : `${dirRel}/${name}`;
|
|
1964
|
+
if (isForbiddenPath(entryRel)) return;
|
|
1965
|
+
let st;
|
|
1966
|
+
try {
|
|
1967
|
+
st = await backend.stat(entryRel);
|
|
1968
|
+
} catch {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (st.isDirectory) {
|
|
1972
|
+
await walkBackend(backend, entryRel, state);
|
|
1973
|
+
} else if (st.isFile) {
|
|
1974
|
+
await scanFileBackend(backend, entryRel, st.size, state);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async function scanFileBackend(backend, relPath, size, state) {
|
|
1978
|
+
if (size > state.maxFileSize) return;
|
|
1979
|
+
let content;
|
|
1980
|
+
try {
|
|
1981
|
+
content = await backend.readFile(relPath);
|
|
1982
|
+
} catch {
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (content.slice(0, BINARY_PROBE_BYTES2).includes("\0")) return;
|
|
1986
|
+
const lines = content.split("\n");
|
|
1987
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1988
|
+
const line = lines[i];
|
|
1989
|
+
if (!line.includes(state.query)) continue;
|
|
1990
|
+
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1794
1993
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
1795
1994
|
var MAX_TIMEOUT_MS = 3e5;
|
|
1796
1995
|
var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
1996
|
+
async function execViaSandbox(sandbox$1, ctx, command, timeoutMs) {
|
|
1997
|
+
const backend = await sandbox.resolveSandbox(sandbox$1, ctx ?? {});
|
|
1998
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
1999
|
+
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 });
|
|
2000
|
+
}
|
|
1797
2001
|
function createShellTool(opts) {
|
|
1798
|
-
const {
|
|
2002
|
+
const {
|
|
2003
|
+
projectRoot,
|
|
2004
|
+
defaultTimeoutMs = DEFAULT_TIMEOUT_MS3,
|
|
2005
|
+
allowCatastrophic = false,
|
|
2006
|
+
sandbox
|
|
2007
|
+
} = opts;
|
|
1799
2008
|
return sdk.Tool.create({
|
|
1800
2009
|
name: "shell_exec",
|
|
1801
2010
|
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 }.",
|
|
@@ -1803,7 +2012,7 @@ function createShellTool(opts) {
|
|
|
1803
2012
|
command: zod.z.string().min(1).describe("Shell command to execute."),
|
|
1804
2013
|
timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
1805
2014
|
}),
|
|
1806
|
-
handler: async ({ command, timeout_ms }) => {
|
|
2015
|
+
handler: async ({ command, timeout_ms }, ctx) => {
|
|
1807
2016
|
if (!allowCatastrophic) {
|
|
1808
2017
|
const reason = catastrophicShellReason(command);
|
|
1809
2018
|
if (reason) {
|
|
@@ -1812,8 +2021,8 @@ function createShellTool(opts) {
|
|
|
1812
2021
|
}
|
|
1813
2022
|
}
|
|
1814
2023
|
const timeoutMs = Math.min(timeout_ms ?? defaultTimeoutMs, MAX_TIMEOUT_MS);
|
|
1815
|
-
|
|
1816
|
-
return
|
|
2024
|
+
if (sandbox !== void 0) return execViaSandbox(sandbox, ctx, command, timeoutMs);
|
|
2025
|
+
return runShell(projectRoot, command, timeoutMs);
|
|
1817
2026
|
}
|
|
1818
2027
|
});
|
|
1819
2028
|
}
|
|
@@ -2360,6 +2569,7 @@ exports.createEditFileTool = createEditFileTool;
|
|
|
2360
2569
|
exports.createGenericHttpSearchAdapter = createGenericHttpSearchAdapter;
|
|
2361
2570
|
exports.createGitDiffTool = createGitDiffTool;
|
|
2362
2571
|
exports.createGlobTool = createGlobTool;
|
|
2572
|
+
exports.createInteractiveShellTool = createInteractiveShellTool;
|
|
2363
2573
|
exports.createListDirTool = createListDirTool;
|
|
2364
2574
|
exports.createPlanModeTool = createPlanModeTool;
|
|
2365
2575
|
exports.createQuestionTool = createQuestionTool;
|
|
@@ -2372,6 +2582,7 @@ exports.createTodolistTool = createTodolistTool;
|
|
|
2372
2582
|
exports.createWebFetchTool = createWebFetchTool;
|
|
2373
2583
|
exports.createWebSearchTool = createWebSearchTool;
|
|
2374
2584
|
exports.createWriteFileTool = createWriteFileTool;
|
|
2585
|
+
exports.createWriteStdinTool = createWriteStdinTool;
|
|
2375
2586
|
exports.denyCatastrophicCommands = denyCatastrophicCommands;
|
|
2376
2587
|
exports.formatCode = formatCode;
|
|
2377
2588
|
exports.formatDiff = formatDiff;
|