@dexto/tools-filesystem 1.7.1 → 1.8.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/dist/directory-approval.cjs +5 -4
- package/dist/directory-approval.d.ts +2 -1
- package/dist/directory-approval.d.ts.map +1 -1
- package/dist/directory-approval.integration.test.cjs +73 -33
- package/dist/directory-approval.integration.test.js +73 -33
- package/dist/directory-approval.js +2 -1
- package/dist/edit-file-tool.cjs +52 -37
- package/dist/edit-file-tool.d.ts +1 -1
- package/dist/edit-file-tool.d.ts.map +1 -1
- package/dist/edit-file-tool.js +43 -29
- package/dist/edit-file-tool.test.cjs +159 -2
- package/dist/edit-file-tool.test.js +159 -2
- package/dist/errors.cjs +53 -53
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/file-tool-types.d.ts +1 -1
- package/dist/file-tool-types.d.ts.map +1 -1
- package/dist/filesystem-service.cjs +60 -60
- package/dist/filesystem-service.d.ts +1 -1
- package/dist/filesystem-service.d.ts.map +1 -1
- package/dist/filesystem-service.js +5 -5
- package/dist/filesystem-service.test.cjs +1 -3
- package/dist/filesystem-service.test.js +1 -3
- package/dist/glob-files-tool.cjs +27 -24
- package/dist/glob-files-tool.d.ts +1 -1
- package/dist/glob-files-tool.d.ts.map +1 -1
- package/dist/glob-files-tool.js +24 -21
- package/dist/glob-files-tool.test.cjs +100 -88
- package/dist/glob-files-tool.test.js +101 -67
- package/dist/grep-content-tool.cjs +129 -44
- package/dist/grep-content-tool.d.ts +1 -1
- package/dist/grep-content-tool.d.ts.map +1 -1
- package/dist/grep-content-tool.js +120 -41
- package/dist/grep-content-tool.test.cjs +122 -87
- package/dist/grep-content-tool.test.js +123 -66
- package/dist/index.d.cts +3 -4
- package/dist/path-validator.d.ts +1 -1
- package/dist/path-validator.d.ts.map +1 -1
- package/dist/read-file-tool.cjs +43 -14
- package/dist/read-file-tool.d.ts +1 -1
- package/dist/read-file-tool.d.ts.map +1 -1
- package/dist/read-file-tool.js +40 -11
- package/dist/read-file-tool.test.cjs +119 -0
- package/dist/read-file-tool.test.d.ts +2 -0
- package/dist/read-file-tool.test.d.ts.map +1 -0
- package/dist/read-file-tool.test.js +96 -0
- package/dist/read-media-file-tool.cjs +4 -4
- package/dist/read-media-file-tool.d.ts +1 -1
- package/dist/read-media-file-tool.d.ts.map +1 -1
- package/dist/read-media-file-tool.js +1 -1
- package/dist/tool-factory.cjs +2 -2
- package/dist/tool-factory.js +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace-paths.cjs +87 -0
- package/dist/workspace-paths.d.ts +4 -0
- package/dist/workspace-paths.d.ts.map +1 -0
- package/dist/workspace-paths.js +51 -0
- package/dist/write-file-tool.cjs +74 -34
- package/dist/write-file-tool.d.ts +1 -2
- package/dist/write-file-tool.d.ts.map +1 -1
- package/dist/write-file-tool.js +68 -29
- package/dist/write-file-tool.test.cjs +262 -11
- package/dist/write-file-tool.test.js +262 -11
- package/package.json +3 -3
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import
|
|
2
|
+
import safeRegex from "safe-regex";
|
|
3
|
+
import { createLocalToolCallHeader, defineTool, truncateForHeader } from "@dexto/core/tools";
|
|
3
4
|
import { createDirectoryAccessApprovalHandlers } from "./directory-approval.js";
|
|
4
5
|
import { resolveUserPath } from "./path-utils.js";
|
|
6
|
+
import { FileSystemError } from "./errors.js";
|
|
7
|
+
import {
|
|
8
|
+
assertWorkspaceRelativeGlob,
|
|
9
|
+
isWorkspaceFileNotFound,
|
|
10
|
+
toWorkspaceRelativePath
|
|
11
|
+
} from "./workspace-paths.js";
|
|
5
12
|
const GrepContentInputSchema = z.object({
|
|
6
13
|
pattern: z.string().describe("Regular expression pattern to search for"),
|
|
7
14
|
path: z.string().optional().describe("Directory to search in (defaults to working directory)"),
|
|
@@ -42,7 +49,6 @@ function createGrepContentTool(getFileSystemService) {
|
|
|
42
49
|
}
|
|
43
50
|
}),
|
|
44
51
|
async execute(input, context) {
|
|
45
|
-
const resolvedFileSystemService = await getFileSystemService(context);
|
|
46
52
|
const {
|
|
47
53
|
pattern,
|
|
48
54
|
path: searchPath,
|
|
@@ -51,49 +57,122 @@ function createGrepContentTool(getFileSystemService) {
|
|
|
51
57
|
case_insensitive,
|
|
52
58
|
max_results
|
|
53
59
|
} = input;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
60
|
+
if (!context.services) {
|
|
61
|
+
throw new Error("grep_content requires ToolExecutionContext.services");
|
|
62
|
+
}
|
|
63
|
+
if (!safeRegex(pattern)) {
|
|
64
|
+
throw FileSystemError.invalidPattern(
|
|
65
|
+
pattern,
|
|
66
|
+
"Pattern may cause catastrophic backtracking (ReDoS). Please simplify the regex."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const handle = await context.services.workspaceManager.open({ intent: "read" });
|
|
70
|
+
const regex = createRegex(pattern, case_insensitive);
|
|
71
|
+
const files = await findSearchFiles(
|
|
72
|
+
handle.files,
|
|
73
|
+
handle.context.path,
|
|
74
|
+
searchPath,
|
|
75
|
+
glob
|
|
76
|
+
);
|
|
77
|
+
const matches = [];
|
|
78
|
+
let filesSearched = 0;
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = await handle.files.readText(file);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
context.logger.debug(
|
|
85
|
+
`Skipping file ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
86
|
+
);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
filesSearched += 1;
|
|
90
|
+
const lines = content.split("\n");
|
|
91
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
92
|
+
const line = lines[index] ?? "";
|
|
93
|
+
regex.lastIndex = 0;
|
|
94
|
+
if (!regex.test(line)) continue;
|
|
95
|
+
const matchContext = context_lines > 0 ? collectContext(lines, index, context_lines) : void 0;
|
|
96
|
+
matches.push({
|
|
97
|
+
file,
|
|
98
|
+
lineNumber: index + 1,
|
|
99
|
+
line,
|
|
100
|
+
...matchContext !== void 0 && { context: matchContext }
|
|
101
|
+
});
|
|
102
|
+
if (matches.length >= max_results) {
|
|
103
|
+
return formatSearchResult(pattern, matches, filesSearched, true);
|
|
87
104
|
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
truncated: result.truncated,
|
|
92
|
-
_display
|
|
93
|
-
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return formatSearchResult(pattern, matches, filesSearched, false);
|
|
94
108
|
}
|
|
95
109
|
});
|
|
96
110
|
}
|
|
111
|
+
function createRegex(pattern, caseInsensitive) {
|
|
112
|
+
try {
|
|
113
|
+
return new RegExp(pattern, caseInsensitive ? "i" : "");
|
|
114
|
+
} catch {
|
|
115
|
+
throw FileSystemError.invalidPattern(pattern, "Invalid regular expression syntax");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function findSearchFiles(files, workspaceRoot, searchPath, globPattern) {
|
|
119
|
+
const workspacePath = toWorkspaceRelativePath("grep_content", workspaceRoot, searchPath || ".");
|
|
120
|
+
if (searchPath && !globPattern) {
|
|
121
|
+
try {
|
|
122
|
+
await files.readText(workspacePath);
|
|
123
|
+
return [workspacePath];
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!isWorkspaceFileNotFound(error)) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const pattern = globPattern ?? "**/*";
|
|
131
|
+
assertWorkspaceRelativeGlob("grep_content", pattern);
|
|
132
|
+
if (workspacePath === "." || workspacePath === "") {
|
|
133
|
+
return files.glob(pattern);
|
|
134
|
+
}
|
|
135
|
+
return files.glob(`${workspacePath.replace(/\/$/, "")}/${pattern}`);
|
|
136
|
+
}
|
|
137
|
+
function collectContext(lines, matchIndex, contextLines) {
|
|
138
|
+
return {
|
|
139
|
+
before: lines.slice(Math.max(0, matchIndex - contextLines), matchIndex),
|
|
140
|
+
after: lines.slice(matchIndex + 1, matchIndex + contextLines + 1)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function formatSearchResult(pattern, matches, filesSearched, truncated) {
|
|
144
|
+
const _display = {
|
|
145
|
+
type: "search",
|
|
146
|
+
pattern,
|
|
147
|
+
matches: matches.map((match) => ({
|
|
148
|
+
file: match.file,
|
|
149
|
+
line: match.lineNumber,
|
|
150
|
+
content: match.line,
|
|
151
|
+
...match.context && {
|
|
152
|
+
context: [...match.context.before, ...match.context.after]
|
|
153
|
+
}
|
|
154
|
+
})),
|
|
155
|
+
totalMatches: matches.length,
|
|
156
|
+
truncated
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
matches: matches.map((match) => ({
|
|
160
|
+
file: match.file,
|
|
161
|
+
line_number: match.lineNumber,
|
|
162
|
+
line: match.line,
|
|
163
|
+
...match.context && {
|
|
164
|
+
context: {
|
|
165
|
+
before: match.context.before,
|
|
166
|
+
after: match.context.after
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})),
|
|
170
|
+
total_matches: matches.length,
|
|
171
|
+
files_searched: filesSearched,
|
|
172
|
+
truncated,
|
|
173
|
+
_display
|
|
174
|
+
};
|
|
175
|
+
}
|
|
97
176
|
export {
|
|
98
177
|
createGrepContentTool
|
|
99
178
|
};
|
|
@@ -1,31 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
for (let key of __getOwnPropNames(from))
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
-
}
|
|
14
|
-
return to;
|
|
15
|
-
};
|
|
16
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
-
mod
|
|
23
|
-
));
|
|
24
2
|
var import_vitest = require("vitest");
|
|
25
|
-
var
|
|
26
|
-
var
|
|
27
|
-
var os = __toESM(require("node:os"), 1);
|
|
28
|
-
var import_filesystem_service = require("./filesystem-service.js");
|
|
3
|
+
var import_core = require("@dexto/core");
|
|
4
|
+
var import_workspace = require("@dexto/core/workspace");
|
|
29
5
|
var import_grep_content_tool = require("./grep-content-tool.js");
|
|
30
6
|
const createMockLogger = () => {
|
|
31
7
|
const logger = {
|
|
@@ -44,72 +20,131 @@ const createMockLogger = () => {
|
|
|
44
20
|
};
|
|
45
21
|
return logger;
|
|
46
22
|
};
|
|
47
|
-
function createToolContext(logger) {
|
|
48
|
-
|
|
23
|
+
function createToolContext(logger, filesByPath, glob = import_vitest.vi.fn(async () => Object.keys(filesByPath))) {
|
|
24
|
+
const workspaceManager = {
|
|
25
|
+
open: import_vitest.vi.fn(async () => ({
|
|
26
|
+
context: {
|
|
27
|
+
id: "test-workspace",
|
|
28
|
+
path: "/repo",
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
lastActiveAt: Date.now()
|
|
31
|
+
},
|
|
32
|
+
capabilities: ["files"],
|
|
33
|
+
files: {
|
|
34
|
+
readFile: async (filePath) => readTestFile(filesByPath, filePath),
|
|
35
|
+
readText: async (filePath) => readTestFile(filesByPath, filePath),
|
|
36
|
+
glob,
|
|
37
|
+
writeFile: import_vitest.vi.fn(async () => void 0),
|
|
38
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
39
|
+
}
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
logger,
|
|
44
|
+
services: {
|
|
45
|
+
approval: {},
|
|
46
|
+
search: {},
|
|
47
|
+
resources: {},
|
|
48
|
+
prompts: {},
|
|
49
|
+
skills: {},
|
|
50
|
+
mcp: {},
|
|
51
|
+
taskForker: null,
|
|
52
|
+
workspaceManager
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function readTestFile(filesByPath, filePath) {
|
|
57
|
+
const content = filesByPath[filePath];
|
|
58
|
+
if (content === void 0) {
|
|
59
|
+
throw import_workspace.WorkspaceError.fileNotFound(filePath);
|
|
60
|
+
}
|
|
61
|
+
return content;
|
|
49
62
|
}
|
|
50
63
|
(0, import_vitest.describe)("grep_content tool", () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
(0, import_vitest.it)("searches content through WorkspaceManager.open without FileSystemService execution", async () => {
|
|
65
|
+
const mockLogger = createMockLogger();
|
|
66
|
+
const getFileSystemService = import_vitest.vi.fn(async () => {
|
|
67
|
+
throw new Error("grep_content execute must not use FileSystemService");
|
|
68
|
+
});
|
|
69
|
+
const glob = import_vitest.vi.fn(async () => ["src/notes.txt"]);
|
|
70
|
+
const tool = (0, import_grep_content_tool.createGrepContentTool)(getFileSystemService);
|
|
71
|
+
const parsedInput = tool.inputSchema.parse({
|
|
72
|
+
pattern: "needle",
|
|
73
|
+
path: "src",
|
|
74
|
+
glob: "**/*.txt",
|
|
75
|
+
context_lines: 1
|
|
76
|
+
});
|
|
77
|
+
const result = await tool.execute(
|
|
78
|
+
parsedInput,
|
|
79
|
+
createToolContext(mockLogger, { "src/notes.txt": "alpha\nneedle\nomega\n" }, glob)
|
|
80
|
+
);
|
|
81
|
+
(0, import_vitest.expect)(getFileSystemService).not.toHaveBeenCalled();
|
|
82
|
+
(0, import_vitest.expect)(glob).toHaveBeenCalledWith("src/**/*.txt");
|
|
83
|
+
(0, import_vitest.expect)(result.files_searched).toBe(1);
|
|
84
|
+
(0, import_vitest.expect)(result.matches).toEqual([
|
|
59
85
|
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
86
|
+
file: "src/notes.txt",
|
|
87
|
+
line_number: 2,
|
|
88
|
+
line: "needle",
|
|
89
|
+
context: {
|
|
90
|
+
before: ["alpha"],
|
|
91
|
+
after: ["omega"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
(0, import_vitest.it)("normalizes workspace-contained absolute search paths before globbing", async () => {
|
|
97
|
+
const mockLogger = createMockLogger();
|
|
98
|
+
const glob = import_vitest.vi.fn(async () => ["src/notes.txt"]);
|
|
99
|
+
const tool = (0, import_grep_content_tool.createGrepContentTool)(import_vitest.vi.fn());
|
|
100
|
+
await tool.execute(
|
|
101
|
+
tool.inputSchema.parse({
|
|
102
|
+
pattern: "needle",
|
|
103
|
+
path: "/repo/src",
|
|
104
|
+
glob: "**/*.txt"
|
|
105
|
+
}),
|
|
106
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
69
107
|
);
|
|
70
|
-
|
|
108
|
+
(0, import_vitest.expect)(glob).toHaveBeenCalledWith("src/**/*.txt");
|
|
71
109
|
});
|
|
72
|
-
(0, import_vitest.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
(0, import_vitest.it)("rejects external absolute search paths before file provider calls", async () => {
|
|
111
|
+
const mockLogger = createMockLogger();
|
|
112
|
+
const glob = import_vitest.vi.fn(async () => ["src/notes.txt"]);
|
|
113
|
+
const tool = (0, import_grep_content_tool.createGrepContentTool)(import_vitest.vi.fn());
|
|
114
|
+
await (0, import_vitest.expect)(
|
|
115
|
+
tool.execute(
|
|
116
|
+
tool.inputSchema.parse({
|
|
117
|
+
pattern: "needle",
|
|
118
|
+
path: "/outside/src",
|
|
119
|
+
glob: "**/*.txt"
|
|
120
|
+
}),
|
|
121
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
122
|
+
)
|
|
123
|
+
).rejects.toMatchObject({ code: import_core.ToolErrorCode.VALIDATION_FAILED });
|
|
124
|
+
(0, import_vitest.expect)(glob).not.toHaveBeenCalled();
|
|
78
125
|
});
|
|
79
|
-
(0, import_vitest.it)("
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const result = await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
103
|
-
(0, import_vitest.expect)(result.files_searched).toBe(1);
|
|
104
|
-
(0, import_vitest.expect)(result.matches).toEqual([
|
|
105
|
-
{
|
|
106
|
-
file: filePath,
|
|
107
|
-
line_number: 2,
|
|
108
|
-
line: "needle"
|
|
109
|
-
}
|
|
110
|
-
]);
|
|
111
|
-
} finally {
|
|
112
|
-
await fs.rm(homeTempDir, { recursive: true, force: true });
|
|
113
|
-
}
|
|
126
|
+
(0, import_vitest.it)("rejects absolute and escaping glob filters before file provider calls", async () => {
|
|
127
|
+
const mockLogger = createMockLogger();
|
|
128
|
+
const glob = import_vitest.vi.fn(async () => ["src/notes.txt"]);
|
|
129
|
+
const tool = (0, import_grep_content_tool.createGrepContentTool)(import_vitest.vi.fn());
|
|
130
|
+
await (0, import_vitest.expect)(
|
|
131
|
+
tool.execute(
|
|
132
|
+
tool.inputSchema.parse({
|
|
133
|
+
pattern: "needle",
|
|
134
|
+
glob: "/outside/**/*.txt"
|
|
135
|
+
}),
|
|
136
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
137
|
+
)
|
|
138
|
+
).rejects.toMatchObject({ code: import_core.ToolErrorCode.VALIDATION_FAILED });
|
|
139
|
+
await (0, import_vitest.expect)(
|
|
140
|
+
tool.execute(
|
|
141
|
+
tool.inputSchema.parse({
|
|
142
|
+
pattern: "needle",
|
|
143
|
+
glob: "../**/*.txt"
|
|
144
|
+
}),
|
|
145
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
146
|
+
)
|
|
147
|
+
).rejects.toMatchObject({ code: import_core.ToolErrorCode.VALIDATION_FAILED });
|
|
148
|
+
(0, import_vitest.expect)(glob).not.toHaveBeenCalled();
|
|
114
149
|
});
|
|
115
150
|
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import * as os from "node:os";
|
|
5
|
-
import { FileSystemService } from "./filesystem-service.js";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ToolErrorCode } from "@dexto/core";
|
|
3
|
+
import { WorkspaceError } from "@dexto/core/workspace";
|
|
6
4
|
import { createGrepContentTool } from "./grep-content-tool.js";
|
|
7
5
|
const createMockLogger = () => {
|
|
8
6
|
const logger = {
|
|
@@ -21,72 +19,131 @@ const createMockLogger = () => {
|
|
|
21
19
|
};
|
|
22
20
|
return logger;
|
|
23
21
|
};
|
|
24
|
-
function createToolContext(logger) {
|
|
25
|
-
|
|
22
|
+
function createToolContext(logger, filesByPath, glob = vi.fn(async () => Object.keys(filesByPath))) {
|
|
23
|
+
const workspaceManager = {
|
|
24
|
+
open: vi.fn(async () => ({
|
|
25
|
+
context: {
|
|
26
|
+
id: "test-workspace",
|
|
27
|
+
path: "/repo",
|
|
28
|
+
createdAt: Date.now(),
|
|
29
|
+
lastActiveAt: Date.now()
|
|
30
|
+
},
|
|
31
|
+
capabilities: ["files"],
|
|
32
|
+
files: {
|
|
33
|
+
readFile: async (filePath) => readTestFile(filesByPath, filePath),
|
|
34
|
+
readText: async (filePath) => readTestFile(filesByPath, filePath),
|
|
35
|
+
glob,
|
|
36
|
+
writeFile: vi.fn(async () => void 0),
|
|
37
|
+
listFiles: vi.fn(async () => [])
|
|
38
|
+
}
|
|
39
|
+
}))
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
logger,
|
|
43
|
+
services: {
|
|
44
|
+
approval: {},
|
|
45
|
+
search: {},
|
|
46
|
+
resources: {},
|
|
47
|
+
prompts: {},
|
|
48
|
+
skills: {},
|
|
49
|
+
mcp: {},
|
|
50
|
+
taskForker: null,
|
|
51
|
+
workspaceManager
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function readTestFile(filesByPath, filePath) {
|
|
56
|
+
const content = filesByPath[filePath];
|
|
57
|
+
if (content === void 0) {
|
|
58
|
+
throw WorkspaceError.fileNotFound(filePath);
|
|
59
|
+
}
|
|
60
|
+
return content;
|
|
26
61
|
}
|
|
27
62
|
describe("grep_content tool", () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
it("searches content through WorkspaceManager.open without FileSystemService execution", async () => {
|
|
64
|
+
const mockLogger = createMockLogger();
|
|
65
|
+
const getFileSystemService = vi.fn(async () => {
|
|
66
|
+
throw new Error("grep_content execute must not use FileSystemService");
|
|
67
|
+
});
|
|
68
|
+
const glob = vi.fn(async () => ["src/notes.txt"]);
|
|
69
|
+
const tool = createGrepContentTool(getFileSystemService);
|
|
70
|
+
const parsedInput = tool.inputSchema.parse({
|
|
71
|
+
pattern: "needle",
|
|
72
|
+
path: "src",
|
|
73
|
+
glob: "**/*.txt",
|
|
74
|
+
context_lines: 1
|
|
75
|
+
});
|
|
76
|
+
const result = await tool.execute(
|
|
77
|
+
parsedInput,
|
|
78
|
+
createToolContext(mockLogger, { "src/notes.txt": "alpha\nneedle\nomega\n" }, glob)
|
|
79
|
+
);
|
|
80
|
+
expect(getFileSystemService).not.toHaveBeenCalled();
|
|
81
|
+
expect(glob).toHaveBeenCalledWith("src/**/*.txt");
|
|
82
|
+
expect(result.files_searched).toBe(1);
|
|
83
|
+
expect(result.matches).toEqual([
|
|
36
84
|
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
85
|
+
file: "src/notes.txt",
|
|
86
|
+
line_number: 2,
|
|
87
|
+
line: "needle",
|
|
88
|
+
context: {
|
|
89
|
+
before: ["alpha"],
|
|
90
|
+
after: ["omega"]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
it("normalizes workspace-contained absolute search paths before globbing", async () => {
|
|
96
|
+
const mockLogger = createMockLogger();
|
|
97
|
+
const glob = vi.fn(async () => ["src/notes.txt"]);
|
|
98
|
+
const tool = createGrepContentTool(vi.fn());
|
|
99
|
+
await tool.execute(
|
|
100
|
+
tool.inputSchema.parse({
|
|
101
|
+
pattern: "needle",
|
|
102
|
+
path: "/repo/src",
|
|
103
|
+
glob: "**/*.txt"
|
|
104
|
+
}),
|
|
105
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
46
106
|
);
|
|
47
|
-
|
|
107
|
+
expect(glob).toHaveBeenCalledWith("src/**/*.txt");
|
|
48
108
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
109
|
+
it("rejects external absolute search paths before file provider calls", async () => {
|
|
110
|
+
const mockLogger = createMockLogger();
|
|
111
|
+
const glob = vi.fn(async () => ["src/notes.txt"]);
|
|
112
|
+
const tool = createGrepContentTool(vi.fn());
|
|
113
|
+
await expect(
|
|
114
|
+
tool.execute(
|
|
115
|
+
tool.inputSchema.parse({
|
|
116
|
+
pattern: "needle",
|
|
117
|
+
path: "/outside/src",
|
|
118
|
+
glob: "**/*.txt"
|
|
119
|
+
}),
|
|
120
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
121
|
+
)
|
|
122
|
+
).rejects.toMatchObject({ code: ToolErrorCode.VALIDATION_FAILED });
|
|
123
|
+
expect(glob).not.toHaveBeenCalled();
|
|
55
124
|
});
|
|
56
|
-
it("
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const result = await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
80
|
-
expect(result.files_searched).toBe(1);
|
|
81
|
-
expect(result.matches).toEqual([
|
|
82
|
-
{
|
|
83
|
-
file: filePath,
|
|
84
|
-
line_number: 2,
|
|
85
|
-
line: "needle"
|
|
86
|
-
}
|
|
87
|
-
]);
|
|
88
|
-
} finally {
|
|
89
|
-
await fs.rm(homeTempDir, { recursive: true, force: true });
|
|
90
|
-
}
|
|
125
|
+
it("rejects absolute and escaping glob filters before file provider calls", async () => {
|
|
126
|
+
const mockLogger = createMockLogger();
|
|
127
|
+
const glob = vi.fn(async () => ["src/notes.txt"]);
|
|
128
|
+
const tool = createGrepContentTool(vi.fn());
|
|
129
|
+
await expect(
|
|
130
|
+
tool.execute(
|
|
131
|
+
tool.inputSchema.parse({
|
|
132
|
+
pattern: "needle",
|
|
133
|
+
glob: "/outside/**/*.txt"
|
|
134
|
+
}),
|
|
135
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
136
|
+
)
|
|
137
|
+
).rejects.toMatchObject({ code: ToolErrorCode.VALIDATION_FAILED });
|
|
138
|
+
await expect(
|
|
139
|
+
tool.execute(
|
|
140
|
+
tool.inputSchema.parse({
|
|
141
|
+
pattern: "needle",
|
|
142
|
+
glob: "../**/*.txt"
|
|
143
|
+
}),
|
|
144
|
+
createToolContext(mockLogger, { "src/notes.txt": "needle" }, glob)
|
|
145
|
+
)
|
|
146
|
+
).rejects.toMatchObject({ code: ToolErrorCode.VALIDATION_FAILED });
|
|
147
|
+
expect(glob).not.toHaveBeenCalled();
|
|
91
148
|
});
|
|
92
149
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ToolFactory } from '@dexto/agent-config';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { ToolExecutionContext, Tool } from '@dexto/core/tools';
|
|
4
|
+
import { Logger } from '@dexto/core/logger';
|
|
5
|
+
import { DextoRuntimeError } from '@dexto/core/errors';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* FileSystem Tools Factory
|
|
@@ -219,8 +221,6 @@ interface SearchResult {
|
|
|
219
221
|
* Options for writing files
|
|
220
222
|
*/
|
|
221
223
|
interface WriteFileOptions {
|
|
222
|
-
/** Create parent directories if they don't exist */
|
|
223
|
-
createDirs?: boolean | undefined;
|
|
224
224
|
/** File encoding (default: utf-8) */
|
|
225
225
|
encoding?: BufferEncoding | undefined;
|
|
226
226
|
/** Create backup before overwriting */
|
|
@@ -749,7 +749,6 @@ declare function createReadMediaFileTool(getFileSystemService: FileSystemService
|
|
|
749
749
|
declare const WriteFileInputSchema: z.ZodObject<{
|
|
750
750
|
file_path: z.ZodString;
|
|
751
751
|
content: z.ZodString;
|
|
752
|
-
create_dirs: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
753
752
|
encoding: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
754
753
|
ascii: "ascii";
|
|
755
754
|
"utf-8": "utf-8";
|
package/dist/path-validator.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Security-focused path validation for file system operations
|
|
5
5
|
*/
|
|
6
6
|
import { FileSystemConfig, PathValidation } from './types.js';
|
|
7
|
-
import type { Logger } from '@dexto/core';
|
|
7
|
+
import type { Logger } from '@dexto/core/logger';
|
|
8
8
|
/**
|
|
9
9
|
* Callback type for checking if a path is in an approved directory.
|
|
10
10
|
* Used to consult ApprovalManager without creating a direct dependency.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-validator.d.ts","sourceRoot":"","sources":["../src/path-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"path-validator.d.ts","sourceRoot":"","sources":["../src/path-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGjD;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAErE;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,sBAAsB,CAAW;IACzC,OAAO,CAAC,sBAAsB,CAAW;IACzC,OAAO,CAAC,2BAA2B,CAAW;IAC9C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,wBAAwB,CAAuC;gBAE3D,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;IA0BpD;;;;;OAKG;IACH,2BAA2B,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI;IAKpE;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAI7D;;;;;;;;;;;OAWG;IACG,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;YAIzD,oBAAoB;IA0FlC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAgBxB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;;OAGG;IACH,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAInD;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;;;;;;;;OASG;IACG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+B7D;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;OAEG;IACH,eAAe,IAAI,MAAM,EAAE;IAI3B;;OAEG;IACH,eAAe,IAAI,MAAM,EAAE;CAG9B"}
|