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