@dexto/tools-filesystem 1.5.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 (62) hide show
  1. package/LICENSE +44 -0
  2. package/dist/directory-approval.integration.test.cjs +467 -0
  3. package/dist/directory-approval.integration.test.d.cts +2 -0
  4. package/dist/directory-approval.integration.test.d.ts +2 -0
  5. package/dist/directory-approval.integration.test.js +444 -0
  6. package/dist/edit-file-tool.cjs +181 -0
  7. package/dist/edit-file-tool.d.cts +17 -0
  8. package/dist/edit-file-tool.d.ts +17 -0
  9. package/dist/edit-file-tool.js +147 -0
  10. package/dist/error-codes.cjs +53 -0
  11. package/dist/error-codes.d.cts +32 -0
  12. package/dist/error-codes.d.ts +32 -0
  13. package/dist/error-codes.js +29 -0
  14. package/dist/errors.cjs +302 -0
  15. package/dist/errors.d.cts +112 -0
  16. package/dist/errors.d.ts +112 -0
  17. package/dist/errors.js +278 -0
  18. package/dist/file-tool-types.cjs +16 -0
  19. package/dist/file-tool-types.d.cts +46 -0
  20. package/dist/file-tool-types.d.ts +46 -0
  21. package/dist/file-tool-types.js +0 -0
  22. package/dist/filesystem-service.cjs +526 -0
  23. package/dist/filesystem-service.d.cts +107 -0
  24. package/dist/filesystem-service.d.ts +107 -0
  25. package/dist/filesystem-service.js +492 -0
  26. package/dist/glob-files-tool.cjs +70 -0
  27. package/dist/glob-files-tool.d.cts +16 -0
  28. package/dist/glob-files-tool.d.ts +16 -0
  29. package/dist/glob-files-tool.js +46 -0
  30. package/dist/grep-content-tool.cjs +86 -0
  31. package/dist/grep-content-tool.d.cts +16 -0
  32. package/dist/grep-content-tool.d.ts +16 -0
  33. package/dist/grep-content-tool.js +62 -0
  34. package/dist/index.cjs +55 -0
  35. package/dist/index.d.cts +14 -0
  36. package/dist/index.d.ts +14 -0
  37. package/dist/index.js +22 -0
  38. package/dist/path-validator.cjs +232 -0
  39. package/dist/path-validator.d.cts +90 -0
  40. package/dist/path-validator.d.ts +90 -0
  41. package/dist/path-validator.js +198 -0
  42. package/dist/path-validator.test.cjs +444 -0
  43. package/dist/path-validator.test.d.cts +2 -0
  44. package/dist/path-validator.test.d.ts +2 -0
  45. package/dist/path-validator.test.js +443 -0
  46. package/dist/read-file-tool.cjs +117 -0
  47. package/dist/read-file-tool.d.cts +17 -0
  48. package/dist/read-file-tool.d.ts +17 -0
  49. package/dist/read-file-tool.js +83 -0
  50. package/dist/tool-provider.cjs +108 -0
  51. package/dist/tool-provider.d.cts +74 -0
  52. package/dist/tool-provider.d.ts +74 -0
  53. package/dist/tool-provider.js +84 -0
  54. package/dist/types.cjs +16 -0
  55. package/dist/types.d.cts +172 -0
  56. package/dist/types.d.ts +172 -0
  57. package/dist/types.js +0 -0
  58. package/dist/write-file-tool.cjs +177 -0
  59. package/dist/write-file-tool.d.cts +17 -0
  60. package/dist/write-file-tool.d.ts +17 -0
  61. package/dist/write-file-tool.js +143 -0
  62. package/package.json +42 -0
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ const GlobFilesInputSchema = z.object({
3
+ pattern: z.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js")'),
4
+ path: z.string().optional().describe("Base directory to search from (defaults to working directory)"),
5
+ max_results: z.number().int().positive().optional().default(1e3).describe("Maximum number of results to return (default: 1000)")
6
+ }).strict();
7
+ function createGlobFilesTool(fileSystemService) {
8
+ return {
9
+ id: "glob_files",
10
+ description: "Find files matching a glob pattern. Supports standard glob syntax like **/*.js for recursive matches, *.ts for files in current directory, and src/**/*.tsx for nested paths. Returns array of file paths with metadata (size, modified date). Results are limited to allowed paths only.",
11
+ inputSchema: GlobFilesInputSchema,
12
+ execute: async (input, _context) => {
13
+ const { pattern, path, max_results } = input;
14
+ const result = await fileSystemService.globFiles(pattern, {
15
+ cwd: path,
16
+ maxResults: max_results,
17
+ includeMetadata: true
18
+ });
19
+ const _display = {
20
+ type: "search",
21
+ pattern,
22
+ matches: result.files.map((file) => ({
23
+ file: file.path,
24
+ line: 0,
25
+ // No line number for glob
26
+ content: file.path
27
+ })),
28
+ totalMatches: result.totalFound,
29
+ truncated: result.truncated
30
+ };
31
+ return {
32
+ files: result.files.map((file) => ({
33
+ path: file.path,
34
+ size: file.size,
35
+ modified: file.modified.toISOString()
36
+ })),
37
+ total_found: result.totalFound,
38
+ truncated: result.truncated,
39
+ _display
40
+ };
41
+ }
42
+ };
43
+ }
44
+ export {
45
+ createGlobFilesTool
46
+ };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var grep_content_tool_exports = {};
20
+ __export(grep_content_tool_exports, {
21
+ createGrepContentTool: () => createGrepContentTool
22
+ });
23
+ module.exports = __toCommonJS(grep_content_tool_exports);
24
+ var import_zod = require("zod");
25
+ const GrepContentInputSchema = import_zod.z.object({
26
+ pattern: import_zod.z.string().describe("Regular expression pattern to search for"),
27
+ path: import_zod.z.string().optional().describe("Directory to search in (defaults to working directory)"),
28
+ glob: import_zod.z.string().optional().describe('Glob pattern to filter files (e.g., "*.ts", "**/*.js")'),
29
+ context_lines: import_zod.z.number().int().min(0).optional().default(0).describe(
30
+ "Number of context lines to include before and after each match (default: 0)"
31
+ ),
32
+ case_insensitive: import_zod.z.boolean().optional().default(false).describe("Perform case-insensitive search (default: false)"),
33
+ max_results: import_zod.z.number().int().positive().optional().default(100).describe("Maximum number of results to return (default: 100)")
34
+ }).strict();
35
+ function createGrepContentTool(fileSystemService) {
36
+ return {
37
+ id: "grep_content",
38
+ description: 'Search for text patterns in files using regular expressions. Returns matching lines with file path, line number, and optional context lines. Use glob parameter to filter specific file types (e.g., "*.ts"). Supports case-insensitive search. Great for finding code patterns, function definitions, or specific text across multiple files.',
39
+ inputSchema: GrepContentInputSchema,
40
+ execute: async (input, _context) => {
41
+ const { pattern, path, glob, context_lines, case_insensitive, max_results } = input;
42
+ const result = await fileSystemService.searchContent(pattern, {
43
+ path,
44
+ glob,
45
+ contextLines: context_lines,
46
+ caseInsensitive: case_insensitive,
47
+ maxResults: max_results
48
+ });
49
+ const _display = {
50
+ type: "search",
51
+ pattern,
52
+ matches: result.matches.map((match) => ({
53
+ file: match.file,
54
+ line: match.lineNumber,
55
+ content: match.line,
56
+ ...match.context && {
57
+ context: [...match.context.before, ...match.context.after]
58
+ }
59
+ })),
60
+ totalMatches: result.totalMatches,
61
+ truncated: result.truncated
62
+ };
63
+ return {
64
+ matches: result.matches.map((match) => ({
65
+ file: match.file,
66
+ line_number: match.lineNumber,
67
+ line: match.line,
68
+ ...match.context && {
69
+ context: {
70
+ before: match.context.before,
71
+ after: match.context.after
72
+ }
73
+ }
74
+ })),
75
+ total_matches: result.totalMatches,
76
+ files_searched: result.filesSearched,
77
+ truncated: result.truncated,
78
+ _display
79
+ };
80
+ }
81
+ };
82
+ }
83
+ // Annotate the CommonJS export names for ESM import in node:
84
+ 0 && (module.exports = {
85
+ createGrepContentTool
86
+ });
@@ -0,0 +1,16 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { FileSystemService } from './filesystem-service.cjs';
3
+ import './types.cjs';
4
+
5
+ /**
6
+ * Grep Content Tool
7
+ *
8
+ * Internal tool for searching file contents using regex patterns
9
+ */
10
+
11
+ /**
12
+ * Create the grep_content internal tool
13
+ */
14
+ declare function createGrepContentTool(fileSystemService: FileSystemService): InternalTool;
15
+
16
+ export { createGrepContentTool };
@@ -0,0 +1,16 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { FileSystemService } from './filesystem-service.js';
3
+ import './types.js';
4
+
5
+ /**
6
+ * Grep Content Tool
7
+ *
8
+ * Internal tool for searching file contents using regex patterns
9
+ */
10
+
11
+ /**
12
+ * Create the grep_content internal tool
13
+ */
14
+ declare function createGrepContentTool(fileSystemService: FileSystemService): InternalTool;
15
+
16
+ export { createGrepContentTool };
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ const GrepContentInputSchema = z.object({
3
+ pattern: z.string().describe("Regular expression pattern to search for"),
4
+ path: z.string().optional().describe("Directory to search in (defaults to working directory)"),
5
+ glob: z.string().optional().describe('Glob pattern to filter files (e.g., "*.ts", "**/*.js")'),
6
+ context_lines: z.number().int().min(0).optional().default(0).describe(
7
+ "Number of context lines to include before and after each match (default: 0)"
8
+ ),
9
+ case_insensitive: z.boolean().optional().default(false).describe("Perform case-insensitive search (default: false)"),
10
+ max_results: z.number().int().positive().optional().default(100).describe("Maximum number of results to return (default: 100)")
11
+ }).strict();
12
+ function createGrepContentTool(fileSystemService) {
13
+ return {
14
+ id: "grep_content",
15
+ description: 'Search for text patterns in files using regular expressions. Returns matching lines with file path, line number, and optional context lines. Use glob parameter to filter specific file types (e.g., "*.ts"). Supports case-insensitive search. Great for finding code patterns, function definitions, or specific text across multiple files.',
16
+ inputSchema: GrepContentInputSchema,
17
+ execute: async (input, _context) => {
18
+ const { pattern, path, glob, context_lines, case_insensitive, max_results } = input;
19
+ const result = await fileSystemService.searchContent(pattern, {
20
+ path,
21
+ glob,
22
+ contextLines: context_lines,
23
+ caseInsensitive: case_insensitive,
24
+ maxResults: max_results
25
+ });
26
+ const _display = {
27
+ type: "search",
28
+ pattern,
29
+ matches: result.matches.map((match) => ({
30
+ file: match.file,
31
+ line: match.lineNumber,
32
+ content: match.line,
33
+ ...match.context && {
34
+ context: [...match.context.before, ...match.context.after]
35
+ }
36
+ })),
37
+ totalMatches: result.totalMatches,
38
+ truncated: result.truncated
39
+ };
40
+ return {
41
+ matches: result.matches.map((match) => ({
42
+ file: match.file,
43
+ line_number: match.lineNumber,
44
+ line: match.line,
45
+ ...match.context && {
46
+ context: {
47
+ before: match.context.before,
48
+ after: match.context.after
49
+ }
50
+ }
51
+ })),
52
+ total_matches: result.totalMatches,
53
+ files_searched: result.filesSearched,
54
+ truncated: result.truncated,
55
+ _display
56
+ };
57
+ }
58
+ };
59
+ }
60
+ export {
61
+ createGrepContentTool
62
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var index_exports = {};
20
+ __export(index_exports, {
21
+ FileSystemError: () => import_errors.FileSystemError,
22
+ FileSystemErrorCode: () => import_error_codes.FileSystemErrorCode,
23
+ FileSystemService: () => import_filesystem_service.FileSystemService,
24
+ PathValidator: () => import_path_validator.PathValidator,
25
+ createEditFileTool: () => import_edit_file_tool.createEditFileTool,
26
+ createGlobFilesTool: () => import_glob_files_tool.createGlobFilesTool,
27
+ createGrepContentTool: () => import_grep_content_tool.createGrepContentTool,
28
+ createReadFileTool: () => import_read_file_tool.createReadFileTool,
29
+ createWriteFileTool: () => import_write_file_tool.createWriteFileTool,
30
+ fileSystemToolsProvider: () => import_tool_provider.fileSystemToolsProvider
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+ var import_tool_provider = require("./tool-provider.js");
34
+ var import_filesystem_service = require("./filesystem-service.js");
35
+ var import_path_validator = require("./path-validator.js");
36
+ var import_errors = require("./errors.js");
37
+ var import_error_codes = require("./error-codes.js");
38
+ var import_read_file_tool = require("./read-file-tool.js");
39
+ var import_write_file_tool = require("./write-file-tool.js");
40
+ var import_edit_file_tool = require("./edit-file-tool.js");
41
+ var import_glob_files_tool = require("./glob-files-tool.js");
42
+ var import_grep_content_tool = require("./grep-content-tool.js");
43
+ // Annotate the CommonJS export names for ESM import in node:
44
+ 0 && (module.exports = {
45
+ FileSystemError,
46
+ FileSystemErrorCode,
47
+ FileSystemService,
48
+ PathValidator,
49
+ createEditFileTool,
50
+ createGlobFilesTool,
51
+ createGrepContentTool,
52
+ createReadFileTool,
53
+ createWriteFileTool,
54
+ fileSystemToolsProvider
55
+ });
@@ -0,0 +1,14 @@
1
+ export { fileSystemToolsProvider } from './tool-provider.cjs';
2
+ export { DirectoryApprovalCallbacks, FileToolOptions } from './file-tool-types.cjs';
3
+ export { FileSystemService } from './filesystem-service.cjs';
4
+ export { PathValidator } from './path-validator.cjs';
5
+ export { FileSystemError } from './errors.cjs';
6
+ export { FileSystemErrorCode } from './error-codes.cjs';
7
+ export { BufferEncoding, EditFileOptions, EditOperation, EditResult, FileContent, FileMetadata, FileSystemConfig, GlobOptions, GlobResult, GrepOptions, PathValidation, ReadFileOptions, SearchMatch, SearchResult, WriteFileOptions, WriteResult } from './types.cjs';
8
+ export { createReadFileTool } from './read-file-tool.cjs';
9
+ export { createWriteFileTool } from './write-file-tool.cjs';
10
+ export { createEditFileTool } from './edit-file-tool.cjs';
11
+ export { createGlobFilesTool } from './glob-files-tool.cjs';
12
+ export { createGrepContentTool } from './grep-content-tool.cjs';
13
+ import 'zod';
14
+ import '@dexto/core';
@@ -0,0 +1,14 @@
1
+ export { fileSystemToolsProvider } from './tool-provider.js';
2
+ export { DirectoryApprovalCallbacks, FileToolOptions } from './file-tool-types.js';
3
+ export { FileSystemService } from './filesystem-service.js';
4
+ export { PathValidator } from './path-validator.js';
5
+ export { FileSystemError } from './errors.js';
6
+ export { FileSystemErrorCode } from './error-codes.js';
7
+ export { BufferEncoding, EditFileOptions, EditOperation, EditResult, FileContent, FileMetadata, FileSystemConfig, GlobOptions, GlobResult, GrepOptions, PathValidation, ReadFileOptions, SearchMatch, SearchResult, WriteFileOptions, WriteResult } from './types.js';
8
+ export { createReadFileTool } from './read-file-tool.js';
9
+ export { createWriteFileTool } from './write-file-tool.js';
10
+ export { createEditFileTool } from './edit-file-tool.js';
11
+ export { createGlobFilesTool } from './glob-files-tool.js';
12
+ export { createGrepContentTool } from './grep-content-tool.js';
13
+ import 'zod';
14
+ import '@dexto/core';
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import { fileSystemToolsProvider } from "./tool-provider.js";
2
+ import { FileSystemService } from "./filesystem-service.js";
3
+ import { PathValidator } from "./path-validator.js";
4
+ import { FileSystemError } from "./errors.js";
5
+ import { FileSystemErrorCode } from "./error-codes.js";
6
+ import { createReadFileTool } from "./read-file-tool.js";
7
+ import { createWriteFileTool } from "./write-file-tool.js";
8
+ import { createEditFileTool } from "./edit-file-tool.js";
9
+ import { createGlobFilesTool } from "./glob-files-tool.js";
10
+ import { createGrepContentTool } from "./grep-content-tool.js";
11
+ export {
12
+ FileSystemError,
13
+ FileSystemErrorCode,
14
+ FileSystemService,
15
+ PathValidator,
16
+ createEditFileTool,
17
+ createGlobFilesTool,
18
+ createGrepContentTool,
19
+ createReadFileTool,
20
+ createWriteFileTool,
21
+ fileSystemToolsProvider
22
+ };
@@ -0,0 +1,232 @@
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 __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var path_validator_exports = {};
30
+ __export(path_validator_exports, {
31
+ PathValidator: () => PathValidator
32
+ });
33
+ module.exports = __toCommonJS(path_validator_exports);
34
+ var path = __toESM(require("node:path"), 1);
35
+ var import_node_fs = require("node:fs");
36
+ class PathValidator {
37
+ config;
38
+ normalizedAllowedPaths;
39
+ normalizedBlockedPaths;
40
+ normalizedBlockedExtensions;
41
+ logger;
42
+ directoryApprovalChecker;
43
+ constructor(config, logger) {
44
+ this.config = config;
45
+ this.logger = logger;
46
+ const workingDir = config.workingDirectory || process.cwd();
47
+ this.normalizedAllowedPaths = config.allowedPaths.map((p) => path.resolve(workingDir, p));
48
+ this.normalizedBlockedPaths = config.blockedPaths.map((p) => path.normalize(p));
49
+ this.normalizedBlockedExtensions = (config.blockedExtensions || []).map((ext) => {
50
+ const e = ext.startsWith(".") ? ext : `.${ext}`;
51
+ return e.toLowerCase();
52
+ });
53
+ this.logger.debug(
54
+ `PathValidator initialized with ${this.normalizedAllowedPaths.length} allowed paths`
55
+ );
56
+ }
57
+ /**
58
+ * Set a callback to check if a path is in an approved directory.
59
+ * This allows PathValidator to consult ApprovalManager without a direct dependency.
60
+ *
61
+ * @param checker Function that returns true if path is in an approved directory
62
+ */
63
+ setDirectoryApprovalChecker(checker) {
64
+ this.directoryApprovalChecker = checker;
65
+ this.logger.debug("Directory approval checker configured");
66
+ }
67
+ /**
68
+ * Validate a file path for security and policy compliance
69
+ */
70
+ validatePath(filePath) {
71
+ if (!filePath || filePath.trim() === "") {
72
+ return {
73
+ isValid: false,
74
+ error: "Path cannot be empty"
75
+ };
76
+ }
77
+ const workingDir = this.config.workingDirectory || process.cwd();
78
+ let normalizedPath;
79
+ try {
80
+ normalizedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
81
+ try {
82
+ normalizedPath = import_node_fs.realpathSync.native(normalizedPath);
83
+ } catch {
84
+ }
85
+ } catch (error) {
86
+ return {
87
+ isValid: false,
88
+ error: `Failed to normalize path: ${error instanceof Error ? error.message : String(error)}`
89
+ };
90
+ }
91
+ if (this.hasPathTraversal(filePath, normalizedPath)) {
92
+ return {
93
+ isValid: false,
94
+ error: "Path traversal detected"
95
+ };
96
+ }
97
+ if (!this.isPathAllowed(normalizedPath)) {
98
+ return {
99
+ isValid: false,
100
+ error: `Path is not within allowed paths. Allowed: ${this.normalizedAllowedPaths.join(", ")}`
101
+ };
102
+ }
103
+ const blockedReason = this.isPathBlocked(normalizedPath);
104
+ if (blockedReason) {
105
+ return {
106
+ isValid: false,
107
+ error: `Path is blocked: ${blockedReason}`
108
+ };
109
+ }
110
+ const ext = path.extname(normalizedPath).toLowerCase();
111
+ if (ext && this.normalizedBlockedExtensions.includes(ext)) {
112
+ return {
113
+ isValid: false,
114
+ error: `File extension ${ext} is not allowed`
115
+ };
116
+ }
117
+ return {
118
+ isValid: true,
119
+ normalizedPath
120
+ };
121
+ }
122
+ /**
123
+ * Check if path contains traversal attempts
124
+ */
125
+ hasPathTraversal(originalPath, normalizedPath) {
126
+ if (originalPath.includes("../") || originalPath.includes("..\\")) {
127
+ const workingDir = this.config.workingDirectory || process.cwd();
128
+ const relative = path.relative(workingDir, normalizedPath);
129
+ if (relative.startsWith("..")) {
130
+ return true;
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+ /**
136
+ * Check if path is within allowed paths (whitelist check)
137
+ * Also consults the directory approval checker if configured.
138
+ */
139
+ isPathAllowed(normalizedPath) {
140
+ if (this.normalizedAllowedPaths.length === 0) {
141
+ return true;
142
+ }
143
+ const isInConfigPaths = this.normalizedAllowedPaths.some((allowedPath) => {
144
+ const relative = path.relative(allowedPath, normalizedPath);
145
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
146
+ });
147
+ if (isInConfigPaths) {
148
+ return true;
149
+ }
150
+ if (this.directoryApprovalChecker) {
151
+ return this.directoryApprovalChecker(normalizedPath);
152
+ }
153
+ return false;
154
+ }
155
+ /**
156
+ * Check if path matches blocked patterns (blacklist check)
157
+ */
158
+ isPathBlocked(normalizedPath) {
159
+ const roots = this.normalizedAllowedPaths.length > 0 ? this.normalizedAllowedPaths : [this.config.workingDirectory || process.cwd()];
160
+ for (const blocked of this.normalizedBlockedPaths) {
161
+ for (const root of roots) {
162
+ const blockedFull = path.isAbsolute(blocked) ? path.normalize(blocked) : path.resolve(root, blocked);
163
+ if (normalizedPath === blockedFull || normalizedPath.startsWith(blockedFull + path.sep)) {
164
+ return `Within blocked directory: ${blocked}`;
165
+ }
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ /**
171
+ * Quick check if a path is allowed (for internal use)
172
+ */
173
+ isPathAllowedQuick(normalizedPath) {
174
+ return this.isPathAllowed(normalizedPath) && !this.isPathBlocked(normalizedPath);
175
+ }
176
+ /**
177
+ * Check if a file path is within the configured allowed paths (from config only).
178
+ * This method does NOT consult ApprovalManager - it only checks the static config paths.
179
+ *
180
+ * This is used by file tools to determine if a path needs directory approval.
181
+ * Paths within config-allowed directories don't need directory approval prompts.
182
+ *
183
+ * @param filePath The file path to check (can be relative or absolute)
184
+ * @returns true if the path is within config-allowed paths, false otherwise
185
+ */
186
+ isPathWithinAllowed(filePath) {
187
+ if (!filePath || filePath.trim() === "") {
188
+ return false;
189
+ }
190
+ const workingDir = this.config.workingDirectory || process.cwd();
191
+ let normalizedPath;
192
+ try {
193
+ normalizedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
194
+ try {
195
+ normalizedPath = import_node_fs.realpathSync.native(normalizedPath);
196
+ } catch {
197
+ }
198
+ } catch {
199
+ return false;
200
+ }
201
+ return this.isInConfigAllowedPaths(normalizedPath);
202
+ }
203
+ /**
204
+ * Check if path is within config-allowed paths only (no approval checker).
205
+ * Used for prompting decisions.
206
+ */
207
+ isInConfigAllowedPaths(normalizedPath) {
208
+ if (this.normalizedAllowedPaths.length === 0) {
209
+ return true;
210
+ }
211
+ return this.normalizedAllowedPaths.some((allowedPath) => {
212
+ const relative = path.relative(allowedPath, normalizedPath);
213
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
214
+ });
215
+ }
216
+ /**
217
+ * Get normalized allowed paths
218
+ */
219
+ getAllowedPaths() {
220
+ return [...this.normalizedAllowedPaths];
221
+ }
222
+ /**
223
+ * Get blocked paths
224
+ */
225
+ getBlockedPaths() {
226
+ return [...this.normalizedBlockedPaths];
227
+ }
228
+ }
229
+ // Annotate the CommonJS export names for ESM import in node:
230
+ 0 && (module.exports = {
231
+ PathValidator
232
+ });
@@ -0,0 +1,90 @@
1
+ import { FileSystemConfig, PathValidation } from './types.cjs';
2
+ import { IDextoLogger } from '@dexto/core';
3
+
4
+ /**
5
+ * Path Validator
6
+ *
7
+ * Security-focused path validation for file system operations
8
+ */
9
+
10
+ /**
11
+ * Callback type for checking if a path is in an approved directory.
12
+ * Used to consult ApprovalManager without creating a direct dependency.
13
+ */
14
+ type DirectoryApprovalChecker = (filePath: string) => boolean;
15
+ /**
16
+ * PathValidator - Validates file paths for security and policy compliance
17
+ *
18
+ * Security checks:
19
+ * 1. Path traversal detection (../, symbolic links)
20
+ * 2. Allowed paths enforcement (whitelist + approved directories)
21
+ * 3. Blocked paths detection (blacklist)
22
+ * 4. File extension restrictions
23
+ * 5. Absolute path normalization
24
+ *
25
+ * PathValidator can optionally consult an external approval checker (e.g., ApprovalManager)
26
+ * to determine if paths outside the config's allowed paths are accessible.
27
+ */
28
+ declare class PathValidator {
29
+ private config;
30
+ private normalizedAllowedPaths;
31
+ private normalizedBlockedPaths;
32
+ private normalizedBlockedExtensions;
33
+ private logger;
34
+ private directoryApprovalChecker;
35
+ constructor(config: FileSystemConfig, logger: IDextoLogger);
36
+ /**
37
+ * Set a callback to check if a path is in an approved directory.
38
+ * This allows PathValidator to consult ApprovalManager without a direct dependency.
39
+ *
40
+ * @param checker Function that returns true if path is in an approved directory
41
+ */
42
+ setDirectoryApprovalChecker(checker: DirectoryApprovalChecker): void;
43
+ /**
44
+ * Validate a file path for security and policy compliance
45
+ */
46
+ validatePath(filePath: string): PathValidation;
47
+ /**
48
+ * Check if path contains traversal attempts
49
+ */
50
+ private hasPathTraversal;
51
+ /**
52
+ * Check if path is within allowed paths (whitelist check)
53
+ * Also consults the directory approval checker if configured.
54
+ */
55
+ private isPathAllowed;
56
+ /**
57
+ * Check if path matches blocked patterns (blacklist check)
58
+ */
59
+ private isPathBlocked;
60
+ /**
61
+ * Quick check if a path is allowed (for internal use)
62
+ */
63
+ isPathAllowedQuick(normalizedPath: string): boolean;
64
+ /**
65
+ * Check if a file path is within the configured allowed paths (from config only).
66
+ * This method does NOT consult ApprovalManager - it only checks the static config paths.
67
+ *
68
+ * This is used by file tools to determine if a path needs directory approval.
69
+ * Paths within config-allowed directories don't need directory approval prompts.
70
+ *
71
+ * @param filePath The file path to check (can be relative or absolute)
72
+ * @returns true if the path is within config-allowed paths, false otherwise
73
+ */
74
+ isPathWithinAllowed(filePath: string): boolean;
75
+ /**
76
+ * Check if path is within config-allowed paths only (no approval checker).
77
+ * Used for prompting decisions.
78
+ */
79
+ private isInConfigAllowedPaths;
80
+ /**
81
+ * Get normalized allowed paths
82
+ */
83
+ getAllowedPaths(): string[];
84
+ /**
85
+ * Get blocked paths
86
+ */
87
+ getBlockedPaths(): string[];
88
+ }
89
+
90
+ export { type DirectoryApprovalChecker, PathValidator };