@gaunt-sloth/tools 0.0.1

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 (38) hide show
  1. package/README.md +25 -0
  2. package/dist/builtInToolsConfig.d.ts +22 -0
  3. package/dist/builtInToolsConfig.js +134 -0
  4. package/dist/builtInToolsConfig.js.map +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/middleware/binaryContentInjectionMiddleware.d.ts +22 -0
  9. package/dist/middleware/binaryContentInjectionMiddleware.js +125 -0
  10. package/dist/middleware/binaryContentInjectionMiddleware.js.map +1 -0
  11. package/dist/middleware/registry.d.ts +34 -0
  12. package/dist/middleware/registry.js +124 -0
  13. package/dist/middleware/registry.js.map +1 -0
  14. package/dist/middleware/types.d.ts +89 -0
  15. package/dist/middleware/types.js +9 -0
  16. package/dist/middleware/types.js.map +1 -0
  17. package/dist/tools/GthCustomToolkit.d.ts +41 -0
  18. package/dist/tools/GthCustomToolkit.js +280 -0
  19. package/dist/tools/GthCustomToolkit.js.map +1 -0
  20. package/dist/tools/GthDevToolkit.d.ts +24 -0
  21. package/dist/tools/GthDevToolkit.js +189 -0
  22. package/dist/tools/GthDevToolkit.js.map +1 -0
  23. package/dist/tools/GthFileSystemToolkit.d.ts +36 -0
  24. package/dist/tools/GthFileSystemToolkit.js +775 -0
  25. package/dist/tools/GthFileSystemToolkit.js.map +1 -0
  26. package/dist/tools/binaryUtils.d.ts +13 -0
  27. package/dist/tools/binaryUtils.js +55 -0
  28. package/dist/tools/binaryUtils.js.map +1 -0
  29. package/dist/tools/gthStatusUpdateTool.d.ts +2 -0
  30. package/dist/tools/gthStatusUpdateTool.js +15 -0
  31. package/dist/tools/gthStatusUpdateTool.js.map +1 -0
  32. package/dist/tools/gthWebFetchTool.d.ts +2 -0
  33. package/dist/tools/gthWebFetchTool.js +47 -0
  34. package/dist/tools/gthWebFetchTool.js.map +1 -0
  35. package/dist/utils/aiignoreUtils.d.ts +29 -0
  36. package/dist/utils/aiignoreUtils.js +82 -0
  37. package/dist/utils/aiignoreUtils.js.map +1 -0
  38. package/package.json +28 -0
@@ -0,0 +1,775 @@
1
+ import { BaseToolkit, tool } from '@langchain/core/tools';
2
+ import { z } from 'zod';
3
+ import fs from 'fs/promises';
4
+ import path from 'node:path';
5
+ import os from 'os';
6
+ import { createTwoFilesPatch } from 'diff';
7
+ import { displayInfo } from '@gaunt-sloth/core/utils/consoleUtils.js';
8
+ import { shouldIgnoreFile } from '#src/utils/aiignoreUtils.js';
9
+ import { getCurrentWorkDir } from '@gaunt-sloth/core/utils/systemUtils.js';
10
+ import { getFormatForExtension, getMimeType, readBinaryFile } from '#src/tools/binaryUtils.js';
11
+ /**
12
+ * Filesystem toolkit
13
+ * Inspired by https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
14
+ */
15
+ // TODO make it configurable
16
+ const IGNORED_DIRS = ['node_modules', '.git', '.idea', 'dist'];
17
+ // Helper function to create a tool with filesystem type
18
+ function createGthTool(fn, config, gthFileSystemType) {
19
+ const toolInstance = tool(fn, config);
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ toolInstance.gthFileSystemType = gthFileSystemType;
22
+ return toolInstance;
23
+ }
24
+ // Schema definitions
25
+ const ReadFileArgsSchema = z.object({
26
+ path: z.string(),
27
+ tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
28
+ head: z.number().optional().describe('If provided, returns only the first N lines of the file'),
29
+ });
30
+ const ReadBinaryArgsSchema = z.object({
31
+ path: z.string().describe('Path to the binary file to read'),
32
+ formatHint: z
33
+ .enum(['image', 'file', 'audio', 'video'])
34
+ .optional()
35
+ .describe('Optional hint for the format type. If not provided, determined from file extension via config.'),
36
+ });
37
+ const ReadMultipleFilesArgsSchema = z.object({
38
+ paths: z.array(z.string()),
39
+ });
40
+ const WriteFileArgsSchema = z.object({
41
+ path: z.string(),
42
+ content: z.string(),
43
+ });
44
+ const EditOperation = z.object({
45
+ oldText: z.string().describe('Text to search for - must match exactly'),
46
+ newText: z.string().describe('Text to replace with'),
47
+ });
48
+ const EditFileArgsSchema = z.object({
49
+ path: z.string(),
50
+ edits: z.array(EditOperation),
51
+ dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format'),
52
+ });
53
+ const CreateDirectoryArgsSchema = z.object({
54
+ path: z.string(),
55
+ });
56
+ const ListDirectoryArgsSchema = z.object({
57
+ path: z.string(),
58
+ });
59
+ const ListDirectoryWithSizesArgsSchema = z.object({
60
+ path: z.string(),
61
+ sortBy: z
62
+ .enum(['name', 'size'])
63
+ .optional()
64
+ .default('name')
65
+ .describe('Sort entries by name or size'),
66
+ });
67
+ const DirectoryTreeArgsSchema = z.object({
68
+ path: z.string(),
69
+ });
70
+ const MoveFileArgsSchema = z.object({
71
+ source: z.string(),
72
+ destination: z.string(),
73
+ });
74
+ const SearchFilesArgsSchema = z.object({
75
+ path: z.string(),
76
+ pattern: z.string(),
77
+ excludePatterns: z.array(z.string()).optional().default([]),
78
+ });
79
+ const GetFileInfoArgsSchema = z.object({
80
+ path: z.string(),
81
+ });
82
+ const DeleteFileArgsSchema = z.object({
83
+ path: z.string(),
84
+ });
85
+ const DeleteDirectoryArgsSchema = z.object({
86
+ path: z.string(),
87
+ recursive: z.boolean().default(false).describe('If true, delete directory and all its contents'),
88
+ });
89
+ export default class GthFileSystemToolkit extends BaseToolkit {
90
+ tools;
91
+ allowedDirectories;
92
+ aiignoreConfig;
93
+ binaryFormats;
94
+ constructor(options = {}) {
95
+ super();
96
+ const allowedDirectories = options.allowedDirectories ?? [getCurrentWorkDir()];
97
+ this.allowedDirectories = allowedDirectories.map((dir) => this.normalizePath(path.resolve(this.expandHome(dir))));
98
+ this.aiignoreConfig = options.aiignoreConfig;
99
+ this.binaryFormats = options.binaryFormats;
100
+ this.tools = this.createTools();
101
+ }
102
+ /**
103
+ * Get tools filtered by operation type
104
+ */
105
+ getFilteredTools(allowedOperations) {
106
+ return this.tools.filter((tool) => {
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ const toolType = tool.gthFileSystemType;
109
+ return allowedOperations.includes(toolType);
110
+ });
111
+ }
112
+ normalizePath(p) {
113
+ return path.normalize(p);
114
+ }
115
+ isProtectedDirectory(dirPath) {
116
+ const normalizedPath = this.normalizePath(path.resolve(dirPath));
117
+ return this.allowedDirectories.some((allowedDir) => this.normalizePath(allowedDir) === normalizedPath);
118
+ }
119
+ expandHome(filepath) {
120
+ if (filepath.startsWith('~/') || filepath === '~') {
121
+ return path.join(os.homedir(), filepath.slice(1));
122
+ }
123
+ return filepath;
124
+ }
125
+ sanitizeRequestedPath(requestedPath) {
126
+ const trimmedPath = requestedPath.trim();
127
+ if (trimmedPath.length === 0) {
128
+ throw new Error('Path cannot be empty');
129
+ }
130
+ const unquotedPath = trimmedPath.replace(/^(['"`])(.*)\1$/, '$2');
131
+ if (path.isAbsolute(unquotedPath)) {
132
+ return unquotedPath;
133
+ }
134
+ if (unquotedPath.startsWith('./') || unquotedPath.startsWith('../')) {
135
+ return unquotedPath;
136
+ }
137
+ return `./${unquotedPath}`;
138
+ }
139
+ async validatePath(requestedPath) {
140
+ const sanitizedPath = this.sanitizeRequestedPath(requestedPath);
141
+ const expandedPath = this.expandHome(sanitizedPath);
142
+ const absolute = path.isAbsolute(expandedPath)
143
+ ? path.resolve(expandedPath)
144
+ : path.resolve(getCurrentWorkDir(), expandedPath);
145
+ const normalizedRequested = this.normalizePath(absolute);
146
+ // Helper function to check if a path is within allowed directories
147
+ const isWithinAllowedDir = (checkPath) => {
148
+ return this.allowedDirectories.some((allowedDir) => checkPath.startsWith(allowedDir));
149
+ };
150
+ // Check if the requested path is within allowed directories
151
+ if (!isWithinAllowedDir(normalizedRequested)) {
152
+ throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${this.allowedDirectories.join(', ')}`);
153
+ }
154
+ try {
155
+ // Try to get the real path for existing files/directories
156
+ const realPath = await fs.realpath(absolute);
157
+ const normalizedReal = this.normalizePath(realPath);
158
+ // Verify the real path (after resolving symlinks) is still within allowed directories
159
+ if (!isWithinAllowedDir(normalizedReal)) {
160
+ throw new Error('Access denied - symlink target outside allowed directories');
161
+ }
162
+ return realPath;
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ }
165
+ catch (error) {
166
+ if (error.code === 'ENOENT') {
167
+ // Path doesn't exist - validate that its parent directory exists and is within allowed directories
168
+ let currentDir = path.dirname(absolute);
169
+ while (currentDir !== path.dirname(currentDir)) {
170
+ try {
171
+ const realParentPath = await fs.realpath(currentDir);
172
+ const normalizedParent = this.normalizePath(realParentPath);
173
+ if (!isWithinAllowedDir(normalizedParent)) {
174
+ throw new Error('Access denied - parent directory outside allowed directories');
175
+ }
176
+ return absolute; // Valid parent exists, return the original absolute path
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ }
179
+ catch (parentError) {
180
+ if (parentError.code === 'ENOENT') {
181
+ currentDir = path.dirname(currentDir); // Move up one level
182
+ }
183
+ else {
184
+ throw parentError; // Some other error
185
+ }
186
+ }
187
+ }
188
+ // Could not find any existing parent in the path
189
+ throw new Error(`Cannot determine valid parent directory for: ${absolute}`);
190
+ }
191
+ // Some other error
192
+ throw error;
193
+ }
194
+ }
195
+ async getFileStats(filePath) {
196
+ const stats = await fs.stat(filePath);
197
+ return {
198
+ size: stats.size,
199
+ created: stats.birthtime,
200
+ modified: stats.mtime,
201
+ accessed: stats.atime,
202
+ isDirectory: stats.isDirectory(),
203
+ isFile: stats.isFile(),
204
+ permissions: stats.mode.toString(8).slice(-3),
205
+ };
206
+ }
207
+ async searchFiles(rootPath, pattern, excludePatterns = []) {
208
+ const results = [];
209
+ const aiignoreConfig = this.aiignoreConfig;
210
+ const pendingDirs = [rootPath];
211
+ while (pendingDirs.length > 0) {
212
+ const currentPath = pendingDirs.pop();
213
+ if (!currentPath) {
214
+ continue;
215
+ }
216
+ let entries;
217
+ try {
218
+ entries = (await fs.readdir(currentPath, {
219
+ withFileTypes: true,
220
+ encoding: 'utf8',
221
+ }));
222
+ }
223
+ catch {
224
+ continue;
225
+ }
226
+ for (const entry of entries) {
227
+ const entryName = entry.name.toString();
228
+ const fullPath = path.join(currentPath, entryName);
229
+ try {
230
+ await this.validatePath(fullPath);
231
+ const relativePath = path.relative(rootPath, fullPath);
232
+ const shouldExclude = excludePatterns.some((pattern) => {
233
+ const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`;
234
+ return path.matchesGlob(relativePath, globPattern);
235
+ });
236
+ if (shouldExclude) {
237
+ continue;
238
+ }
239
+ const shouldIgnore = shouldIgnoreFile(fullPath, getCurrentWorkDir(), aiignoreConfig?.patterns, aiignoreConfig?.enabled);
240
+ if (shouldIgnore) {
241
+ continue;
242
+ }
243
+ if (entryName.toLowerCase().includes(pattern.toLowerCase())) {
244
+ results.push(fullPath);
245
+ }
246
+ let isDirectory = entry.isDirectory();
247
+ if (!isDirectory &&
248
+ typeof entry.isSymbolicLink === 'function' &&
249
+ entry.isSymbolicLink()) {
250
+ try {
251
+ const stats = await fs.stat(fullPath);
252
+ isDirectory = stats.isDirectory();
253
+ }
254
+ catch {
255
+ isDirectory = false;
256
+ }
257
+ }
258
+ if (isDirectory) {
259
+ pendingDirs.push(fullPath);
260
+ }
261
+ }
262
+ catch {
263
+ continue;
264
+ }
265
+ }
266
+ }
267
+ return results;
268
+ }
269
+ normalizeLineEndings(text) {
270
+ return text.replace(/\r\n/g, '\n');
271
+ }
272
+ createUnifiedDiff(originalContent, newContent, filepath = 'file') {
273
+ const normalizedOriginal = this.normalizeLineEndings(originalContent);
274
+ const normalizedNew = this.normalizeLineEndings(newContent);
275
+ return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified');
276
+ }
277
+ async applyFileEdits(filePath, edits, dryRun = false) {
278
+ const content = this.normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
279
+ let modifiedContent = content;
280
+ for (const edit of edits) {
281
+ const normalizedOld = this.normalizeLineEndings(edit.oldText);
282
+ const normalizedNew = this.normalizeLineEndings(edit.newText);
283
+ if (modifiedContent.includes(normalizedOld)) {
284
+ modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
285
+ continue;
286
+ }
287
+ const oldLines = normalizedOld.split('\n');
288
+ const contentLines = modifiedContent.split('\n');
289
+ let matchFound = false;
290
+ for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
291
+ const potentialMatch = contentLines.slice(i, i + oldLines.length);
292
+ const isMatch = oldLines.every((oldLine, j) => {
293
+ const contentLine = potentialMatch[j];
294
+ return oldLine.trim() === contentLine.trim();
295
+ });
296
+ if (isMatch) {
297
+ const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
298
+ const newLines = normalizedNew.split('\n').map((line, j) => {
299
+ if (j === 0)
300
+ return originalIndent + line.trimStart();
301
+ const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
302
+ const newIndent = line.match(/^\s*/)?.[0] || '';
303
+ if (oldIndent && newIndent) {
304
+ const relativeIndent = newIndent.length - oldIndent.length;
305
+ return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
306
+ }
307
+ return line;
308
+ });
309
+ contentLines.splice(i, oldLines.length, ...newLines);
310
+ modifiedContent = contentLines.join('\n');
311
+ matchFound = true;
312
+ break;
313
+ }
314
+ }
315
+ if (!matchFound) {
316
+ throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
317
+ }
318
+ }
319
+ const diff = this.createUnifiedDiff(content, modifiedContent, filePath);
320
+ let numBackticks = 3;
321
+ while (diff.includes('`'.repeat(numBackticks))) {
322
+ numBackticks++;
323
+ }
324
+ const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
325
+ if (!dryRun) {
326
+ await fs.writeFile(filePath, modifiedContent, 'utf-8');
327
+ }
328
+ return formattedDiff;
329
+ }
330
+ formatSize(bytes) {
331
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
332
+ if (bytes === 0)
333
+ return '0 B';
334
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
335
+ if (i === 0)
336
+ return `${bytes} ${units[i]}`;
337
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
338
+ }
339
+ async tailFile(filePath, numLines) {
340
+ const CHUNK_SIZE = 1024;
341
+ const stats = await fs.stat(filePath);
342
+ const fileSize = stats.size;
343
+ if (fileSize === 0)
344
+ return '';
345
+ const fileHandle = await fs.open(filePath, 'r');
346
+ try {
347
+ const lines = [];
348
+ let position = fileSize;
349
+ let chunk = Buffer.alloc(CHUNK_SIZE);
350
+ let linesFound = 0;
351
+ let remainingText = '';
352
+ while (position > 0 && linesFound < numLines) {
353
+ const size = Math.min(CHUNK_SIZE, position);
354
+ position -= size;
355
+ const { bytesRead } = await fileHandle.read(chunk, 0, size, position);
356
+ if (!bytesRead)
357
+ break;
358
+ const readData = chunk.slice(0, bytesRead).toString('utf-8');
359
+ const chunkText = readData + remainingText;
360
+ const chunkLines = this.normalizeLineEndings(chunkText).split('\n');
361
+ if (position > 0) {
362
+ remainingText = chunkLines[0];
363
+ chunkLines.shift();
364
+ }
365
+ for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) {
366
+ lines.unshift(chunkLines[i]);
367
+ linesFound++;
368
+ }
369
+ }
370
+ return lines.join('\n');
371
+ }
372
+ finally {
373
+ await fileHandle.close();
374
+ }
375
+ }
376
+ async headFile(filePath, numLines) {
377
+ const fileHandle = await fs.open(filePath, 'r');
378
+ try {
379
+ const lines = [];
380
+ let buffer = '';
381
+ let bytesRead = 0;
382
+ const chunk = Buffer.alloc(1024);
383
+ while (lines.length < numLines) {
384
+ const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead);
385
+ if (result.bytesRead === 0)
386
+ break;
387
+ bytesRead += result.bytesRead;
388
+ buffer += chunk.slice(0, result.bytesRead).toString('utf-8');
389
+ const newLineIndex = buffer.lastIndexOf('\n');
390
+ if (newLineIndex !== -1) {
391
+ const completeLines = buffer.slice(0, newLineIndex).split('\n');
392
+ buffer = buffer.slice(newLineIndex + 1);
393
+ for (const line of completeLines) {
394
+ lines.push(line);
395
+ if (lines.length >= numLines)
396
+ break;
397
+ }
398
+ }
399
+ }
400
+ if (buffer.length > 0 && lines.length < numLines) {
401
+ lines.push(buffer);
402
+ }
403
+ return lines.join('\n');
404
+ }
405
+ finally {
406
+ await fileHandle.close();
407
+ }
408
+ }
409
+ createReadBinaryTool() {
410
+ return createGthTool(async (args) => {
411
+ if (!this.binaryFormats || !Array.isArray(this.binaryFormats)) {
412
+ return 'Binary formats are not configured. Add binaryFormats to your config to enable this feature.';
413
+ }
414
+ displayInfo(`\nšŸ“ Reading binary file: ${args.path}\n`);
415
+ let validPath;
416
+ try {
417
+ validPath = await this.validatePath(args.path);
418
+ }
419
+ catch {
420
+ return 'Path is not within allowed directories or is blocked by .aiignore';
421
+ }
422
+ const aiignoreConfig = this.aiignoreConfig;
423
+ const shouldIgnore = shouldIgnoreFile(validPath, getCurrentWorkDir(), aiignoreConfig?.patterns, aiignoreConfig?.enabled);
424
+ if (shouldIgnore) {
425
+ return 'Path is not within allowed directories or is blocked by .aiignore';
426
+ }
427
+ const ext = path.extname(validPath).toLowerCase().slice(1);
428
+ let formatType = null;
429
+ let formatConfig = null;
430
+ if (args.formatHint) {
431
+ const hintedConfig = this.binaryFormats.find((config) => config.type === args.formatHint && config.extensions.includes(ext));
432
+ if (hintedConfig) {
433
+ formatType = hintedConfig.type;
434
+ formatConfig = hintedConfig;
435
+ }
436
+ }
437
+ else {
438
+ const formatMatch = getFormatForExtension(validPath, this.binaryFormats);
439
+ if (formatMatch) {
440
+ formatType = formatMatch.type;
441
+ formatConfig = formatMatch.config;
442
+ }
443
+ }
444
+ if (!formatType || !formatConfig) {
445
+ return `Extension '.${ext}' is not configured for any binary format type. Configure it in binaryFormats.`;
446
+ }
447
+ const maxSize = formatConfig.maxSize ?? 10 * 1024 * 1024;
448
+ const mimeType = getMimeType(ext, formatConfig);
449
+ try {
450
+ const result = await readBinaryFile(validPath, maxSize, mimeType);
451
+ // Return special format string that middleware will parse and process:
452
+ // Format: gth_read_binary;type:${type};path:${encodedPath};data:${media_type};base64,${data}
453
+ // Path is URL-encoded to handle special characters like semicolons
454
+ const encodedPath = encodeURIComponent(validPath);
455
+ return `gth_read_binary;type:${formatType};path:${encodedPath};data:${mimeType};base64,${result.data}`;
456
+ }
457
+ catch (error) {
458
+ const message = error instanceof Error ? error.message : String(error);
459
+ return `Error reading binary file: ${message}`;
460
+ }
461
+ }, {
462
+ name: 'gth_read_binary',
463
+ description: 'Read a binary file (image, file, audio, video) and return its base64-encoded content. ' +
464
+ 'Only works for file types configured in binaryFormats.',
465
+ schema: ReadBinaryArgsSchema,
466
+ }, 'read');
467
+ }
468
+ createTools() {
469
+ const tools = [
470
+ createGthTool(async (args) => {
471
+ displayInfo(`\nšŸ“ Reading file: ${args.path}\n`);
472
+ const validPath = await this.validatePath(args.path);
473
+ if (args.head && args.tail) {
474
+ throw new Error('Cannot specify both head and tail parameters simultaneously');
475
+ }
476
+ if (args.tail) {
477
+ return await this.tailFile(validPath, args.tail);
478
+ }
479
+ if (args.head) {
480
+ return await this.headFile(validPath, args.head);
481
+ }
482
+ return await fs.readFile(validPath, 'utf-8');
483
+ }, {
484
+ name: 'read_file',
485
+ description: 'Read the complete contents of a file from the file system. ' +
486
+ 'Handles various text encodings and provides detailed error messages ' +
487
+ 'if the file cannot be read. Use this tool when you need to examine ' +
488
+ "the contents of a single file. Use the 'head' parameter to read only " +
489
+ "the first N lines of a file, or the 'tail' parameter to read only " +
490
+ 'the last N lines of a file. Only works within allowed directories.',
491
+ schema: ReadFileArgsSchema,
492
+ }, 'read'),
493
+ createGthTool(async (args) => {
494
+ displayInfo(`\nšŸ“ Reading ${args.paths.length} files\n`);
495
+ const results = await Promise.all(args.paths.map(async (filePath) => {
496
+ try {
497
+ const validPath = await this.validatePath(filePath);
498
+ const content = await fs.readFile(validPath, 'utf-8');
499
+ return `${filePath}:\n${content}\n`;
500
+ }
501
+ catch (error) {
502
+ const errorMessage = error instanceof Error ? error.message : String(error);
503
+ return `${filePath}: Error - ${errorMessage}`;
504
+ }
505
+ }));
506
+ return results.join('\n---\n');
507
+ }, {
508
+ name: 'read_multiple_files',
509
+ description: 'Read the contents of multiple files simultaneously. This is more ' +
510
+ 'efficient than reading files one by one when you need to analyze ' +
511
+ "or compare multiple files. Each file's content is returned with its " +
512
+ "path as a reference. Failed reads for individual files won't stop " +
513
+ 'the entire operation. Only works within allowed directories.',
514
+ schema: ReadMultipleFilesArgsSchema,
515
+ }, 'read'),
516
+ createGthTool(async (args) => {
517
+ displayInfo(`\nšŸ“ Writing file: ${args.path}\n`);
518
+ const validPath = await this.validatePath(args.path);
519
+ await fs.writeFile(validPath, args.content, 'utf-8');
520
+ return `Successfully wrote to ${args.path}`;
521
+ }, {
522
+ name: 'write_file',
523
+ description: 'Create a new file or completely overwrite an existing file with new content. ' +
524
+ 'Use with caution as it will overwrite existing files without warning. ' +
525
+ 'Handles text content with proper encoding. Only works within allowed directories.',
526
+ schema: WriteFileArgsSchema,
527
+ }, 'write'),
528
+ createGthTool(async (args) => {
529
+ displayInfo(`\nšŸ“ Editing file: ${args.path}\n`);
530
+ const validPath = await this.validatePath(args.path);
531
+ return await this.applyFileEdits(validPath, args.edits, args.dryRun);
532
+ }, {
533
+ name: 'edit_file',
534
+ description: 'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
535
+ 'with new content. Returns a git-style diff showing the changes made. ' +
536
+ 'Only works within allowed directories.' +
537
+ 'Always present diff returned by this tool back to the user.' +
538
+ 'Prefer applying small edits, eg. one function at a time, one block or one condition.' +
539
+ 'Fall back to using the "write_file" tool if you need to make large edits.' +
540
+ 'or of the "edit_file" fails for some reason.' +
541
+ 'Always read file before every edit to ensure that the file is not corrupted.',
542
+ schema: EditFileArgsSchema,
543
+ }, 'write'),
544
+ createGthTool(async (args) => {
545
+ displayInfo(`\nšŸ“ Creating directory: ${args.path}\n`);
546
+ const validPath = await this.validatePath(args.path);
547
+ await fs.mkdir(validPath, { recursive: true });
548
+ return `Successfully created directory ${args.path}`;
549
+ }, {
550
+ name: 'create_directory',
551
+ description: 'Create a new directory or ensure a directory exists. Can create multiple ' +
552
+ 'nested directories in one operation. If the directory already exists, ' +
553
+ 'this operation will succeed silently. Perfect for setting up directory ' +
554
+ 'structures for projects or ensuring required paths exist. Only works within allowed directories.',
555
+ schema: CreateDirectoryArgsSchema,
556
+ }, 'write'),
557
+ createGthTool(async (args) => {
558
+ displayInfo(`\nšŸ“ Listing directory: ${args.path}\n`);
559
+ const validPath = await this.validatePath(args.path);
560
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
561
+ const aiignoreConfig = this.aiignoreConfig;
562
+ const filteredEntries = entries.filter((entry) => {
563
+ const fullPath = path.join(validPath, entry.name);
564
+ return !shouldIgnoreFile(fullPath, getCurrentWorkDir(), aiignoreConfig?.patterns, aiignoreConfig?.enabled);
565
+ });
566
+ return filteredEntries
567
+ .map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
568
+ .join('\n');
569
+ }, {
570
+ name: 'list_directory',
571
+ description: 'Get a detailed listing of all files and directories in a specified path. ' +
572
+ 'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
573
+ 'prefixes. This tool is essential for understanding directory structure and ' +
574
+ 'finding specific files within a directory. Only works within allowed directories.',
575
+ schema: ListDirectoryArgsSchema,
576
+ }, 'read'),
577
+ createGthTool(async (args) => {
578
+ displayInfo(`\nšŸ“ Listing directory with sizes: ${args.path}\n`);
579
+ const validPath = await this.validatePath(args.path);
580
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
581
+ const aiignoreConfig = this.aiignoreConfig;
582
+ const filteredEntries = entries.filter((entry) => {
583
+ const fullPath = path.join(validPath, entry.name);
584
+ return !shouldIgnoreFile(fullPath, getCurrentWorkDir(), aiignoreConfig?.patterns, aiignoreConfig?.enabled);
585
+ });
586
+ const detailedEntries = await Promise.all(filteredEntries.map(async (entry) => {
587
+ const entryPath = path.join(validPath, entry.name);
588
+ try {
589
+ const stats = await fs.stat(entryPath);
590
+ return {
591
+ name: entry.name,
592
+ isDirectory: entry.isDirectory(),
593
+ size: stats.size,
594
+ mtime: stats.mtime,
595
+ };
596
+ }
597
+ catch {
598
+ return {
599
+ name: entry.name,
600
+ isDirectory: entry.isDirectory(),
601
+ size: 0,
602
+ mtime: new Date(0),
603
+ };
604
+ }
605
+ }));
606
+ const sortedEntries = [...detailedEntries].sort((a, b) => {
607
+ if (args.sortBy === 'size') {
608
+ return b.size - a.size;
609
+ }
610
+ return a.name.localeCompare(b.name);
611
+ });
612
+ const formattedEntries = sortedEntries.map((entry) => `${entry.isDirectory ? '[DIR]' : '[FILE]'} ${entry.name.padEnd(30)} ${entry.isDirectory ? '' : this.formatSize(entry.size).padStart(10)}`);
613
+ const totalFiles = detailedEntries.filter((e) => !e.isDirectory).length;
614
+ const totalDirs = detailedEntries.filter((e) => e.isDirectory).length;
615
+ const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
616
+ const summary = [
617
+ '',
618
+ `Total: ${totalFiles} files, ${totalDirs} directories`,
619
+ `Combined size: ${this.formatSize(totalSize)}`,
620
+ ];
621
+ return [...formattedEntries, ...summary].join('\n');
622
+ }, {
623
+ name: 'list_directory_with_sizes',
624
+ description: 'Get a detailed listing of all files and directories in a specified path, including sizes. ' +
625
+ 'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
626
+ 'prefixes. This tool is useful for understanding directory structure and ' +
627
+ 'finding specific files within a directory. Only works within allowed directories.',
628
+ schema: ListDirectoryWithSizesArgsSchema,
629
+ }, 'read'),
630
+ createGthTool(async (args) => {
631
+ displayInfo(`\nšŸ“ Building directory tree: ${args.path}\n`);
632
+ const buildTree = async (currentPath) => {
633
+ const validPath = await this.validatePath(currentPath);
634
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
635
+ const result = [];
636
+ const aiignoreConfig = this.aiignoreConfig;
637
+ for (const entry of entries) {
638
+ const entryData = {
639
+ name: entry.name,
640
+ type: entry.isDirectory() ? 'directory' : 'file',
641
+ };
642
+ if (IGNORED_DIRS.indexOf(entry.name) >= 0) {
643
+ entryData.ignored = true;
644
+ }
645
+ // Check if file should be ignored by aiignore
646
+ const fullPath = path.join(currentPath, entry.name);
647
+ const shouldIgnore = shouldIgnoreFile(fullPath, getCurrentWorkDir(), aiignoreConfig?.patterns, aiignoreConfig?.enabled);
648
+ if (shouldIgnore) {
649
+ entryData.ignored = true;
650
+ }
651
+ if (entry.isDirectory() && !entryData.ignored) {
652
+ const subPath = path.join(currentPath, entry.name);
653
+ entryData.children = await buildTree(subPath);
654
+ }
655
+ result.push(entryData);
656
+ }
657
+ return result;
658
+ };
659
+ const treeData = await buildTree(args.path);
660
+ return JSON.stringify(treeData, null, 2);
661
+ }, {
662
+ name: 'directory_tree',
663
+ description: 'Get a recursive tree view of files and directories as a JSON structure. ' +
664
+ "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
665
+ 'Files have no children array, while directories always have a children array (which may be empty). ' +
666
+ 'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
667
+ schema: DirectoryTreeArgsSchema,
668
+ }, 'read'),
669
+ createGthTool(async (args) => {
670
+ displayInfo(`\nšŸ“ Moving ${args.source} to ${args.destination}\n`);
671
+ const validSourcePath = await this.validatePath(args.source);
672
+ const validDestPath = await this.validatePath(args.destination);
673
+ await fs.rename(validSourcePath, validDestPath);
674
+ return `Successfully moved ${args.source} to ${args.destination}`;
675
+ }, {
676
+ name: 'move_file',
677
+ description: 'Move or rename files and directories. Can move files between directories ' +
678
+ 'and rename them in a single operation. If the destination exists, the ' +
679
+ 'operation will fail. Works across different directories and can be used ' +
680
+ 'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
681
+ schema: MoveFileArgsSchema,
682
+ }, 'write'),
683
+ createGthTool(async (args) => {
684
+ displayInfo(`\nšŸ“ Searching for '${args.pattern}' in ${args.path}\n`);
685
+ const validPath = await this.validatePath(args.path);
686
+ const results = await this.searchFiles(validPath, args.pattern, args.excludePatterns);
687
+ return results.length > 0 ? results.join('\n') : 'No matches found';
688
+ }, {
689
+ name: 'search_files',
690
+ description: 'Recursively search for files and directories matching a pattern. ' +
691
+ 'Searches through all subdirectories from the starting path. The search ' +
692
+ 'is case-insensitive and matches partial names. Returns full paths to all ' +
693
+ "matching items. Great for finding files when you don't know their exact location. " +
694
+ 'Only searches within allowed directories.',
695
+ schema: SearchFilesArgsSchema,
696
+ }, 'read'),
697
+ createGthTool(async (args) => {
698
+ displayInfo(`\nšŸ“ Getting file info: ${args.path}\n`);
699
+ const validPath = await this.validatePath(args.path);
700
+ const info = await this.getFileStats(validPath);
701
+ return Object.entries(info)
702
+ .map(([key, value]) => `${key}: ${value}`)
703
+ .join('\n');
704
+ }, {
705
+ name: 'get_file_info',
706
+ description: 'Retrieve detailed metadata about a file or directory. Returns comprehensive ' +
707
+ 'information including size, creation time, last modified time, permissions, ' +
708
+ 'and type. This tool is perfect for understanding file characteristics ' +
709
+ 'without reading the actual content. Only works within allowed directories.',
710
+ schema: GetFileInfoArgsSchema,
711
+ }, 'read'),
712
+ createGthTool(async (args) => {
713
+ displayInfo(`\nšŸ“ Deleting file: ${args.path}\n`);
714
+ const validPath = await this.validatePath(args.path);
715
+ const stats = await fs.stat(validPath);
716
+ if (stats.isDirectory()) {
717
+ throw new Error(`Cannot delete directory: ${args.path}. Use rmdir or a recursive delete tool for directories.`);
718
+ }
719
+ await fs.unlink(validPath);
720
+ return `Successfully deleted file: ${args.path}`;
721
+ }, {
722
+ name: 'delete_file',
723
+ description: 'Delete a file from the filesystem. This operation cannot be undone. ' +
724
+ 'Only works for files, not directories. Use with caution. ' +
725
+ 'Only works within allowed directories.',
726
+ schema: DeleteFileArgsSchema,
727
+ }, 'write'),
728
+ createGthTool(async (args) => {
729
+ displayInfo(`\nšŸ“ Deleting directory: ${args.path}${args.recursive ? ' (recursive)' : ''}\n`);
730
+ const validPath = await this.validatePath(args.path);
731
+ // Check if this is a protected directory
732
+ if (this.isProtectedDirectory(validPath)) {
733
+ throw new Error(`Cannot delete protected directory: ${args.path}. This is one of the allowed root directories.`);
734
+ }
735
+ const stats = await fs.stat(validPath);
736
+ if (!stats.isDirectory()) {
737
+ throw new Error(`Not a directory: ${args.path}. Use delete_file for files.`);
738
+ }
739
+ if (args.recursive) {
740
+ await fs.rm(validPath, { recursive: true, force: true });
741
+ return `Successfully deleted directory and all contents: ${args.path}`;
742
+ }
743
+ else {
744
+ // For non-recursive delete, check if directory is empty
745
+ const entries = await fs.readdir(validPath);
746
+ if (entries.length > 0) {
747
+ throw new Error(`Directory not empty: ${args.path}. Use recursive: true to delete non-empty directories.`);
748
+ }
749
+ await fs.rmdir(validPath);
750
+ return `Successfully deleted empty directory: ${args.path}`;
751
+ }
752
+ }, {
753
+ name: 'delete_directory',
754
+ description: 'Delete a directory from the filesystem. Can delete empty directories or recursively delete ' +
755
+ 'directories with contents. Cannot delete protected directories (allowed root directories). ' +
756
+ 'This operation cannot be undone. Use with extreme caution. ' +
757
+ 'Only works within allowed directories.',
758
+ schema: DeleteDirectoryArgsSchema,
759
+ }, 'write'),
760
+ createGthTool(async () => {
761
+ return `Allowed directories:\n${this.allowedDirectories.join('\n')}`;
762
+ }, {
763
+ name: 'list_allowed_directories',
764
+ description: 'Returns the list of directories that this server is allowed to access. ' +
765
+ 'Use this to understand which directories are available before trying to access files.',
766
+ schema: z.object({}),
767
+ }, 'read'),
768
+ ];
769
+ if (this.binaryFormats && Array.isArray(this.binaryFormats) && this.binaryFormats.length > 0) {
770
+ tools.push(this.createReadBinaryTool());
771
+ }
772
+ return tools;
773
+ }
774
+ }
775
+ //# sourceMappingURL=GthFileSystemToolkit.js.map