@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.
Files changed (66) hide show
  1. package/dist/directory-approval.cjs +5 -4
  2. package/dist/directory-approval.d.ts +2 -1
  3. package/dist/directory-approval.d.ts.map +1 -1
  4. package/dist/directory-approval.integration.test.cjs +73 -33
  5. package/dist/directory-approval.integration.test.js +73 -33
  6. package/dist/directory-approval.js +2 -1
  7. package/dist/edit-file-tool.cjs +52 -37
  8. package/dist/edit-file-tool.d.ts +1 -1
  9. package/dist/edit-file-tool.d.ts.map +1 -1
  10. package/dist/edit-file-tool.js +43 -29
  11. package/dist/edit-file-tool.test.cjs +159 -2
  12. package/dist/edit-file-tool.test.js +159 -2
  13. package/dist/errors.cjs +53 -53
  14. package/dist/errors.d.ts +1 -1
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +1 -1
  17. package/dist/file-tool-types.d.ts +1 -1
  18. package/dist/file-tool-types.d.ts.map +1 -1
  19. package/dist/filesystem-service.cjs +60 -60
  20. package/dist/filesystem-service.d.ts +1 -1
  21. package/dist/filesystem-service.d.ts.map +1 -1
  22. package/dist/filesystem-service.js +5 -5
  23. package/dist/filesystem-service.test.cjs +1 -3
  24. package/dist/filesystem-service.test.js +1 -3
  25. package/dist/glob-files-tool.cjs +27 -24
  26. package/dist/glob-files-tool.d.ts +1 -1
  27. package/dist/glob-files-tool.d.ts.map +1 -1
  28. package/dist/glob-files-tool.js +24 -21
  29. package/dist/glob-files-tool.test.cjs +100 -88
  30. package/dist/glob-files-tool.test.js +101 -67
  31. package/dist/grep-content-tool.cjs +129 -44
  32. package/dist/grep-content-tool.d.ts +1 -1
  33. package/dist/grep-content-tool.d.ts.map +1 -1
  34. package/dist/grep-content-tool.js +120 -41
  35. package/dist/grep-content-tool.test.cjs +122 -87
  36. package/dist/grep-content-tool.test.js +123 -66
  37. package/dist/index.d.cts +3 -4
  38. package/dist/path-validator.d.ts +1 -1
  39. package/dist/path-validator.d.ts.map +1 -1
  40. package/dist/read-file-tool.cjs +43 -14
  41. package/dist/read-file-tool.d.ts +1 -1
  42. package/dist/read-file-tool.d.ts.map +1 -1
  43. package/dist/read-file-tool.js +40 -11
  44. package/dist/read-file-tool.test.cjs +119 -0
  45. package/dist/read-file-tool.test.d.ts +2 -0
  46. package/dist/read-file-tool.test.d.ts.map +1 -0
  47. package/dist/read-file-tool.test.js +96 -0
  48. package/dist/read-media-file-tool.cjs +4 -4
  49. package/dist/read-media-file-tool.d.ts +1 -1
  50. package/dist/read-media-file-tool.d.ts.map +1 -1
  51. package/dist/read-media-file-tool.js +1 -1
  52. package/dist/tool-factory.cjs +2 -2
  53. package/dist/tool-factory.js +1 -1
  54. package/dist/types.d.ts +0 -2
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/workspace-paths.cjs +87 -0
  57. package/dist/workspace-paths.d.ts +4 -0
  58. package/dist/workspace-paths.d.ts.map +1 -0
  59. package/dist/workspace-paths.js +51 -0
  60. package/dist/write-file-tool.cjs +74 -34
  61. package/dist/write-file-tool.d.ts +1 -2
  62. package/dist/write-file-tool.d.ts.map +1 -1
  63. package/dist/write-file-tool.js +68 -29
  64. package/dist/write-file-tool.test.cjs +262 -11
  65. package/dist/write-file-tool.test.js +262 -11
  66. 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 import_core = require("@dexto/core");
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 import_core.ToolError.configInvalid(
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: import_core.ApprovalType.DIRECTORY_ACCESS,
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 !== import_core.ApprovalStatus.APPROVED) {
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, ToolExecutionContext } from '@dexto/core';
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,aAAa,CAAC;AAClG,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"}
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 createInMemorySessionApprovalStore() {
53
- const states = /* @__PURE__ */ new Map();
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
- createInMemorySessionApprovalStore()
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.execute(
337
- writeTool.inputSchema.parse({
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
- ).rejects.toBeInstanceOf(import_core.DextoRuntimeError);
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
- ...baseContext,
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
- ...baseContext,
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(mockLogger, approvalManager, "session-a");
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 createInMemorySessionApprovalStore() {
34
- const states = /* @__PURE__ */ new Map();
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
- createInMemorySessionApprovalStore()
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.execute(
318
- writeTool.inputSchema.parse({
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
- ).rejects.toBeInstanceOf(DextoRuntimeError);
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
- ...baseContext,
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
- ...baseContext,
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(mockLogger, approvalManager, "session-a");
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, ToolError } from "@dexto/core";
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);
@@ -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 import_core = require("@dexto/core");
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, import_core.defineTool)({
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, import_core.createLocalToolCallHeader)({
70
- title: "Update",
71
- argsText: (0, import_core.truncateForHeader)(input.file_path, 140)
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 import_core.DextoRuntimeError && error.code === import_error_codes.FileSystemErrorCode.INVALID_PATH) {
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 import_core.ToolError.validationFailed(
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 import_core.ToolError.validationFailed(
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 import_core.DextoRuntimeError && error.code === import_core.ToolErrorCode.VALIDATION_FAILED) {
131
+ if (error instanceof import_errors.DextoRuntimeError && error.code === import_tools.ToolErrorCode.VALIDATION_FAILED) {
130
132
  throw error;
131
133
  }
132
- if (error instanceof import_core.DextoRuntimeError) {
133
- throw import_core.ToolError.validationFailed("edit_file", error.message, {
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 { path: resolvedPath } = (0, import_directory_approval.resolveFilePath)(
146
- resolvedFileSystemService.getWorkingDirectory(),
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
- const currentFile = await resolvedFileSystemService.readFile(resolvedPath);
158
- currentContent = currentFile.content;
159
- } catch (error) {
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 import_core.ToolError.fileModifiedSincePreview("edit_file", resolvedPath);
166
+ throw import_tools.ToolError.fileModifiedSincePreview("edit_file", file_path);
168
167
  }
169
168
  }
170
169
  }
171
- const result = await resolvedFileSystemService.editFile(resolvedPath, {
172
- oldString: old_string,
173
- newString: new_string,
174
- replaceAll: replace_all
175
- });
176
- const _display = generateDiffPreview(
177
- resolvedPath,
178
- result.originalContent,
179
- result.newContent
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: result.success,
183
- path: result.path,
184
- changes_count: result.changesCount,
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
@@ -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,EAAE,IAAI,EAAwB,MAAM,aAAa,CAAC;AAE9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAkBpE,QAAA,MAAM,mBAAmB;;;;;kBAaZ,CAAC;AA0Bd;;GAEG;AACH,wBAAgB,kBAAkB,CAC9B,oBAAoB,EAAE,uBAAuB,GAC9C,IAAI,CAAC,OAAO,mBAAmB,CAAC,CA2LlC"}
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"}