@loxia-labs/loxia-autopilot-one 1.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 (80) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +509 -0
  3. package/bin/cli.js +117 -0
  4. package/package.json +94 -0
  5. package/scripts/install-scanners.js +236 -0
  6. package/src/analyzers/CSSAnalyzer.js +297 -0
  7. package/src/analyzers/ConfigValidator.js +690 -0
  8. package/src/analyzers/ESLintAnalyzer.js +320 -0
  9. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  10. package/src/analyzers/PrettierFormatter.js +247 -0
  11. package/src/analyzers/PythonAnalyzer.js +266 -0
  12. package/src/analyzers/SecurityAnalyzer.js +729 -0
  13. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  14. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  15. package/src/analyzers/codeCloneDetector/detector.js +203 -0
  16. package/src/analyzers/codeCloneDetector/index.js +160 -0
  17. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  18. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  19. package/src/analyzers/codeCloneDetector/scanner.js +59 -0
  20. package/src/core/agentPool.js +1474 -0
  21. package/src/core/agentScheduler.js +2147 -0
  22. package/src/core/contextManager.js +709 -0
  23. package/src/core/messageProcessor.js +732 -0
  24. package/src/core/orchestrator.js +548 -0
  25. package/src/core/stateManager.js +877 -0
  26. package/src/index.js +631 -0
  27. package/src/interfaces/cli.js +549 -0
  28. package/src/interfaces/webServer.js +2162 -0
  29. package/src/modules/fileExplorer/controller.js +280 -0
  30. package/src/modules/fileExplorer/index.js +37 -0
  31. package/src/modules/fileExplorer/middleware.js +92 -0
  32. package/src/modules/fileExplorer/routes.js +125 -0
  33. package/src/modules/fileExplorer/types.js +44 -0
  34. package/src/services/aiService.js +1232 -0
  35. package/src/services/apiKeyManager.js +164 -0
  36. package/src/services/benchmarkService.js +366 -0
  37. package/src/services/budgetService.js +539 -0
  38. package/src/services/contextInjectionService.js +247 -0
  39. package/src/services/conversationCompactionService.js +637 -0
  40. package/src/services/errorHandler.js +810 -0
  41. package/src/services/fileAttachmentService.js +544 -0
  42. package/src/services/modelRouterService.js +366 -0
  43. package/src/services/modelsService.js +322 -0
  44. package/src/services/qualityInspector.js +796 -0
  45. package/src/services/tokenCountingService.js +536 -0
  46. package/src/tools/agentCommunicationTool.js +1344 -0
  47. package/src/tools/agentDelayTool.js +485 -0
  48. package/src/tools/asyncToolManager.js +604 -0
  49. package/src/tools/baseTool.js +800 -0
  50. package/src/tools/browserTool.js +920 -0
  51. package/src/tools/cloneDetectionTool.js +621 -0
  52. package/src/tools/dependencyResolverTool.js +1215 -0
  53. package/src/tools/fileContentReplaceTool.js +875 -0
  54. package/src/tools/fileSystemTool.js +1107 -0
  55. package/src/tools/fileTreeTool.js +853 -0
  56. package/src/tools/imageTool.js +901 -0
  57. package/src/tools/importAnalyzerTool.js +1060 -0
  58. package/src/tools/jobDoneTool.js +248 -0
  59. package/src/tools/seekTool.js +956 -0
  60. package/src/tools/staticAnalysisTool.js +1778 -0
  61. package/src/tools/taskManagerTool.js +2873 -0
  62. package/src/tools/terminalTool.js +2304 -0
  63. package/src/tools/webTool.js +1430 -0
  64. package/src/types/agent.js +519 -0
  65. package/src/types/contextReference.js +972 -0
  66. package/src/types/conversation.js +730 -0
  67. package/src/types/toolCommand.js +747 -0
  68. package/src/utilities/attachmentValidator.js +292 -0
  69. package/src/utilities/configManager.js +582 -0
  70. package/src/utilities/constants.js +722 -0
  71. package/src/utilities/directoryAccessManager.js +535 -0
  72. package/src/utilities/fileProcessor.js +307 -0
  73. package/src/utilities/logger.js +436 -0
  74. package/src/utilities/tagParser.js +1246 -0
  75. package/src/utilities/toolConstants.js +317 -0
  76. package/web-ui/build/index.html +15 -0
  77. package/web-ui/build/logo.png +0 -0
  78. package/web-ui/build/logo2.png +0 -0
  79. package/web-ui/build/static/index-CjkkcnFA.js +344 -0
  80. package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
@@ -0,0 +1,1107 @@
1
+ /**
2
+ * FileSystemTool - Handle file system operations safely
3
+ *
4
+ * Purpose:
5
+ * - Read, write, and manipulate files
6
+ * - Directory operations
7
+ * - File metadata and permissions
8
+ * - Safe file operations with validation
9
+ */
10
+
11
+ import { BaseTool } from './baseTool.js';
12
+ import TagParser from '../utilities/tagParser.js';
13
+ import DirectoryAccessManager from '../utilities/directoryAccessManager.js';
14
+ import fs from 'fs/promises';
15
+ import path from 'path';
16
+ import crypto from 'crypto';
17
+
18
+ import {
19
+ TOOL_STATUS,
20
+ FILE_EXTENSIONS,
21
+ SYSTEM_DEFAULTS
22
+ } from '../utilities/constants.js';
23
+
24
+ class FileSystemTool extends BaseTool {
25
+ constructor(config = {}, logger = null) {
26
+ super(config, logger);
27
+
28
+ // Tool metadata
29
+ this.requiresProject = true;
30
+ this.isAsync = false; // Most file operations are quick
31
+ this.timeout = config.timeout || 30000; // 30 seconds
32
+ this.maxConcurrentOperations = config.maxConcurrentOperations || 5;
33
+
34
+ // Security settings
35
+ this.maxFileSize = config.maxFileSize || SYSTEM_DEFAULTS.MAX_FILE_SIZE;
36
+ this.allowedExtensions = config.allowedExtensions || null; // null = all allowed
37
+ this.blockedExtensions = config.blockedExtensions || ['.exe', '.bat', '.cmd', '.scr', '.com'];
38
+ this.allowedDirectories = config.allowedDirectories || null; // null = project dir only
39
+
40
+ // File operation history
41
+ this.operationHistory = [];
42
+
43
+ // Directory access manager
44
+ this.directoryAccessManager = new DirectoryAccessManager(config, logger);
45
+ }
46
+
47
+ /**
48
+ * Get tool description for LLM consumption
49
+ * @returns {string} Tool description
50
+ */
51
+ getDescription() {
52
+ return `
53
+ File System Tool: Perform file and directory operations safely within the project scope.
54
+
55
+ CRITICAL: Both XML and JSON formats are fully supported. When using XML format, commands are automatically converted to the appropriate structure internally - you don't need to manually wrap them in an actions array.
56
+
57
+ USAGE - TWO FORMATS SUPPORTED:
58
+
59
+ FORMAT 1 - XML STYLE (Recommended):
60
+ [tool id="filesystem"]
61
+ <read file-path="src/index.js" />
62
+ <write output-path="src/components/Button.js">
63
+ const Button = () => {
64
+ return <button>Click me</button>;
65
+ };
66
+ export default Button;
67
+ </write>
68
+ [/tool]
69
+
70
+ FORMAT 2 - JSON STYLE:
71
+ \`\`\`json
72
+ {
73
+ "toolId": "filesystem",
74
+ "actions": [
75
+ {
76
+ "type": "read",
77
+ "filePath": "src/index.js"
78
+ },
79
+ {
80
+ "type": "write",
81
+ "outputPath": "src/components/Button.js",
82
+ "content": "const Button = () => { return <button>Click me</button>; };"
83
+ }
84
+ ]
85
+ }
86
+ \`\`\`
87
+
88
+ IMPORTANT:
89
+ - Use either format consistently. Do NOT mix formats in a single command.
90
+ - XML format is recommended for single operations and better readability
91
+ - JSON format is recommended for complex multi-action operations
92
+ - Both formats work equally well and produce the same results
93
+
94
+ SUPPORTED ACTIONS:
95
+ - read: Read file contents
96
+ - write: Write content to file
97
+ - append: Append content to existing file
98
+ - delete: Delete a file
99
+ - copy: Copy file from source to destination
100
+ - move: Move/rename file
101
+ - create-dir: Create directory
102
+ - list: List directory contents
103
+ - exists: Check if file/directory exists
104
+ - stats: Get file/directory metadata
105
+
106
+ PARAMETERS:
107
+ - file-path: Path to file to read
108
+ - output-path: Path where to write/create file
109
+ - source-path: Source path for copy/move operations
110
+ - dest-path: Destination path for copy/move operations
111
+ - directory: Directory path for directory operations
112
+ - content: Content to write/append
113
+ - encoding: File encoding (default: utf8)
114
+ - create-dirs: Create parent directories if they don't exist (true/false)
115
+
116
+ EXAMPLES:
117
+
118
+ SIMPLE FILE CREATION (XML format - recommended):
119
+ [tool id="filesystem"]
120
+ <write output-path="hello.c">
121
+ #include <stdio.h>
122
+
123
+ int main() {
124
+ printf("Hello, World!\\n");
125
+ return 0;
126
+ }
127
+ </write>
128
+ [/tool]
129
+
130
+ READING FILES (XML format):
131
+ [tool id="filesystem"]
132
+ <read file-path="package.json" />
133
+ [/tool]
134
+
135
+ MULTIPLE OPERATIONS (JSON format recommended):
136
+ \`\`\`json
137
+ {
138
+ "toolId": "filesystem",
139
+ "actions": [
140
+ {
141
+ "type": "read",
142
+ "filePath": "src/index.js"
143
+ },
144
+ {
145
+ "type": "write",
146
+ "outputPath": "src/backup.js",
147
+ "content": "// Backup file\\nconsole.log('backup');"
148
+ }
149
+ ]
150
+ }
151
+ \`\`\`
152
+
153
+ OTHER XML EXAMPLES:
154
+ [tool id="filesystem"]
155
+ <copy source-path="template.js" dest-path="src/component.js" />
156
+ <create-dir directory="src/components/ui" />
157
+ <list directory="src" />
158
+ [/tool]
159
+
160
+ SECURITY:
161
+ - Operations restricted to project directory
162
+ - File size limits enforced (max ${Math.round(this.maxFileSize / 1024 / 1024)}MB)
163
+ - Dangerous file types blocked
164
+ - Path traversal protection
165
+ - Backup created for destructive operations
166
+
167
+ ENCODING SUPPORT:
168
+ - utf8 (default)
169
+ - ascii
170
+ - base64
171
+ - binary
172
+ - hex
173
+ `;
174
+ }
175
+
176
+ /**
177
+ * Parse parameters from tool command content
178
+ * @param {string} content - Raw tool command content
179
+ * @returns {Object} Parsed parameters
180
+ */
181
+ parseParameters(content) {
182
+ try {
183
+ const params = {};
184
+ const actions = [];
185
+
186
+ this.logger?.debug('FileSystem tool parsing parameters', {
187
+ contentLength: content.length,
188
+ contentPreview: content.substring(0, 200)
189
+ });
190
+
191
+ // Extract self-closing tags (read, delete, copy, etc.)
192
+ const selfClosingTags = [
193
+ 'read', 'delete', 'copy', 'move', 'create-dir',
194
+ 'list', 'exists', 'stats', 'append'
195
+ ];
196
+
197
+ for (const tagName of selfClosingTags) {
198
+ const tags = TagParser.extractTagsWithAttributes(content, tagName);
199
+ for (const tag of tags) {
200
+ const action = {
201
+ type: tagName,
202
+ ...tag.attributes
203
+ };
204
+
205
+ // Normalize common attribute names
206
+ if (action['file-path']) {
207
+ action.filePath = action['file-path'];
208
+ delete action['file-path'];
209
+ }
210
+ if (action['output-path']) {
211
+ action.outputPath = action['output-path'];
212
+ delete action['output-path'];
213
+ }
214
+ if (action['source-path']) {
215
+ action.sourcePath = action['source-path'];
216
+ delete action['source-path'];
217
+ }
218
+ if (action['dest-path']) {
219
+ action.destPath = action['dest-path'];
220
+ delete action['dest-path'];
221
+ }
222
+ if (action['create-dirs']) {
223
+ action.createDirs = action['create-dirs'] === 'true';
224
+ delete action['create-dirs'];
225
+ }
226
+
227
+ actions.push(action);
228
+ }
229
+ }
230
+
231
+ // Extract write and append tags with content
232
+ const writeMatches = content.matchAll(/<write\s+([^>]*)>(.*?)<\/write>/gs);
233
+ for (const match of writeMatches) {
234
+ const parser = new TagParser();
235
+ const attributes = parser.parseAttributes(match[1]);
236
+ const writeContent = match[2].trim();
237
+
238
+ const action = {
239
+ type: 'write',
240
+ content: writeContent,
241
+ ...attributes
242
+ };
243
+
244
+ // Normalize attribute names
245
+ if (action['output-path']) {
246
+ action.outputPath = action['output-path'];
247
+ delete action['output-path'];
248
+ }
249
+ if (action['create-dirs']) {
250
+ action.createDirs = action['create-dirs'] === 'true';
251
+ delete action['create-dirs'];
252
+ }
253
+
254
+ actions.push(action);
255
+ }
256
+
257
+ const appendMatches = content.matchAll(/<append\s+([^>]*)>(.*?)<\/append>/gs);
258
+ for (const match of appendMatches) {
259
+ const parser = new TagParser();
260
+ const attributes = parser.parseAttributes(match[1]);
261
+ const appendContent = match[2].trim();
262
+
263
+ const action = {
264
+ type: 'append',
265
+ content: appendContent,
266
+ ...attributes
267
+ };
268
+
269
+ // Normalize attribute names
270
+ if (action['file-path']) {
271
+ action.filePath = action['file-path'];
272
+ delete action['file-path'];
273
+ }
274
+
275
+ actions.push(action);
276
+ }
277
+
278
+ params.actions = actions;
279
+ params.rawContent = content.trim();
280
+
281
+ this.logger?.debug('Parsed FileSystem tool parameters', {
282
+ totalActions: actions.length,
283
+ actionTypes: actions.map(a => a.type),
284
+ actions: actions.map(a => ({
285
+ type: a.type,
286
+ filePath: a.filePath,
287
+ outputPath: a.outputPath,
288
+ hasContent: !!a.content
289
+ }))
290
+ });
291
+
292
+ return params;
293
+
294
+ } catch (error) {
295
+ throw new Error(`Failed to parse filesystem parameters: ${error.message}`);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Get required parameters
301
+ * @returns {Array<string>} Array of required parameter names
302
+ */
303
+ getRequiredParameters() {
304
+ return ['actions'];
305
+ }
306
+
307
+ /**
308
+ * Custom parameter validation
309
+ * @param {Object} params - Parameters to validate
310
+ * @returns {Object} Validation result
311
+ */
312
+ customValidateParameters(params) {
313
+ const errors = [];
314
+
315
+ if (!params.actions || !Array.isArray(params.actions) || params.actions.length === 0) {
316
+ errors.push('At least one action is required');
317
+ } else {
318
+ // Validate each action
319
+ for (const [index, action] of params.actions.entries()) {
320
+ if (!action.type) {
321
+ errors.push(`Action ${index + 1}: type is required`);
322
+ continue;
323
+ }
324
+
325
+ switch (action.type) {
326
+ case 'read':
327
+ case 'delete':
328
+ case 'exists':
329
+ case 'stats':
330
+ if (!action.filePath) {
331
+ errors.push(`Action ${index + 1}: file-path is required for ${action.type}`);
332
+ }
333
+ break;
334
+
335
+ case 'write':
336
+ if (!action.outputPath) {
337
+ errors.push(`Action ${index + 1}: output-path is required for write`);
338
+ }
339
+ if (!action.content && action.content !== '') {
340
+ errors.push(`Action ${index + 1}: content is required for write`);
341
+ }
342
+ break;
343
+
344
+ case 'append':
345
+ if (!action.filePath) {
346
+ errors.push(`Action ${index + 1}: file-path is required for append`);
347
+ }
348
+ if (!action.content && action.content !== '') {
349
+ errors.push(`Action ${index + 1}: content is required for append`);
350
+ }
351
+ break;
352
+
353
+ case 'copy':
354
+ case 'move':
355
+ if (!action.sourcePath) {
356
+ errors.push(`Action ${index + 1}: source-path is required for ${action.type}`);
357
+ }
358
+ if (!action.destPath) {
359
+ errors.push(`Action ${index + 1}: dest-path is required for ${action.type}`);
360
+ }
361
+ break;
362
+
363
+ case 'create-dir':
364
+ case 'list':
365
+ if (!action.directory) {
366
+ errors.push(`Action ${index + 1}: directory is required for ${action.type}`);
367
+ }
368
+ break;
369
+
370
+ default:
371
+ errors.push(`Action ${index + 1}: unknown action type: ${action.type}`);
372
+ }
373
+
374
+ // Validate file extensions if specified
375
+ if (action.filePath && !this.isAllowedFileExtension(action.filePath)) {
376
+ errors.push(`Action ${index + 1}: file type not allowed: ${path.extname(action.filePath)}`);
377
+ }
378
+ if (action.outputPath && !this.isAllowedFileExtension(action.outputPath)) {
379
+ errors.push(`Action ${index + 1}: file type not allowed: ${path.extname(action.outputPath)}`);
380
+ }
381
+ }
382
+ }
383
+
384
+ return {
385
+ valid: errors.length === 0,
386
+ errors
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Execute tool with parsed parameters
392
+ * @param {Object} params - Parsed parameters
393
+ * @param {Object} context - Execution context
394
+ * @returns {Promise<Object>} Execution result
395
+ */
396
+ async execute(params, context) {
397
+ // Validate params structure
398
+ if (!params || typeof params !== 'object') {
399
+ throw new Error('Invalid parameters: params must be an object');
400
+ }
401
+
402
+ const { actions } = params;
403
+
404
+ // Validate actions array
405
+ if (!actions) {
406
+ throw new Error('Invalid parameters: actions is required. Received params: ' + JSON.stringify(Object.keys(params)));
407
+ }
408
+
409
+ if (!Array.isArray(actions)) {
410
+ throw new Error('Invalid parameters: actions must be an array. Received type: ' + typeof actions);
411
+ }
412
+
413
+ if (actions.length === 0) {
414
+ throw new Error('Invalid parameters: actions array is empty');
415
+ }
416
+
417
+ const { projectDir, agentId, directoryAccess } = context;
418
+
419
+ // Get directory access configuration from agent or create default
420
+ const accessConfig = directoryAccess ||
421
+ this.directoryAccessManager.createDirectoryAccess({
422
+ workingDirectory: projectDir || process.cwd(),
423
+ writeEnabledDirectories: [projectDir || process.cwd()],
424
+ restrictToProject: true
425
+ });
426
+
427
+ // IMPORTANT: If the agent has directoryAccess configured, use its workingDirectory
428
+ // This ensures UI-configured project directories are respected
429
+ if (directoryAccess && directoryAccess.workingDirectory) {
430
+ // Agent has explicitly configured working directory from UI - use it
431
+ console.log('FileSystem DEBUG: Using agent configured working directory:', directoryAccess.workingDirectory);
432
+ console.log('FileSystem DEBUG: Full directoryAccess object:', JSON.stringify(directoryAccess, null, 2));
433
+ } else {
434
+ // Using fallback to projectDir or process.cwd()
435
+ console.log('FileSystem DEBUG: Using fallback working directory:', projectDir || process.cwd());
436
+ console.log('FileSystem DEBUG: directoryAccess is:', directoryAccess);
437
+ console.log('FileSystem DEBUG: projectDir is:', projectDir);
438
+ }
439
+
440
+ const results = [];
441
+
442
+ for (const action of actions) {
443
+ try {
444
+ let result;
445
+
446
+ switch (action.type) {
447
+ case 'read':
448
+ result = await this.readFile(action.filePath, accessConfig, action.encoding);
449
+ break;
450
+
451
+ case 'write':
452
+ result = await this.writeFile(action.outputPath, action.content, accessConfig, {
453
+ encoding: action.encoding,
454
+ createDirs: action.createDirs
455
+ });
456
+ break;
457
+
458
+ case 'append':
459
+ result = await this.appendToFile(action.filePath, action.content, accessConfig, action.encoding);
460
+ break;
461
+
462
+ case 'delete':
463
+ result = await this.deleteFile(action.filePath, accessConfig);
464
+ break;
465
+
466
+ case 'copy':
467
+ result = await this.copyFile(action.sourcePath, action.destPath, accessConfig);
468
+ break;
469
+
470
+ case 'move':
471
+ result = await this.moveFile(action.sourcePath, action.destPath, accessConfig);
472
+ break;
473
+
474
+ case 'create-dir':
475
+ result = await this.createDirectory(action.directory, accessConfig);
476
+ break;
477
+
478
+ case 'list':
479
+ result = await this.listDirectory(action.directory, accessConfig);
480
+ break;
481
+
482
+ case 'exists':
483
+ result = await this.checkExists(action.filePath, accessConfig);
484
+ break;
485
+
486
+ case 'stats':
487
+ result = await this.getFileStats(action.filePath, accessConfig);
488
+ break;
489
+
490
+ default:
491
+ throw new Error(`Unknown action type: ${action.type}`);
492
+ }
493
+
494
+ results.push(result);
495
+ this.addToHistory(action, result, context.agentId);
496
+
497
+ } catch (error) {
498
+ const errorResult = {
499
+ success: false,
500
+ action: action.type,
501
+ error: error.message,
502
+ filePath: action.filePath || action.outputPath || action.directory
503
+ };
504
+
505
+ results.push(errorResult);
506
+ this.addToHistory(action, errorResult, context.agentId);
507
+ }
508
+ }
509
+
510
+ return {
511
+ success: true,
512
+ actions: results,
513
+ executedActions: actions.length,
514
+ toolUsed: 'filesys'
515
+ };
516
+ }
517
+
518
+ /**
519
+ * Read file contents
520
+ * @private
521
+ */
522
+ async readFile(filePath, accessConfig, encoding = 'utf8') {
523
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
524
+ const fullPath = this.resolvePath(filePath, workingDir);
525
+
526
+ // Validate read access using DirectoryAccessManager
527
+ const accessResult = this.directoryAccessManager.validateReadAccess(fullPath, accessConfig);
528
+ if (!accessResult.allowed) {
529
+ throw new Error(`Read access denied: ${accessResult.reason} (${accessResult.path})`);
530
+ }
531
+
532
+ try {
533
+ const stats = await fs.stat(fullPath);
534
+
535
+ if (stats.size > this.maxFileSize) {
536
+ throw new Error(`File too large: ${stats.size} bytes (max ${this.maxFileSize})`);
537
+ }
538
+
539
+ const content = await fs.readFile(fullPath, encoding);
540
+
541
+ return {
542
+ success: true,
543
+ action: 'read',
544
+ filePath: this.directoryAccessManager.createRelativePath(fullPath, accessConfig),
545
+ content,
546
+ size: stats.size,
547
+ encoding,
548
+ lastModified: stats.mtime.toISOString(),
549
+ message: `Read ${stats.size} bytes from ${filePath}`
550
+ };
551
+
552
+ } catch (error) {
553
+ throw new Error(`Failed to read file ${filePath}: ${error.message}`);
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Write content to file
559
+ * @private
560
+ */
561
+ async writeFile(outputPath, content, accessConfig, options = {}) {
562
+ const { encoding = 'utf8', createDirs = true } = options;
563
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
564
+ const fullPath = this.resolvePath(outputPath, workingDir);
565
+
566
+ // Validate write access using DirectoryAccessManager
567
+ const accessResult = this.directoryAccessManager.validateWriteAccess(fullPath, accessConfig);
568
+ if (!accessResult.allowed) {
569
+ throw new Error(`Write access denied: ${accessResult.reason} (${accessResult.path})`);
570
+ }
571
+
572
+ try {
573
+ // Check content size
574
+ const contentSize = Buffer.byteLength(content, encoding);
575
+ if (contentSize > this.maxFileSize) {
576
+ throw new Error(`Content too large: ${contentSize} bytes (max ${this.maxFileSize})`);
577
+ }
578
+
579
+ // Create parent directories if requested
580
+ if (createDirs) {
581
+ const dirPath = path.dirname(fullPath);
582
+ await fs.mkdir(dirPath, { recursive: true });
583
+ }
584
+
585
+ // Create backup if file exists
586
+ let backupPath = null;
587
+ try {
588
+ await fs.access(fullPath);
589
+ backupPath = `${fullPath}.backup-${Date.now()}`;
590
+ await fs.copyFile(fullPath, backupPath);
591
+ } catch {
592
+ // File doesn't exist, no backup needed
593
+ }
594
+
595
+ await fs.writeFile(fullPath, content, encoding);
596
+
597
+ const stats = await fs.stat(fullPath);
598
+
599
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
600
+
601
+ return {
602
+ success: true,
603
+ action: 'write',
604
+ outputPath: relativePath,
605
+ fullPath: fullPath,
606
+ size: stats.size,
607
+ encoding,
608
+ backupPath: backupPath ? this.directoryAccessManager.createRelativePath(backupPath, accessConfig) : null,
609
+ backupFullPath: backupPath || null,
610
+ message: `Wrote ${stats.size} bytes to ${fullPath}`
611
+ };
612
+
613
+ } catch (error) {
614
+ throw new Error(`Failed to write file ${fullPath}: ${error.message}`);
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Append content to file
620
+ * @private
621
+ */
622
+ async appendToFile(filePath, content, accessConfig, encoding = 'utf8') {
623
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
624
+ const fullPath = this.resolvePath(filePath, workingDir);
625
+
626
+ // Validate write access using DirectoryAccessManager
627
+ const accessResult = this.directoryAccessManager.validateWriteAccess(fullPath, accessConfig);
628
+ if (!accessResult.allowed) {
629
+ throw new Error(`Write access denied: ${accessResult.reason} (${accessResult.path})`);
630
+ }
631
+
632
+ try {
633
+ // Check if file exists and get current size
634
+ let currentSize = 0;
635
+ try {
636
+ const stats = await fs.stat(fullPath);
637
+ currentSize = stats.size;
638
+ } catch {
639
+ // File doesn't exist, will be created
640
+ }
641
+
642
+ const contentSize = Buffer.byteLength(content, encoding);
643
+ if (currentSize + contentSize > this.maxFileSize) {
644
+ throw new Error(`File would become too large: ${currentSize + contentSize} bytes (max ${this.maxFileSize})`);
645
+ }
646
+
647
+ await fs.appendFile(fullPath, content, encoding);
648
+
649
+ const stats = await fs.stat(fullPath);
650
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
651
+
652
+ return {
653
+ success: true,
654
+ action: 'append',
655
+ filePath: relativePath,
656
+ fullPath: fullPath,
657
+ appendedBytes: contentSize,
658
+ totalSize: stats.size,
659
+ encoding,
660
+ message: `Appended ${contentSize} bytes to ${fullPath}`
661
+ };
662
+
663
+ } catch (error) {
664
+ throw new Error(`Failed to append to file ${fullPath}: ${error.message}`);
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Delete file
670
+ * @private
671
+ */
672
+ async deleteFile(filePath, accessConfig) {
673
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
674
+ const fullPath = this.resolvePath(filePath, workingDir);
675
+
676
+ // Validate write access for deletion
677
+ const accessResult = this.directoryAccessManager.validateWriteAccess(fullPath, accessConfig);
678
+ if (!accessResult.allowed) {
679
+ throw new Error(`Delete access denied: ${accessResult.reason} (${accessResult.path})`);
680
+ }
681
+
682
+ try {
683
+ const stats = await fs.stat(fullPath);
684
+
685
+ // Create backup before deletion
686
+ const backupPath = `${fullPath}.deleted-backup-${Date.now()}`;
687
+ await fs.copyFile(fullPath, backupPath);
688
+
689
+ await fs.unlink(fullPath);
690
+
691
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
692
+ const backupRelativePath = this.directoryAccessManager.createRelativePath(backupPath, accessConfig);
693
+
694
+ return {
695
+ success: true,
696
+ action: 'delete',
697
+ filePath: relativePath,
698
+ fullPath: fullPath,
699
+ size: stats.size,
700
+ backupPath: backupRelativePath,
701
+ backupFullPath: backupPath,
702
+ message: `Deleted ${fullPath} (backup created)`
703
+ };
704
+
705
+ } catch (error) {
706
+ throw new Error(`Failed to delete file ${fullPath}: ${error.message}`);
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Copy file
712
+ * @private
713
+ */
714
+ async copyFile(sourcePath, destPath, accessConfig) {
715
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
716
+ const fullSourcePath = this.resolvePath(sourcePath, workingDir);
717
+ const fullDestPath = this.resolvePath(destPath, workingDir);
718
+
719
+ // Validate read access for source
720
+ const sourceAccessResult = this.directoryAccessManager.validateReadAccess(fullSourcePath, accessConfig);
721
+ if (!sourceAccessResult.allowed) {
722
+ throw new Error(`Source read access denied: ${sourceAccessResult.reason} (${sourceAccessResult.path})`);
723
+ }
724
+
725
+ // Validate write access for destination
726
+ const destAccessResult = this.directoryAccessManager.validateWriteAccess(fullDestPath, accessConfig);
727
+ if (!destAccessResult.allowed) {
728
+ throw new Error(`Destination write access denied: ${destAccessResult.reason} (${destAccessResult.path})`);
729
+ }
730
+
731
+ try {
732
+ const sourceStats = await fs.stat(fullSourcePath);
733
+
734
+ if (sourceStats.size > this.maxFileSize) {
735
+ throw new Error(`Source file too large: ${sourceStats.size} bytes`);
736
+ }
737
+
738
+ // Create destination directory if needed
739
+ const destDir = path.dirname(fullDestPath);
740
+ await fs.mkdir(destDir, { recursive: true });
741
+
742
+ await fs.copyFile(fullSourcePath, fullDestPath);
743
+
744
+ const sourceRelativePath = this.directoryAccessManager.createRelativePath(fullSourcePath, accessConfig);
745
+ const destRelativePath = this.directoryAccessManager.createRelativePath(fullDestPath, accessConfig);
746
+
747
+ return {
748
+ success: true,
749
+ action: 'copy',
750
+ sourcePath: sourceRelativePath,
751
+ destPath: destRelativePath,
752
+ sourceFullPath: fullSourcePath,
753
+ destFullPath: fullDestPath,
754
+ size: sourceStats.size,
755
+ message: `Copied ${fullSourcePath} to ${fullDestPath}`
756
+ };
757
+
758
+ } catch (error) {
759
+ throw new Error(`Failed to copy ${fullSourcePath} to ${fullDestPath}: ${error.message}`);
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Move/rename file
765
+ * @private
766
+ */
767
+ async moveFile(sourcePath, destPath, accessConfig) {
768
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
769
+ const fullSourcePath = this.resolvePath(sourcePath, workingDir);
770
+ const fullDestPath = this.resolvePath(destPath, workingDir);
771
+
772
+ // Validate read access for source
773
+ const readResult = this.directoryAccessManager.validateReadAccess(fullSourcePath, accessConfig);
774
+ if (!readResult.allowed) {
775
+ throw new Error(`Read access denied for source: ${readResult.reason} (${readResult.path})`);
776
+ }
777
+
778
+ // Validate write access for destination
779
+ const writeResult = this.directoryAccessManager.validateWriteAccess(fullDestPath, accessConfig);
780
+ if (!writeResult.allowed) {
781
+ throw new Error(`Write access denied for destination: ${writeResult.reason} (${writeResult.path})`);
782
+ }
783
+
784
+ try {
785
+ const sourceStats = await fs.stat(fullSourcePath);
786
+
787
+ // Create destination directory if needed
788
+ const destDir = path.dirname(fullDestPath);
789
+ await fs.mkdir(destDir, { recursive: true });
790
+
791
+ await fs.rename(fullSourcePath, fullDestPath);
792
+
793
+ const relativeSource = this.directoryAccessManager.createRelativePath(fullSourcePath, accessConfig);
794
+ const relativeDest = this.directoryAccessManager.createRelativePath(fullDestPath, accessConfig);
795
+
796
+ return {
797
+ success: true,
798
+ action: 'move',
799
+ sourcePath: relativeSource,
800
+ destPath: relativeDest,
801
+ fullSourcePath: fullSourcePath,
802
+ fullDestPath: fullDestPath,
803
+ size: sourceStats.size,
804
+ message: `Moved ${sourcePath} to ${destPath}`
805
+ };
806
+
807
+ } catch (error) {
808
+ throw new Error(`Failed to move ${sourcePath} to ${destPath}: ${error.message}`);
809
+ }
810
+ }
811
+
812
+ /**
813
+ * Create directory
814
+ * @private
815
+ */
816
+ async createDirectory(directory, accessConfig) {
817
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
818
+ const fullPath = this.resolvePath(directory, workingDir);
819
+
820
+ // Validate write access using DirectoryAccessManager
821
+ const accessResult = this.directoryAccessManager.validateWriteAccess(fullPath, accessConfig);
822
+ if (!accessResult.allowed) {
823
+ throw new Error(`Write access denied: ${accessResult.reason} (${accessResult.path})`);
824
+ }
825
+
826
+ try {
827
+ await fs.mkdir(fullPath, { recursive: true });
828
+
829
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
830
+
831
+ return {
832
+ success: true,
833
+ action: 'create-dir',
834
+ directory: relativePath,
835
+ fullPath: fullPath,
836
+ message: `Created directory ${directory}`
837
+ };
838
+
839
+ } catch (error) {
840
+ throw new Error(`Failed to create directory ${directory}: ${error.message}`);
841
+ }
842
+ }
843
+
844
+ /**
845
+ * List directory contents
846
+ * @private
847
+ */
848
+ async listDirectory(directory, accessConfig) {
849
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
850
+ const fullPath = this.resolvePath(directory, workingDir);
851
+
852
+ // Validate read access using DirectoryAccessManager
853
+ const accessResult = this.directoryAccessManager.validateReadAccess(fullPath, accessConfig);
854
+ if (!accessResult.allowed) {
855
+ throw new Error(`Read access denied: ${accessResult.reason} (${accessResult.path})`);
856
+ }
857
+
858
+ try {
859
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
860
+
861
+ const contents = [];
862
+ for (const entry of entries) {
863
+ const entryPath = path.join(fullPath, entry.name);
864
+ const stats = await fs.stat(entryPath);
865
+
866
+ contents.push({
867
+ name: entry.name,
868
+ type: entry.isDirectory() ? 'directory' : 'file',
869
+ size: entry.isFile() ? stats.size : undefined,
870
+ lastModified: stats.mtime.toISOString(),
871
+ permissions: stats.mode,
872
+ isSymlink: entry.isSymbolicLink()
873
+ });
874
+ }
875
+
876
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
877
+
878
+ return {
879
+ success: true,
880
+ action: 'list',
881
+ directory: relativePath,
882
+ fullPath: fullPath,
883
+ contents,
884
+ totalItems: contents.length,
885
+ directories: contents.filter(item => item.type === 'directory').length,
886
+ files: contents.filter(item => item.type === 'file').length,
887
+ message: `Listed ${contents.length} items in ${directory}`
888
+ };
889
+
890
+ } catch (error) {
891
+ throw new Error(`Failed to list directory ${directory}: ${error.message}`);
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Check if file/directory exists
897
+ * @private
898
+ */
899
+ async checkExists(filePath, accessConfig) {
900
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
901
+ const fullPath = this.resolvePath(filePath, workingDir);
902
+
903
+ // Validate read access
904
+ const accessResult = this.directoryAccessManager.validateReadAccess(fullPath, accessConfig);
905
+ if (!accessResult.allowed) {
906
+ throw new Error(`Read access denied: ${accessResult.reason} (${accessResult.path})`);
907
+ }
908
+
909
+ try {
910
+ const stats = await fs.stat(fullPath);
911
+
912
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
913
+
914
+ return {
915
+ success: true,
916
+ action: 'exists',
917
+ filePath: relativePath,
918
+ fullPath: fullPath,
919
+ exists: true,
920
+ type: stats.isDirectory() ? 'directory' : 'file',
921
+ message: `${filePath} exists as ${stats.isDirectory() ? 'directory' : 'file'}`
922
+ };
923
+
924
+ } catch (error) {
925
+ if (error.code === 'ENOENT') {
926
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
927
+ return {
928
+ success: true,
929
+ action: 'exists',
930
+ filePath: relativePath,
931
+ fullPath: fullPath,
932
+ exists: false,
933
+ message: `${filePath} does not exist`
934
+ };
935
+ }
936
+
937
+ throw new Error(`Failed to check existence of ${filePath}: ${error.message}`);
938
+ }
939
+ }
940
+
941
+ /**
942
+ * Get file statistics
943
+ * @private
944
+ */
945
+ async getFileStats(filePath, accessConfig) {
946
+ const workingDir = this.directoryAccessManager.getWorkingDirectory(accessConfig);
947
+ const fullPath = this.resolvePath(filePath, workingDir);
948
+
949
+ // Validate read access
950
+ const accessResult = this.directoryAccessManager.validateReadAccess(fullPath, accessConfig);
951
+ if (!accessResult.allowed) {
952
+ throw new Error(`Read access denied: ${accessResult.reason} (${accessResult.path})`);
953
+ }
954
+
955
+ try {
956
+ const stats = await fs.stat(fullPath);
957
+
958
+ const relativePath = this.directoryAccessManager.createRelativePath(fullPath, accessConfig);
959
+
960
+ return {
961
+ success: true,
962
+ action: 'stats',
963
+ filePath: relativePath,
964
+ fullPath: fullPath,
965
+ stats: {
966
+ size: stats.size,
967
+ type: stats.isDirectory() ? 'directory' : 'file',
968
+ lastModified: stats.mtime.toISOString(),
969
+ lastAccessed: stats.atime.toISOString(),
970
+ created: stats.birthtime.toISOString(),
971
+ permissions: stats.mode,
972
+ isSymlink: stats.isSymbolicLink()
973
+ },
974
+ message: `Retrieved stats for ${filePath}`
975
+ };
976
+
977
+ } catch (error) {
978
+ throw new Error(`Failed to get stats for ${filePath}: ${error.message}`);
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Resolve file path safely (legacy method for compatibility)
984
+ * @private
985
+ */
986
+ resolvePath(filePath, workingDir) {
987
+ if (path.isAbsolute(filePath)) {
988
+ return path.normalize(filePath);
989
+ }
990
+ return path.resolve(workingDir, filePath);
991
+ }
992
+
993
+ /**
994
+ * Validate path access using DirectoryAccessManager
995
+ * @private
996
+ */
997
+ validatePathAccess(fullPath, accessConfig, operation = 'read') {
998
+ const accessResult = operation === 'write'
999
+ ? this.directoryAccessManager.validateWriteAccess(fullPath, accessConfig)
1000
+ : this.directoryAccessManager.validateReadAccess(fullPath, accessConfig);
1001
+
1002
+ if (!accessResult.allowed) {
1003
+ throw new Error(`${operation} access denied: ${accessResult.reason} (${accessResult.path})`);
1004
+ }
1005
+
1006
+ return accessResult;
1007
+ }
1008
+
1009
+ /**
1010
+ * Check if file extension is allowed
1011
+ * @private
1012
+ */
1013
+ isAllowedFileExtension(filePath) {
1014
+ const ext = path.extname(filePath).toLowerCase();
1015
+
1016
+ if (this.blockedExtensions.includes(ext)) {
1017
+ return false;
1018
+ }
1019
+
1020
+ if (this.allowedExtensions && !this.allowedExtensions.includes(ext)) {
1021
+ return false;
1022
+ }
1023
+
1024
+ return true;
1025
+ }
1026
+
1027
+ /**
1028
+ * Add operation to history
1029
+ * @private
1030
+ */
1031
+ addToHistory(action, result, agentId) {
1032
+ const historyEntry = {
1033
+ timestamp: new Date().toISOString(),
1034
+ agentId,
1035
+ action: action.type,
1036
+ filePath: action.filePath || action.outputPath || action.directory,
1037
+ success: result.success,
1038
+ size: result.size
1039
+ };
1040
+
1041
+ this.operationHistory.push(historyEntry);
1042
+
1043
+ // Keep only last 200 entries
1044
+ if (this.operationHistory.length > 200) {
1045
+ this.operationHistory = this.operationHistory.slice(-200);
1046
+ }
1047
+ }
1048
+
1049
+ /**
1050
+ * Get supported actions for this tool
1051
+ * @returns {Array<string>} Array of supported action names
1052
+ */
1053
+ getSupportedActions() {
1054
+ return [
1055
+ 'read', 'write', 'append', 'delete', 'copy', 'move',
1056
+ 'create-dir', 'list', 'exists', 'stats'
1057
+ ];
1058
+ }
1059
+
1060
+ /**
1061
+ * Get parameter schema for validation
1062
+ * @returns {Object} Parameter schema
1063
+ */
1064
+ getParameterSchema() {
1065
+ return {
1066
+ type: 'object',
1067
+ properties: {
1068
+ actions: {
1069
+ type: 'array',
1070
+ minItems: 1,
1071
+ items: {
1072
+ type: 'object',
1073
+ properties: {
1074
+ type: {
1075
+ type: 'string',
1076
+ enum: this.getSupportedActions()
1077
+ },
1078
+ filePath: { type: 'string' },
1079
+ outputPath: { type: 'string' },
1080
+ sourcePath: { type: 'string' },
1081
+ destPath: { type: 'string' },
1082
+ directory: { type: 'string' },
1083
+ content: { type: 'string' },
1084
+ encoding: { type: 'string' },
1085
+ createDirs: { type: 'boolean' }
1086
+ },
1087
+ required: ['type']
1088
+ }
1089
+ }
1090
+ },
1091
+ required: ['actions']
1092
+ };
1093
+ }
1094
+
1095
+ /**
1096
+ * Get operation history for debugging
1097
+ * @returns {Array} Operation history
1098
+ */
1099
+ getOperationHistory(agentId = null) {
1100
+ if (agentId) {
1101
+ return this.operationHistory.filter(entry => entry.agentId === agentId);
1102
+ }
1103
+ return [...this.operationHistory];
1104
+ }
1105
+ }
1106
+
1107
+ export default FileSystemTool;