@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
|
@@ -29,6 +29,7 @@ var import_write_file_tool = require("./write-file-tool.js");
|
|
|
29
29
|
var import_filesystem_service = require("./filesystem-service.js");
|
|
30
30
|
var import_core = require("@dexto/core");
|
|
31
31
|
var import_core2 = require("@dexto/core");
|
|
32
|
+
var import_workspace = require("@dexto/core/workspace");
|
|
32
33
|
const createMockLogger = () => {
|
|
33
34
|
const logger = {
|
|
34
35
|
debug: import_vitest.vi.fn(),
|
|
@@ -46,8 +47,68 @@ const createMockLogger = () => {
|
|
|
46
47
|
};
|
|
47
48
|
return logger;
|
|
48
49
|
};
|
|
49
|
-
function createToolContext(logger, overrides = {}) {
|
|
50
|
-
return {
|
|
50
|
+
function createToolContext(logger, overrides = {}, workspaceRoot = currentWorkspaceRoot) {
|
|
51
|
+
return {
|
|
52
|
+
logger,
|
|
53
|
+
services: createWorkspaceServices(workspaceRoot),
|
|
54
|
+
...overrides
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let currentWorkspaceRoot = process.cwd();
|
|
58
|
+
function createWorkspaceServices(workspaceRoot, overrides = {}) {
|
|
59
|
+
const workspaceManager = {
|
|
60
|
+
open: import_vitest.vi.fn(async () => ({
|
|
61
|
+
context: {
|
|
62
|
+
id: "test-workspace",
|
|
63
|
+
path: workspaceRoot,
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
lastActiveAt: Date.now()
|
|
66
|
+
},
|
|
67
|
+
capabilities: ["files"],
|
|
68
|
+
files: {
|
|
69
|
+
readFile: async (filePath) => readWorkspaceFile(workspaceRoot, filePath),
|
|
70
|
+
readText: async (filePath) => readWorkspaceFile(workspaceRoot, filePath),
|
|
71
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
72
|
+
writeFile: async (filePath, content) => {
|
|
73
|
+
const resolvedPath = resolveWorkspacePath(workspaceRoot, filePath);
|
|
74
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
75
|
+
try {
|
|
76
|
+
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") {
|
|
79
|
+
throw import_workspace.WorkspaceError.fileNotFound(filePath);
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
85
|
+
}
|
|
86
|
+
})),
|
|
87
|
+
...overrides
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
approval: {},
|
|
91
|
+
search: {},
|
|
92
|
+
resources: {},
|
|
93
|
+
prompts: {},
|
|
94
|
+
skills: {},
|
|
95
|
+
mcp: {},
|
|
96
|
+
taskForker: null,
|
|
97
|
+
workspaceManager
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function resolveWorkspacePath(workspaceRoot, filePath) {
|
|
101
|
+
return path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath);
|
|
102
|
+
}
|
|
103
|
+
async function readWorkspaceFile(workspaceRoot, filePath) {
|
|
104
|
+
try {
|
|
105
|
+
return await fs.readFile(resolveWorkspacePath(workspaceRoot, filePath), "utf-8");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code === "ENOENT") {
|
|
108
|
+
throw import_workspace.WorkspaceError.fileNotFound(filePath);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
51
112
|
}
|
|
52
113
|
(0, import_vitest.describe)("write_file tool", () => {
|
|
53
114
|
let mockLogger;
|
|
@@ -70,6 +131,7 @@ function createToolContext(logger, overrides = {}) {
|
|
|
70
131
|
mockLogger
|
|
71
132
|
);
|
|
72
133
|
await fileSystemService.initialize();
|
|
134
|
+
currentWorkspaceRoot = tempDir;
|
|
73
135
|
import_vitest.vi.clearAllMocks();
|
|
74
136
|
});
|
|
75
137
|
(0, import_vitest.afterEach)(async () => {
|
|
@@ -78,6 +140,186 @@ function createToolContext(logger, overrides = {}) {
|
|
|
78
140
|
} catch {
|
|
79
141
|
}
|
|
80
142
|
});
|
|
143
|
+
(0, import_vitest.it)("writes through WorkspaceManager.open without FileSystemService execution", async () => {
|
|
144
|
+
const getFileSystemService = import_vitest.vi.fn(async () => {
|
|
145
|
+
throw new Error("write_file execute must not use FileSystemService");
|
|
146
|
+
});
|
|
147
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(getFileSystemService);
|
|
148
|
+
const parsedInput = tool.inputSchema.parse({
|
|
149
|
+
file_path: "workspace.txt",
|
|
150
|
+
content: "workspace write"
|
|
151
|
+
});
|
|
152
|
+
const result = await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
153
|
+
(0, import_vitest.expect)(getFileSystemService).not.toHaveBeenCalled();
|
|
154
|
+
(0, import_vitest.expect)(result).toMatchObject({ success: true, path: "workspace.txt" });
|
|
155
|
+
await (0, import_vitest.expect)(fs.readFile(path.join(tempDir, "workspace.txt"), "utf-8")).resolves.toBe(
|
|
156
|
+
"workspace write"
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
(0, import_vitest.it)("creates missing parent directories by default", async () => {
|
|
160
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(import_vitest.vi.fn());
|
|
161
|
+
const parsedInput = tool.inputSchema.parse({
|
|
162
|
+
file_path: "missing-parent/file.txt",
|
|
163
|
+
content: "content"
|
|
164
|
+
});
|
|
165
|
+
await (0, import_vitest.expect)(
|
|
166
|
+
tool.execute(parsedInput, createToolContext(mockLogger))
|
|
167
|
+
).resolves.toMatchObject({
|
|
168
|
+
success: true,
|
|
169
|
+
path: "missing-parent/file.txt"
|
|
170
|
+
});
|
|
171
|
+
await (0, import_vitest.expect)(
|
|
172
|
+
fs.readFile(path.join(tempDir, "missing-parent", "file.txt"), "utf-8")
|
|
173
|
+
).resolves.toBe("content");
|
|
174
|
+
});
|
|
175
|
+
(0, import_vitest.it)("rejects obsolete create_dirs input", async () => {
|
|
176
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(import_vitest.vi.fn());
|
|
177
|
+
(0, import_vitest.expect)(
|
|
178
|
+
() => tool.inputSchema.parse({
|
|
179
|
+
file_path: "file.txt",
|
|
180
|
+
content: "content",
|
|
181
|
+
create_dirs: true
|
|
182
|
+
})
|
|
183
|
+
).toThrow();
|
|
184
|
+
});
|
|
185
|
+
(0, import_vitest.it)("passes normalized workspace paths to the provider without write options", async () => {
|
|
186
|
+
const writeFile = import_vitest.vi.fn(async () => void 0);
|
|
187
|
+
const workspaceManager = {
|
|
188
|
+
open: import_vitest.vi.fn(async () => ({
|
|
189
|
+
context: {
|
|
190
|
+
id: "test-workspace",
|
|
191
|
+
path: tempDir,
|
|
192
|
+
createdAt: Date.now(),
|
|
193
|
+
lastActiveAt: Date.now()
|
|
194
|
+
},
|
|
195
|
+
capabilities: ["files"],
|
|
196
|
+
files: {
|
|
197
|
+
readFile: import_vitest.vi.fn(async () => {
|
|
198
|
+
throw import_workspace.WorkspaceError.fileNotFound("nested/file.txt");
|
|
199
|
+
}),
|
|
200
|
+
readText: import_vitest.vi.fn(async () => {
|
|
201
|
+
throw import_workspace.WorkspaceError.fileNotFound("nested/file.txt");
|
|
202
|
+
}),
|
|
203
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
204
|
+
writeFile,
|
|
205
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
206
|
+
}
|
|
207
|
+
}))
|
|
208
|
+
};
|
|
209
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(import_vitest.vi.fn());
|
|
210
|
+
const context = createToolContext(mockLogger, {
|
|
211
|
+
services: {
|
|
212
|
+
...createWorkspaceServices(tempDir),
|
|
213
|
+
workspaceManager
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
await tool.execute(
|
|
217
|
+
tool.inputSchema.parse({
|
|
218
|
+
file_path: path.join(tempDir, "nested", "file.txt"),
|
|
219
|
+
content: "content"
|
|
220
|
+
}),
|
|
221
|
+
context
|
|
222
|
+
);
|
|
223
|
+
(0, import_vitest.expect)(writeFile).toHaveBeenCalledWith("nested/file.txt", "content");
|
|
224
|
+
});
|
|
225
|
+
(0, import_vitest.it)("generates file creation previews through WorkspaceManager.open in hosted mode", async () => {
|
|
226
|
+
const getFileSystemService = import_vitest.vi.fn(async () => {
|
|
227
|
+
throw new Error("write_file preview must not use FileSystemService in workspace mode");
|
|
228
|
+
});
|
|
229
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(getFileSystemService);
|
|
230
|
+
const preview = await tool.presentation.preview(
|
|
231
|
+
tool.inputSchema.parse({
|
|
232
|
+
file_path: "workspace-new.txt",
|
|
233
|
+
content: "workspace content"
|
|
234
|
+
}),
|
|
235
|
+
createToolContext(mockLogger, { toolCallId: "workspace-preview-create" })
|
|
236
|
+
);
|
|
237
|
+
(0, import_vitest.expect)(getFileSystemService).not.toHaveBeenCalled();
|
|
238
|
+
(0, import_vitest.expect)(preview).toMatchObject({
|
|
239
|
+
content: "workspace content",
|
|
240
|
+
operation: "create",
|
|
241
|
+
path: "workspace-new.txt",
|
|
242
|
+
title: "Create file",
|
|
243
|
+
type: "file"
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
(0, import_vitest.it)("rejects external absolute paths before file provider calls", async () => {
|
|
247
|
+
const readText = import_vitest.vi.fn(async () => "existing");
|
|
248
|
+
const writeFile = import_vitest.vi.fn(async () => void 0);
|
|
249
|
+
const workspaceManager = {
|
|
250
|
+
open: import_vitest.vi.fn(async () => ({
|
|
251
|
+
context: {
|
|
252
|
+
id: "test-workspace",
|
|
253
|
+
path: tempDir,
|
|
254
|
+
createdAt: Date.now(),
|
|
255
|
+
lastActiveAt: Date.now()
|
|
256
|
+
},
|
|
257
|
+
capabilities: ["files"],
|
|
258
|
+
files: {
|
|
259
|
+
readFile: readText,
|
|
260
|
+
readText,
|
|
261
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
262
|
+
writeFile,
|
|
263
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
264
|
+
}
|
|
265
|
+
}))
|
|
266
|
+
};
|
|
267
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(import_vitest.vi.fn());
|
|
268
|
+
const context = createToolContext(mockLogger, {
|
|
269
|
+
services: {
|
|
270
|
+
...createWorkspaceServices(tempDir),
|
|
271
|
+
workspaceManager
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
await (0, import_vitest.expect)(
|
|
275
|
+
tool.execute(
|
|
276
|
+
tool.inputSchema.parse({ file_path: "/outside/file.txt", content: "content" }),
|
|
277
|
+
context
|
|
278
|
+
)
|
|
279
|
+
).rejects.toMatchObject({ code: import_core.ToolErrorCode.VALIDATION_FAILED });
|
|
280
|
+
(0, import_vitest.expect)(readText).not.toHaveBeenCalled();
|
|
281
|
+
(0, import_vitest.expect)(writeFile).not.toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
(0, import_vitest.it)("propagates non-not-found read errors and does not overwrite", async () => {
|
|
284
|
+
const readError = new Error("permission denied");
|
|
285
|
+
const writeFile = import_vitest.vi.fn(async () => void 0);
|
|
286
|
+
const workspaceManager = {
|
|
287
|
+
open: import_vitest.vi.fn(async () => ({
|
|
288
|
+
context: {
|
|
289
|
+
id: "test-workspace",
|
|
290
|
+
path: tempDir,
|
|
291
|
+
createdAt: Date.now(),
|
|
292
|
+
lastActiveAt: Date.now()
|
|
293
|
+
},
|
|
294
|
+
capabilities: ["files"],
|
|
295
|
+
files: {
|
|
296
|
+
readFile: import_vitest.vi.fn(async () => {
|
|
297
|
+
throw readError;
|
|
298
|
+
}),
|
|
299
|
+
readText: import_vitest.vi.fn(async () => {
|
|
300
|
+
throw readError;
|
|
301
|
+
}),
|
|
302
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
303
|
+
writeFile,
|
|
304
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
305
|
+
}
|
|
306
|
+
}))
|
|
307
|
+
};
|
|
308
|
+
const tool = (0, import_write_file_tool.createWriteFileTool)(import_vitest.vi.fn());
|
|
309
|
+
const context = createToolContext(mockLogger, {
|
|
310
|
+
services: {
|
|
311
|
+
...createWorkspaceServices(tempDir),
|
|
312
|
+
workspaceManager
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
await (0, import_vitest.expect)(
|
|
316
|
+
tool.execute(
|
|
317
|
+
tool.inputSchema.parse({ file_path: "file.txt", content: "content" }),
|
|
318
|
+
context
|
|
319
|
+
)
|
|
320
|
+
).rejects.toBe(readError);
|
|
321
|
+
(0, import_vitest.expect)(writeFile).not.toHaveBeenCalled();
|
|
322
|
+
});
|
|
81
323
|
(0, import_vitest.describe)("File Modification Detection - Existing Files", () => {
|
|
82
324
|
(0, import_vitest.it)("should generate preview for existing files outside config-allowed roots (preview read only)", async () => {
|
|
83
325
|
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemService);
|
|
@@ -95,7 +337,13 @@ function createToolContext(logger, overrides = {}) {
|
|
|
95
337
|
});
|
|
96
338
|
const preview = await tool.presentation.preview(
|
|
97
339
|
parsedInput,
|
|
98
|
-
createToolContext(mockLogger, {
|
|
340
|
+
createToolContext(mockLogger, {
|
|
341
|
+
services: {
|
|
342
|
+
...createWorkspaceServices(tempDir),
|
|
343
|
+
workspaceManager: void 0
|
|
344
|
+
},
|
|
345
|
+
toolCallId
|
|
346
|
+
})
|
|
99
347
|
);
|
|
100
348
|
(0, import_vitest.expect)(preview).toBeDefined();
|
|
101
349
|
(0, import_vitest.expect)(preview?.type).toBe("diff");
|
|
@@ -193,11 +441,10 @@ function createToolContext(logger, overrides = {}) {
|
|
|
193
441
|
});
|
|
194
442
|
});
|
|
195
443
|
(0, import_vitest.describe)("File Modification Detection - New Files", () => {
|
|
196
|
-
(0, import_vitest.it)("should
|
|
444
|
+
(0, import_vitest.it)("should create a workspace-relative file", async () => {
|
|
197
445
|
const homeTempDir = await fs.mkdtemp(
|
|
198
446
|
path.join(os.homedir(), ".dexto-write-home-test-")
|
|
199
447
|
);
|
|
200
|
-
const homeRelativeDir = `~/${path.basename(homeTempDir)}`;
|
|
201
448
|
const fileSystemServiceForHome = new import_filesystem_service.FileSystemService(
|
|
202
449
|
{
|
|
203
450
|
allowedPaths: [homeTempDir],
|
|
@@ -214,14 +461,18 @@ function createToolContext(logger, overrides = {}) {
|
|
|
214
461
|
const tool = (0, import_write_file_tool.createWriteFileTool)(async () => fileSystemServiceForHome);
|
|
215
462
|
try {
|
|
216
463
|
const parsedInput = tool.inputSchema.parse({
|
|
217
|
-
file_path:
|
|
218
|
-
content: "brand new content"
|
|
219
|
-
create_dirs: true
|
|
464
|
+
file_path: "nested/new-file.txt",
|
|
465
|
+
content: "brand new content"
|
|
220
466
|
});
|
|
221
|
-
const result = await tool.execute(
|
|
467
|
+
const result = await tool.execute(
|
|
468
|
+
parsedInput,
|
|
469
|
+
createToolContext(mockLogger, {}, homeTempDir)
|
|
470
|
+
);
|
|
222
471
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
223
|
-
(0, import_vitest.expect)(result.path).toBe(
|
|
224
|
-
(0, import_vitest.expect)(await fs.readFile(result.path, "utf-8")).toBe(
|
|
472
|
+
(0, import_vitest.expect)(result.path).toBe("nested/new-file.txt");
|
|
473
|
+
(0, import_vitest.expect)(await fs.readFile(path.join(homeTempDir, result.path), "utf-8")).toBe(
|
|
474
|
+
"brand new content"
|
|
475
|
+
);
|
|
225
476
|
} finally {
|
|
226
477
|
await fs.rm(homeTempDir, { recursive: true, force: true });
|
|
227
478
|
}
|
|
@@ -6,6 +6,7 @@ import { createWriteFileTool } from "./write-file-tool.js";
|
|
|
6
6
|
import { FileSystemService } from "./filesystem-service.js";
|
|
7
7
|
import { ToolErrorCode } from "@dexto/core";
|
|
8
8
|
import { DextoRuntimeError } from "@dexto/core";
|
|
9
|
+
import { WorkspaceError } from "@dexto/core/workspace";
|
|
9
10
|
const createMockLogger = () => {
|
|
10
11
|
const logger = {
|
|
11
12
|
debug: vi.fn(),
|
|
@@ -23,8 +24,68 @@ const createMockLogger = () => {
|
|
|
23
24
|
};
|
|
24
25
|
return logger;
|
|
25
26
|
};
|
|
26
|
-
function createToolContext(logger, overrides = {}) {
|
|
27
|
-
return {
|
|
27
|
+
function createToolContext(logger, overrides = {}, workspaceRoot = currentWorkspaceRoot) {
|
|
28
|
+
return {
|
|
29
|
+
logger,
|
|
30
|
+
services: createWorkspaceServices(workspaceRoot),
|
|
31
|
+
...overrides
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let currentWorkspaceRoot = process.cwd();
|
|
35
|
+
function createWorkspaceServices(workspaceRoot, overrides = {}) {
|
|
36
|
+
const workspaceManager = {
|
|
37
|
+
open: vi.fn(async () => ({
|
|
38
|
+
context: {
|
|
39
|
+
id: "test-workspace",
|
|
40
|
+
path: workspaceRoot,
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
lastActiveAt: Date.now()
|
|
43
|
+
},
|
|
44
|
+
capabilities: ["files"],
|
|
45
|
+
files: {
|
|
46
|
+
readFile: async (filePath) => readWorkspaceFile(workspaceRoot, filePath),
|
|
47
|
+
readText: async (filePath) => readWorkspaceFile(workspaceRoot, filePath),
|
|
48
|
+
glob: vi.fn(async () => []),
|
|
49
|
+
writeFile: async (filePath, content) => {
|
|
50
|
+
const resolvedPath = resolveWorkspacePath(workspaceRoot, filePath);
|
|
51
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
52
|
+
try {
|
|
53
|
+
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code === "ENOENT") {
|
|
56
|
+
throw WorkspaceError.fileNotFound(filePath);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
listFiles: vi.fn(async () => [])
|
|
62
|
+
}
|
|
63
|
+
})),
|
|
64
|
+
...overrides
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
approval: {},
|
|
68
|
+
search: {},
|
|
69
|
+
resources: {},
|
|
70
|
+
prompts: {},
|
|
71
|
+
skills: {},
|
|
72
|
+
mcp: {},
|
|
73
|
+
taskForker: null,
|
|
74
|
+
workspaceManager
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolveWorkspacePath(workspaceRoot, filePath) {
|
|
78
|
+
return path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath);
|
|
79
|
+
}
|
|
80
|
+
async function readWorkspaceFile(workspaceRoot, filePath) {
|
|
81
|
+
try {
|
|
82
|
+
return await fs.readFile(resolveWorkspacePath(workspaceRoot, filePath), "utf-8");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error.code === "ENOENT") {
|
|
85
|
+
throw WorkspaceError.fileNotFound(filePath);
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
28
89
|
}
|
|
29
90
|
describe("write_file tool", () => {
|
|
30
91
|
let mockLogger;
|
|
@@ -47,6 +108,7 @@ describe("write_file tool", () => {
|
|
|
47
108
|
mockLogger
|
|
48
109
|
);
|
|
49
110
|
await fileSystemService.initialize();
|
|
111
|
+
currentWorkspaceRoot = tempDir;
|
|
50
112
|
vi.clearAllMocks();
|
|
51
113
|
});
|
|
52
114
|
afterEach(async () => {
|
|
@@ -55,6 +117,186 @@ describe("write_file tool", () => {
|
|
|
55
117
|
} catch {
|
|
56
118
|
}
|
|
57
119
|
});
|
|
120
|
+
it("writes through WorkspaceManager.open without FileSystemService execution", async () => {
|
|
121
|
+
const getFileSystemService = vi.fn(async () => {
|
|
122
|
+
throw new Error("write_file execute must not use FileSystemService");
|
|
123
|
+
});
|
|
124
|
+
const tool = createWriteFileTool(getFileSystemService);
|
|
125
|
+
const parsedInput = tool.inputSchema.parse({
|
|
126
|
+
file_path: "workspace.txt",
|
|
127
|
+
content: "workspace write"
|
|
128
|
+
});
|
|
129
|
+
const result = await tool.execute(parsedInput, createToolContext(mockLogger));
|
|
130
|
+
expect(getFileSystemService).not.toHaveBeenCalled();
|
|
131
|
+
expect(result).toMatchObject({ success: true, path: "workspace.txt" });
|
|
132
|
+
await expect(fs.readFile(path.join(tempDir, "workspace.txt"), "utf-8")).resolves.toBe(
|
|
133
|
+
"workspace write"
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
it("creates missing parent directories by default", async () => {
|
|
137
|
+
const tool = createWriteFileTool(vi.fn());
|
|
138
|
+
const parsedInput = tool.inputSchema.parse({
|
|
139
|
+
file_path: "missing-parent/file.txt",
|
|
140
|
+
content: "content"
|
|
141
|
+
});
|
|
142
|
+
await expect(
|
|
143
|
+
tool.execute(parsedInput, createToolContext(mockLogger))
|
|
144
|
+
).resolves.toMatchObject({
|
|
145
|
+
success: true,
|
|
146
|
+
path: "missing-parent/file.txt"
|
|
147
|
+
});
|
|
148
|
+
await expect(
|
|
149
|
+
fs.readFile(path.join(tempDir, "missing-parent", "file.txt"), "utf-8")
|
|
150
|
+
).resolves.toBe("content");
|
|
151
|
+
});
|
|
152
|
+
it("rejects obsolete create_dirs input", async () => {
|
|
153
|
+
const tool = createWriteFileTool(vi.fn());
|
|
154
|
+
expect(
|
|
155
|
+
() => tool.inputSchema.parse({
|
|
156
|
+
file_path: "file.txt",
|
|
157
|
+
content: "content",
|
|
158
|
+
create_dirs: true
|
|
159
|
+
})
|
|
160
|
+
).toThrow();
|
|
161
|
+
});
|
|
162
|
+
it("passes normalized workspace paths to the provider without write options", async () => {
|
|
163
|
+
const writeFile = vi.fn(async () => void 0);
|
|
164
|
+
const workspaceManager = {
|
|
165
|
+
open: vi.fn(async () => ({
|
|
166
|
+
context: {
|
|
167
|
+
id: "test-workspace",
|
|
168
|
+
path: tempDir,
|
|
169
|
+
createdAt: Date.now(),
|
|
170
|
+
lastActiveAt: Date.now()
|
|
171
|
+
},
|
|
172
|
+
capabilities: ["files"],
|
|
173
|
+
files: {
|
|
174
|
+
readFile: vi.fn(async () => {
|
|
175
|
+
throw WorkspaceError.fileNotFound("nested/file.txt");
|
|
176
|
+
}),
|
|
177
|
+
readText: vi.fn(async () => {
|
|
178
|
+
throw WorkspaceError.fileNotFound("nested/file.txt");
|
|
179
|
+
}),
|
|
180
|
+
glob: vi.fn(async () => []),
|
|
181
|
+
writeFile,
|
|
182
|
+
listFiles: vi.fn(async () => [])
|
|
183
|
+
}
|
|
184
|
+
}))
|
|
185
|
+
};
|
|
186
|
+
const tool = createWriteFileTool(vi.fn());
|
|
187
|
+
const context = createToolContext(mockLogger, {
|
|
188
|
+
services: {
|
|
189
|
+
...createWorkspaceServices(tempDir),
|
|
190
|
+
workspaceManager
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
await tool.execute(
|
|
194
|
+
tool.inputSchema.parse({
|
|
195
|
+
file_path: path.join(tempDir, "nested", "file.txt"),
|
|
196
|
+
content: "content"
|
|
197
|
+
}),
|
|
198
|
+
context
|
|
199
|
+
);
|
|
200
|
+
expect(writeFile).toHaveBeenCalledWith("nested/file.txt", "content");
|
|
201
|
+
});
|
|
202
|
+
it("generates file creation previews through WorkspaceManager.open in hosted mode", async () => {
|
|
203
|
+
const getFileSystemService = vi.fn(async () => {
|
|
204
|
+
throw new Error("write_file preview must not use FileSystemService in workspace mode");
|
|
205
|
+
});
|
|
206
|
+
const tool = createWriteFileTool(getFileSystemService);
|
|
207
|
+
const preview = await tool.presentation.preview(
|
|
208
|
+
tool.inputSchema.parse({
|
|
209
|
+
file_path: "workspace-new.txt",
|
|
210
|
+
content: "workspace content"
|
|
211
|
+
}),
|
|
212
|
+
createToolContext(mockLogger, { toolCallId: "workspace-preview-create" })
|
|
213
|
+
);
|
|
214
|
+
expect(getFileSystemService).not.toHaveBeenCalled();
|
|
215
|
+
expect(preview).toMatchObject({
|
|
216
|
+
content: "workspace content",
|
|
217
|
+
operation: "create",
|
|
218
|
+
path: "workspace-new.txt",
|
|
219
|
+
title: "Create file",
|
|
220
|
+
type: "file"
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
it("rejects external absolute paths before file provider calls", async () => {
|
|
224
|
+
const readText = vi.fn(async () => "existing");
|
|
225
|
+
const writeFile = vi.fn(async () => void 0);
|
|
226
|
+
const workspaceManager = {
|
|
227
|
+
open: vi.fn(async () => ({
|
|
228
|
+
context: {
|
|
229
|
+
id: "test-workspace",
|
|
230
|
+
path: tempDir,
|
|
231
|
+
createdAt: Date.now(),
|
|
232
|
+
lastActiveAt: Date.now()
|
|
233
|
+
},
|
|
234
|
+
capabilities: ["files"],
|
|
235
|
+
files: {
|
|
236
|
+
readFile: readText,
|
|
237
|
+
readText,
|
|
238
|
+
glob: vi.fn(async () => []),
|
|
239
|
+
writeFile,
|
|
240
|
+
listFiles: vi.fn(async () => [])
|
|
241
|
+
}
|
|
242
|
+
}))
|
|
243
|
+
};
|
|
244
|
+
const tool = createWriteFileTool(vi.fn());
|
|
245
|
+
const context = createToolContext(mockLogger, {
|
|
246
|
+
services: {
|
|
247
|
+
...createWorkspaceServices(tempDir),
|
|
248
|
+
workspaceManager
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
await expect(
|
|
252
|
+
tool.execute(
|
|
253
|
+
tool.inputSchema.parse({ file_path: "/outside/file.txt", content: "content" }),
|
|
254
|
+
context
|
|
255
|
+
)
|
|
256
|
+
).rejects.toMatchObject({ code: ToolErrorCode.VALIDATION_FAILED });
|
|
257
|
+
expect(readText).not.toHaveBeenCalled();
|
|
258
|
+
expect(writeFile).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
it("propagates non-not-found read errors and does not overwrite", async () => {
|
|
261
|
+
const readError = new Error("permission denied");
|
|
262
|
+
const writeFile = vi.fn(async () => void 0);
|
|
263
|
+
const workspaceManager = {
|
|
264
|
+
open: vi.fn(async () => ({
|
|
265
|
+
context: {
|
|
266
|
+
id: "test-workspace",
|
|
267
|
+
path: tempDir,
|
|
268
|
+
createdAt: Date.now(),
|
|
269
|
+
lastActiveAt: Date.now()
|
|
270
|
+
},
|
|
271
|
+
capabilities: ["files"],
|
|
272
|
+
files: {
|
|
273
|
+
readFile: vi.fn(async () => {
|
|
274
|
+
throw readError;
|
|
275
|
+
}),
|
|
276
|
+
readText: vi.fn(async () => {
|
|
277
|
+
throw readError;
|
|
278
|
+
}),
|
|
279
|
+
glob: vi.fn(async () => []),
|
|
280
|
+
writeFile,
|
|
281
|
+
listFiles: vi.fn(async () => [])
|
|
282
|
+
}
|
|
283
|
+
}))
|
|
284
|
+
};
|
|
285
|
+
const tool = createWriteFileTool(vi.fn());
|
|
286
|
+
const context = createToolContext(mockLogger, {
|
|
287
|
+
services: {
|
|
288
|
+
...createWorkspaceServices(tempDir),
|
|
289
|
+
workspaceManager
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
await expect(
|
|
293
|
+
tool.execute(
|
|
294
|
+
tool.inputSchema.parse({ file_path: "file.txt", content: "content" }),
|
|
295
|
+
context
|
|
296
|
+
)
|
|
297
|
+
).rejects.toBe(readError);
|
|
298
|
+
expect(writeFile).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
58
300
|
describe("File Modification Detection - Existing Files", () => {
|
|
59
301
|
it("should generate preview for existing files outside config-allowed roots (preview read only)", async () => {
|
|
60
302
|
const tool = createWriteFileTool(async () => fileSystemService);
|
|
@@ -72,7 +314,13 @@ describe("write_file tool", () => {
|
|
|
72
314
|
});
|
|
73
315
|
const preview = await tool.presentation.preview(
|
|
74
316
|
parsedInput,
|
|
75
|
-
createToolContext(mockLogger, {
|
|
317
|
+
createToolContext(mockLogger, {
|
|
318
|
+
services: {
|
|
319
|
+
...createWorkspaceServices(tempDir),
|
|
320
|
+
workspaceManager: void 0
|
|
321
|
+
},
|
|
322
|
+
toolCallId
|
|
323
|
+
})
|
|
76
324
|
);
|
|
77
325
|
expect(preview).toBeDefined();
|
|
78
326
|
expect(preview?.type).toBe("diff");
|
|
@@ -170,11 +418,10 @@ describe("write_file tool", () => {
|
|
|
170
418
|
});
|
|
171
419
|
});
|
|
172
420
|
describe("File Modification Detection - New Files", () => {
|
|
173
|
-
it("should
|
|
421
|
+
it("should create a workspace-relative file", async () => {
|
|
174
422
|
const homeTempDir = await fs.mkdtemp(
|
|
175
423
|
path.join(os.homedir(), ".dexto-write-home-test-")
|
|
176
424
|
);
|
|
177
|
-
const homeRelativeDir = `~/${path.basename(homeTempDir)}`;
|
|
178
425
|
const fileSystemServiceForHome = new FileSystemService(
|
|
179
426
|
{
|
|
180
427
|
allowedPaths: [homeTempDir],
|
|
@@ -191,14 +438,18 @@ describe("write_file tool", () => {
|
|
|
191
438
|
const tool = createWriteFileTool(async () => fileSystemServiceForHome);
|
|
192
439
|
try {
|
|
193
440
|
const parsedInput = tool.inputSchema.parse({
|
|
194
|
-
file_path:
|
|
195
|
-
content: "brand new content"
|
|
196
|
-
create_dirs: true
|
|
441
|
+
file_path: "nested/new-file.txt",
|
|
442
|
+
content: "brand new content"
|
|
197
443
|
});
|
|
198
|
-
const result = await tool.execute(
|
|
444
|
+
const result = await tool.execute(
|
|
445
|
+
parsedInput,
|
|
446
|
+
createToolContext(mockLogger, {}, homeTempDir)
|
|
447
|
+
);
|
|
199
448
|
expect(result.success).toBe(true);
|
|
200
|
-
expect(result.path).toBe(
|
|
201
|
-
expect(await fs.readFile(result.path, "utf-8")).toBe(
|
|
449
|
+
expect(result.path).toBe("nested/new-file.txt");
|
|
450
|
+
expect(await fs.readFile(path.join(homeTempDir, result.path), "utf-8")).toBe(
|
|
451
|
+
"brand new content"
|
|
452
|
+
);
|
|
202
453
|
} finally {
|
|
203
454
|
await fs.rm(homeTempDir, { recursive: true, force: true });
|
|
204
455
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexto/tools-filesystem",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "FileSystem tools factory for Dexto agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"glob": "^12.0.0",
|
|
23
23
|
"safe-regex": "^2.1.1",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@dexto/agent-config": "1.
|
|
26
|
-
"@dexto/core": "1.
|
|
25
|
+
"@dexto/agent-config": "1.8.0",
|
|
26
|
+
"@dexto/core": "1.8.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/diff": "^5.2.3",
|