converse-mcp-server 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/.env.example +177 -0
- package/README.md +425 -0
- package/bin/converse.js +45 -0
- package/docs/API.md +897 -0
- package/docs/ARCHITECTURE.md +552 -0
- package/docs/EXAMPLES.md +736 -0
- package/package.json +101 -0
- package/src/config.js +521 -0
- package/src/continuationStore.js +340 -0
- package/src/index.js +216 -0
- package/src/providers/google.js +441 -0
- package/src/providers/index.js +87 -0
- package/src/providers/openai.js +348 -0
- package/src/providers/xai.js +305 -0
- package/src/router.js +497 -0
- package/src/systemPrompts.js +90 -0
- package/src/tools/chat.js +336 -0
- package/src/tools/consensus.js +478 -0
- package/src/tools/index.js +156 -0
- package/src/transport/httpTransport.js +548 -0
- package/src/utils/console.js +64 -0
- package/src/utils/contextProcessor.js +475 -0
- package/src/utils/errorHandler.js +555 -0
- package/src/utils/logger.js +450 -0
- package/src/utils/tokenLimiter.js +217 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Processor Utilities
|
|
3
|
+
*
|
|
4
|
+
* Unified interface for handling files, images, and web search context processing.
|
|
5
|
+
* Uses Node.js built-in modules with security validation and comprehensive error handling.
|
|
6
|
+
* Includes placeholders for advanced features that can be enhanced later.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, stat, access } from 'fs/promises';
|
|
10
|
+
import { extname, resolve, isAbsolute } from 'path';
|
|
11
|
+
import { constants } from 'fs';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { dirname } from 'path';
|
|
14
|
+
|
|
15
|
+
// Security: Define allowed directories for file access
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const PROJECT_ROOT = resolve(__dirname, '../../..');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom error class for context processing operations
|
|
22
|
+
*/
|
|
23
|
+
export class ContextProcessorError extends Error {
|
|
24
|
+
constructor(message, code = 'CONTEXT_ERROR', details = {}) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'ContextProcessorError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.details = details;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Supported file types for context processing
|
|
34
|
+
*/
|
|
35
|
+
const SUPPORTED_TEXT_EXTENSIONS = [
|
|
36
|
+
'.txt', '.md', '.js', '.ts', '.json', '.yaml', '.yml',
|
|
37
|
+
'.py', '.java', '.c', '.cpp', '.h', '.css', '.html',
|
|
38
|
+
'.xml', '.csv', '.sql', '.sh', '.bat', '.log'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const SUPPORTED_IMAGE_EXTENSIONS = [
|
|
42
|
+
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Security validation for file paths
|
|
47
|
+
* @param {string} filePath - Path to validate
|
|
48
|
+
* @param {object} options - Validation options
|
|
49
|
+
* @returns {Promise<string>} Validated absolute path
|
|
50
|
+
* @throws {ContextProcessorError} If path is invalid or unsafe
|
|
51
|
+
*/
|
|
52
|
+
async function validateFilePath(filePath, options = {}) {
|
|
53
|
+
// Check if path is provided
|
|
54
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
55
|
+
throw new ContextProcessorError(
|
|
56
|
+
'File path must be a non-empty string',
|
|
57
|
+
'INVALID_PATH'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convert to absolute path
|
|
62
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
63
|
+
|
|
64
|
+
// Security: Check if path is within allowed directories
|
|
65
|
+
const allowedDirs = options.allowedDirectories || [process.cwd(), PROJECT_ROOT];
|
|
66
|
+
const isAllowed = allowedDirs.some(dir => {
|
|
67
|
+
const resolvedDir = resolve(dir);
|
|
68
|
+
return absolutePath.startsWith(resolvedDir);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!isAllowed && !options.skipSecurityCheck) {
|
|
72
|
+
throw new ContextProcessorError(
|
|
73
|
+
'File access denied: path outside allowed directories',
|
|
74
|
+
'SECURITY_VIOLATION',
|
|
75
|
+
{ path: absolutePath, allowedDirs }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if file exists and is readable
|
|
80
|
+
try {
|
|
81
|
+
await access(absolutePath, constants.R_OK);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new ContextProcessorError(
|
|
84
|
+
`File not accessible: ${error.message}`,
|
|
85
|
+
'FILE_ACCESS_ERROR',
|
|
86
|
+
{ path: absolutePath }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return absolutePath;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Process file content for inclusion in AI context
|
|
95
|
+
* @param {string} filePath - Absolute or relative path to the file
|
|
96
|
+
* @param {object} options - Processing options
|
|
97
|
+
* @param {string[]} options.allowedDirectories - Allowed directories for security
|
|
98
|
+
* @param {number} options.maxTextSize - Maximum text file size in bytes
|
|
99
|
+
* @param {number} options.maxImageSize - Maximum image file size in bytes
|
|
100
|
+
* @param {boolean} options.skipSecurityCheck - Skip security validation (for testing)
|
|
101
|
+
* @returns {Promise<object>} Processed content with metadata
|
|
102
|
+
*/
|
|
103
|
+
export async function processFileContent(filePath, options = {}) {
|
|
104
|
+
try {
|
|
105
|
+
// Security validation
|
|
106
|
+
const validatedPath = await validateFilePath(filePath, options);
|
|
107
|
+
|
|
108
|
+
const fileStats = await stat(validatedPath);
|
|
109
|
+
const extension = extname(validatedPath).toLowerCase();
|
|
110
|
+
|
|
111
|
+
// Check if it's actually a file (not a directory)
|
|
112
|
+
if (!fileStats.isFile()) {
|
|
113
|
+
throw new ContextProcessorError(
|
|
114
|
+
'Path is not a file',
|
|
115
|
+
'NOT_A_FILE',
|
|
116
|
+
{ path: validatedPath }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = {
|
|
121
|
+
path: validatedPath,
|
|
122
|
+
originalPath: filePath,
|
|
123
|
+
size: fileStats.size,
|
|
124
|
+
extension,
|
|
125
|
+
type: 'unknown',
|
|
126
|
+
content: null,
|
|
127
|
+
error: null,
|
|
128
|
+
lastModified: fileStats.mtime,
|
|
129
|
+
encoding: null,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Check file size limits
|
|
133
|
+
const maxTextSize = options.maxTextSize || 1024 * 1024; // 1MB default
|
|
134
|
+
const maxImageSize = options.maxImageSize || 10 * 1024 * 1024; // 10MB default
|
|
135
|
+
|
|
136
|
+
if (SUPPORTED_TEXT_EXTENSIONS.includes(extension)) {
|
|
137
|
+
result.type = 'text';
|
|
138
|
+
|
|
139
|
+
if (fileStats.size > maxTextSize) {
|
|
140
|
+
result.error = `File too large (${fileStats.size} bytes, max ${maxTextSize})`;
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const content = await readFile(validatedPath, 'utf8');
|
|
145
|
+
result.content = content;
|
|
146
|
+
result.lineCount = content.split('\n').length;
|
|
147
|
+
result.encoding = 'utf8';
|
|
148
|
+
result.charCount = content.length;
|
|
149
|
+
|
|
150
|
+
} else if (SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
|
151
|
+
result.type = 'image';
|
|
152
|
+
|
|
153
|
+
if (fileStats.size > maxImageSize) {
|
|
154
|
+
result.error = `Image too large (${fileStats.size} bytes, max ${maxImageSize})`;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For images, read as base64 for AI processing (placeholder for advanced features)
|
|
159
|
+
const buffer = await readFile(validatedPath);
|
|
160
|
+
result.content = buffer.toString('base64');
|
|
161
|
+
result.mimeType = getMimeType(extension);
|
|
162
|
+
result.encoding = 'base64';
|
|
163
|
+
|
|
164
|
+
// Placeholder: Advanced image processing could be added here
|
|
165
|
+
// - Image resizing, format conversion
|
|
166
|
+
// - EXIF data extraction
|
|
167
|
+
// - Image analysis/description generation
|
|
168
|
+
|
|
169
|
+
} else {
|
|
170
|
+
result.error = `Unsupported file type: ${extension}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result;
|
|
174
|
+
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
path: filePath,
|
|
178
|
+
originalPath: filePath,
|
|
179
|
+
type: 'error',
|
|
180
|
+
error: error instanceof ContextProcessorError ? error.message : `Unexpected error: ${error.message}`,
|
|
181
|
+
errorCode: error.code || 'UNKNOWN_ERROR',
|
|
182
|
+
content: null,
|
|
183
|
+
lastModified: null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Process multiple files for context with error isolation
|
|
190
|
+
* @param {string[]} filePaths - Array of file paths (absolute or relative)
|
|
191
|
+
* @param {object} options - Processing options
|
|
192
|
+
* @returns {Promise<object[]>} Array of processed file contents
|
|
193
|
+
*/
|
|
194
|
+
export async function processMultipleFiles(filePaths, options = {}) {
|
|
195
|
+
if (!Array.isArray(filePaths)) {
|
|
196
|
+
throw new ContextProcessorError(
|
|
197
|
+
'filePaths must be an array',
|
|
198
|
+
'INVALID_INPUT'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Process files in parallel but isolate errors
|
|
203
|
+
const results = await Promise.allSettled(
|
|
204
|
+
filePaths.map(path => processFileContent(path, options))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Convert Promise.allSettled results to consistent format
|
|
208
|
+
return results.map((result, index) => {
|
|
209
|
+
if (result.status === 'fulfilled') {
|
|
210
|
+
return result.value;
|
|
211
|
+
} else {
|
|
212
|
+
return {
|
|
213
|
+
path: filePaths[index],
|
|
214
|
+
originalPath: filePaths[index],
|
|
215
|
+
type: 'error',
|
|
216
|
+
error: result.reason.message || 'Unknown processing error',
|
|
217
|
+
errorCode: result.reason.code || 'PROCESSING_ERROR',
|
|
218
|
+
content: null,
|
|
219
|
+
lastModified: null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Web search context integration (placeholder)
|
|
227
|
+
* @param {string} query - Search query
|
|
228
|
+
* @param {object} options - Search options
|
|
229
|
+
* @returns {Promise<object>} Web search results for context
|
|
230
|
+
*/
|
|
231
|
+
export async function processWebSearchContext(query, options = {}) {
|
|
232
|
+
// Placeholder implementation - can be enhanced with actual web search API
|
|
233
|
+
return {
|
|
234
|
+
type: 'web_search',
|
|
235
|
+
query,
|
|
236
|
+
results: [],
|
|
237
|
+
error: null,
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
// Placeholder: Future implementation could integrate with:
|
|
240
|
+
// - Google Search API
|
|
241
|
+
// - Bing Search API
|
|
242
|
+
// - DuckDuckGo API
|
|
243
|
+
// - Custom search engines
|
|
244
|
+
placeholder: true,
|
|
245
|
+
message: 'Web search integration placeholder - not yet implemented'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Unified context processor - handles all context types
|
|
251
|
+
* @param {object} contextRequest - Context processing request
|
|
252
|
+
* @param {string[]} contextRequest.files - Array of file paths
|
|
253
|
+
* @param {string[]} contextRequest.images - Array of image paths (for explicit image processing)
|
|
254
|
+
* @param {string} contextRequest.webSearch - Web search query
|
|
255
|
+
* @param {object} options - Processing options
|
|
256
|
+
* @returns {Promise<object>} Unified context result
|
|
257
|
+
*/
|
|
258
|
+
export async function processUnifiedContext(contextRequest, options = {}) {
|
|
259
|
+
const result = {
|
|
260
|
+
files: [],
|
|
261
|
+
images: [],
|
|
262
|
+
webSearch: null,
|
|
263
|
+
errors: [],
|
|
264
|
+
timestamp: new Date().toISOString(),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Process files if provided
|
|
269
|
+
if (contextRequest.files && Array.isArray(contextRequest.files)) {
|
|
270
|
+
result.files = await processMultipleFiles(contextRequest.files, options);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Process images if provided (currently same as files, placeholder for advanced features)
|
|
274
|
+
if (contextRequest.images && Array.isArray(contextRequest.images)) {
|
|
275
|
+
result.images = await processMultipleFiles(contextRequest.images, {
|
|
276
|
+
...options,
|
|
277
|
+
imageProcessingMode: true // Placeholder for future image-specific processing
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Process web search if provided
|
|
282
|
+
if (contextRequest.webSearch && typeof contextRequest.webSearch === 'string') {
|
|
283
|
+
result.webSearch = await processWebSearchContext(contextRequest.webSearch, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
result.errors.push({
|
|
288
|
+
type: 'unified_processing_error',
|
|
289
|
+
message: error.message,
|
|
290
|
+
code: error.code || 'UNKNOWN_ERROR'
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create context message from file contents
|
|
299
|
+
* @param {object[]} processedFiles - Array of processed file contents
|
|
300
|
+
* @param {object} options - Message creation options
|
|
301
|
+
* @returns {object|null} Context message for AI
|
|
302
|
+
*/
|
|
303
|
+
export function createFileContext(processedFiles, options = {}) {
|
|
304
|
+
if (!Array.isArray(processedFiles)) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const textFiles = processedFiles.filter(f => f.type === 'text' && !f.error);
|
|
309
|
+
const imageFiles = processedFiles.filter(f => f.type === 'image' && !f.error);
|
|
310
|
+
const errors = processedFiles.filter(f => f.error);
|
|
311
|
+
|
|
312
|
+
const includeErrors = options.includeErrors !== false; // Default to true
|
|
313
|
+
|
|
314
|
+
let contextText = '';
|
|
315
|
+
|
|
316
|
+
if (textFiles.length > 0) {
|
|
317
|
+
contextText += '=== FILE CONTEXT ===\n\n';
|
|
318
|
+
for (const file of textFiles) {
|
|
319
|
+
contextText += `--- ${file.originalPath || file.path} ---\n`;
|
|
320
|
+
if (options.includeMetadata) {
|
|
321
|
+
contextText += `Size: ${file.size} bytes, Lines: ${file.lineCount || 'N/A'}\n`;
|
|
322
|
+
contextText += `Last Modified: ${file.lastModified || 'N/A'}\n`;
|
|
323
|
+
}
|
|
324
|
+
contextText += `${file.content}\n\n`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (errors.length > 0 && includeErrors) {
|
|
329
|
+
contextText += '=== FILE ERRORS ===\n';
|
|
330
|
+
for (const error of errors) {
|
|
331
|
+
contextText += `${error.originalPath || error.path}: ${error.error}`;
|
|
332
|
+
if (error.errorCode) {
|
|
333
|
+
contextText += ` (${error.errorCode})`;
|
|
334
|
+
}
|
|
335
|
+
contextText += '\n';
|
|
336
|
+
}
|
|
337
|
+
contextText += '\n';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const message = {
|
|
341
|
+
role: 'user',
|
|
342
|
+
content: [],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (contextText) {
|
|
346
|
+
message.content.push({
|
|
347
|
+
type: 'text',
|
|
348
|
+
text: contextText,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Add images with metadata
|
|
353
|
+
for (const image of imageFiles) {
|
|
354
|
+
message.content.push({
|
|
355
|
+
type: 'image',
|
|
356
|
+
source: {
|
|
357
|
+
type: 'base64',
|
|
358
|
+
media_type: image.mimeType,
|
|
359
|
+
data: image.content,
|
|
360
|
+
},
|
|
361
|
+
// Add metadata for debugging
|
|
362
|
+
metadata: options.includeMetadata ? {
|
|
363
|
+
path: image.originalPath || image.path,
|
|
364
|
+
size: image.size,
|
|
365
|
+
lastModified: image.lastModified
|
|
366
|
+
} : undefined
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return message.content.length > 0 ? message : null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get MIME type for file extension
|
|
375
|
+
* @param {string} extension - File extension
|
|
376
|
+
* @returns {string} MIME type
|
|
377
|
+
*/
|
|
378
|
+
function getMimeType(extension) {
|
|
379
|
+
const mimeTypes = {
|
|
380
|
+
// Images
|
|
381
|
+
'.jpg': 'image/jpeg',
|
|
382
|
+
'.jpeg': 'image/jpeg',
|
|
383
|
+
'.png': 'image/png',
|
|
384
|
+
'.gif': 'image/gif',
|
|
385
|
+
'.webp': 'image/webp',
|
|
386
|
+
'.bmp': 'image/bmp',
|
|
387
|
+
// Text files
|
|
388
|
+
'.txt': 'text/plain',
|
|
389
|
+
'.md': 'text/markdown',
|
|
390
|
+
'.js': 'text/javascript',
|
|
391
|
+
'.ts': 'text/typescript',
|
|
392
|
+
'.json': 'application/json',
|
|
393
|
+
'.yaml': 'text/yaml',
|
|
394
|
+
'.yml': 'text/yaml',
|
|
395
|
+
'.html': 'text/html',
|
|
396
|
+
'.css': 'text/css',
|
|
397
|
+
'.xml': 'text/xml',
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
return mimeTypes[extension] || 'application/octet-stream';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Validate file paths with security checks
|
|
405
|
+
* @param {string[]} filePaths - Array of file paths to validate
|
|
406
|
+
* @param {object} options - Validation options
|
|
407
|
+
* @returns {Promise<object>} Validation result with valid/invalid paths
|
|
408
|
+
*/
|
|
409
|
+
export async function validateFilePaths(filePaths, options = {}) {
|
|
410
|
+
if (!Array.isArray(filePaths)) {
|
|
411
|
+
throw new ContextProcessorError(
|
|
412
|
+
'filePaths must be an array',
|
|
413
|
+
'INVALID_INPUT'
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const results = {
|
|
418
|
+
valid: [],
|
|
419
|
+
invalid: [],
|
|
420
|
+
securityViolations: [],
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
for (const path of filePaths) {
|
|
424
|
+
try {
|
|
425
|
+
const validatedPath = await validateFilePath(path, options);
|
|
426
|
+
results.valid.push({
|
|
427
|
+
originalPath: path,
|
|
428
|
+
validatedPath,
|
|
429
|
+
isValid: true
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
const errorInfo = {
|
|
433
|
+
path,
|
|
434
|
+
error: error.message,
|
|
435
|
+
code: error.code
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (error.code === 'SECURITY_VIOLATION') {
|
|
439
|
+
results.securityViolations.push(errorInfo);
|
|
440
|
+
} else {
|
|
441
|
+
results.invalid.push(errorInfo);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return results;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get supported file extensions
|
|
451
|
+
* @returns {object} Object containing supported extensions by type
|
|
452
|
+
*/
|
|
453
|
+
export function getSupportedExtensions() {
|
|
454
|
+
return {
|
|
455
|
+
text: [...SUPPORTED_TEXT_EXTENSIONS],
|
|
456
|
+
image: [...SUPPORTED_IMAGE_EXTENSIONS],
|
|
457
|
+
all: [...SUPPORTED_TEXT_EXTENSIONS, ...SUPPORTED_IMAGE_EXTENSIONS]
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Check if file type is supported
|
|
463
|
+
* @param {string} filePath - Path to check
|
|
464
|
+
* @returns {object} Support information
|
|
465
|
+
*/
|
|
466
|
+
export function isFileTypeSupported(filePath) {
|
|
467
|
+
const extension = extname(filePath).toLowerCase();
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
extension,
|
|
471
|
+
isSupported: SUPPORTED_TEXT_EXTENSIONS.includes(extension) || SUPPORTED_IMAGE_EXTENSIONS.includes(extension),
|
|
472
|
+
type: SUPPORTED_TEXT_EXTENSIONS.includes(extension) ? 'text' :
|
|
473
|
+
SUPPORTED_IMAGE_EXTENSIONS.includes(extension) ? 'image' : 'unknown'
|
|
474
|
+
};
|
|
475
|
+
}
|