@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.
- package/LICENSE +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- 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;
|