@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/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CustomTool, ConfigurationError } from '@theokit/sdk';
|
|
2
|
-
import { InteractiveProvider } from '@theokit/sdk/interactive';
|
|
3
2
|
import { FilesystemProvider } from '@theokit/sdk/filesystem';
|
|
3
|
+
import { SandboxProvider } from '@theokit/sdk/sandbox';
|
|
4
|
+
import { InteractiveProvider } from '@theokit/sdk/interactive';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `apply_patch` — built-in tool for coding agents.
|
|
@@ -89,6 +90,9 @@ declare function createSessionArtifactStore(options: SessionArtifactStoreOptions
|
|
|
89
90
|
interface CreateEditFileToolOptions {
|
|
90
91
|
/** Absolute path to the project root. Every edit is gated against this boundary. */
|
|
91
92
|
projectRoot: string;
|
|
93
|
+
/** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the read + `.bak` backup +
|
|
94
|
+
* write go through the backend (surface-agnostic); omitted ⇒ the local `fs` path (byte-identical). */
|
|
95
|
+
filesystem?: FilesystemProvider;
|
|
92
96
|
}
|
|
93
97
|
declare function createEditFileTool(opts: CreateEditFileToolOptions): CustomTool;
|
|
94
98
|
|
|
@@ -134,6 +138,9 @@ interface CreateGitDiffToolOptions {
|
|
|
134
138
|
projectRoot: string;
|
|
135
139
|
timeoutMs?: number;
|
|
136
140
|
maxStdoutBytes?: number;
|
|
141
|
+
/** Optional injected execution backend (`@theokit/sdk/sandbox`) — when provided, `git diff` runs via
|
|
142
|
+
* `SandboxBackend.execute` (surface-agnostic); omitted ⇒ the local `git` child process (unchanged). */
|
|
143
|
+
sandbox?: SandboxProvider;
|
|
137
144
|
}
|
|
138
145
|
declare function createGitDiffTool(opts: CreateGitDiffToolOptions): CustomTool;
|
|
139
146
|
|
|
@@ -151,6 +158,9 @@ declare function createGitDiffTool(opts: CreateGitDiffToolOptions): CustomTool;
|
|
|
151
158
|
interface CreateGlobToolOptions {
|
|
152
159
|
/** Absolute path to the project root. */
|
|
153
160
|
projectRoot: string;
|
|
161
|
+
/** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the walk reads through the
|
|
162
|
+
* backend (Local/remote), so the tool is surface-agnostic; omitted ⇒ the local `readdir` (unchanged). */
|
|
163
|
+
filesystem?: FilesystemProvider;
|
|
154
164
|
}
|
|
155
165
|
declare function createGlobTool(opts: CreateGlobToolOptions): CustomTool;
|
|
156
166
|
|
|
@@ -734,6 +744,9 @@ interface CreateSearchTextToolOptions {
|
|
|
734
744
|
maxMatches?: number;
|
|
735
745
|
/** Skip files larger than this (bytes). Default 1 MB. */
|
|
736
746
|
maxFileSize?: number;
|
|
747
|
+
/** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the recursive walk reads
|
|
748
|
+
* through the backend (surface-agnostic); omitted ⇒ the local `readdir`/`readFile` walk (unchanged). */
|
|
749
|
+
filesystem?: FilesystemProvider;
|
|
737
750
|
}
|
|
738
751
|
declare function createSearchTextTool(opts: CreateSearchTextToolOptions): CustomTool;
|
|
739
752
|
|
|
@@ -760,6 +773,14 @@ interface CreateShellToolOptions {
|
|
|
760
773
|
* guardrail, not a sandbox.
|
|
761
774
|
*/
|
|
762
775
|
allowCatastrophic?: boolean;
|
|
776
|
+
/**
|
|
777
|
+
* Optional injected execution backend (`@theokit/sdk/sandbox`) — a backend or a per-request resolver.
|
|
778
|
+
* When provided, the command runs via `SandboxBackend.execute` (Local/Docker/E2B) so the tool is
|
|
779
|
+
* surface-agnostic (a cluster/desktop host runs it in the sandbox, not the local host); omitted ⇒ the
|
|
780
|
+
* local `/bin/sh -c` child process (byte-identical to before). The catastrophic-command guard runs
|
|
781
|
+
* before either path.
|
|
782
|
+
*/
|
|
783
|
+
sandbox?: SandboxProvider;
|
|
763
784
|
}
|
|
764
785
|
declare function createShellTool(opts: CreateShellToolOptions): CustomTool;
|
|
765
786
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CustomTool, ConfigurationError } from '@theokit/sdk';
|
|
2
|
-
import { InteractiveProvider } from '@theokit/sdk/interactive';
|
|
3
2
|
import { FilesystemProvider } from '@theokit/sdk/filesystem';
|
|
3
|
+
import { SandboxProvider } from '@theokit/sdk/sandbox';
|
|
4
|
+
import { InteractiveProvider } from '@theokit/sdk/interactive';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `apply_patch` — built-in tool for coding agents.
|
|
@@ -89,6 +90,9 @@ declare function createSessionArtifactStore(options: SessionArtifactStoreOptions
|
|
|
89
90
|
interface CreateEditFileToolOptions {
|
|
90
91
|
/** Absolute path to the project root. Every edit is gated against this boundary. */
|
|
91
92
|
projectRoot: string;
|
|
93
|
+
/** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the read + `.bak` backup +
|
|
94
|
+
* write go through the backend (surface-agnostic); omitted ⇒ the local `fs` path (byte-identical). */
|
|
95
|
+
filesystem?: FilesystemProvider;
|
|
92
96
|
}
|
|
93
97
|
declare function createEditFileTool(opts: CreateEditFileToolOptions): CustomTool;
|
|
94
98
|
|
|
@@ -134,6 +138,9 @@ interface CreateGitDiffToolOptions {
|
|
|
134
138
|
projectRoot: string;
|
|
135
139
|
timeoutMs?: number;
|
|
136
140
|
maxStdoutBytes?: number;
|
|
141
|
+
/** Optional injected execution backend (`@theokit/sdk/sandbox`) — when provided, `git diff` runs via
|
|
142
|
+
* `SandboxBackend.execute` (surface-agnostic); omitted ⇒ the local `git` child process (unchanged). */
|
|
143
|
+
sandbox?: SandboxProvider;
|
|
137
144
|
}
|
|
138
145
|
declare function createGitDiffTool(opts: CreateGitDiffToolOptions): CustomTool;
|
|
139
146
|
|
|
@@ -151,6 +158,9 @@ declare function createGitDiffTool(opts: CreateGitDiffToolOptions): CustomTool;
|
|
|
151
158
|
interface CreateGlobToolOptions {
|
|
152
159
|
/** Absolute path to the project root. */
|
|
153
160
|
projectRoot: string;
|
|
161
|
+
/** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the walk reads through the
|
|
162
|
+
* backend (Local/remote), so the tool is surface-agnostic; omitted ⇒ the local `readdir` (unchanged). */
|
|
163
|
+
filesystem?: FilesystemProvider;
|
|
154
164
|
}
|
|
155
165
|
declare function createGlobTool(opts: CreateGlobToolOptions): CustomTool;
|
|
156
166
|
|
|
@@ -734,6 +744,9 @@ interface CreateSearchTextToolOptions {
|
|
|
734
744
|
maxMatches?: number;
|
|
735
745
|
/** Skip files larger than this (bytes). Default 1 MB. */
|
|
736
746
|
maxFileSize?: number;
|
|
747
|
+
/** Optional injected filesystem (`@theokit/sdk/filesystem`) — when provided, the recursive walk reads
|
|
748
|
+
* through the backend (surface-agnostic); omitted ⇒ the local `readdir`/`readFile` walk (unchanged). */
|
|
749
|
+
filesystem?: FilesystemProvider;
|
|
737
750
|
}
|
|
738
751
|
declare function createSearchTextTool(opts: CreateSearchTextToolOptions): CustomTool;
|
|
739
752
|
|
|
@@ -760,6 +773,14 @@ interface CreateShellToolOptions {
|
|
|
760
773
|
* guardrail, not a sandbox.
|
|
761
774
|
*/
|
|
762
775
|
allowCatastrophic?: boolean;
|
|
776
|
+
/**
|
|
777
|
+
* Optional injected execution backend (`@theokit/sdk/sandbox`) — a backend or a per-request resolver.
|
|
778
|
+
* When provided, the command runs via `SandboxBackend.execute` (Local/Docker/E2B) so the tool is
|
|
779
|
+
* surface-agnostic (a cluster/desktop host runs it in the sandbox, not the local host); omitted ⇒ the
|
|
780
|
+
* local `/bin/sh -c` child process (byte-identical to before). The catastrophic-command guard runs
|
|
781
|
+
* before either path.
|
|
782
|
+
*/
|
|
783
|
+
sandbox?: SandboxProvider;
|
|
763
784
|
}
|
|
764
785
|
declare function createShellTool(opts: CreateShellToolOptions): CustomTool;
|
|
765
786
|
|
package/dist/index.js
CHANGED
|
@@ -5,11 +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';
|
|
9
11
|
import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
|
|
10
12
|
import { lookup } from 'dns/promises';
|
|
11
13
|
import { isIP } from 'net';
|
|
12
|
-
import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
|
|
13
14
|
|
|
14
15
|
// src/apply-patch.ts
|
|
15
16
|
var PathTraversalError = class extends ConfigurationError {
|
|
@@ -348,8 +349,82 @@ ${find}`);
|
|
|
348
349
|
}
|
|
349
350
|
|
|
350
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 (err) {
|
|
383
|
+
if (err instanceof FileNotFoundError) {
|
|
384
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
385
|
+
}
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
389
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
390
|
+
await backend.writeFile(`${path}.bak`, content);
|
|
391
|
+
await backend.writeFile(path, outcome.result);
|
|
392
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
393
|
+
}
|
|
394
|
+
async function editViaLocal(projectRoot, path, old_string, new_string) {
|
|
395
|
+
const absolutePath = safePathJoin(projectRoot, path);
|
|
396
|
+
let content;
|
|
397
|
+
try {
|
|
398
|
+
content = await readFile(absolutePath, "utf-8");
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const e = err;
|
|
401
|
+
if (e.code === "ENOENT") {
|
|
402
|
+
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
403
|
+
}
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
const outcome = computeEdit(content, old_string, new_string);
|
|
407
|
+
if (!outcome.ok) return JSON.stringify({ ok: false, error: "no_match", path });
|
|
408
|
+
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
409
|
+
await writeFile(absolutePath, outcome.result, "utf-8");
|
|
410
|
+
return JSON.stringify({ ok: true, replacements: 1 });
|
|
411
|
+
}
|
|
412
|
+
function editScopeError(path, projectRoot) {
|
|
413
|
+
if (isForbiddenPath(path)) {
|
|
414
|
+
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, path), projectRoot);
|
|
418
|
+
return null;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
421
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
351
426
|
function createEditFileTool(opts) {
|
|
352
|
-
const { projectRoot } = opts;
|
|
427
|
+
const { projectRoot, filesystem } = opts;
|
|
353
428
|
return Tool.create({
|
|
354
429
|
name: "edit_file",
|
|
355
430
|
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 }.",
|
|
@@ -358,67 +433,13 @@ function createEditFileTool(opts) {
|
|
|
358
433
|
old_string: z.string().min(1).describe("String to find in the file."),
|
|
359
434
|
new_string: z.string().describe("Replacement string.")
|
|
360
435
|
}),
|
|
361
|
-
|
|
362
|
-
handler: async ({ path, old_string, new_string }) => {
|
|
436
|
+
handler: async ({ path, old_string, new_string }, ctx) => {
|
|
363
437
|
if (old_string === new_string) {
|
|
364
438
|
return JSON.stringify({ ok: false, error: "no_change", path });
|
|
365
439
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
let absolutePath;
|
|
370
|
-
try {
|
|
371
|
-
absolutePath = safePathJoin(projectRoot, path);
|
|
372
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
373
|
-
} catch (err) {
|
|
374
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
375
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
376
|
-
}
|
|
377
|
-
throw err;
|
|
378
|
-
}
|
|
379
|
-
let content;
|
|
380
|
-
try {
|
|
381
|
-
content = await readFile(absolutePath, "utf-8");
|
|
382
|
-
} catch (err) {
|
|
383
|
-
const e = err;
|
|
384
|
-
if (e.code === "ENOENT") {
|
|
385
|
-
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
386
|
-
}
|
|
387
|
-
throw err;
|
|
388
|
-
}
|
|
389
|
-
const exactIdx = content.indexOf(old_string);
|
|
390
|
-
if (exactIdx !== -1) {
|
|
391
|
-
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
392
|
-
const result = content.slice(0, exactIdx) + new_string + content.slice(exactIdx + old_string.length);
|
|
393
|
-
await writeFile(absolutePath, result, "utf-8");
|
|
394
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
395
|
-
}
|
|
396
|
-
const normalizedContent = normalizeWhitespace(content);
|
|
397
|
-
const normalizedOld = normalizeWhitespace(old_string);
|
|
398
|
-
const normalizedIdx = normalizedContent.indexOf(normalizedOld);
|
|
399
|
-
if (normalizedIdx !== -1) {
|
|
400
|
-
const span = findOriginalSpan(
|
|
401
|
-
content,
|
|
402
|
-
normalizedContent,
|
|
403
|
-
normalizedIdx,
|
|
404
|
-
normalizedOld.length
|
|
405
|
-
);
|
|
406
|
-
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
407
|
-
const result = content.slice(0, span.start) + new_string + content.slice(span.end);
|
|
408
|
-
await writeFile(absolutePath, result, "utf-8");
|
|
409
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
410
|
-
}
|
|
411
|
-
try {
|
|
412
|
-
const result = replaceUnique(content, old_string, new_string);
|
|
413
|
-
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
414
|
-
await writeFile(absolutePath, result, "utf-8");
|
|
415
|
-
return JSON.stringify({ ok: true, replacements: 1 });
|
|
416
|
-
} catch (err) {
|
|
417
|
-
if (err instanceof ContextMatchError) {
|
|
418
|
-
return JSON.stringify({ ok: false, error: "no_match", path });
|
|
419
|
-
}
|
|
420
|
-
throw err;
|
|
421
|
-
}
|
|
440
|
+
const scopeErr = editScopeError(path, projectRoot);
|
|
441
|
+
if (scopeErr !== null) return scopeErr;
|
|
442
|
+
return filesystem !== void 0 ? editViaBackend(filesystem, ctx, path, old_string, new_string) : editViaLocal(projectRoot, path, old_string, new_string);
|
|
422
443
|
}
|
|
423
444
|
});
|
|
424
445
|
}
|
|
@@ -531,11 +552,27 @@ function attachChildSettlers(child, gate, onClose, onError, resolve2) {
|
|
|
531
552
|
// src/git-diff.ts
|
|
532
553
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
533
554
|
var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
|
|
555
|
+
function shq(arg) {
|
|
556
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
557
|
+
}
|
|
558
|
+
async function diffViaSandbox(sandbox, ctx, cached, path, projectRoot, timeoutMs) {
|
|
559
|
+
const scopeCheck = checkPathScope(path, projectRoot);
|
|
560
|
+
if (scopeCheck !== null) return scopeCheck;
|
|
561
|
+
const command = ["git", ...buildDiffArgs(cached, path)].map(shq).join(" ");
|
|
562
|
+
const backend = await resolveSandbox(sandbox, ctx ?? {});
|
|
563
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
564
|
+
if (r.timedOut) return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
|
|
565
|
+
if (r.exitCode !== 0) {
|
|
566
|
+
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 });
|
|
567
|
+
}
|
|
568
|
+
return JSON.stringify({ ok: true, diff: r.stdout, truncated: false });
|
|
569
|
+
}
|
|
534
570
|
function createGitDiffTool(opts) {
|
|
535
571
|
const {
|
|
536
572
|
projectRoot,
|
|
537
573
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
538
|
-
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES
|
|
574
|
+
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES,
|
|
575
|
+
sandbox
|
|
539
576
|
} = opts;
|
|
540
577
|
return Tool.create({
|
|
541
578
|
name: "git_diff",
|
|
@@ -544,7 +581,10 @@ function createGitDiffTool(opts) {
|
|
|
544
581
|
path: z.string().optional().describe("Optional project-relative file or dir scope."),
|
|
545
582
|
cached: z.boolean().optional().describe("If true, show staged changes (git diff --cached). Default false.")
|
|
546
583
|
}),
|
|
547
|
-
handler: async ({ path, cached }) => {
|
|
584
|
+
handler: async ({ path, cached }, ctx) => {
|
|
585
|
+
if (sandbox !== void 0) {
|
|
586
|
+
return diffViaSandbox(sandbox, ctx, cached, path, projectRoot, timeoutMs);
|
|
587
|
+
}
|
|
548
588
|
if (!existsSync(join(projectRoot, ".git"))) {
|
|
549
589
|
return JSON.stringify({ ok: false, error: "not_a_repo" });
|
|
550
590
|
}
|
|
@@ -617,8 +657,9 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
617
657
|
});
|
|
618
658
|
}
|
|
619
659
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
660
|
+
var MAX_BACKEND_WALK_DEPTH = 64;
|
|
620
661
|
function createGlobTool(opts) {
|
|
621
|
-
const { projectRoot } = opts;
|
|
662
|
+
const { projectRoot, filesystem } = opts;
|
|
622
663
|
return Tool.create({
|
|
623
664
|
name: "glob_files",
|
|
624
665
|
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 }.",
|
|
@@ -626,20 +667,18 @@ function createGlobTool(opts) {
|
|
|
626
667
|
pattern: z.string().min(1).describe("Glob pattern (e.g. '**/*.ts', 'src/**/*.json')."),
|
|
627
668
|
cwd: z.string().optional().describe("Project-relative subdirectory to search from.")
|
|
628
669
|
}),
|
|
629
|
-
handler: async ({ pattern, cwd }) => {
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
try {
|
|
633
|
-
searchRoot = safePathJoin(projectRoot, cwd);
|
|
634
|
-
assertNoSymlinkEscape(searchRoot, projectRoot);
|
|
635
|
-
} catch (err) {
|
|
636
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
637
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
638
|
-
}
|
|
639
|
-
throw err;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
670
|
+
handler: async ({ pattern, cwd }, ctx) => {
|
|
671
|
+
const scopeErr = globScopeError(cwd, projectRoot);
|
|
672
|
+
if (scopeErr !== null) return scopeErr;
|
|
642
673
|
const regex = globToRegex(pattern);
|
|
674
|
+
if (filesystem !== void 0) {
|
|
675
|
+
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
676
|
+
const searchRel = cwd ?? "";
|
|
677
|
+
const found = [];
|
|
678
|
+
await walkDirBackend(backend, searchRel, searchRel, regex, found, 0);
|
|
679
|
+
return JSON.stringify({ ok: true, files: found.sort(), count: found.length });
|
|
680
|
+
}
|
|
681
|
+
const searchRoot = cwd ? safePathJoin(projectRoot, cwd) : projectRoot;
|
|
643
682
|
const files = [];
|
|
644
683
|
await walkDir(searchRoot, searchRoot, regex, files);
|
|
645
684
|
const relativePaths = files.map((f) => relative(projectRoot, f)).sort();
|
|
@@ -647,6 +686,18 @@ function createGlobTool(opts) {
|
|
|
647
686
|
}
|
|
648
687
|
});
|
|
649
688
|
}
|
|
689
|
+
function globScopeError(cwd, projectRoot) {
|
|
690
|
+
if (!cwd) return null;
|
|
691
|
+
try {
|
|
692
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, cwd), projectRoot);
|
|
693
|
+
return null;
|
|
694
|
+
} catch (err) {
|
|
695
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
696
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
697
|
+
}
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
650
701
|
async function walkDir(base, dir, pattern, results) {
|
|
651
702
|
let entries;
|
|
652
703
|
try {
|
|
@@ -665,6 +716,34 @@ async function walkDir(base, dir, pattern, results) {
|
|
|
665
716
|
}
|
|
666
717
|
}
|
|
667
718
|
}
|
|
719
|
+
async function walkDirBackend(backend, base, dir, pattern, results, depth) {
|
|
720
|
+
if (depth > MAX_BACKEND_WALK_DEPTH) return;
|
|
721
|
+
let names;
|
|
722
|
+
try {
|
|
723
|
+
names = await backend.list(dir);
|
|
724
|
+
} catch {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
for (const name of names) {
|
|
728
|
+
await walkBackendEntry(backend, base, dir, name, pattern, results, depth);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function walkBackendEntry(backend, base, dir, name, pattern, results, depth) {
|
|
732
|
+
if (DEFAULT_EXCLUDES.has(name)) return;
|
|
733
|
+
const fullRel = dir === "" ? name : `${dir}/${name}`;
|
|
734
|
+
const relPath = base === "" ? fullRel : relative(base, fullRel);
|
|
735
|
+
let st;
|
|
736
|
+
try {
|
|
737
|
+
st = await backend.stat(fullRel);
|
|
738
|
+
} catch {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (st.isDirectory) {
|
|
742
|
+
await walkDirBackend(backend, base, fullRel, pattern, results, depth + 1);
|
|
743
|
+
} else if (st.isFile && pattern.test(relPath)) {
|
|
744
|
+
results.push(fullRel);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
668
747
|
function globToRegex(pattern) {
|
|
669
748
|
let regexStr = "";
|
|
670
749
|
let i = 0;
|
|
@@ -1732,11 +1811,13 @@ var DEFAULT_MAX_MATCHES = 100;
|
|
|
1732
1811
|
var DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
1733
1812
|
var BINARY_PROBE_BYTES2 = 8 * 1024;
|
|
1734
1813
|
var PREVIEW_MAX = 200;
|
|
1814
|
+
var MAX_BACKEND_WALK_DEPTH2 = 64;
|
|
1735
1815
|
function createSearchTextTool(opts) {
|
|
1736
1816
|
const {
|
|
1737
1817
|
projectRoot,
|
|
1738
1818
|
maxMatches = DEFAULT_MAX_MATCHES,
|
|
1739
|
-
maxFileSize = DEFAULT_MAX_FILE_SIZE
|
|
1819
|
+
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
1820
|
+
filesystem
|
|
1740
1821
|
} = opts;
|
|
1741
1822
|
return Tool.create({
|
|
1742
1823
|
name: "search_text",
|
|
@@ -1745,9 +1826,7 @@ function createSearchTextTool(opts) {
|
|
|
1745
1826
|
query: z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
1746
1827
|
path: z.string().optional().describe("Optional project-relative directory to scope the search.")
|
|
1747
1828
|
}),
|
|
1748
|
-
handler: async ({ query, path }) => {
|
|
1749
|
-
const scope = resolveSearchScope(path, projectRoot);
|
|
1750
|
-
if ("error" in scope) return scope.error;
|
|
1829
|
+
handler: async ({ query, path }, ctx) => {
|
|
1751
1830
|
const state = {
|
|
1752
1831
|
matches: [],
|
|
1753
1832
|
totalMatches: 0,
|
|
@@ -1757,6 +1836,20 @@ function createSearchTextTool(opts) {
|
|
|
1757
1836
|
maxFileSize,
|
|
1758
1837
|
projectRoot
|
|
1759
1838
|
};
|
|
1839
|
+
if (filesystem !== void 0) {
|
|
1840
|
+
const scopeRel = resolveScopeRel(path, projectRoot);
|
|
1841
|
+
if ("error" in scopeRel) return scopeRel.error;
|
|
1842
|
+
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
1843
|
+
await walkBackend(backend, scopeRel.rel, state, 0);
|
|
1844
|
+
return JSON.stringify({
|
|
1845
|
+
ok: true,
|
|
1846
|
+
matches: state.matches,
|
|
1847
|
+
truncated: state.truncated,
|
|
1848
|
+
totalMatches: state.totalMatches
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
const scope = resolveSearchScope(path, projectRoot);
|
|
1852
|
+
if ("error" in scope) return scope.error;
|
|
1760
1853
|
await walk(scope.scopeAbs, state);
|
|
1761
1854
|
return JSON.stringify({
|
|
1762
1855
|
ok: true,
|
|
@@ -1844,11 +1937,79 @@ async function scanFile(absPath, relPath, state) {
|
|
|
1844
1937
|
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1845
1938
|
}
|
|
1846
1939
|
}
|
|
1940
|
+
function resolveScopeRel(path, projectRoot) {
|
|
1941
|
+
const scopeRel = path === void 0 || path === "" || path === "." ? "" : path;
|
|
1942
|
+
if (scopeRel === "") return { rel: "" };
|
|
1943
|
+
try {
|
|
1944
|
+
assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
1945
|
+
return { rel: scopeRel };
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1948
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path }) };
|
|
1949
|
+
}
|
|
1950
|
+
throw err;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
async function walkBackend(backend, dirRel, state, depth) {
|
|
1954
|
+
if (state.truncated) return;
|
|
1955
|
+
if (depth > MAX_BACKEND_WALK_DEPTH2) return;
|
|
1956
|
+
let names;
|
|
1957
|
+
try {
|
|
1958
|
+
names = await backend.list(dirRel);
|
|
1959
|
+
} catch {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
for (const name of names) {
|
|
1963
|
+
if (state.truncated) return;
|
|
1964
|
+
await handleBackendEntry(backend, dirRel, name, state, depth);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function handleBackendEntry(backend, dirRel, name, state, depth) {
|
|
1968
|
+
const entryRel = dirRel === "" ? name : `${dirRel}/${name}`;
|
|
1969
|
+
if (isForbiddenPath(entryRel)) return;
|
|
1970
|
+
let st;
|
|
1971
|
+
try {
|
|
1972
|
+
st = await backend.stat(entryRel);
|
|
1973
|
+
} catch {
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (st.isDirectory) {
|
|
1977
|
+
await walkBackend(backend, entryRel, state, depth + 1);
|
|
1978
|
+
} else if (st.isFile) {
|
|
1979
|
+
await scanFileBackend(backend, entryRel, st.size, state);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
async function scanFileBackend(backend, relPath, size, state) {
|
|
1983
|
+
if (size > state.maxFileSize) return;
|
|
1984
|
+
let content;
|
|
1985
|
+
try {
|
|
1986
|
+
content = await backend.readFile(relPath);
|
|
1987
|
+
} catch {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
if (content.slice(0, BINARY_PROBE_BYTES2).includes("\0")) return;
|
|
1991
|
+
const lines = content.split("\n");
|
|
1992
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1993
|
+
const line = lines[i];
|
|
1994
|
+
if (!line.includes(state.query)) continue;
|
|
1995
|
+
if (!recordMatch(state, relPath, i + 1, line)) return;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1847
1998
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
1848
1999
|
var MAX_TIMEOUT_MS = 3e5;
|
|
1849
2000
|
var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
2001
|
+
async function execViaSandbox(sandbox, ctx, command, timeoutMs) {
|
|
2002
|
+
const backend = await resolveSandbox(sandbox, ctx ?? {});
|
|
2003
|
+
const r = await backend.execute(command, { timeoutMs });
|
|
2004
|
+
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 });
|
|
2005
|
+
}
|
|
1850
2006
|
function createShellTool(opts) {
|
|
1851
|
-
const {
|
|
2007
|
+
const {
|
|
2008
|
+
projectRoot,
|
|
2009
|
+
defaultTimeoutMs = DEFAULT_TIMEOUT_MS3,
|
|
2010
|
+
allowCatastrophic = false,
|
|
2011
|
+
sandbox
|
|
2012
|
+
} = opts;
|
|
1852
2013
|
return Tool.create({
|
|
1853
2014
|
name: "shell_exec",
|
|
1854
2015
|
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 }.",
|
|
@@ -1856,7 +2017,7 @@ function createShellTool(opts) {
|
|
|
1856
2017
|
command: z.string().min(1).describe("Shell command to execute."),
|
|
1857
2018
|
timeout_ms: z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
1858
2019
|
}),
|
|
1859
|
-
handler: async ({ command, timeout_ms }) => {
|
|
2020
|
+
handler: async ({ command, timeout_ms }, ctx) => {
|
|
1860
2021
|
if (!allowCatastrophic) {
|
|
1861
2022
|
const reason = catastrophicShellReason(command);
|
|
1862
2023
|
if (reason) {
|
|
@@ -1865,8 +2026,8 @@ function createShellTool(opts) {
|
|
|
1865
2026
|
}
|
|
1866
2027
|
}
|
|
1867
2028
|
const timeoutMs = Math.min(timeout_ms ?? defaultTimeoutMs, MAX_TIMEOUT_MS);
|
|
1868
|
-
|
|
1869
|
-
return
|
|
2029
|
+
if (sandbox !== void 0) return execViaSandbox(sandbox, ctx, command, timeoutMs);
|
|
2030
|
+
return runShell(projectRoot, command, timeoutMs);
|
|
1870
2031
|
}
|
|
1871
2032
|
});
|
|
1872
2033
|
}
|