@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
|
@@ -33,7 +33,8 @@ __export(directory_approval_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(directory_approval_exports);
|
|
35
35
|
var path = __toESM(require("node:path"), 1);
|
|
36
|
-
var
|
|
36
|
+
var import_approval = require("@dexto/core/approval");
|
|
37
|
+
var import_tools = require("@dexto/core/tools");
|
|
37
38
|
var import_path_utils = require("./path-utils.js");
|
|
38
39
|
function resolveFilePath(workingDirectory, filePath) {
|
|
39
40
|
const resolvedPath = (0, import_path_utils.resolveUserPath)(workingDirectory, filePath);
|
|
@@ -53,7 +54,7 @@ function createDirectoryAccessApprovalHandlers(options) {
|
|
|
53
54
|
}
|
|
54
55
|
const approvalManager = context.services?.approval;
|
|
55
56
|
if (!approvalManager) {
|
|
56
|
-
throw
|
|
57
|
+
throw import_tools.ToolError.configInvalid(
|
|
57
58
|
`${options.toolName} requires ToolExecutionContext.services.approval`
|
|
58
59
|
);
|
|
59
60
|
}
|
|
@@ -61,7 +62,7 @@ function createDirectoryAccessApprovalHandlers(options) {
|
|
|
61
62
|
return null;
|
|
62
63
|
}
|
|
63
64
|
return {
|
|
64
|
-
type:
|
|
65
|
+
type: import_approval.ApprovalType.DIRECTORY_ACCESS,
|
|
65
66
|
metadata: {
|
|
66
67
|
path: paths.path,
|
|
67
68
|
parentDir: paths.parentDir,
|
|
@@ -75,7 +76,7 @@ function createDirectoryAccessApprovalHandlers(options) {
|
|
|
75
76
|
if (!approvalManager) {
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
|
-
if (response.status !==
|
|
79
|
+
if (response.status !== import_approval.ApprovalStatus.APPROVED) {
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
const data = response.data;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z, ZodTypeAny } from 'zod';
|
|
2
|
-
import type { ApprovalRequestDetails, ApprovalResponse
|
|
2
|
+
import type { ApprovalRequestDetails, ApprovalResponse } from '@dexto/core/approval';
|
|
3
|
+
import type { ToolExecutionContext } from '@dexto/core/tools';
|
|
3
4
|
import type { FileSystemService } from './filesystem-service.js';
|
|
4
5
|
import type { FileSystemServiceGetter } from './file-tool-types.js';
|
|
5
6
|
type DirectoryApprovalOperation = 'read' | 'write' | 'edit';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"directory-approval.d.ts","sourceRoot":"","sources":["../src/directory-approval.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAEzC,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"directory-approval.d.ts","sourceRoot":"","sources":["../src/directory-approval.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAEzC,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAErF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAGpE,KAAK,0BAA0B,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE5D,KAAK,sBAAsB,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,wBAAgB,eAAe,CAC3B,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,GACjB,sBAAsB,CAGxB;AAED,wBAAgB,qCAAqC,CAAC,KAAK,CAAC,OAAO,SAAS,UAAU,EAAE,OAAO,EAAE;IAC7F,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,0BAA0B,CAAC;IACtC,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,EAAE,uBAAuB,CAAC;IAC9C,YAAY,EAAE,CACV,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EACxB,iBAAiB,EAAE,iBAAiB,KACnC,sBAAsB,CAAC;CAC/B,GAAG;IACA,QAAQ,EAAE;QACN,QAAQ,EAAE,CACN,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EACxB,OAAO,EAAE,oBAAoB,KAC5B,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;QAC5C,SAAS,EAAE,CACP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,oBAAoB,EAC7B,eAAe,EAAE,sBAAsB,KACtC,OAAO,CAAC,IAAI,CAAC,CAAC;KACtB,CAAC;CACL,CA6DA"}
|
|
@@ -26,6 +26,8 @@ var path = __toESM(require("node:path"), 1);
|
|
|
26
26
|
var fs = __toESM(require("node:fs/promises"), 1);
|
|
27
27
|
var os = __toESM(require("node:os"), 1);
|
|
28
28
|
var import_core = require("@dexto/core");
|
|
29
|
+
var import_storage = require("@dexto/core/storage");
|
|
30
|
+
var import_workspace = require("@dexto/core/workspace");
|
|
29
31
|
var import_filesystem_service = require("./filesystem-service.js");
|
|
30
32
|
var import_read_file_tool = require("./read-file-tool.js");
|
|
31
33
|
var import_write_file_tool = require("./write-file-tool.js");
|
|
@@ -49,26 +51,8 @@ const createMockLogger = () => {
|
|
|
49
51
|
};
|
|
50
52
|
return logger;
|
|
51
53
|
};
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
return {
|
|
55
|
-
async load(sessionId) {
|
|
56
|
-
return structuredClone(
|
|
57
|
-
states.get(sessionId ?? "__global__") ?? {
|
|
58
|
-
toolPatterns: {},
|
|
59
|
-
approvedDirectories: []
|
|
60
|
-
}
|
|
61
|
-
);
|
|
62
|
-
},
|
|
63
|
-
async save(sessionId, state) {
|
|
64
|
-
states.set(sessionId ?? "__global__", structuredClone(state));
|
|
65
|
-
},
|
|
66
|
-
async delete(sessionId) {
|
|
67
|
-
states.delete(sessionId ?? "__global__");
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function createToolContext(logger, approval, sessionId) {
|
|
54
|
+
function createToolContext(logger, approval, sessionId, workspacePath) {
|
|
55
|
+
const workspaceRoot = workspacePath ?? tempWorkspaceRoot;
|
|
72
56
|
return {
|
|
73
57
|
logger,
|
|
74
58
|
...sessionId !== void 0 ? { sessionId } : {},
|
|
@@ -77,11 +61,57 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
77
61
|
search: {},
|
|
78
62
|
resources: {},
|
|
79
63
|
prompts: {},
|
|
64
|
+
skills: {},
|
|
80
65
|
mcp: {},
|
|
81
|
-
taskForker: null
|
|
66
|
+
taskForker: null,
|
|
67
|
+
workspaceManager: createWorkspaceManager(workspaceRoot)
|
|
82
68
|
}
|
|
83
69
|
};
|
|
84
70
|
}
|
|
71
|
+
let tempWorkspaceRoot = process.cwd();
|
|
72
|
+
function createWorkspaceManager(workspaceRoot) {
|
|
73
|
+
return {
|
|
74
|
+
open: async () => ({
|
|
75
|
+
context: {
|
|
76
|
+
id: "test-workspace",
|
|
77
|
+
path: workspaceRoot,
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
lastActiveAt: Date.now()
|
|
80
|
+
},
|
|
81
|
+
capabilities: ["files"],
|
|
82
|
+
files: {
|
|
83
|
+
readFile: async (filePath) => {
|
|
84
|
+
const resolved = resolveWorkspacePath(workspaceRoot, filePath);
|
|
85
|
+
return readWorkspaceFile(resolved, filePath);
|
|
86
|
+
},
|
|
87
|
+
readText: async (filePath) => {
|
|
88
|
+
const resolved = resolveWorkspacePath(workspaceRoot, filePath);
|
|
89
|
+
return readWorkspaceFile(resolved, filePath);
|
|
90
|
+
},
|
|
91
|
+
writeFile: async (filePath, content) => {
|
|
92
|
+
const resolved = resolveWorkspacePath(workspaceRoot, filePath);
|
|
93
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
94
|
+
await fs.writeFile(resolved, content, "utf8");
|
|
95
|
+
},
|
|
96
|
+
glob: import_vitest.vi.fn(async () => []),
|
|
97
|
+
listFiles: import_vitest.vi.fn(async () => [])
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function resolveWorkspacePath(workspaceRoot, filePath) {
|
|
103
|
+
return path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath);
|
|
104
|
+
}
|
|
105
|
+
async function readWorkspaceFile(resolvedPath, filePath) {
|
|
106
|
+
try {
|
|
107
|
+
return await fs.readFile(resolvedPath, "utf8");
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code === "ENOENT") {
|
|
110
|
+
throw import_workspace.WorkspaceError.fileNotFound(filePath);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
85
115
|
(0, import_vitest.describe)("Directory Approval Integration Tests", () => {
|
|
86
116
|
let mockLogger;
|
|
87
117
|
let tempDir;
|
|
@@ -106,13 +136,14 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
106
136
|
mockLogger
|
|
107
137
|
);
|
|
108
138
|
await fileSystemService.initialize();
|
|
139
|
+
tempWorkspaceRoot = tempDir;
|
|
109
140
|
approvalManager = new import_core.ApprovalManager(
|
|
110
141
|
{
|
|
111
142
|
permissions: { mode: "manual" },
|
|
112
143
|
elicitation: { enabled: true }
|
|
113
144
|
},
|
|
114
145
|
mockLogger,
|
|
115
|
-
|
|
146
|
+
new import_storage.InMemoryDextoStores().getStore("approvals")
|
|
116
147
|
);
|
|
117
148
|
toolContext = createToolContext(mockLogger, approvalManager);
|
|
118
149
|
import_vitest.vi.clearAllMocks();
|
|
@@ -321,7 +352,7 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
321
352
|
file_path: externalFile,
|
|
322
353
|
content: "session-scoped write"
|
|
323
354
|
}),
|
|
324
|
-
createToolContext(mockLogger, approvalManager, "session-a")
|
|
355
|
+
createToolContext(mockLogger, approvalManager, "session-a", externalDir)
|
|
325
356
|
)
|
|
326
357
|
).resolves.toEqual(
|
|
327
358
|
import_vitest.expect.objectContaining({
|
|
@@ -332,15 +363,20 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
332
363
|
await (0, import_vitest.expect)(fs.readFile(externalFile, "utf8")).resolves.toBe(
|
|
333
364
|
"session-scoped write"
|
|
334
365
|
);
|
|
366
|
+
const blockedInput = writeTool.inputSchema.parse({
|
|
367
|
+
file_path: path.join(externalDir, "blocked.txt"),
|
|
368
|
+
content: "should fail"
|
|
369
|
+
});
|
|
335
370
|
await (0, import_vitest.expect)(
|
|
336
|
-
writeTool.
|
|
337
|
-
|
|
338
|
-
file_path: path.join(externalDir, "blocked.txt"),
|
|
339
|
-
content: "should fail"
|
|
340
|
-
}),
|
|
371
|
+
writeTool.approval.override(
|
|
372
|
+
blockedInput,
|
|
341
373
|
createToolContext(mockLogger, approvalManager, "session-b")
|
|
342
374
|
)
|
|
343
|
-
).
|
|
375
|
+
).resolves.toEqual(
|
|
376
|
+
import_vitest.expect.objectContaining({
|
|
377
|
+
type: "directory_access"
|
|
378
|
+
})
|
|
379
|
+
);
|
|
344
380
|
} finally {
|
|
345
381
|
await fs.rm(externalDir, { recursive: true, force: true });
|
|
346
382
|
}
|
|
@@ -367,9 +403,8 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
367
403
|
});
|
|
368
404
|
const readTool = tools.find((tool) => tool.id === "read_file");
|
|
369
405
|
(0, import_vitest.expect)(readTool).toBeDefined();
|
|
370
|
-
const baseContext = createToolContext(mockLogger, approvalManager);
|
|
371
406
|
const contextA = {
|
|
372
|
-
...
|
|
407
|
+
...createToolContext(mockLogger, approvalManager, "session-a", workspaceA),
|
|
373
408
|
sessionId: "session-a",
|
|
374
409
|
workspace: {
|
|
375
410
|
id: "workspace-a",
|
|
@@ -379,7 +414,7 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
379
414
|
}
|
|
380
415
|
};
|
|
381
416
|
const contextB = {
|
|
382
|
-
...
|
|
417
|
+
...createToolContext(mockLogger, approvalManager, "session-b", workspaceB),
|
|
383
418
|
sessionId: "session-b",
|
|
384
419
|
workspace: {
|
|
385
420
|
id: "workspace-b",
|
|
@@ -441,7 +476,12 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
441
476
|
setWorkingDirectory: import_vitest.vi.fn(),
|
|
442
477
|
setDirectoryApprovalChecker: import_vitest.vi.fn()
|
|
443
478
|
};
|
|
444
|
-
const baseContext = createToolContext(
|
|
479
|
+
const baseContext = createToolContext(
|
|
480
|
+
mockLogger,
|
|
481
|
+
approvalManager,
|
|
482
|
+
"session-a",
|
|
483
|
+
workspace
|
|
484
|
+
);
|
|
445
485
|
const context = {
|
|
446
486
|
...baseContext,
|
|
447
487
|
workspace: {
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
ApprovalStatus,
|
|
8
8
|
DextoRuntimeError
|
|
9
9
|
} from "@dexto/core";
|
|
10
|
+
import { InMemoryDextoStores } from "@dexto/core/storage";
|
|
11
|
+
import { WorkspaceError } from "@dexto/core/workspace";
|
|
10
12
|
import { FileSystemService } from "./filesystem-service.js";
|
|
11
13
|
import { createReadFileTool } from "./read-file-tool.js";
|
|
12
14
|
import { createWriteFileTool } from "./write-file-tool.js";
|
|
@@ -30,26 +32,8 @@ const createMockLogger = () => {
|
|
|
30
32
|
};
|
|
31
33
|
return logger;
|
|
32
34
|
};
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
return {
|
|
36
|
-
async load(sessionId) {
|
|
37
|
-
return structuredClone(
|
|
38
|
-
states.get(sessionId ?? "__global__") ?? {
|
|
39
|
-
toolPatterns: {},
|
|
40
|
-
approvedDirectories: []
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
-
},
|
|
44
|
-
async save(sessionId, state) {
|
|
45
|
-
states.set(sessionId ?? "__global__", structuredClone(state));
|
|
46
|
-
},
|
|
47
|
-
async delete(sessionId) {
|
|
48
|
-
states.delete(sessionId ?? "__global__");
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function createToolContext(logger, approval, sessionId) {
|
|
35
|
+
function createToolContext(logger, approval, sessionId, workspacePath) {
|
|
36
|
+
const workspaceRoot = workspacePath ?? tempWorkspaceRoot;
|
|
53
37
|
return {
|
|
54
38
|
logger,
|
|
55
39
|
...sessionId !== void 0 ? { sessionId } : {},
|
|
@@ -58,11 +42,57 @@ function createToolContext(logger, approval, sessionId) {
|
|
|
58
42
|
search: {},
|
|
59
43
|
resources: {},
|
|
60
44
|
prompts: {},
|
|
45
|
+
skills: {},
|
|
61
46
|
mcp: {},
|
|
62
|
-
taskForker: null
|
|
47
|
+
taskForker: null,
|
|
48
|
+
workspaceManager: createWorkspaceManager(workspaceRoot)
|
|
63
49
|
}
|
|
64
50
|
};
|
|
65
51
|
}
|
|
52
|
+
let tempWorkspaceRoot = process.cwd();
|
|
53
|
+
function createWorkspaceManager(workspaceRoot) {
|
|
54
|
+
return {
|
|
55
|
+
open: async () => ({
|
|
56
|
+
context: {
|
|
57
|
+
id: "test-workspace",
|
|
58
|
+
path: workspaceRoot,
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
lastActiveAt: Date.now()
|
|
61
|
+
},
|
|
62
|
+
capabilities: ["files"],
|
|
63
|
+
files: {
|
|
64
|
+
readFile: async (filePath) => {
|
|
65
|
+
const resolved = resolveWorkspacePath(workspaceRoot, filePath);
|
|
66
|
+
return readWorkspaceFile(resolved, filePath);
|
|
67
|
+
},
|
|
68
|
+
readText: async (filePath) => {
|
|
69
|
+
const resolved = resolveWorkspacePath(workspaceRoot, filePath);
|
|
70
|
+
return readWorkspaceFile(resolved, filePath);
|
|
71
|
+
},
|
|
72
|
+
writeFile: async (filePath, content) => {
|
|
73
|
+
const resolved = resolveWorkspacePath(workspaceRoot, filePath);
|
|
74
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
75
|
+
await fs.writeFile(resolved, content, "utf8");
|
|
76
|
+
},
|
|
77
|
+
glob: vi.fn(async () => []),
|
|
78
|
+
listFiles: vi.fn(async () => [])
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function resolveWorkspacePath(workspaceRoot, filePath) {
|
|
84
|
+
return path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath);
|
|
85
|
+
}
|
|
86
|
+
async function readWorkspaceFile(resolvedPath, filePath) {
|
|
87
|
+
try {
|
|
88
|
+
return await fs.readFile(resolvedPath, "utf8");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === "ENOENT") {
|
|
91
|
+
throw WorkspaceError.fileNotFound(filePath);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
66
96
|
describe("Directory Approval Integration Tests", () => {
|
|
67
97
|
let mockLogger;
|
|
68
98
|
let tempDir;
|
|
@@ -87,13 +117,14 @@ describe("Directory Approval Integration Tests", () => {
|
|
|
87
117
|
mockLogger
|
|
88
118
|
);
|
|
89
119
|
await fileSystemService.initialize();
|
|
120
|
+
tempWorkspaceRoot = tempDir;
|
|
90
121
|
approvalManager = new ApprovalManager(
|
|
91
122
|
{
|
|
92
123
|
permissions: { mode: "manual" },
|
|
93
124
|
elicitation: { enabled: true }
|
|
94
125
|
},
|
|
95
126
|
mockLogger,
|
|
96
|
-
|
|
127
|
+
new InMemoryDextoStores().getStore("approvals")
|
|
97
128
|
);
|
|
98
129
|
toolContext = createToolContext(mockLogger, approvalManager);
|
|
99
130
|
vi.clearAllMocks();
|
|
@@ -302,7 +333,7 @@ describe("Directory Approval Integration Tests", () => {
|
|
|
302
333
|
file_path: externalFile,
|
|
303
334
|
content: "session-scoped write"
|
|
304
335
|
}),
|
|
305
|
-
createToolContext(mockLogger, approvalManager, "session-a")
|
|
336
|
+
createToolContext(mockLogger, approvalManager, "session-a", externalDir)
|
|
306
337
|
)
|
|
307
338
|
).resolves.toEqual(
|
|
308
339
|
expect.objectContaining({
|
|
@@ -313,15 +344,20 @@ describe("Directory Approval Integration Tests", () => {
|
|
|
313
344
|
await expect(fs.readFile(externalFile, "utf8")).resolves.toBe(
|
|
314
345
|
"session-scoped write"
|
|
315
346
|
);
|
|
347
|
+
const blockedInput = writeTool.inputSchema.parse({
|
|
348
|
+
file_path: path.join(externalDir, "blocked.txt"),
|
|
349
|
+
content: "should fail"
|
|
350
|
+
});
|
|
316
351
|
await expect(
|
|
317
|
-
writeTool.
|
|
318
|
-
|
|
319
|
-
file_path: path.join(externalDir, "blocked.txt"),
|
|
320
|
-
content: "should fail"
|
|
321
|
-
}),
|
|
352
|
+
writeTool.approval.override(
|
|
353
|
+
blockedInput,
|
|
322
354
|
createToolContext(mockLogger, approvalManager, "session-b")
|
|
323
355
|
)
|
|
324
|
-
).
|
|
356
|
+
).resolves.toEqual(
|
|
357
|
+
expect.objectContaining({
|
|
358
|
+
type: "directory_access"
|
|
359
|
+
})
|
|
360
|
+
);
|
|
325
361
|
} finally {
|
|
326
362
|
await fs.rm(externalDir, { recursive: true, force: true });
|
|
327
363
|
}
|
|
@@ -348,9 +384,8 @@ describe("Directory Approval Integration Tests", () => {
|
|
|
348
384
|
});
|
|
349
385
|
const readTool = tools.find((tool) => tool.id === "read_file");
|
|
350
386
|
expect(readTool).toBeDefined();
|
|
351
|
-
const baseContext = createToolContext(mockLogger, approvalManager);
|
|
352
387
|
const contextA = {
|
|
353
|
-
...
|
|
388
|
+
...createToolContext(mockLogger, approvalManager, "session-a", workspaceA),
|
|
354
389
|
sessionId: "session-a",
|
|
355
390
|
workspace: {
|
|
356
391
|
id: "workspace-a",
|
|
@@ -360,7 +395,7 @@ describe("Directory Approval Integration Tests", () => {
|
|
|
360
395
|
}
|
|
361
396
|
};
|
|
362
397
|
const contextB = {
|
|
363
|
-
...
|
|
398
|
+
...createToolContext(mockLogger, approvalManager, "session-b", workspaceB),
|
|
364
399
|
sessionId: "session-b",
|
|
365
400
|
workspace: {
|
|
366
401
|
id: "workspace-b",
|
|
@@ -422,7 +457,12 @@ describe("Directory Approval Integration Tests", () => {
|
|
|
422
457
|
setWorkingDirectory: vi.fn(),
|
|
423
458
|
setDirectoryApprovalChecker: vi.fn()
|
|
424
459
|
};
|
|
425
|
-
const baseContext = createToolContext(
|
|
460
|
+
const baseContext = createToolContext(
|
|
461
|
+
mockLogger,
|
|
462
|
+
approvalManager,
|
|
463
|
+
"session-a",
|
|
464
|
+
workspace
|
|
465
|
+
);
|
|
426
466
|
const context = {
|
|
427
467
|
...baseContext,
|
|
428
468
|
workspace: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { ApprovalStatus, ApprovalType
|
|
2
|
+
import { ApprovalStatus, ApprovalType } from "@dexto/core/approval";
|
|
3
|
+
import { ToolError } from "@dexto/core/tools";
|
|
3
4
|
import { resolveUserPath } from "./path-utils.js";
|
|
4
5
|
function resolveFilePath(workingDirectory, filePath) {
|
|
5
6
|
const resolvedPath = resolveUserPath(workingDirectory, filePath);
|
package/dist/edit-file-tool.cjs
CHANGED
|
@@ -24,9 +24,11 @@ module.exports = __toCommonJS(edit_file_tool_exports);
|
|
|
24
24
|
var import_node_crypto = require("node:crypto");
|
|
25
25
|
var import_zod = require("zod");
|
|
26
26
|
var import_diff = require("diff");
|
|
27
|
-
var
|
|
27
|
+
var import_tools = require("@dexto/core/tools");
|
|
28
|
+
var import_errors = require("@dexto/core/errors");
|
|
28
29
|
var import_error_codes = require("./error-codes.js");
|
|
29
30
|
var import_directory_approval = require("./directory-approval.js");
|
|
31
|
+
var import_workspace_paths = require("./workspace-paths.js");
|
|
30
32
|
const previewContentHashCache = /* @__PURE__ */ new Map();
|
|
31
33
|
function computeContentHash(content) {
|
|
32
34
|
return (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
|
|
@@ -53,7 +55,7 @@ function generateDiffPreview(filePath, originalContent, newContent) {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
function createEditFileTool(getFileSystemService) {
|
|
56
|
-
return (0,
|
|
58
|
+
return (0, import_tools.defineTool)({
|
|
57
59
|
id: "edit_file",
|
|
58
60
|
aliases: ["edit"],
|
|
59
61
|
description: "Edit a file by replacing text. By default, old_string must be unique in the file (will error if found multiple times). Set replace_all=true to replace all occurrences. Automatically creates backup before editing. Requires approval. Returns success status, path, number of changes made, and backup path.",
|
|
@@ -66,9 +68,9 @@ function createEditFileTool(getFileSystemService) {
|
|
|
66
68
|
resolvePaths: (input, fileSystemService) => (0, import_directory_approval.resolveFilePath)(fileSystemService.getWorkingDirectory(), input.file_path)
|
|
67
69
|
}),
|
|
68
70
|
presentation: {
|
|
69
|
-
describeHeader: (input) => (0,
|
|
70
|
-
title: "
|
|
71
|
-
argsText: (0,
|
|
71
|
+
describeHeader: (input) => (0, import_tools.createLocalToolCallHeader)({
|
|
72
|
+
title: "Edit",
|
|
73
|
+
argsText: (0, import_tools.truncateForHeader)(input.file_path, 140)
|
|
72
74
|
}),
|
|
73
75
|
/**
|
|
74
76
|
* Generate preview for approval UI - shows diff without modifying file
|
|
@@ -88,7 +90,7 @@ function createEditFileTool(getFileSystemService) {
|
|
|
88
90
|
const originalFile = await resolvedFileSystemService.readFile(resolvedPath);
|
|
89
91
|
originalContent = originalFile.content;
|
|
90
92
|
} catch (error) {
|
|
91
|
-
if (error instanceof
|
|
93
|
+
if (error instanceof import_errors.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.INVALID_PATH) {
|
|
92
94
|
const originalFile = await resolvedFileSystemService.readFileForToolPreview(
|
|
93
95
|
resolvedPath
|
|
94
96
|
);
|
|
@@ -106,7 +108,7 @@ function createEditFileTool(getFileSystemService) {
|
|
|
106
108
|
if (!replace_all) {
|
|
107
109
|
const occurrences = originalContent.split(old_string).length - 1;
|
|
108
110
|
if (occurrences > 1) {
|
|
109
|
-
throw
|
|
111
|
+
throw import_tools.ToolError.validationFailed(
|
|
110
112
|
"edit_file",
|
|
111
113
|
`String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
|
|
112
114
|
{ file_path: resolvedPath, occurrences }
|
|
@@ -115,7 +117,7 @@ function createEditFileTool(getFileSystemService) {
|
|
|
115
117
|
}
|
|
116
118
|
const newContent = replace_all ? originalContent.split(old_string).join(new_string) : originalContent.replace(old_string, new_string);
|
|
117
119
|
if (originalContent === newContent) {
|
|
118
|
-
throw
|
|
120
|
+
throw import_tools.ToolError.validationFailed(
|
|
119
121
|
"edit_file",
|
|
120
122
|
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
121
123
|
{
|
|
@@ -126,11 +128,11 @@ function createEditFileTool(getFileSystemService) {
|
|
|
126
128
|
}
|
|
127
129
|
return generateDiffPreview(resolvedPath, originalContent, newContent);
|
|
128
130
|
} catch (error) {
|
|
129
|
-
if (error instanceof
|
|
131
|
+
if (error instanceof import_errors.DextoRuntimeError && error.code === import_tools.ToolErrorCode.VALIDATION_FAILED) {
|
|
130
132
|
throw error;
|
|
131
133
|
}
|
|
132
|
-
if (error instanceof
|
|
133
|
-
throw
|
|
134
|
+
if (error instanceof import_errors.DextoRuntimeError) {
|
|
135
|
+
throw import_tools.ToolError.validationFailed("edit_file", error.message, {
|
|
134
136
|
file_path: resolvedPath,
|
|
135
137
|
originalErrorCode: error.code
|
|
136
138
|
});
|
|
@@ -140,54 +142,67 @@ function createEditFileTool(getFileSystemService) {
|
|
|
140
142
|
}
|
|
141
143
|
},
|
|
142
144
|
async execute(input, context) {
|
|
143
|
-
const resolvedFileSystemService = await getFileSystemService(context);
|
|
144
145
|
const { file_path, old_string, new_string, replace_all } = input;
|
|
145
|
-
const
|
|
146
|
-
|
|
146
|
+
const handle = await openWorkspace(context, "edit_file");
|
|
147
|
+
const workspacePath = (0, import_workspace_paths.toWorkspaceRelativePath)(
|
|
148
|
+
"edit_file",
|
|
149
|
+
handle.context.path,
|
|
147
150
|
file_path
|
|
148
151
|
);
|
|
152
|
+
let currentContent = await handle.files.readText(workspacePath);
|
|
149
153
|
const toolCallId = context.toolCallId;
|
|
150
154
|
if (toolCallId) {
|
|
151
155
|
const expectedHash = previewContentHashCache.get(toolCallId);
|
|
152
156
|
if (expectedHash === void 0) {
|
|
153
157
|
} else {
|
|
154
158
|
previewContentHashCache.delete(toolCallId);
|
|
155
|
-
let currentContent;
|
|
156
159
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (error instanceof import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.FILE_NOT_FOUND) {
|
|
161
|
-
throw import_core.ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
|
|
162
|
-
}
|
|
163
|
-
throw error;
|
|
160
|
+
currentContent = await handle.files.readText(workspacePath);
|
|
161
|
+
} catch {
|
|
162
|
+
throw import_tools.ToolError.fileModifiedSincePreview("edit_file", file_path);
|
|
164
163
|
}
|
|
165
164
|
const currentHash = computeContentHash(currentContent);
|
|
166
165
|
if (expectedHash !== currentHash) {
|
|
167
|
-
throw
|
|
166
|
+
throw import_tools.ToolError.fileModifiedSincePreview("edit_file", file_path);
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
const occurrences = currentContent.split(old_string).length - 1;
|
|
171
|
+
if (occurrences === 0) {
|
|
172
|
+
throw import_tools.ToolError.validationFailed(
|
|
173
|
+
"edit_file",
|
|
174
|
+
`String not found in file: "${old_string.slice(0, 50)}${old_string.length > 50 ? "..." : ""}"`,
|
|
175
|
+
{
|
|
176
|
+
file_path,
|
|
177
|
+
old_string_preview: old_string.slice(0, 100)
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!replace_all && occurrences > 1) {
|
|
182
|
+
throw import_tools.ToolError.validationFailed(
|
|
183
|
+
"edit_file",
|
|
184
|
+
`String found ${occurrences} times in file. Set replace_all=true to replace all, or provide more context to make old_string unique.`,
|
|
185
|
+
{ file_path, occurrences }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const newContent = replace_all ? currentContent.split(old_string).join(new_string) : currentContent.replace(old_string, new_string);
|
|
189
|
+
await handle.files.writeFile(workspacePath, newContent);
|
|
190
|
+
const _display = generateDiffPreview(file_path, currentContent, newContent);
|
|
181
191
|
return {
|
|
182
|
-
success:
|
|
183
|
-
path:
|
|
184
|
-
changes_count:
|
|
185
|
-
...result.backupPath && { backup_path: result.backupPath },
|
|
192
|
+
success: true,
|
|
193
|
+
path: file_path,
|
|
194
|
+
changes_count: occurrences,
|
|
186
195
|
_display
|
|
187
196
|
};
|
|
188
197
|
}
|
|
189
198
|
});
|
|
190
199
|
}
|
|
200
|
+
async function openWorkspace(context, toolName) {
|
|
201
|
+
if (!context.services) {
|
|
202
|
+
throw new Error(`${toolName} requires ToolExecutionContext.services`);
|
|
203
|
+
}
|
|
204
|
+
return context.services.workspaceManager.open({ intent: "write" });
|
|
205
|
+
}
|
|
191
206
|
// Annotate the CommonJS export names for ESM import in node:
|
|
192
207
|
0 && (module.exports = {
|
|
193
208
|
createEditFileTool
|
package/dist/edit-file-tool.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Internal tool for editing files by replacing text (requires approval)
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import type { Tool } from '@dexto/core';
|
|
7
|
+
import type { Tool } from '@dexto/core/tools';
|
|
8
8
|
import type { FileSystemServiceGetter } from './file-tool-types.js';
|
|
9
9
|
declare const EditFileInputSchema: z.ZodObject<{
|
|
10
10
|
file_path: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-file-tool.d.ts","sourceRoot":"","sources":["../src/edit-file-tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"edit-file-tool.d.ts","sourceRoot":"","sources":["../src/edit-file-tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,KAAK,EAAmB,IAAI,EAAwB,MAAM,mBAAmB,CAAC;AACrF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAmBpE,QAAA,MAAM,mBAAmB;;;;;kBAaZ,CAAC;AA0Bd;;GAEG;AACH,wBAAgB,kBAAkB,CAC9B,oBAAoB,EAAE,uBAAuB,GAC9C,IAAI,CAAC,OAAO,mBAAmB,CAAC,CA4LlC"}
|