@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,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileTreeTool - Generate hierarchical file tree representations
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Generate ASCII tree of project directory structure
|
|
6
|
+
* - Help agents understand project organization
|
|
7
|
+
* - Support filtering by file types (whitelist/blacklist)
|
|
8
|
+
* - Respect directory access permissions
|
|
9
|
+
* - Provide focused views of codebases
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { BaseTool } from './baseTool.js';
|
|
13
|
+
import { promises as fs } from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
|
|
16
|
+
// Configuration constants
|
|
17
|
+
const TREE_CONFIG = {
|
|
18
|
+
// Depth limits
|
|
19
|
+
DEFAULT_MAX_DEPTH: 3,
|
|
20
|
+
MAX_DEPTH_LIMIT: 10, // Absolute maximum to prevent performance issues
|
|
21
|
+
|
|
22
|
+
// Size limits
|
|
23
|
+
MAX_FILES_IN_TREE: 10000, // Maximum files to include
|
|
24
|
+
MAX_DIRECTORIES: 1000, // Maximum directories to scan
|
|
25
|
+
|
|
26
|
+
// Display settings
|
|
27
|
+
SHOW_FILES_DEFAULT: true,
|
|
28
|
+
SHOW_HIDDEN_DEFAULT: false,
|
|
29
|
+
SHOW_SIZES_DEFAULT: false,
|
|
30
|
+
|
|
31
|
+
// Tree symbols
|
|
32
|
+
SYMBOLS: {
|
|
33
|
+
BRANCH: '├── ',
|
|
34
|
+
LAST_BRANCH: '└── ',
|
|
35
|
+
VERTICAL: '│ ',
|
|
36
|
+
INDENT: ' '
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Default directories to ignore
|
|
41
|
+
const DEFAULT_IGNORE_DIRECTORIES = [
|
|
42
|
+
'node_modules',
|
|
43
|
+
'.git',
|
|
44
|
+
'dist',
|
|
45
|
+
'build',
|
|
46
|
+
'coverage',
|
|
47
|
+
'.next',
|
|
48
|
+
'.nuxt',
|
|
49
|
+
'.cache',
|
|
50
|
+
'out',
|
|
51
|
+
'target',
|
|
52
|
+
'vendor',
|
|
53
|
+
'bower_components',
|
|
54
|
+
'.vscode',
|
|
55
|
+
'.idea',
|
|
56
|
+
'.vs',
|
|
57
|
+
'__pycache__',
|
|
58
|
+
'venv',
|
|
59
|
+
'env',
|
|
60
|
+
'.env',
|
|
61
|
+
'.pytest_cache',
|
|
62
|
+
'.mypy_cache',
|
|
63
|
+
'eggs',
|
|
64
|
+
'.eggs',
|
|
65
|
+
'lib',
|
|
66
|
+
'lib64',
|
|
67
|
+
'parts',
|
|
68
|
+
'sdist',
|
|
69
|
+
'wheels',
|
|
70
|
+
'.tox',
|
|
71
|
+
'.nox',
|
|
72
|
+
'htmlcov',
|
|
73
|
+
'.hypothesis',
|
|
74
|
+
'tmp',
|
|
75
|
+
'temp',
|
|
76
|
+
'.tmp',
|
|
77
|
+
'.temp'
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Default file extensions to ignore (blacklist)
|
|
81
|
+
const DEFAULT_IGNORE_EXTENSIONS = [
|
|
82
|
+
'.map',
|
|
83
|
+
'.min.js',
|
|
84
|
+
'.min.css',
|
|
85
|
+
'.lock',
|
|
86
|
+
'.log',
|
|
87
|
+
'.tmp',
|
|
88
|
+
'.temp',
|
|
89
|
+
'.pyc',
|
|
90
|
+
'.pyo',
|
|
91
|
+
'.pyd',
|
|
92
|
+
'.so',
|
|
93
|
+
'.dll',
|
|
94
|
+
'.dylib',
|
|
95
|
+
'.exe',
|
|
96
|
+
'.obj',
|
|
97
|
+
'.o',
|
|
98
|
+
'.a',
|
|
99
|
+
'.class',
|
|
100
|
+
'.jar',
|
|
101
|
+
'.war'
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Common binary/media file extensions to optionally ignore
|
|
105
|
+
const BINARY_EXTENSIONS = [
|
|
106
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg',
|
|
107
|
+
'.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
|
|
108
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wmv',
|
|
109
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
110
|
+
'.bin', '.dat', '.db', '.sqlite'
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
class FileTreeTool extends BaseTool {
|
|
114
|
+
constructor(config = {}, logger = null) {
|
|
115
|
+
super(config, logger);
|
|
116
|
+
|
|
117
|
+
// Tool metadata
|
|
118
|
+
this.requiresProject = true;
|
|
119
|
+
this.isAsync = true;
|
|
120
|
+
this.timeout = config.timeout || 120000; // 2 minutes default
|
|
121
|
+
|
|
122
|
+
// Merge config with defaults
|
|
123
|
+
this.treeConfig = {
|
|
124
|
+
...TREE_CONFIG,
|
|
125
|
+
...config.treeConfig
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Track statistics during tree generation
|
|
129
|
+
this.stats = {
|
|
130
|
+
filesCount: 0,
|
|
131
|
+
directoriesCount: 0,
|
|
132
|
+
skippedCount: 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get tool description for LLM consumption
|
|
138
|
+
* @returns {string} Tool description
|
|
139
|
+
*/
|
|
140
|
+
getDescription() {
|
|
141
|
+
return `
|
|
142
|
+
File Tree Tool: Generate hierarchical tree representation of project directory structure.
|
|
143
|
+
|
|
144
|
+
XML USAGE:
|
|
145
|
+
<file-tree>
|
|
146
|
+
<directory>src</directory>
|
|
147
|
+
<max-depth>4</max-depth>
|
|
148
|
+
<include-extensions>.js,.jsx,.ts,.tsx</include-extensions>
|
|
149
|
+
<exclude-extensions>.test.js,.spec.js</exclude-extensions>
|
|
150
|
+
<show-files>true</show-files>
|
|
151
|
+
<show-hidden>false</show-hidden>
|
|
152
|
+
</file-tree>
|
|
153
|
+
|
|
154
|
+
JSON USAGE:
|
|
155
|
+
\`\`\`json
|
|
156
|
+
{
|
|
157
|
+
"toolId": "file-tree",
|
|
158
|
+
"directory": "src",
|
|
159
|
+
"maxDepth": 4,
|
|
160
|
+
"includeExtensions": [".js", ".jsx", ".ts", ".tsx"],
|
|
161
|
+
"excludeExtensions": [".test.js", ".spec.js"],
|
|
162
|
+
"showFiles": true,
|
|
163
|
+
"showHidden": false
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
PARAMETERS:
|
|
168
|
+
|
|
169
|
+
directory (optional):
|
|
170
|
+
- Directory to scan (default: working directory)
|
|
171
|
+
- Can be relative or absolute path
|
|
172
|
+
- Examples: "src", "src/components", "/home/user/project"
|
|
173
|
+
|
|
174
|
+
maxDepth (optional):
|
|
175
|
+
- Maximum depth to scan (default: ${TREE_CONFIG.DEFAULT_MAX_DEPTH})
|
|
176
|
+
- Range: 1-${TREE_CONFIG.MAX_DEPTH_LIMIT}
|
|
177
|
+
- Deeper = more detail, slower performance
|
|
178
|
+
|
|
179
|
+
includeExtensions (optional - WHITELIST):
|
|
180
|
+
- Include ONLY these file extensions
|
|
181
|
+
- Comma-separated list
|
|
182
|
+
- Examples: ".js,.jsx,.ts,.tsx" or ".py,.pyw"
|
|
183
|
+
- Takes precedence over excludeExtensions
|
|
184
|
+
|
|
185
|
+
excludeExtensions (optional - BLACKLIST):
|
|
186
|
+
- Exclude these file extensions
|
|
187
|
+
- Comma-separated list
|
|
188
|
+
- Examples: ".test.js,.spec.js,.min.js"
|
|
189
|
+
- Only applies if includeExtensions is not set
|
|
190
|
+
|
|
191
|
+
excludeDirectories (optional):
|
|
192
|
+
- Additional directories to ignore
|
|
193
|
+
- Already ignores: node_modules, .git, dist, build, etc.
|
|
194
|
+
- Examples: "tests,fixtures,mocks"
|
|
195
|
+
|
|
196
|
+
showFiles (optional):
|
|
197
|
+
- Whether to show files (default: true)
|
|
198
|
+
- Set to false to show only directory structure
|
|
199
|
+
|
|
200
|
+
showHidden (optional):
|
|
201
|
+
- Whether to show hidden files/folders (default: false)
|
|
202
|
+
- Hidden = starts with '.'
|
|
203
|
+
|
|
204
|
+
showSizes (optional):
|
|
205
|
+
- Whether to show file sizes (default: false)
|
|
206
|
+
- Format: KB, MB
|
|
207
|
+
|
|
208
|
+
ignoreBinaryFiles (optional):
|
|
209
|
+
- Whether to ignore binary/media files (default: false)
|
|
210
|
+
- Ignores: images, videos, archives, fonts, etc.
|
|
211
|
+
|
|
212
|
+
FILTERING STRATEGIES:
|
|
213
|
+
|
|
214
|
+
Strategy 1: WHITELIST (Include Only)
|
|
215
|
+
<file-tree>
|
|
216
|
+
<directory>src</directory>
|
|
217
|
+
<include-extensions>.js,.jsx</include-extensions>
|
|
218
|
+
</file-tree>
|
|
219
|
+
Result: Shows ONLY .js and .jsx files
|
|
220
|
+
|
|
221
|
+
Strategy 2: BLACKLIST (Exclude)
|
|
222
|
+
<file-tree>
|
|
223
|
+
<directory>src</directory>
|
|
224
|
+
<exclude-extensions>.test.js,.spec.js</exclude-extensions>
|
|
225
|
+
</file-tree>
|
|
226
|
+
Result: Shows all files EXCEPT .test.js and .spec.js
|
|
227
|
+
|
|
228
|
+
Strategy 3: COMBINED
|
|
229
|
+
<file-tree>
|
|
230
|
+
<directory>src</directory>
|
|
231
|
+
<include-extensions>.js,.jsx,.ts,.tsx</include-extensions>
|
|
232
|
+
<exclude-extensions>.test.js,.spec.ts</exclude-extensions>
|
|
233
|
+
</file-tree>
|
|
234
|
+
Result: Shows JS/JSX/TS/TSX files, but excludes test files
|
|
235
|
+
|
|
236
|
+
EXAMPLES:
|
|
237
|
+
|
|
238
|
+
Example 1 - Basic tree:
|
|
239
|
+
<file-tree>
|
|
240
|
+
<directory>.</directory>
|
|
241
|
+
</file-tree>
|
|
242
|
+
|
|
243
|
+
Example 2 - JavaScript/TypeScript only:
|
|
244
|
+
<file-tree>
|
|
245
|
+
<directory>src</directory>
|
|
246
|
+
<include-extensions>.js,.jsx,.ts,.tsx</include-extensions>
|
|
247
|
+
<max-depth>5</max-depth>
|
|
248
|
+
</file-tree>
|
|
249
|
+
|
|
250
|
+
Example 3 - Python project structure:
|
|
251
|
+
<file-tree>
|
|
252
|
+
<directory>.</directory>
|
|
253
|
+
<include-extensions>.py</include-extensions>
|
|
254
|
+
<exclude-extensions>.pyc</exclude-extensions>
|
|
255
|
+
<show-files>true</show-files>
|
|
256
|
+
</file-tree>
|
|
257
|
+
|
|
258
|
+
Example 4 - Directory structure only:
|
|
259
|
+
<file-tree>
|
|
260
|
+
<directory>src</directory>
|
|
261
|
+
<show-files>false</show-files>
|
|
262
|
+
<max-depth>3</max-depth>
|
|
263
|
+
</file-tree>
|
|
264
|
+
|
|
265
|
+
Example 5 - Exclude test files:
|
|
266
|
+
<file-tree>
|
|
267
|
+
<directory>src</directory>
|
|
268
|
+
<exclude-extensions>.test.js,.spec.js,.test.ts,.spec.ts</exclude-extensions>
|
|
269
|
+
<exclude-directories>__tests__,tests,specs</exclude-directories>
|
|
270
|
+
</file-tree>
|
|
271
|
+
|
|
272
|
+
OUTPUT FORMAT:
|
|
273
|
+
src/
|
|
274
|
+
├── components/
|
|
275
|
+
│ ├── Header.jsx
|
|
276
|
+
│ ├── Footer.jsx
|
|
277
|
+
│ └── Layout.jsx
|
|
278
|
+
├── utils/
|
|
279
|
+
│ ├── helpers.js
|
|
280
|
+
│ └── config.js
|
|
281
|
+
└── index.js
|
|
282
|
+
|
|
283
|
+
AUTOMATIC IGNORES:
|
|
284
|
+
The tool automatically ignores common directories:
|
|
285
|
+
- node_modules, .git, dist, build, coverage
|
|
286
|
+
- .next, .nuxt, .cache, out, target
|
|
287
|
+
- .vscode, .idea, __pycache__, venv
|
|
288
|
+
And more (see full list in DEFAULT_IGNORE_DIRECTORIES)
|
|
289
|
+
|
|
290
|
+
LIMITATIONS:
|
|
291
|
+
- Maximum depth: ${TREE_CONFIG.MAX_DEPTH_LIMIT}
|
|
292
|
+
- Maximum files: ${TREE_CONFIG.MAX_FILES_IN_TREE}
|
|
293
|
+
- Maximum directories: ${TREE_CONFIG.MAX_DIRECTORIES}
|
|
294
|
+
- Binary files can be filtered with ignoreBinaryFiles option
|
|
295
|
+
|
|
296
|
+
MULTI-DIRECTORY SUPPORT:
|
|
297
|
+
Works with agent's configured accessible directories.
|
|
298
|
+
Validates paths against directoryAccess configuration.
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse parameters from tool command content
|
|
304
|
+
* @param {string} content - Raw tool command content
|
|
305
|
+
* @returns {Object} Parsed parameters
|
|
306
|
+
*/
|
|
307
|
+
parseParameters(content) {
|
|
308
|
+
try {
|
|
309
|
+
// Try JSON first
|
|
310
|
+
if (content.trim().startsWith('{')) {
|
|
311
|
+
const parsed = JSON.parse(content);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
directory: parsed.directory || '.',
|
|
315
|
+
maxDepth: parsed.maxDepth || TREE_CONFIG.DEFAULT_MAX_DEPTH,
|
|
316
|
+
includeExtensions: parsed.includeExtensions || [],
|
|
317
|
+
excludeExtensions: parsed.excludeExtensions || [],
|
|
318
|
+
excludeDirectories: parsed.excludeDirectories || [],
|
|
319
|
+
showFiles: parsed.showFiles !== false,
|
|
320
|
+
showHidden: parsed.showHidden === true,
|
|
321
|
+
showSizes: parsed.showSizes === true,
|
|
322
|
+
ignoreBinaryFiles: parsed.ignoreBinaryFiles === true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// XML parsing
|
|
327
|
+
const params = {
|
|
328
|
+
directory: '.',
|
|
329
|
+
maxDepth: TREE_CONFIG.DEFAULT_MAX_DEPTH,
|
|
330
|
+
includeExtensions: [],
|
|
331
|
+
excludeExtensions: [],
|
|
332
|
+
excludeDirectories: [],
|
|
333
|
+
showFiles: true,
|
|
334
|
+
showHidden: false,
|
|
335
|
+
showSizes: false,
|
|
336
|
+
ignoreBinaryFiles: false
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Extract directory
|
|
340
|
+
const dirPattern = /<directory>(.*?)<\/directory>/i;
|
|
341
|
+
const dirMatch = dirPattern.exec(content);
|
|
342
|
+
if (dirMatch) {
|
|
343
|
+
params.directory = dirMatch[1].trim();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Extract max-depth
|
|
347
|
+
const depthPattern = /<max-depth>(\d+)<\/max-depth>/i;
|
|
348
|
+
const depthMatch = depthPattern.exec(content);
|
|
349
|
+
if (depthMatch) {
|
|
350
|
+
params.maxDepth = parseInt(depthMatch[1], 10);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Extract include-extensions (whitelist)
|
|
354
|
+
const includePattern = /<include-extensions>(.*?)<\/include-extensions>/i;
|
|
355
|
+
const includeMatch = includePattern.exec(content);
|
|
356
|
+
if (includeMatch) {
|
|
357
|
+
params.includeExtensions = includeMatch[1]
|
|
358
|
+
.split(',')
|
|
359
|
+
.map(ext => ext.trim())
|
|
360
|
+
.filter(ext => ext.length > 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Extract exclude-extensions (blacklist)
|
|
364
|
+
const excludePattern = /<exclude-extensions>(.*?)<\/exclude-extensions>/i;
|
|
365
|
+
const excludeMatch = excludePattern.exec(content);
|
|
366
|
+
if (excludeMatch) {
|
|
367
|
+
params.excludeExtensions = excludeMatch[1]
|
|
368
|
+
.split(',')
|
|
369
|
+
.map(ext => ext.trim())
|
|
370
|
+
.filter(ext => ext.length > 0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Extract exclude-directories
|
|
374
|
+
const excludeDirsPattern = /<exclude-directories>(.*?)<\/exclude-directories>/i;
|
|
375
|
+
const excludeDirsMatch = excludeDirsPattern.exec(content);
|
|
376
|
+
if (excludeDirsMatch) {
|
|
377
|
+
params.excludeDirectories = excludeDirsMatch[1]
|
|
378
|
+
.split(',')
|
|
379
|
+
.map(dir => dir.trim())
|
|
380
|
+
.filter(dir => dir.length > 0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Extract boolean flags
|
|
384
|
+
const showFilesPattern = /<show-files>(.*?)<\/show-files>/i;
|
|
385
|
+
const showFilesMatch = showFilesPattern.exec(content);
|
|
386
|
+
if (showFilesMatch) {
|
|
387
|
+
params.showFiles = showFilesMatch[1].trim().toLowerCase() !== 'false';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const showHiddenPattern = /<show-hidden>(.*?)<\/show-hidden>/i;
|
|
391
|
+
const showHiddenMatch = showHiddenPattern.exec(content);
|
|
392
|
+
if (showHiddenMatch) {
|
|
393
|
+
params.showHidden = showHiddenMatch[1].trim().toLowerCase() === 'true';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const showSizesPattern = /<show-sizes>(.*?)<\/show-sizes>/i;
|
|
397
|
+
const showSizesMatch = showSizesPattern.exec(content);
|
|
398
|
+
if (showSizesMatch) {
|
|
399
|
+
params.showSizes = showSizesMatch[1].trim().toLowerCase() === 'true';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const ignoreBinaryPattern = /<ignore-binary-files>(.*?)<\/ignore-binary-files>/i;
|
|
403
|
+
const ignoreBinaryMatch = ignoreBinaryPattern.exec(content);
|
|
404
|
+
if (ignoreBinaryMatch) {
|
|
405
|
+
params.ignoreBinaryFiles = ignoreBinaryMatch[1].trim().toLowerCase() === 'true';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return params;
|
|
409
|
+
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger?.error('Failed to parse file-tree parameters', { error: error.message });
|
|
412
|
+
return {
|
|
413
|
+
directory: '.',
|
|
414
|
+
maxDepth: TREE_CONFIG.DEFAULT_MAX_DEPTH,
|
|
415
|
+
includeExtensions: [],
|
|
416
|
+
excludeExtensions: [],
|
|
417
|
+
excludeDirectories: [],
|
|
418
|
+
showFiles: true,
|
|
419
|
+
showHidden: false,
|
|
420
|
+
showSizes: false,
|
|
421
|
+
ignoreBinaryFiles: false,
|
|
422
|
+
parseError: error.message
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get required parameters
|
|
429
|
+
* @returns {Array<string>} Array of required parameter names
|
|
430
|
+
*/
|
|
431
|
+
getRequiredParameters() {
|
|
432
|
+
return []; // All parameters are optional
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Custom parameter validation
|
|
437
|
+
* @param {Object} params - Parameters to validate
|
|
438
|
+
* @returns {Object} Validation result
|
|
439
|
+
*/
|
|
440
|
+
customValidateParameters(params) {
|
|
441
|
+
const errors = [];
|
|
442
|
+
|
|
443
|
+
// Validate maxDepth
|
|
444
|
+
if (params.maxDepth !== undefined) {
|
|
445
|
+
if (typeof params.maxDepth !== 'number' || params.maxDepth < 1) {
|
|
446
|
+
errors.push('max-depth must be a positive number');
|
|
447
|
+
} else if (params.maxDepth > this.treeConfig.MAX_DEPTH_LIMIT) {
|
|
448
|
+
errors.push(`max-depth cannot exceed ${this.treeConfig.MAX_DEPTH_LIMIT}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Validate directory path
|
|
453
|
+
if (params.directory && params.directory.includes('..')) {
|
|
454
|
+
errors.push('Path traversal (..) not allowed for security');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Validate includeExtensions format
|
|
458
|
+
if (params.includeExtensions !== undefined && params.includeExtensions !== null) {
|
|
459
|
+
if (!Array.isArray(params.includeExtensions)) {
|
|
460
|
+
errors.push('includeExtensions must be an array');
|
|
461
|
+
} else {
|
|
462
|
+
for (const ext of params.includeExtensions) {
|
|
463
|
+
if (!ext.startsWith('.')) {
|
|
464
|
+
errors.push(`Extension "${ext}" must start with a dot (e.g., ".js")`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Validate excludeExtensions format
|
|
471
|
+
if (params.excludeExtensions !== undefined && params.excludeExtensions !== null) {
|
|
472
|
+
if (!Array.isArray(params.excludeExtensions)) {
|
|
473
|
+
errors.push('excludeExtensions must be an array');
|
|
474
|
+
} else {
|
|
475
|
+
for (const ext of params.excludeExtensions) {
|
|
476
|
+
if (!ext.startsWith('.')) {
|
|
477
|
+
errors.push(`Extension "${ext}" must start with a dot (e.g., ".js")`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Validate excludeDirectories format
|
|
484
|
+
if (params.excludeDirectories !== undefined && params.excludeDirectories !== null) {
|
|
485
|
+
if (!Array.isArray(params.excludeDirectories)) {
|
|
486
|
+
errors.push('excludeDirectories must be an array');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Throw error if validation fails
|
|
491
|
+
if (errors.length > 0) {
|
|
492
|
+
throw new Error(`Parameter validation failed: ${errors.join(', ')}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
valid: true,
|
|
497
|
+
errors: []
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Execute tool with parsed parameters
|
|
503
|
+
* @param {Object} params - Parsed parameters
|
|
504
|
+
* @param {Object} context - Execution context
|
|
505
|
+
* @returns {Promise<Object>} Execution result
|
|
506
|
+
*/
|
|
507
|
+
async execute(params, context) {
|
|
508
|
+
// Extract params with defaults
|
|
509
|
+
const {
|
|
510
|
+
directory = '.',
|
|
511
|
+
maxDepth = TREE_CONFIG.DEFAULT_MAX_DEPTH,
|
|
512
|
+
includeExtensions = [],
|
|
513
|
+
excludeExtensions = [],
|
|
514
|
+
excludeDirectories = [],
|
|
515
|
+
showFiles = true,
|
|
516
|
+
showHidden = false,
|
|
517
|
+
showSizes = false,
|
|
518
|
+
ignoreBinaryFiles = false
|
|
519
|
+
} = params;
|
|
520
|
+
const { projectDir, agentId, directoryAccess } = context;
|
|
521
|
+
|
|
522
|
+
// Determine working directory (respect multi-directory access)
|
|
523
|
+
let workingDirectory = projectDir || process.cwd();
|
|
524
|
+
|
|
525
|
+
if (directoryAccess && directoryAccess.workingDirectory) {
|
|
526
|
+
workingDirectory = directoryAccess.workingDirectory;
|
|
527
|
+
this.logger?.info('Using agent configured working directory', {
|
|
528
|
+
workingDirectory: directoryAccess.workingDirectory,
|
|
529
|
+
agentId
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Resolve target directory
|
|
534
|
+
const targetDir = path.isAbsolute(directory)
|
|
535
|
+
? directory
|
|
536
|
+
: path.resolve(workingDirectory, directory);
|
|
537
|
+
|
|
538
|
+
// Validate directory exists
|
|
539
|
+
try {
|
|
540
|
+
const stats = await fs.stat(targetDir);
|
|
541
|
+
if (!stats.isDirectory()) {
|
|
542
|
+
throw new Error('Path is not a directory');
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw new Error(`Directory does not exist or is inaccessible: ${directory}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this.logger?.info('Generating file tree', {
|
|
549
|
+
directory,
|
|
550
|
+
targetDir,
|
|
551
|
+
maxDepth,
|
|
552
|
+
includeExtensions: includeExtensions?.length || 0,
|
|
553
|
+
excludeExtensions: excludeExtensions?.length || 0,
|
|
554
|
+
agentId
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Reset statistics
|
|
558
|
+
this.stats = {
|
|
559
|
+
filesCount: 0,
|
|
560
|
+
directoriesCount: 0,
|
|
561
|
+
skippedCount: 0
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// Build ignore lists (ensure arrays exist)
|
|
565
|
+
const ignoreDirs = [...DEFAULT_IGNORE_DIRECTORIES, ...(excludeDirectories || [])];
|
|
566
|
+
const ignoreExts = [...DEFAULT_IGNORE_EXTENSIONS];
|
|
567
|
+
|
|
568
|
+
if (ignoreBinaryFiles) {
|
|
569
|
+
ignoreExts.push(...BINARY_EXTENSIONS);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Generate tree (ensure arrays exist)
|
|
573
|
+
const tree = await this.buildTree(
|
|
574
|
+
targetDir,
|
|
575
|
+
targetDir,
|
|
576
|
+
0,
|
|
577
|
+
maxDepth,
|
|
578
|
+
ignoreDirs,
|
|
579
|
+
includeExtensions || [],
|
|
580
|
+
[...(excludeExtensions || []), ...ignoreExts],
|
|
581
|
+
showFiles,
|
|
582
|
+
showHidden,
|
|
583
|
+
showSizes
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
// Format tree as string
|
|
587
|
+
const treeString = this.formatTree(tree);
|
|
588
|
+
|
|
589
|
+
// Generate summary
|
|
590
|
+
const summary = this.generateSummary(directory, this.stats, maxDepth);
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
success: true,
|
|
594
|
+
directory: targetDir,
|
|
595
|
+
tree: treeString,
|
|
596
|
+
summary,
|
|
597
|
+
maxDepth,
|
|
598
|
+
totalFiles: this.stats.filesCount,
|
|
599
|
+
totalDirectories: this.stats.directoriesCount,
|
|
600
|
+
skippedCount: this.stats.skippedCount,
|
|
601
|
+
statistics: { ...this.stats },
|
|
602
|
+
toolUsed: 'file-tree'
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Build tree data structure recursively
|
|
608
|
+
* @param {string} basePath - Base path for relative calculations
|
|
609
|
+
* @param {string} currentPath - Current directory being processed
|
|
610
|
+
* @param {number} currentDepth - Current depth in tree
|
|
611
|
+
* @param {number} maxDepth - Maximum depth to scan
|
|
612
|
+
* @param {Array<string>} ignoreDirs - Directories to ignore
|
|
613
|
+
* @param {Array<string>} includeExts - Extensions to include (whitelist)
|
|
614
|
+
* @param {Array<string>} excludeExts - Extensions to exclude (blacklist)
|
|
615
|
+
* @param {boolean} showFiles - Whether to show files
|
|
616
|
+
* @param {boolean} showHidden - Whether to show hidden files/folders
|
|
617
|
+
* @param {boolean} showSizes - Whether to show file sizes
|
|
618
|
+
* @returns {Promise<Object>} Tree node
|
|
619
|
+
* @private
|
|
620
|
+
*/
|
|
621
|
+
async buildTree(
|
|
622
|
+
basePath,
|
|
623
|
+
currentPath,
|
|
624
|
+
currentDepth,
|
|
625
|
+
maxDepth,
|
|
626
|
+
ignoreDirs,
|
|
627
|
+
includeExts,
|
|
628
|
+
excludeExts,
|
|
629
|
+
showFiles,
|
|
630
|
+
showHidden,
|
|
631
|
+
showSizes
|
|
632
|
+
) {
|
|
633
|
+
// Check depth limit
|
|
634
|
+
if (currentDepth > maxDepth) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Check directory count limit
|
|
639
|
+
if (this.stats.directoriesCount >= this.treeConfig.MAX_DIRECTORIES) {
|
|
640
|
+
this.logger?.warn('Maximum directory count reached', {
|
|
641
|
+
maxDirectories: this.treeConfig.MAX_DIRECTORIES
|
|
642
|
+
});
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
648
|
+
|
|
649
|
+
const node = {
|
|
650
|
+
name: currentDepth === 0 ? path.basename(currentPath) + '/' : path.basename(currentPath),
|
|
651
|
+
path: path.relative(basePath, currentPath),
|
|
652
|
+
type: 'directory',
|
|
653
|
+
children: []
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
this.stats.directoriesCount++;
|
|
657
|
+
|
|
658
|
+
for (const entry of entries) {
|
|
659
|
+
// Check file count limit
|
|
660
|
+
if (this.stats.filesCount >= this.treeConfig.MAX_FILES_IN_TREE) {
|
|
661
|
+
this.logger?.warn('Maximum file count reached', {
|
|
662
|
+
maxFiles: this.treeConfig.MAX_FILES_IN_TREE
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Skip hidden files/folders if not showing hidden
|
|
668
|
+
if (!showHidden && entry.name.startsWith('.')) {
|
|
669
|
+
this.stats.skippedCount++;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
674
|
+
|
|
675
|
+
if (entry.isDirectory()) {
|
|
676
|
+
// Skip ignored directories
|
|
677
|
+
if (ignoreDirs.includes(entry.name)) {
|
|
678
|
+
this.stats.skippedCount++;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Recursively build subtree
|
|
683
|
+
const subTree = await this.buildTree(
|
|
684
|
+
basePath,
|
|
685
|
+
entryPath,
|
|
686
|
+
currentDepth + 1,
|
|
687
|
+
maxDepth,
|
|
688
|
+
ignoreDirs,
|
|
689
|
+
includeExts,
|
|
690
|
+
excludeExts,
|
|
691
|
+
showFiles,
|
|
692
|
+
showHidden,
|
|
693
|
+
showSizes
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
if (subTree && (subTree.children.length > 0 || currentDepth === 0)) {
|
|
697
|
+
node.children.push(subTree);
|
|
698
|
+
}
|
|
699
|
+
} else if (entry.isFile() && showFiles) {
|
|
700
|
+
// Files are children of current directory, so they're at currentDepth + 1
|
|
701
|
+
// Don't show files if they would exceed maxDepth
|
|
702
|
+
if (currentDepth + 1 > maxDepth) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const ext = path.extname(entry.name);
|
|
707
|
+
|
|
708
|
+
// WHITELIST: If includeExtensions is specified, ONLY include those
|
|
709
|
+
if (includeExts.length > 0) {
|
|
710
|
+
if (!includeExts.includes(ext)) {
|
|
711
|
+
this.stats.skippedCount++;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// BLACKLIST: Otherwise, exclude based on excludeExtensions
|
|
716
|
+
else if (excludeExts.includes(ext)) {
|
|
717
|
+
this.stats.skippedCount++;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Get file size if requested
|
|
722
|
+
let sizeInfo = '';
|
|
723
|
+
if (showSizes) {
|
|
724
|
+
try {
|
|
725
|
+
const stats = await fs.stat(entryPath);
|
|
726
|
+
sizeInfo = ` (${this.formatFileSize(stats.size)})`;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
// Ignore size errors
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
node.children.push({
|
|
733
|
+
name: entry.name + sizeInfo,
|
|
734
|
+
path: path.relative(basePath, entryPath),
|
|
735
|
+
type: 'file',
|
|
736
|
+
ext
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
this.stats.filesCount++;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Sort children: directories first, then files, both alphabetically
|
|
744
|
+
node.children.sort((a, b) => {
|
|
745
|
+
if (a.type === b.type) {
|
|
746
|
+
return a.name.localeCompare(b.name);
|
|
747
|
+
}
|
|
748
|
+
return a.type === 'directory' ? -1 : 1;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
return node;
|
|
752
|
+
|
|
753
|
+
} catch (error) {
|
|
754
|
+
this.logger?.error('Error building tree', {
|
|
755
|
+
currentPath,
|
|
756
|
+
error: error.message
|
|
757
|
+
});
|
|
758
|
+
return {
|
|
759
|
+
name: path.basename(currentPath),
|
|
760
|
+
path: path.relative(basePath, currentPath),
|
|
761
|
+
type: 'directory',
|
|
762
|
+
error: error.message,
|
|
763
|
+
children: []
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Format tree as ASCII string
|
|
770
|
+
* @param {Object} node - Tree node
|
|
771
|
+
* @param {string} prefix - Prefix for current line
|
|
772
|
+
* @param {boolean} isLast - Whether this is the last child
|
|
773
|
+
* @param {boolean} isRoot - Whether this is the root node
|
|
774
|
+
* @returns {string} Formatted tree string
|
|
775
|
+
* @private
|
|
776
|
+
*/
|
|
777
|
+
formatTree(node, prefix = '', isLast = true, isRoot = true) {
|
|
778
|
+
if (!node) return '';
|
|
779
|
+
|
|
780
|
+
const symbols = this.treeConfig.SYMBOLS;
|
|
781
|
+
let result = '';
|
|
782
|
+
|
|
783
|
+
// Root node special case
|
|
784
|
+
if (isRoot) {
|
|
785
|
+
result = `${node.name}\n`;
|
|
786
|
+
} else {
|
|
787
|
+
result = `${prefix}${isLast ? symbols.LAST_BRANCH : symbols.BRANCH}${node.name}\n`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Add children
|
|
791
|
+
if (node.children && node.children.length > 0) {
|
|
792
|
+
const childPrefix = isRoot ? '' : prefix + (isLast ? symbols.INDENT : symbols.VERTICAL);
|
|
793
|
+
|
|
794
|
+
node.children.forEach((child, index) => {
|
|
795
|
+
const isLastChild = index === node.children.length - 1;
|
|
796
|
+
result += this.formatTree(child, childPrefix, isLastChild, false);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return result;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Format file size in human-readable format
|
|
805
|
+
* @param {number} bytes - File size in bytes
|
|
806
|
+
* @returns {string} Formatted size
|
|
807
|
+
* @private
|
|
808
|
+
*/
|
|
809
|
+
formatFileSize(bytes) {
|
|
810
|
+
const KB = 1024;
|
|
811
|
+
const MB = KB * 1024;
|
|
812
|
+
const GB = MB * 1024;
|
|
813
|
+
|
|
814
|
+
if (bytes >= GB) {
|
|
815
|
+
return `${(bytes / GB).toFixed(2)} GB`;
|
|
816
|
+
} else if (bytes >= MB) {
|
|
817
|
+
return `${(bytes / MB).toFixed(2)} MB`;
|
|
818
|
+
} else if (bytes >= KB) {
|
|
819
|
+
return `${(bytes / KB).toFixed(2)} KB`;
|
|
820
|
+
} else {
|
|
821
|
+
return `${bytes} B`;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Generate summary text
|
|
827
|
+
* @param {string} directory - Directory scanned
|
|
828
|
+
* @param {Object} stats - Statistics object
|
|
829
|
+
* @param {number} maxDepth - Maximum depth used
|
|
830
|
+
* @returns {string} Summary text
|
|
831
|
+
* @private
|
|
832
|
+
*/
|
|
833
|
+
generateSummary(directory, stats, maxDepth) {
|
|
834
|
+
return `
|
|
835
|
+
Directory: ${directory}
|
|
836
|
+
Max Depth: ${maxDepth}
|
|
837
|
+
Files: ${stats.filesCount}
|
|
838
|
+
Directories: ${stats.directoriesCount}
|
|
839
|
+
Skipped: ${stats.skippedCount}
|
|
840
|
+
`.trim();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Resource cleanup
|
|
845
|
+
* @param {string} operationId - Operation identifier
|
|
846
|
+
*/
|
|
847
|
+
async cleanup(operationId) {
|
|
848
|
+
// No persistent resources to clean up
|
|
849
|
+
this.logger?.info('File tree tool cleanup completed', { operationId });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export default FileTreeTool;
|