@wonderwhy-er/desktop-commander 0.2.3 → 0.2.4
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/README.md +25 -3
- package/dist/config-manager.d.ts +10 -0
- package/dist/config-manager.js +7 -1
- package/dist/handlers/edit-search-handlers.js +25 -6
- package/dist/handlers/terminal-handlers.d.ts +8 -4
- package/dist/handlers/terminal-handlers.js +16 -10
- package/dist/index-dxt.d.ts +2 -0
- package/dist/index-dxt.js +76 -0
- package/dist/index-with-startup-detection.d.ts +5 -0
- package/dist/index-with-startup-detection.js +180 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +343 -42
- package/dist/terminal-manager.d.ts +7 -0
- package/dist/terminal-manager.js +93 -18
- package/dist/tools/client.d.ts +10 -0
- package/dist/tools/client.js +13 -0
- package/dist/tools/config.d.ts +1 -1
- package/dist/tools/config.js +21 -3
- package/dist/tools/edit.js +4 -3
- package/dist/tools/environment.d.ts +55 -0
- package/dist/tools/environment.js +65 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.js +152 -57
- package/dist/tools/improved-process-tools.js +170 -29
- package/dist/tools/schemas.d.ts +20 -2
- package/dist/tools/schemas.js +20 -2
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/utils/capture.js +23 -1
- package/dist/utils/early-logger.d.ts +4 -0
- package/dist/utils/early-logger.js +35 -0
- package/dist/utils/mcp-logger.d.ts +30 -0
- package/dist/utils/mcp-logger.js +59 -0
- package/dist/utils/smithery-detector.d.ts +94 -0
- package/dist/utils/smithery-detector.js +292 -0
- package/dist/utils/startup-detector.d.ts +65 -0
- package/dist/utils/startup-detector.js +390 -0
- package/dist/utils/usageTracker.d.ts +85 -0
- package/dist/utils/usageTracker.js +280 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
package/dist/tools/filesystem.js
CHANGED
|
@@ -7,6 +7,77 @@ import { createInterface } from 'readline';
|
|
|
7
7
|
import { capture } from '../utils/capture.js';
|
|
8
8
|
import { withTimeout } from '../utils/withTimeout.js';
|
|
9
9
|
import { configManager } from '../config-manager.js';
|
|
10
|
+
// CONSTANTS SECTION - Consolidate all timeouts and thresholds
|
|
11
|
+
const FILE_OPERATION_TIMEOUTS = {
|
|
12
|
+
PATH_VALIDATION: 10000, // 10 seconds
|
|
13
|
+
URL_FETCH: 30000, // 30 seconds
|
|
14
|
+
FILE_READ: 30000, // 30 seconds
|
|
15
|
+
};
|
|
16
|
+
const FILE_SIZE_LIMITS = {
|
|
17
|
+
LARGE_FILE_THRESHOLD: 10 * 1024 * 1024, // 10MB
|
|
18
|
+
LINE_COUNT_LIMIT: 10 * 1024 * 1024, // 10MB for line counting
|
|
19
|
+
};
|
|
20
|
+
const READ_PERFORMANCE_THRESHOLDS = {
|
|
21
|
+
SMALL_READ_THRESHOLD: 100, // For very small reads
|
|
22
|
+
DEEP_OFFSET_THRESHOLD: 1000, // For byte estimation
|
|
23
|
+
SAMPLE_SIZE: 10000, // Sample size for estimation
|
|
24
|
+
CHUNK_SIZE: 8192, // 8KB chunks for reverse reading
|
|
25
|
+
};
|
|
26
|
+
// UTILITY FUNCTIONS - Eliminate duplication
|
|
27
|
+
/**
|
|
28
|
+
* Count lines in text content efficiently
|
|
29
|
+
* @param content Text content to count lines in
|
|
30
|
+
* @returns Number of lines
|
|
31
|
+
*/
|
|
32
|
+
function countLines(content) {
|
|
33
|
+
return content.split('\n').length;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Count lines in a file efficiently (for files under size limit)
|
|
37
|
+
* @param filePath Path to the file
|
|
38
|
+
* @returns Line count or undefined if file too large/can't read
|
|
39
|
+
*/
|
|
40
|
+
async function getFileLineCount(filePath) {
|
|
41
|
+
try {
|
|
42
|
+
const stats = await fs.stat(filePath);
|
|
43
|
+
// Only count lines for reasonably sized files to avoid performance issues
|
|
44
|
+
if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
45
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
46
|
+
return countLines(content);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// If we can't read the file, just return undefined
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get MIME type information for a file
|
|
56
|
+
* @param filePath Path to the file
|
|
57
|
+
* @returns Object with mimeType and isImage properties
|
|
58
|
+
*/
|
|
59
|
+
async function getMimeTypeInfo(filePath) {
|
|
60
|
+
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
61
|
+
const mimeType = getMimeType(filePath);
|
|
62
|
+
const isImage = isImageFile(mimeType);
|
|
63
|
+
return { mimeType, isImage };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get file extension for telemetry purposes
|
|
67
|
+
* @param filePath Path to the file
|
|
68
|
+
* @returns Lowercase file extension
|
|
69
|
+
*/
|
|
70
|
+
function getFileExtension(filePath) {
|
|
71
|
+
return path.extname(filePath).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get default read length from configuration
|
|
75
|
+
* @returns Default number of lines to read
|
|
76
|
+
*/
|
|
77
|
+
async function getDefaultReadLength() {
|
|
78
|
+
const config = await configManager.getConfig();
|
|
79
|
+
return config.fileReadLineLimit ?? 1000; // Default to 1000 lines if not set
|
|
80
|
+
}
|
|
10
81
|
// Initialize allowed directories from configuration
|
|
11
82
|
async function getAllowedDirs() {
|
|
12
83
|
try {
|
|
@@ -116,7 +187,6 @@ async function isPathAllowed(pathToCheck) {
|
|
|
116
187
|
* @throws Error if the path or its parent directories don't exist or if the path is not allowed
|
|
117
188
|
*/
|
|
118
189
|
export async function validatePath(requestedPath) {
|
|
119
|
-
const PATH_VALIDATION_TIMEOUT = 10000; // 10 seconds timeout
|
|
120
190
|
const validationOperation = async () => {
|
|
121
191
|
// Expand home directory if present
|
|
122
192
|
const expandedPath = expandHome(requestedPath);
|
|
@@ -150,12 +220,12 @@ export async function validatePath(requestedPath) {
|
|
|
150
220
|
}
|
|
151
221
|
};
|
|
152
222
|
// Execute with timeout
|
|
153
|
-
const result = await withTimeout(validationOperation(),
|
|
223
|
+
const result = await withTimeout(validationOperation(), FILE_OPERATION_TIMEOUTS.PATH_VALIDATION, `Path validation operation`, // Generic name for telemetry
|
|
154
224
|
null);
|
|
155
225
|
if (result === null) {
|
|
156
226
|
// Keep original path in error for AI but a generic message for telemetry
|
|
157
227
|
capture('server_path_validation_timeout', {
|
|
158
|
-
timeoutMs:
|
|
228
|
+
timeoutMs: FILE_OPERATION_TIMEOUTS.PATH_VALIDATION
|
|
159
229
|
});
|
|
160
230
|
throw new Error(`Path validation failed for path: ${requestedPath}`);
|
|
161
231
|
}
|
|
@@ -170,9 +240,8 @@ export async function readFileFromUrl(url) {
|
|
|
170
240
|
// Import the MIME type utilities
|
|
171
241
|
const { isImageFile } = await import('./mime-types.js');
|
|
172
242
|
// Set up fetch with timeout
|
|
173
|
-
const FETCH_TIMEOUT_MS = 30000;
|
|
174
243
|
const controller = new AbortController();
|
|
175
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
244
|
+
const timeoutId = setTimeout(() => controller.abort(), FILE_OPERATION_TIMEOUTS.URL_FETCH);
|
|
176
245
|
try {
|
|
177
246
|
const response = await fetch(url, {
|
|
178
247
|
signal: controller.signal
|
|
@@ -202,11 +271,52 @@ export async function readFileFromUrl(url) {
|
|
|
202
271
|
clearTimeout(timeoutId);
|
|
203
272
|
// Return error information instead of throwing
|
|
204
273
|
const errorMessage = error instanceof DOMException && error.name === 'AbortError'
|
|
205
|
-
? `URL fetch timed out after ${
|
|
274
|
+
? `URL fetch timed out after ${FILE_OPERATION_TIMEOUTS.URL_FETCH}ms: ${url}`
|
|
206
275
|
: `Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`;
|
|
207
276
|
throw new Error(errorMessage);
|
|
208
277
|
}
|
|
209
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Generate enhanced status message with total and remaining line information
|
|
281
|
+
* @param readLines Number of lines actually read
|
|
282
|
+
* @param offset Starting offset (line number)
|
|
283
|
+
* @param totalLines Total lines in the file (if available)
|
|
284
|
+
* @param isNegativeOffset Whether this is a tail operation
|
|
285
|
+
* @returns Enhanced status message string
|
|
286
|
+
*/
|
|
287
|
+
function generateEnhancedStatusMessage(readLines, offset, totalLines, isNegativeOffset = false) {
|
|
288
|
+
if (isNegativeOffset) {
|
|
289
|
+
// For tail operations (negative offset)
|
|
290
|
+
if (totalLines !== undefined) {
|
|
291
|
+
return `[Reading last ${readLines} lines (total: ${totalLines} lines)]`;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
return `[Reading last ${readLines} lines]`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// For normal reads (positive offset)
|
|
299
|
+
if (totalLines !== undefined) {
|
|
300
|
+
const endLine = offset + readLines;
|
|
301
|
+
const remainingLines = Math.max(0, totalLines - endLine);
|
|
302
|
+
if (offset === 0) {
|
|
303
|
+
return `[Reading ${readLines} lines from start (total: ${totalLines} lines, ${remainingLines} remaining)]`;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
return `[Reading ${readLines} lines from line ${offset} (total: ${totalLines} lines, ${remainingLines} remaining)]`;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// Fallback when total lines unknown
|
|
311
|
+
if (offset === 0) {
|
|
312
|
+
return `[Reading ${readLines} lines from start]`;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
return `[Reading ${readLines} lines from line ${offset}]`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
210
320
|
/**
|
|
211
321
|
* Read file content using smart positioning for optimal performance
|
|
212
322
|
* @param filePath Path to the file (already validated)
|
|
@@ -219,34 +329,34 @@ export async function readFileFromUrl(url) {
|
|
|
219
329
|
async function readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
220
330
|
const stats = await fs.stat(filePath);
|
|
221
331
|
const fileSize = stats.size;
|
|
222
|
-
|
|
223
|
-
const
|
|
332
|
+
// Get total line count for enhanced status messages (only for smaller files)
|
|
333
|
+
const totalLines = await getFileLineCount(filePath);
|
|
224
334
|
// For negative offsets (tail behavior), use reverse reading
|
|
225
335
|
if (offset < 0) {
|
|
226
336
|
const requestedLines = Math.abs(offset);
|
|
227
|
-
if (fileSize > LARGE_FILE_THRESHOLD && requestedLines <= SMALL_READ_THRESHOLD) {
|
|
337
|
+
if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD && requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
|
|
228
338
|
// Use efficient reverse reading for large files with small tail requests
|
|
229
|
-
return await readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage);
|
|
339
|
+
return await readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
230
340
|
}
|
|
231
341
|
else {
|
|
232
342
|
// Use readline circular buffer for other cases
|
|
233
|
-
return await readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage);
|
|
343
|
+
return await readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
234
344
|
}
|
|
235
345
|
}
|
|
236
346
|
// For positive offsets
|
|
237
347
|
else {
|
|
238
348
|
// For small files or reading from start, use simple readline
|
|
239
|
-
if (fileSize < LARGE_FILE_THRESHOLD || offset === 0) {
|
|
240
|
-
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage);
|
|
349
|
+
if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
|
|
350
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
241
351
|
}
|
|
242
352
|
// For large files with middle/end reads, try to estimate position
|
|
243
353
|
else {
|
|
244
354
|
// If seeking deep into file, try byte estimation
|
|
245
|
-
if (offset >
|
|
246
|
-
return await readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage);
|
|
355
|
+
if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
|
|
356
|
+
return await readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
247
357
|
}
|
|
248
358
|
else {
|
|
249
|
-
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage);
|
|
359
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
250
360
|
}
|
|
251
361
|
}
|
|
252
362
|
}
|
|
@@ -254,17 +364,16 @@ async function readFileWithSmartPositioning(filePath, offset, length, mimeType,
|
|
|
254
364
|
/**
|
|
255
365
|
* Read last N lines efficiently by reading file backwards in chunks
|
|
256
366
|
*/
|
|
257
|
-
async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true) {
|
|
367
|
+
async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
258
368
|
const fd = await fs.open(filePath, 'r');
|
|
259
369
|
try {
|
|
260
370
|
const stats = await fd.stat();
|
|
261
371
|
const fileSize = stats.size;
|
|
262
|
-
const chunkSize = 8192; // 8KB chunks
|
|
263
372
|
let position = fileSize;
|
|
264
373
|
let lines = [];
|
|
265
374
|
let partialLine = '';
|
|
266
375
|
while (position > 0 && lines.length < n) {
|
|
267
|
-
const readSize = Math.min(
|
|
376
|
+
const readSize = Math.min(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE, position);
|
|
268
377
|
position -= readSize;
|
|
269
378
|
const buffer = Buffer.alloc(readSize);
|
|
270
379
|
await fd.read(buffer, 0, readSize, position);
|
|
@@ -280,7 +389,7 @@ async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage
|
|
|
280
389
|
}
|
|
281
390
|
const result = lines.slice(-n); // Get exactly n lines
|
|
282
391
|
const content = includeStatusMessage
|
|
283
|
-
?
|
|
392
|
+
? `${generateEnhancedStatusMessage(result.length, -n, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
284
393
|
: result.join('\n');
|
|
285
394
|
return { content, mimeType, isImage: false };
|
|
286
395
|
}
|
|
@@ -291,7 +400,7 @@ async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage
|
|
|
291
400
|
/**
|
|
292
401
|
* Read from end using readline with circular buffer
|
|
293
402
|
*/
|
|
294
|
-
async function readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true) {
|
|
403
|
+
async function readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
295
404
|
const rl = createInterface({
|
|
296
405
|
input: createReadStream(filePath),
|
|
297
406
|
crlfDelay: Infinity
|
|
@@ -317,14 +426,14 @@ async function readFromEndWithReadline(filePath, requestedLines, mimeType, inclu
|
|
|
317
426
|
result = buffer.slice(0, totalLines);
|
|
318
427
|
}
|
|
319
428
|
const content = includeStatusMessage
|
|
320
|
-
?
|
|
429
|
+
? `${generateEnhancedStatusMessage(result.length, -requestedLines, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
321
430
|
: result.join('\n');
|
|
322
431
|
return { content, mimeType, isImage: false };
|
|
323
432
|
}
|
|
324
433
|
/**
|
|
325
434
|
* Read from start/middle using readline
|
|
326
435
|
*/
|
|
327
|
-
async function readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
436
|
+
async function readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
328
437
|
const rl = createInterface({
|
|
329
438
|
input: createReadStream(filePath),
|
|
330
439
|
crlfDelay: Infinity
|
|
@@ -341,9 +450,7 @@ async function readFromStartWithReadline(filePath, offset, length, mimeType, inc
|
|
|
341
450
|
}
|
|
342
451
|
rl.close();
|
|
343
452
|
if (includeStatusMessage) {
|
|
344
|
-
const statusMessage = offset
|
|
345
|
-
? `[Reading ${result.length} lines from start]`
|
|
346
|
-
: `[Reading ${result.length} lines from line ${offset}]`;
|
|
453
|
+
const statusMessage = generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
|
|
347
454
|
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
348
455
|
return { content, mimeType, isImage: false };
|
|
349
456
|
}
|
|
@@ -355,7 +462,7 @@ async function readFromStartWithReadline(filePath, offset, length, mimeType, inc
|
|
|
355
462
|
/**
|
|
356
463
|
* Read from estimated byte position for very large files
|
|
357
464
|
*/
|
|
358
|
-
async function readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
465
|
+
async function readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
359
466
|
// First, do a quick scan to estimate lines per byte
|
|
360
467
|
const rl = createInterface({
|
|
361
468
|
input: createReadStream(filePath),
|
|
@@ -363,17 +470,16 @@ async function readFromEstimatedPosition(filePath, offset, length, mimeType, inc
|
|
|
363
470
|
});
|
|
364
471
|
let sampleLines = 0;
|
|
365
472
|
let bytesRead = 0;
|
|
366
|
-
const SAMPLE_SIZE = 10000; // Sample first 10KB
|
|
367
473
|
for await (const line of rl) {
|
|
368
474
|
bytesRead += Buffer.byteLength(line, 'utf-8') + 1; // +1 for newline
|
|
369
475
|
sampleLines++;
|
|
370
|
-
if (bytesRead >= SAMPLE_SIZE)
|
|
476
|
+
if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
|
|
371
477
|
break;
|
|
372
478
|
}
|
|
373
479
|
rl.close();
|
|
374
480
|
if (sampleLines === 0) {
|
|
375
481
|
// Fallback to simple read
|
|
376
|
-
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage);
|
|
482
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines);
|
|
377
483
|
}
|
|
378
484
|
// Estimate average line length and seek position
|
|
379
485
|
const avgLineLength = bytesRead / sampleLines;
|
|
@@ -407,7 +513,7 @@ async function readFromEstimatedPosition(filePath, offset, length, mimeType, inc
|
|
|
407
513
|
}
|
|
408
514
|
rl2.close();
|
|
409
515
|
const content = includeStatusMessage
|
|
410
|
-
?
|
|
516
|
+
? `${generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
|
|
411
517
|
: result.join('\n');
|
|
412
518
|
return { content, mimeType, isImage: false };
|
|
413
519
|
}
|
|
@@ -427,16 +533,13 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
427
533
|
if (!filePath || typeof filePath !== 'string') {
|
|
428
534
|
throw new Error('Invalid file path provided');
|
|
429
535
|
}
|
|
430
|
-
// Import the MIME type utilities
|
|
431
|
-
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
432
536
|
// Get default length from config if not provided
|
|
433
537
|
if (length === undefined) {
|
|
434
|
-
|
|
435
|
-
length = config.fileReadLineLimit ?? 1000; // Default to 1000 lines if not set
|
|
538
|
+
length = await getDefaultReadLength();
|
|
436
539
|
}
|
|
437
540
|
const validPath = await validatePath(filePath);
|
|
438
541
|
// Get file extension for telemetry using path module consistently
|
|
439
|
-
const fileExtension =
|
|
542
|
+
const fileExtension = getFileExtension(validPath);
|
|
440
543
|
// Check file size before attempting to read
|
|
441
544
|
try {
|
|
442
545
|
const stats = await fs.stat(validPath);
|
|
@@ -455,9 +558,7 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
455
558
|
// If we can't stat the file, continue anyway and let the read operation handle errors
|
|
456
559
|
}
|
|
457
560
|
// Detect the MIME type based on file extension
|
|
458
|
-
const mimeType =
|
|
459
|
-
const isImage = isImageFile(mimeType);
|
|
460
|
-
const FILE_READ_TIMEOUT = 30000; // 30 seconds timeout for file operations
|
|
561
|
+
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
461
562
|
// Use withTimeout to handle potential hangs
|
|
462
563
|
const readOperation = async () => {
|
|
463
564
|
if (isImage) {
|
|
@@ -481,7 +582,7 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
481
582
|
}
|
|
482
583
|
};
|
|
483
584
|
// Execute with timeout
|
|
484
|
-
const result = await withTimeout(readOperation(),
|
|
585
|
+
const result = await withTimeout(readOperation(), FILE_OPERATION_TIMEOUTS.FILE_READ, `Read file operation for ${filePath}`, null);
|
|
485
586
|
if (result == null) {
|
|
486
587
|
// Handles the impossible case where withTimeout resolves to null instead of throwing
|
|
487
588
|
throw new Error('Failed to read the file');
|
|
@@ -513,20 +614,17 @@ export async function readFile(filePath, isUrl, offset, length) {
|
|
|
513
614
|
export async function readFileInternal(filePath, offset = 0, length) {
|
|
514
615
|
// Get default length from config if not provided
|
|
515
616
|
if (length === undefined) {
|
|
516
|
-
|
|
517
|
-
length = config.fileReadLineLimit ?? 1000;
|
|
617
|
+
length = await getDefaultReadLength();
|
|
518
618
|
}
|
|
519
619
|
const validPath = await validatePath(filePath);
|
|
520
620
|
// Get file extension and MIME type
|
|
521
|
-
const fileExtension =
|
|
522
|
-
const {
|
|
523
|
-
const mimeType = getMimeType(validPath);
|
|
524
|
-
const isImage = isImageFile(mimeType);
|
|
621
|
+
const fileExtension = getFileExtension(validPath);
|
|
622
|
+
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
525
623
|
if (isImage) {
|
|
526
624
|
throw new Error('Cannot read image files as text for internal operations');
|
|
527
625
|
}
|
|
528
626
|
// IMPORTANT: For internal operations (especially edit operations), we must
|
|
529
|
-
// preserve exact file content including original line endings.
|
|
627
|
+
// preserve exact file content including original line endings.
|
|
530
628
|
// We cannot use readline-based reading as it strips line endings.
|
|
531
629
|
// Read entire file content preserving line endings
|
|
532
630
|
const content = await fs.readFile(validPath, 'utf8');
|
|
@@ -582,10 +680,10 @@ function splitLinesPreservingEndings(content) {
|
|
|
582
680
|
export async function writeFile(filePath, content, mode = 'rewrite') {
|
|
583
681
|
const validPath = await validatePath(filePath);
|
|
584
682
|
// Get file extension for telemetry
|
|
585
|
-
const fileExtension =
|
|
683
|
+
const fileExtension = getFileExtension(validPath);
|
|
586
684
|
// Calculate content metrics
|
|
587
685
|
const contentBytes = Buffer.from(content).length;
|
|
588
|
-
const lineCount = content
|
|
686
|
+
const lineCount = countLines(content);
|
|
589
687
|
// Capture file extension and operation details in telemetry without capturing the file path
|
|
590
688
|
capture('server_write_file', {
|
|
591
689
|
fileExtension: fileExtension,
|
|
@@ -698,15 +796,14 @@ export async function getFileInfo(filePath) {
|
|
|
698
796
|
permissions: stats.mode.toString(8).slice(-3),
|
|
699
797
|
};
|
|
700
798
|
// For text files that aren't too large, also count lines
|
|
701
|
-
if (stats.isFile() && stats.size <
|
|
799
|
+
if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
702
800
|
try {
|
|
703
|
-
//
|
|
704
|
-
const {
|
|
705
|
-
const mimeType = getMimeType(validPath);
|
|
801
|
+
// Get MIME type information
|
|
802
|
+
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
706
803
|
// Only count lines for non-image, likely text files
|
|
707
|
-
if (!
|
|
804
|
+
if (!isImage) {
|
|
708
805
|
const content = await fs.readFile(validPath, 'utf8');
|
|
709
|
-
const lineCount = content
|
|
806
|
+
const lineCount = countLines(content);
|
|
710
807
|
info.lineCount = lineCount;
|
|
711
808
|
info.lastLine = lineCount - 1; // Zero-indexed last line
|
|
712
809
|
info.appendPosition = lineCount; // Position to append at end
|
|
@@ -719,5 +816,3 @@ export async function getFileInfo(filePath) {
|
|
|
719
816
|
}
|
|
720
817
|
return info;
|
|
721
818
|
}
|
|
722
|
-
// This function has been replaced with configManager.getConfig()
|
|
723
|
-
// Use get_config tool to retrieve allowedDirectories
|