@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,107 @@
1
+ import { IDextoLogger } from '@dexto/core';
2
+ import { FileSystemConfig, ReadFileOptions, FileContent, GlobOptions, GlobResult, GrepOptions, SearchResult, WriteFileOptions, WriteResult, EditOperation, EditFileOptions, EditResult } from './types.js';
3
+
4
+ /**
5
+ * FileSystem Service
6
+ *
7
+ * Secure file system operations for Dexto internal tools
8
+ */
9
+
10
+ /**
11
+ * FileSystemService - Handles all file system operations with security checks
12
+ *
13
+ * This service receives fully-validated configuration from the FileSystem Tools Provider.
14
+ * All defaults have been applied by the provider's schema, so the service trusts the config
15
+ * and uses it as-is without any fallback logic.
16
+ *
17
+ * TODO: Add tests for this class
18
+ * TODO: instantiate only when internal file tools are enabled to avoid file dependencies which won't work in serverless
19
+ */
20
+ declare class FileSystemService {
21
+ private config;
22
+ private pathValidator;
23
+ private initialized;
24
+ private initPromise;
25
+ private logger;
26
+ /**
27
+ * Create a new FileSystemService with validated configuration.
28
+ *
29
+ * @param config - Fully-validated configuration from provider schema.
30
+ * All required fields have values, defaults already applied.
31
+ * @param logger - Logger instance for this service
32
+ */
33
+ constructor(config: FileSystemConfig, logger: IDextoLogger);
34
+ /**
35
+ * Get backup directory path (context-aware with optional override)
36
+ * TODO: Migrate to explicit configuration via CLI enrichment layer (per-agent paths)
37
+ */
38
+ private getBackupDir;
39
+ /**
40
+ * Initialize the service.
41
+ * Safe to call multiple times - subsequent calls return the same promise.
42
+ */
43
+ initialize(): Promise<void>;
44
+ /**
45
+ * Internal initialization logic.
46
+ */
47
+ private doInitialize;
48
+ /**
49
+ * Ensure the service is initialized before use.
50
+ * Tools should call this at the start of their execute methods.
51
+ * Safe to call multiple times - will await the same initialization promise.
52
+ */
53
+ ensureInitialized(): Promise<void>;
54
+ /**
55
+ * Set a callback to check if a path is in an approved directory.
56
+ * This allows PathValidator to consult ApprovalManager without a direct dependency.
57
+ *
58
+ * @param checker Function that returns true if path is in an approved directory
59
+ */
60
+ setDirectoryApprovalChecker(checker: (filePath: string) => boolean): void;
61
+ /**
62
+ * Check if a file path is within the configured allowed paths (config only).
63
+ * This is used by file tools to determine if directory approval is needed.
64
+ *
65
+ * @param filePath The file path to check (can be relative or absolute)
66
+ * @returns true if the path is within config-allowed paths, false otherwise
67
+ */
68
+ isPathWithinConfigAllowed(filePath: string): boolean;
69
+ /**
70
+ * Read a file with validation and size limits
71
+ */
72
+ readFile(filePath: string, options?: ReadFileOptions): Promise<FileContent>;
73
+ /**
74
+ * Find files matching a glob pattern
75
+ */
76
+ globFiles(pattern: string, options?: GlobOptions): Promise<GlobResult>;
77
+ /**
78
+ * Search for content in files (grep-like functionality)
79
+ */
80
+ searchContent(pattern: string, options?: GrepOptions): Promise<SearchResult>;
81
+ /**
82
+ * Write content to a file
83
+ */
84
+ writeFile(filePath: string, content: string, options?: WriteFileOptions): Promise<WriteResult>;
85
+ /**
86
+ * Edit a file by replacing text
87
+ */
88
+ editFile(filePath: string, operation: EditOperation, options?: EditFileOptions): Promise<EditResult>;
89
+ /**
90
+ * Create a backup of a file
91
+ */
92
+ private createBackup;
93
+ /**
94
+ * Clean up old backup files based on retention policy
95
+ */
96
+ cleanupOldBackups(): Promise<number>;
97
+ /**
98
+ * Get service configuration
99
+ */
100
+ getConfig(): Readonly<FileSystemConfig>;
101
+ /**
102
+ * Check if a path is allowed
103
+ */
104
+ isPathAllowed(filePath: string): boolean;
105
+ }
106
+
107
+ export { FileSystemService };
@@ -0,0 +1,492 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { glob } from "glob";
4
+ import safeRegex from "safe-regex";
5
+ import { getDextoPath, DextoLogComponent } from "@dexto/core";
6
+ import { PathValidator } from "./path-validator.js";
7
+ import { FileSystemError } from "./errors.js";
8
+ const DEFAULT_ENCODING = "utf-8";
9
+ const DEFAULT_MAX_RESULTS = 1e3;
10
+ const DEFAULT_MAX_SEARCH_RESULTS = 100;
11
+ class FileSystemService {
12
+ config;
13
+ pathValidator;
14
+ initialized = false;
15
+ initPromise = null;
16
+ logger;
17
+ /**
18
+ * Create a new FileSystemService with validated configuration.
19
+ *
20
+ * @param config - Fully-validated configuration from provider schema.
21
+ * All required fields have values, defaults already applied.
22
+ * @param logger - Logger instance for this service
23
+ */
24
+ constructor(config, logger) {
25
+ this.config = config;
26
+ this.logger = logger.createChild(DextoLogComponent.FILESYSTEM);
27
+ this.pathValidator = new PathValidator(this.config, this.logger);
28
+ }
29
+ /**
30
+ * Get backup directory path (context-aware with optional override)
31
+ * TODO: Migrate to explicit configuration via CLI enrichment layer (per-agent paths)
32
+ */
33
+ getBackupDir() {
34
+ return this.config.backupPath || getDextoPath("backups");
35
+ }
36
+ /**
37
+ * Initialize the service.
38
+ * Safe to call multiple times - subsequent calls return the same promise.
39
+ */
40
+ initialize() {
41
+ if (this.initPromise) {
42
+ return this.initPromise;
43
+ }
44
+ this.initPromise = this.doInitialize();
45
+ return this.initPromise;
46
+ }
47
+ /**
48
+ * Internal initialization logic.
49
+ */
50
+ async doInitialize() {
51
+ if (this.initialized) {
52
+ this.logger.debug("FileSystemService already initialized");
53
+ return;
54
+ }
55
+ if (this.config.enableBackups) {
56
+ try {
57
+ const backupDir = this.getBackupDir();
58
+ await fs.mkdir(backupDir, { recursive: true });
59
+ this.logger.debug(`Backup directory created/verified: ${backupDir}`);
60
+ } catch (error) {
61
+ this.logger.warn(
62
+ `Failed to create backup directory: ${error instanceof Error ? error.message : String(error)}`
63
+ );
64
+ }
65
+ }
66
+ this.initialized = true;
67
+ this.logger.info("FileSystemService initialized successfully");
68
+ }
69
+ /**
70
+ * Ensure the service is initialized before use.
71
+ * Tools should call this at the start of their execute methods.
72
+ * Safe to call multiple times - will await the same initialization promise.
73
+ */
74
+ async ensureInitialized() {
75
+ if (this.initialized) {
76
+ return;
77
+ }
78
+ await this.initialize();
79
+ }
80
+ /**
81
+ * Set a callback to check if a path is in an approved directory.
82
+ * This allows PathValidator to consult ApprovalManager without a direct dependency.
83
+ *
84
+ * @param checker Function that returns true if path is in an approved directory
85
+ */
86
+ setDirectoryApprovalChecker(checker) {
87
+ this.pathValidator.setDirectoryApprovalChecker(checker);
88
+ }
89
+ /**
90
+ * Check if a file path is within the configured allowed paths (config only).
91
+ * This is used by file tools to determine if directory approval is needed.
92
+ *
93
+ * @param filePath The file path to check (can be relative or absolute)
94
+ * @returns true if the path is within config-allowed paths, false otherwise
95
+ */
96
+ isPathWithinConfigAllowed(filePath) {
97
+ return this.pathValidator.isPathWithinAllowed(filePath);
98
+ }
99
+ /**
100
+ * Read a file with validation and size limits
101
+ */
102
+ async readFile(filePath, options = {}) {
103
+ await this.ensureInitialized();
104
+ const validation = this.pathValidator.validatePath(filePath);
105
+ if (!validation.isValid || !validation.normalizedPath) {
106
+ throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
107
+ }
108
+ const normalizedPath = validation.normalizedPath;
109
+ try {
110
+ const stats = await fs.stat(normalizedPath);
111
+ if (!stats.isFile()) {
112
+ throw FileSystemError.invalidPath(normalizedPath, "Path is not a file");
113
+ }
114
+ if (stats.size > this.config.maxFileSize) {
115
+ throw FileSystemError.fileTooLarge(
116
+ normalizedPath,
117
+ stats.size,
118
+ this.config.maxFileSize
119
+ );
120
+ }
121
+ } catch (error) {
122
+ if (error.code === "ENOENT") {
123
+ throw FileSystemError.fileNotFound(normalizedPath);
124
+ }
125
+ if (error.code === "EACCES") {
126
+ throw FileSystemError.permissionDenied(normalizedPath, "read");
127
+ }
128
+ throw FileSystemError.readFailed(
129
+ normalizedPath,
130
+ error instanceof Error ? error.message : String(error)
131
+ );
132
+ }
133
+ try {
134
+ const encoding = options.encoding || DEFAULT_ENCODING;
135
+ const content = await fs.readFile(normalizedPath, encoding);
136
+ const lines = content.split("\n");
137
+ const limit = options.limit;
138
+ const offset1 = options.offset;
139
+ let selectedLines;
140
+ let truncated = false;
141
+ if (offset1 && offset1 > 0 || limit !== void 0) {
142
+ const start = offset1 && offset1 > 0 ? Math.max(0, offset1 - 1) : 0;
143
+ const end = limit !== void 0 ? start + limit : lines.length;
144
+ selectedLines = lines.slice(start, end);
145
+ truncated = end < lines.length;
146
+ } else {
147
+ selectedLines = lines;
148
+ }
149
+ return {
150
+ content: selectedLines.join("\n"),
151
+ lines: selectedLines.length,
152
+ encoding,
153
+ truncated,
154
+ size: Buffer.byteLength(content, encoding)
155
+ };
156
+ } catch (error) {
157
+ throw FileSystemError.readFailed(
158
+ normalizedPath,
159
+ error instanceof Error ? error.message : String(error)
160
+ );
161
+ }
162
+ }
163
+ /**
164
+ * Find files matching a glob pattern
165
+ */
166
+ async globFiles(pattern, options = {}) {
167
+ await this.ensureInitialized();
168
+ const cwd = options.cwd || this.config.workingDirectory || process.cwd();
169
+ const maxResults = options.maxResults || DEFAULT_MAX_RESULTS;
170
+ try {
171
+ const files = await glob(pattern, {
172
+ cwd,
173
+ absolute: true,
174
+ nodir: true,
175
+ // Only files
176
+ follow: false
177
+ // Don't follow symlinks
178
+ });
179
+ const validFiles = [];
180
+ for (const file of files) {
181
+ const validation = this.pathValidator.validatePath(file);
182
+ if (!validation.isValid || !validation.normalizedPath) {
183
+ this.logger.debug(`Skipping invalid path: ${file}`);
184
+ continue;
185
+ }
186
+ if (options.includeMetadata !== false) {
187
+ try {
188
+ const stats = await fs.stat(validation.normalizedPath);
189
+ validFiles.push({
190
+ path: validation.normalizedPath,
191
+ size: stats.size,
192
+ modified: stats.mtime,
193
+ isDirectory: stats.isDirectory()
194
+ });
195
+ } catch (error) {
196
+ this.logger.debug(
197
+ `Failed to stat file ${file}: ${error instanceof Error ? error.message : String(error)}`
198
+ );
199
+ }
200
+ } else {
201
+ validFiles.push({
202
+ path: validation.normalizedPath,
203
+ size: 0,
204
+ modified: /* @__PURE__ */ new Date(),
205
+ isDirectory: false
206
+ });
207
+ }
208
+ if (validFiles.length >= maxResults) {
209
+ break;
210
+ }
211
+ }
212
+ const limited = validFiles.length >= maxResults;
213
+ return {
214
+ files: validFiles,
215
+ truncated: limited,
216
+ totalFound: validFiles.length
217
+ };
218
+ } catch (error) {
219
+ throw FileSystemError.globFailed(
220
+ pattern,
221
+ error instanceof Error ? error.message : String(error)
222
+ );
223
+ }
224
+ }
225
+ /**
226
+ * Search for content in files (grep-like functionality)
227
+ */
228
+ async searchContent(pattern, options = {}) {
229
+ await this.ensureInitialized();
230
+ const searchPath = options.path || this.config.workingDirectory || process.cwd();
231
+ const globPattern = options.glob || "**/*";
232
+ const maxResults = options.maxResults || DEFAULT_MAX_SEARCH_RESULTS;
233
+ const contextLines = options.contextLines || 0;
234
+ try {
235
+ if (!safeRegex(pattern)) {
236
+ throw FileSystemError.invalidPattern(
237
+ pattern,
238
+ "Pattern may cause catastrophic backtracking (ReDoS). Please simplify the regex."
239
+ );
240
+ }
241
+ const flags = options.caseInsensitive ? "i" : "";
242
+ const regex = new RegExp(pattern, flags);
243
+ const globResult = await this.globFiles(globPattern, {
244
+ cwd: searchPath,
245
+ maxResults: 1e4
246
+ // Search more files, but limit results
247
+ });
248
+ const matches = [];
249
+ let filesSearched = 0;
250
+ for (const fileInfo of globResult.files) {
251
+ try {
252
+ const fileContent = await this.readFile(fileInfo.path);
253
+ const lines = fileContent.content.split("\n");
254
+ filesSearched++;
255
+ for (let i = 0; i < lines.length; i++) {
256
+ const line = lines[i];
257
+ if (regex.test(line)) {
258
+ let context;
259
+ if (contextLines > 0) {
260
+ const before = [];
261
+ const after = [];
262
+ for (let j = Math.max(0, i - contextLines); j < i; j++) {
263
+ before.push(lines[j]);
264
+ }
265
+ for (let j = i + 1; j < Math.min(lines.length, i + contextLines + 1); j++) {
266
+ after.push(lines[j]);
267
+ }
268
+ context = { before, after };
269
+ }
270
+ matches.push({
271
+ file: fileInfo.path,
272
+ lineNumber: i + 1,
273
+ // 1-based line numbers
274
+ line,
275
+ ...context !== void 0 && { context }
276
+ });
277
+ if (matches.length >= maxResults) {
278
+ return {
279
+ matches,
280
+ totalMatches: matches.length,
281
+ truncated: true,
282
+ filesSearched
283
+ };
284
+ }
285
+ }
286
+ }
287
+ } catch (error) {
288
+ this.logger.debug(
289
+ `Skipping file ${fileInfo.path}: ${error instanceof Error ? error.message : String(error)}`
290
+ );
291
+ }
292
+ }
293
+ return {
294
+ matches,
295
+ totalMatches: matches.length,
296
+ truncated: false,
297
+ filesSearched
298
+ };
299
+ } catch (error) {
300
+ if (error instanceof Error && error.message.includes("Invalid regular expression")) {
301
+ throw FileSystemError.invalidPattern(pattern, "Invalid regular expression syntax");
302
+ }
303
+ throw FileSystemError.searchFailed(
304
+ pattern,
305
+ error instanceof Error ? error.message : String(error)
306
+ );
307
+ }
308
+ }
309
+ /**
310
+ * Write content to a file
311
+ */
312
+ async writeFile(filePath, content, options = {}) {
313
+ await this.ensureInitialized();
314
+ const validation = this.pathValidator.validatePath(filePath);
315
+ if (!validation.isValid || !validation.normalizedPath) {
316
+ throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
317
+ }
318
+ const normalizedPath = validation.normalizedPath;
319
+ const encoding = options.encoding || DEFAULT_ENCODING;
320
+ let backupPath;
321
+ let fileExists = false;
322
+ try {
323
+ await fs.access(normalizedPath);
324
+ fileExists = true;
325
+ } catch {
326
+ }
327
+ if (fileExists && (options.backup ?? this.config.enableBackups)) {
328
+ backupPath = await this.createBackup(normalizedPath);
329
+ }
330
+ try {
331
+ if (options.createDirs) {
332
+ const dir = path.dirname(normalizedPath);
333
+ await fs.mkdir(dir, { recursive: true });
334
+ }
335
+ await fs.writeFile(normalizedPath, content, encoding);
336
+ const bytesWritten = Buffer.byteLength(content, encoding);
337
+ this.logger.debug(`File written: ${normalizedPath} (${bytesWritten} bytes)`);
338
+ return {
339
+ success: true,
340
+ path: normalizedPath,
341
+ bytesWritten,
342
+ backupPath
343
+ };
344
+ } catch (error) {
345
+ throw FileSystemError.writeFailed(
346
+ normalizedPath,
347
+ error instanceof Error ? error.message : String(error)
348
+ );
349
+ }
350
+ }
351
+ /**
352
+ * Edit a file by replacing text
353
+ */
354
+ async editFile(filePath, operation, options = {}) {
355
+ await this.ensureInitialized();
356
+ const validation = this.pathValidator.validatePath(filePath);
357
+ if (!validation.isValid || !validation.normalizedPath) {
358
+ throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
359
+ }
360
+ const normalizedPath = validation.normalizedPath;
361
+ const fileContent = await this.readFile(normalizedPath);
362
+ let content = fileContent.content;
363
+ const occurrences = (content.match(
364
+ new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
365
+ ) || []).length;
366
+ if (occurrences === 0) {
367
+ throw FileSystemError.stringNotFound(normalizedPath, operation.oldString);
368
+ }
369
+ if (!operation.replaceAll && occurrences > 1) {
370
+ throw FileSystemError.stringNotUnique(normalizedPath, operation.oldString, occurrences);
371
+ }
372
+ let backupPath;
373
+ if (options.backup ?? this.config.enableBackups) {
374
+ backupPath = await this.createBackup(normalizedPath);
375
+ }
376
+ try {
377
+ if (operation.replaceAll) {
378
+ content = content.replace(
379
+ new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
380
+ operation.newString
381
+ );
382
+ } else {
383
+ content = content.replace(operation.oldString, operation.newString);
384
+ }
385
+ await fs.writeFile(normalizedPath, content, options.encoding || DEFAULT_ENCODING);
386
+ this.logger.debug(`File edited: ${normalizedPath} (${occurrences} replacements)`);
387
+ return {
388
+ success: true,
389
+ path: normalizedPath,
390
+ changesCount: occurrences,
391
+ backupPath
392
+ };
393
+ } catch (error) {
394
+ throw FileSystemError.editFailed(
395
+ normalizedPath,
396
+ error instanceof Error ? error.message : String(error)
397
+ );
398
+ }
399
+ }
400
+ /**
401
+ * Create a backup of a file
402
+ */
403
+ async createBackup(filePath) {
404
+ const backupDir = this.getBackupDir();
405
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
406
+ const basename = path.basename(filePath);
407
+ const backupFilename = `${basename}.${timestamp}.backup`;
408
+ const backupPath = path.join(backupDir, backupFilename);
409
+ try {
410
+ await fs.mkdir(backupDir, { recursive: true });
411
+ await fs.copyFile(filePath, backupPath);
412
+ this.logger.debug(`Backup created: ${backupPath}`);
413
+ await this.cleanupOldBackups();
414
+ return backupPath;
415
+ } catch (error) {
416
+ throw FileSystemError.backupFailed(
417
+ filePath,
418
+ error instanceof Error ? error.message : String(error)
419
+ );
420
+ }
421
+ }
422
+ /**
423
+ * Clean up old backup files based on retention policy
424
+ */
425
+ async cleanupOldBackups() {
426
+ if (!this.config.enableBackups) {
427
+ return 0;
428
+ }
429
+ let backupDir;
430
+ try {
431
+ backupDir = this.getBackupDir();
432
+ } catch (error) {
433
+ this.logger.warn(
434
+ `Failed to resolve backup directory: ${error instanceof Error ? error.message : String(error)}`
435
+ );
436
+ return 0;
437
+ }
438
+ try {
439
+ await fs.access(backupDir);
440
+ } catch {
441
+ return 0;
442
+ }
443
+ const cutoffDate = new Date(
444
+ Date.now() - this.config.backupRetentionDays * 24 * 60 * 60 * 1e3
445
+ );
446
+ let deletedCount = 0;
447
+ try {
448
+ const files = await fs.readdir(backupDir);
449
+ const backupFiles = files.filter((file) => file.endsWith(".backup"));
450
+ for (const file of backupFiles) {
451
+ const filePath = path.join(backupDir, file);
452
+ try {
453
+ const stats = await fs.stat(filePath);
454
+ if (stats.mtime < cutoffDate) {
455
+ await fs.unlink(filePath);
456
+ deletedCount++;
457
+ this.logger.debug(`Cleaned up old backup: ${file}`);
458
+ }
459
+ } catch (error) {
460
+ this.logger.warn(
461
+ `Failed to process backup file ${file}: ${error instanceof Error ? error.message : String(error)}`
462
+ );
463
+ }
464
+ }
465
+ if (deletedCount > 0) {
466
+ this.logger.info(`Backup cleanup: removed ${deletedCount} old backup files`);
467
+ }
468
+ return deletedCount;
469
+ } catch (error) {
470
+ this.logger.warn(
471
+ `Failed to cleanup backup directory: ${error instanceof Error ? error.message : String(error)}`
472
+ );
473
+ return 0;
474
+ }
475
+ }
476
+ /**
477
+ * Get service configuration
478
+ */
479
+ getConfig() {
480
+ return { ...this.config };
481
+ }
482
+ /**
483
+ * Check if a path is allowed
484
+ */
485
+ isPathAllowed(filePath) {
486
+ const validation = this.pathValidator.validatePath(filePath);
487
+ return validation.isValid;
488
+ }
489
+ }
490
+ export {
491
+ FileSystemService
492
+ };
@@ -0,0 +1,70 @@
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 glob_files_tool_exports = {};
20
+ __export(glob_files_tool_exports, {
21
+ createGlobFilesTool: () => createGlobFilesTool
22
+ });
23
+ module.exports = __toCommonJS(glob_files_tool_exports);
24
+ var import_zod = require("zod");
25
+ const GlobFilesInputSchema = import_zod.z.object({
26
+ pattern: import_zod.z.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js")'),
27
+ path: import_zod.z.string().optional().describe("Base directory to search from (defaults to working directory)"),
28
+ max_results: import_zod.z.number().int().positive().optional().default(1e3).describe("Maximum number of results to return (default: 1000)")
29
+ }).strict();
30
+ function createGlobFilesTool(fileSystemService) {
31
+ return {
32
+ id: "glob_files",
33
+ 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.",
34
+ inputSchema: GlobFilesInputSchema,
35
+ execute: async (input, _context) => {
36
+ const { pattern, path, max_results } = input;
37
+ const result = await fileSystemService.globFiles(pattern, {
38
+ cwd: path,
39
+ maxResults: max_results,
40
+ includeMetadata: true
41
+ });
42
+ const _display = {
43
+ type: "search",
44
+ pattern,
45
+ matches: result.files.map((file) => ({
46
+ file: file.path,
47
+ line: 0,
48
+ // No line number for glob
49
+ content: file.path
50
+ })),
51
+ totalMatches: result.totalFound,
52
+ truncated: result.truncated
53
+ };
54
+ return {
55
+ files: result.files.map((file) => ({
56
+ path: file.path,
57
+ size: file.size,
58
+ modified: file.modified.toISOString()
59
+ })),
60
+ total_found: result.totalFound,
61
+ truncated: result.truncated,
62
+ _display
63
+ };
64
+ }
65
+ };
66
+ }
67
+ // Annotate the CommonJS export names for ESM import in node:
68
+ 0 && (module.exports = {
69
+ createGlobFilesTool
70
+ });
@@ -0,0 +1,16 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { FileSystemService } from './filesystem-service.cjs';
3
+ import './types.cjs';
4
+
5
+ /**
6
+ * Glob Files Tool
7
+ *
8
+ * Internal tool for finding files using glob patterns
9
+ */
10
+
11
+ /**
12
+ * Create the glob_files internal tool
13
+ */
14
+ declare function createGlobFilesTool(fileSystemService: FileSystemService): InternalTool;
15
+
16
+ export { createGlobFilesTool };
@@ -0,0 +1,16 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { FileSystemService } from './filesystem-service.js';
3
+ import './types.js';
4
+
5
+ /**
6
+ * Glob Files Tool
7
+ *
8
+ * Internal tool for finding files using glob patterns
9
+ */
10
+
11
+ /**
12
+ * Create the glob_files internal tool
13
+ */
14
+ declare function createGlobFilesTool(fileSystemService: FileSystemService): InternalTool;
15
+
16
+ export { createGlobFilesTool };