@wonderwhy-er/desktop-commander 0.2.2 → 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 +27 -4
- 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/filesystem-handlers.js +2 -4
- 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 +381 -65
- 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.d.ts +10 -0
- package/dist/tools/filesystem.js +410 -60
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +453 -0
- package/dist/tools/schemas.d.ts +20 -2
- package/dist/tools/schemas.js +20 -3
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/utils/capture.d.ts +2 -0
- package/dist/utils/capture.js +40 -9
- 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/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -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/system-info.d.ts +30 -0
- package/dist/utils/system-info.js +146 -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 +4 -1
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,9 +2,82 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fetch from 'cross-fetch';
|
|
5
|
+
import { createReadStream } from 'fs';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
5
7
|
import { capture } from '../utils/capture.js';
|
|
6
8
|
import { withTimeout } from '../utils/withTimeout.js';
|
|
7
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
|
+
}
|
|
8
81
|
// Initialize allowed directories from configuration
|
|
9
82
|
async function getAllowedDirs() {
|
|
10
83
|
try {
|
|
@@ -114,7 +187,6 @@ async function isPathAllowed(pathToCheck) {
|
|
|
114
187
|
* @throws Error if the path or its parent directories don't exist or if the path is not allowed
|
|
115
188
|
*/
|
|
116
189
|
export async function validatePath(requestedPath) {
|
|
117
|
-
const PATH_VALIDATION_TIMEOUT = 10000; // 10 seconds timeout
|
|
118
190
|
const validationOperation = async () => {
|
|
119
191
|
// Expand home directory if present
|
|
120
192
|
const expandedPath = expandHome(requestedPath);
|
|
@@ -148,12 +220,12 @@ export async function validatePath(requestedPath) {
|
|
|
148
220
|
}
|
|
149
221
|
};
|
|
150
222
|
// Execute with timeout
|
|
151
|
-
const result = await withTimeout(validationOperation(),
|
|
223
|
+
const result = await withTimeout(validationOperation(), FILE_OPERATION_TIMEOUTS.PATH_VALIDATION, `Path validation operation`, // Generic name for telemetry
|
|
152
224
|
null);
|
|
153
225
|
if (result === null) {
|
|
154
226
|
// Keep original path in error for AI but a generic message for telemetry
|
|
155
227
|
capture('server_path_validation_timeout', {
|
|
156
|
-
timeoutMs:
|
|
228
|
+
timeoutMs: FILE_OPERATION_TIMEOUTS.PATH_VALIDATION
|
|
157
229
|
});
|
|
158
230
|
throw new Error(`Path validation failed for path: ${requestedPath}`);
|
|
159
231
|
}
|
|
@@ -168,9 +240,8 @@ export async function readFileFromUrl(url) {
|
|
|
168
240
|
// Import the MIME type utilities
|
|
169
241
|
const { isImageFile } = await import('./mime-types.js');
|
|
170
242
|
// Set up fetch with timeout
|
|
171
|
-
const FETCH_TIMEOUT_MS = 30000;
|
|
172
243
|
const controller = new AbortController();
|
|
173
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
244
|
+
const timeoutId = setTimeout(() => controller.abort(), FILE_OPERATION_TIMEOUTS.URL_FETCH);
|
|
174
245
|
try {
|
|
175
246
|
const response = await fetch(url, {
|
|
176
247
|
signal: controller.signal
|
|
@@ -200,11 +271,256 @@ export async function readFileFromUrl(url) {
|
|
|
200
271
|
clearTimeout(timeoutId);
|
|
201
272
|
// Return error information instead of throwing
|
|
202
273
|
const errorMessage = error instanceof DOMException && error.name === 'AbortError'
|
|
203
|
-
? `URL fetch timed out after ${
|
|
274
|
+
? `URL fetch timed out after ${FILE_OPERATION_TIMEOUTS.URL_FETCH}ms: ${url}`
|
|
204
275
|
: `Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`;
|
|
205
276
|
throw new Error(errorMessage);
|
|
206
277
|
}
|
|
207
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
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Read file content using smart positioning for optimal performance
|
|
322
|
+
* @param filePath Path to the file (already validated)
|
|
323
|
+
* @param offset Starting line number (negative for tail behavior)
|
|
324
|
+
* @param length Maximum number of lines to read
|
|
325
|
+
* @param mimeType MIME type of the file
|
|
326
|
+
* @param includeStatusMessage Whether to include status headers (default: true)
|
|
327
|
+
* @returns File result with content
|
|
328
|
+
*/
|
|
329
|
+
async function readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
330
|
+
const stats = await fs.stat(filePath);
|
|
331
|
+
const fileSize = stats.size;
|
|
332
|
+
// Get total line count for enhanced status messages (only for smaller files)
|
|
333
|
+
const totalLines = await getFileLineCount(filePath);
|
|
334
|
+
// For negative offsets (tail behavior), use reverse reading
|
|
335
|
+
if (offset < 0) {
|
|
336
|
+
const requestedLines = Math.abs(offset);
|
|
337
|
+
if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD && requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
|
|
338
|
+
// Use efficient reverse reading for large files with small tail requests
|
|
339
|
+
return await readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Use readline circular buffer for other cases
|
|
343
|
+
return await readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// For positive offsets
|
|
347
|
+
else {
|
|
348
|
+
// For small files or reading from start, use simple readline
|
|
349
|
+
if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
|
|
350
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
351
|
+
}
|
|
352
|
+
// For large files with middle/end reads, try to estimate position
|
|
353
|
+
else {
|
|
354
|
+
// If seeking deep into file, try byte estimation
|
|
355
|
+
if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
|
|
356
|
+
return await readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Read last N lines efficiently by reading file backwards in chunks
|
|
366
|
+
*/
|
|
367
|
+
async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
368
|
+
const fd = await fs.open(filePath, 'r');
|
|
369
|
+
try {
|
|
370
|
+
const stats = await fd.stat();
|
|
371
|
+
const fileSize = stats.size;
|
|
372
|
+
let position = fileSize;
|
|
373
|
+
let lines = [];
|
|
374
|
+
let partialLine = '';
|
|
375
|
+
while (position > 0 && lines.length < n) {
|
|
376
|
+
const readSize = Math.min(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE, position);
|
|
377
|
+
position -= readSize;
|
|
378
|
+
const buffer = Buffer.alloc(readSize);
|
|
379
|
+
await fd.read(buffer, 0, readSize, position);
|
|
380
|
+
const chunk = buffer.toString('utf-8');
|
|
381
|
+
const text = chunk + partialLine;
|
|
382
|
+
const chunkLines = text.split('\n');
|
|
383
|
+
partialLine = chunkLines.shift() || '';
|
|
384
|
+
lines = chunkLines.concat(lines);
|
|
385
|
+
}
|
|
386
|
+
// Add the remaining partial line if we reached the beginning
|
|
387
|
+
if (position === 0 && partialLine) {
|
|
388
|
+
lines.unshift(partialLine);
|
|
389
|
+
}
|
|
390
|
+
const result = lines.slice(-n); // Get exactly n lines
|
|
391
|
+
const content = includeStatusMessage
|
|
392
|
+
? `${generateEnhancedStatusMessage(result.length, -n, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
393
|
+
: result.join('\n');
|
|
394
|
+
return { content, mimeType, isImage: false };
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
await fd.close();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Read from end using readline with circular buffer
|
|
402
|
+
*/
|
|
403
|
+
async function readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
404
|
+
const rl = createInterface({
|
|
405
|
+
input: createReadStream(filePath),
|
|
406
|
+
crlfDelay: Infinity
|
|
407
|
+
});
|
|
408
|
+
const buffer = new Array(requestedLines);
|
|
409
|
+
let bufferIndex = 0;
|
|
410
|
+
let totalLines = 0;
|
|
411
|
+
for await (const line of rl) {
|
|
412
|
+
buffer[bufferIndex] = line;
|
|
413
|
+
bufferIndex = (bufferIndex + 1) % requestedLines;
|
|
414
|
+
totalLines++;
|
|
415
|
+
}
|
|
416
|
+
rl.close();
|
|
417
|
+
// Extract lines in correct order
|
|
418
|
+
let result;
|
|
419
|
+
if (totalLines >= requestedLines) {
|
|
420
|
+
result = [
|
|
421
|
+
...buffer.slice(bufferIndex),
|
|
422
|
+
...buffer.slice(0, bufferIndex)
|
|
423
|
+
].filter(line => line !== undefined);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
result = buffer.slice(0, totalLines);
|
|
427
|
+
}
|
|
428
|
+
const content = includeStatusMessage
|
|
429
|
+
? `${generateEnhancedStatusMessage(result.length, -requestedLines, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
430
|
+
: result.join('\n');
|
|
431
|
+
return { content, mimeType, isImage: false };
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Read from start/middle using readline
|
|
435
|
+
*/
|
|
436
|
+
async function readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
437
|
+
const rl = createInterface({
|
|
438
|
+
input: createReadStream(filePath),
|
|
439
|
+
crlfDelay: Infinity
|
|
440
|
+
});
|
|
441
|
+
const result = [];
|
|
442
|
+
let lineNumber = 0;
|
|
443
|
+
for await (const line of rl) {
|
|
444
|
+
if (lineNumber >= offset && result.length < length) {
|
|
445
|
+
result.push(line);
|
|
446
|
+
}
|
|
447
|
+
if (result.length >= length)
|
|
448
|
+
break; // Early exit optimization
|
|
449
|
+
lineNumber++;
|
|
450
|
+
}
|
|
451
|
+
rl.close();
|
|
452
|
+
if (includeStatusMessage) {
|
|
453
|
+
const statusMessage = generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
|
|
454
|
+
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
455
|
+
return { content, mimeType, isImage: false };
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const content = result.join('\n');
|
|
459
|
+
return { content, mimeType, isImage: false };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Read from estimated byte position for very large files
|
|
464
|
+
*/
|
|
465
|
+
async function readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
466
|
+
// First, do a quick scan to estimate lines per byte
|
|
467
|
+
const rl = createInterface({
|
|
468
|
+
input: createReadStream(filePath),
|
|
469
|
+
crlfDelay: Infinity
|
|
470
|
+
});
|
|
471
|
+
let sampleLines = 0;
|
|
472
|
+
let bytesRead = 0;
|
|
473
|
+
for await (const line of rl) {
|
|
474
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1; // +1 for newline
|
|
475
|
+
sampleLines++;
|
|
476
|
+
if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
rl.close();
|
|
480
|
+
if (sampleLines === 0) {
|
|
481
|
+
// Fallback to simple read
|
|
482
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines);
|
|
483
|
+
}
|
|
484
|
+
// Estimate average line length and seek position
|
|
485
|
+
const avgLineLength = bytesRead / sampleLines;
|
|
486
|
+
const estimatedBytePosition = Math.floor(offset * avgLineLength);
|
|
487
|
+
// Create a new stream starting from estimated position
|
|
488
|
+
const fd = await fs.open(filePath, 'r');
|
|
489
|
+
try {
|
|
490
|
+
const stats = await fd.stat();
|
|
491
|
+
const startPosition = Math.min(estimatedBytePosition, stats.size);
|
|
492
|
+
const stream = createReadStream(filePath, { start: startPosition });
|
|
493
|
+
const rl2 = createInterface({
|
|
494
|
+
input: stream,
|
|
495
|
+
crlfDelay: Infinity
|
|
496
|
+
});
|
|
497
|
+
const result = [];
|
|
498
|
+
let lineCount = 0;
|
|
499
|
+
let firstLineSkipped = false;
|
|
500
|
+
for await (const line of rl2) {
|
|
501
|
+
// Skip first potentially partial line if we didn't start at beginning
|
|
502
|
+
if (!firstLineSkipped && startPosition > 0) {
|
|
503
|
+
firstLineSkipped = true;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (result.length < length) {
|
|
507
|
+
result.push(line);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
lineCount++;
|
|
513
|
+
}
|
|
514
|
+
rl2.close();
|
|
515
|
+
const content = includeStatusMessage
|
|
516
|
+
? `${generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
|
|
517
|
+
: result.join('\n');
|
|
518
|
+
return { content, mimeType, isImage: false };
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
await fd.close();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
208
524
|
/**
|
|
209
525
|
* Read file content from the local filesystem
|
|
210
526
|
* @param filePath Path to the file
|
|
@@ -217,16 +533,13 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
217
533
|
if (!filePath || typeof filePath !== 'string') {
|
|
218
534
|
throw new Error('Invalid file path provided');
|
|
219
535
|
}
|
|
220
|
-
// Import the MIME type utilities
|
|
221
|
-
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
222
536
|
// Get default length from config if not provided
|
|
223
537
|
if (length === undefined) {
|
|
224
|
-
|
|
225
|
-
length = config.fileReadLineLimit ?? 1000; // Default to 1000 lines if not set
|
|
538
|
+
length = await getDefaultReadLength();
|
|
226
539
|
}
|
|
227
540
|
const validPath = await validatePath(filePath);
|
|
228
541
|
// Get file extension for telemetry using path module consistently
|
|
229
|
-
const fileExtension =
|
|
542
|
+
const fileExtension = getFileExtension(validPath);
|
|
230
543
|
// Check file size before attempting to read
|
|
231
544
|
try {
|
|
232
545
|
const stats = await fs.stat(validPath);
|
|
@@ -245,9 +558,7 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
245
558
|
// If we can't stat the file, continue anyway and let the read operation handle errors
|
|
246
559
|
}
|
|
247
560
|
// Detect the MIME type based on file extension
|
|
248
|
-
const mimeType =
|
|
249
|
-
const isImage = isImageFile(mimeType);
|
|
250
|
-
const FILE_READ_TIMEOUT = 30000; // 30 seconds timeout for file operations
|
|
561
|
+
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
251
562
|
// Use withTimeout to handle potential hangs
|
|
252
563
|
const readOperation = async () => {
|
|
253
564
|
if (isImage) {
|
|
@@ -258,42 +569,9 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
258
569
|
return { content, mimeType, isImage };
|
|
259
570
|
}
|
|
260
571
|
else {
|
|
261
|
-
// For all other files,
|
|
572
|
+
// For all other files, use smart positioning approach
|
|
262
573
|
try {
|
|
263
|
-
|
|
264
|
-
const buffer = await fs.readFile(validPath);
|
|
265
|
-
const fullContent = buffer.toString('utf-8');
|
|
266
|
-
// Split into lines for line-based access
|
|
267
|
-
const lines = fullContent.split('\n');
|
|
268
|
-
const totalLines = lines.length;
|
|
269
|
-
// Apply line-based offset and length - handle beyond-file-size scenario
|
|
270
|
-
let startLine = Math.min(offset, totalLines);
|
|
271
|
-
let endLine = Math.min(startLine + length, totalLines);
|
|
272
|
-
// If startLine equals totalLines (reading beyond end), adjust to show some content
|
|
273
|
-
// Only do this if we're not trying to read the whole file
|
|
274
|
-
if (startLine === totalLines && offset > 0 && length < Number.MAX_SAFE_INTEGER) {
|
|
275
|
-
// Show last few lines instead of nothing
|
|
276
|
-
const lastLinesCount = Math.min(10, totalLines); // Show last 10 lines or fewer if file is smaller
|
|
277
|
-
startLine = Math.max(0, totalLines - lastLinesCount);
|
|
278
|
-
endLine = totalLines;
|
|
279
|
-
}
|
|
280
|
-
const selectedLines = lines.slice(startLine, endLine);
|
|
281
|
-
const truncatedContent = selectedLines.join('\n');
|
|
282
|
-
// Add an informational message if truncated or adjusted
|
|
283
|
-
let content = truncatedContent;
|
|
284
|
-
// Only add informational message for normal reads (not when reading entire file)
|
|
285
|
-
const isEntireFileRead = offset === 0 && length >= Number.MAX_SAFE_INTEGER;
|
|
286
|
-
if (!isEntireFileRead) {
|
|
287
|
-
if (offset >= totalLines && totalLines > 0) {
|
|
288
|
-
// Reading beyond end of file case
|
|
289
|
-
content = `[NOTICE: Offset ${offset} exceeds file length (${totalLines} lines). Showing last ${endLine - startLine} lines instead.]\n\n${truncatedContent}`;
|
|
290
|
-
}
|
|
291
|
-
else if (offset > 0 || endLine < totalLines) {
|
|
292
|
-
// Normal partial read case
|
|
293
|
-
content = `[Reading ${endLine - startLine} lines from line ${startLine} of ${totalLines} total lines]\n\n${truncatedContent}`;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return { content, mimeType, isImage };
|
|
574
|
+
return await readFileWithSmartPositioning(validPath, offset, length, mimeType, true);
|
|
297
575
|
}
|
|
298
576
|
catch (error) {
|
|
299
577
|
// If UTF-8 reading fails, treat as binary and return base64 but still as text
|
|
@@ -304,7 +582,7 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
304
582
|
}
|
|
305
583
|
};
|
|
306
584
|
// Execute with timeout
|
|
307
|
-
const result = await withTimeout(readOperation(),
|
|
585
|
+
const result = await withTimeout(readOperation(), FILE_OPERATION_TIMEOUTS.FILE_READ, `Read file operation for ${filePath}`, null);
|
|
308
586
|
if (result == null) {
|
|
309
587
|
// Handles the impossible case where withTimeout resolves to null instead of throwing
|
|
310
588
|
throw new Error('Failed to read the file');
|
|
@@ -324,13 +602,88 @@ export async function readFile(filePath, isUrl, offset, length) {
|
|
|
324
602
|
? readFileFromUrl(filePath)
|
|
325
603
|
: readFileFromDisk(filePath, offset, length);
|
|
326
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Read file content without status messages for internal operations
|
|
607
|
+
* This function preserves exact file content including original line endings,
|
|
608
|
+
* which is essential for edit operations that need to maintain file formatting.
|
|
609
|
+
* @param filePath Path to the file
|
|
610
|
+
* @param offset Starting line number to read from (default: 0)
|
|
611
|
+
* @param length Maximum number of lines to read (default: from config or 1000)
|
|
612
|
+
* @returns File content without status headers, with preserved line endings
|
|
613
|
+
*/
|
|
614
|
+
export async function readFileInternal(filePath, offset = 0, length) {
|
|
615
|
+
// Get default length from config if not provided
|
|
616
|
+
if (length === undefined) {
|
|
617
|
+
length = await getDefaultReadLength();
|
|
618
|
+
}
|
|
619
|
+
const validPath = await validatePath(filePath);
|
|
620
|
+
// Get file extension and MIME type
|
|
621
|
+
const fileExtension = getFileExtension(validPath);
|
|
622
|
+
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
623
|
+
if (isImage) {
|
|
624
|
+
throw new Error('Cannot read image files as text for internal operations');
|
|
625
|
+
}
|
|
626
|
+
// IMPORTANT: For internal operations (especially edit operations), we must
|
|
627
|
+
// preserve exact file content including original line endings.
|
|
628
|
+
// We cannot use readline-based reading as it strips line endings.
|
|
629
|
+
// Read entire file content preserving line endings
|
|
630
|
+
const content = await fs.readFile(validPath, 'utf8');
|
|
631
|
+
// If we need to apply offset/length, do it while preserving line endings
|
|
632
|
+
if (offset === 0 && length >= Number.MAX_SAFE_INTEGER) {
|
|
633
|
+
// Most common case for edit operations: read entire file
|
|
634
|
+
return content;
|
|
635
|
+
}
|
|
636
|
+
// Handle offset/length by splitting on line boundaries while preserving line endings
|
|
637
|
+
const lines = splitLinesPreservingEndings(content);
|
|
638
|
+
// Apply offset and length
|
|
639
|
+
const selectedLines = lines.slice(offset, offset + length);
|
|
640
|
+
// Join back together (this preserves the original line endings)
|
|
641
|
+
return selectedLines.join('');
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Split text into lines while preserving original line endings with each line
|
|
645
|
+
* @param content The text content to split
|
|
646
|
+
* @returns Array of lines, each including its original line ending
|
|
647
|
+
*/
|
|
648
|
+
function splitLinesPreservingEndings(content) {
|
|
649
|
+
if (!content)
|
|
650
|
+
return [''];
|
|
651
|
+
const lines = [];
|
|
652
|
+
let currentLine = '';
|
|
653
|
+
for (let i = 0; i < content.length; i++) {
|
|
654
|
+
const char = content[i];
|
|
655
|
+
currentLine += char;
|
|
656
|
+
// Check for line ending patterns
|
|
657
|
+
if (char === '\n') {
|
|
658
|
+
// LF or end of CRLF
|
|
659
|
+
lines.push(currentLine);
|
|
660
|
+
currentLine = '';
|
|
661
|
+
}
|
|
662
|
+
else if (char === '\r') {
|
|
663
|
+
// Could be CR or start of CRLF
|
|
664
|
+
if (i + 1 < content.length && content[i + 1] === '\n') {
|
|
665
|
+
// It's CRLF, include the \n as well
|
|
666
|
+
currentLine += content[i + 1];
|
|
667
|
+
i++; // Skip the \n in next iteration
|
|
668
|
+
}
|
|
669
|
+
// Either way, we have a complete line
|
|
670
|
+
lines.push(currentLine);
|
|
671
|
+
currentLine = '';
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Handle any remaining content (file not ending with line ending)
|
|
675
|
+
if (currentLine) {
|
|
676
|
+
lines.push(currentLine);
|
|
677
|
+
}
|
|
678
|
+
return lines;
|
|
679
|
+
}
|
|
327
680
|
export async function writeFile(filePath, content, mode = 'rewrite') {
|
|
328
681
|
const validPath = await validatePath(filePath);
|
|
329
682
|
// Get file extension for telemetry
|
|
330
|
-
const fileExtension =
|
|
683
|
+
const fileExtension = getFileExtension(validPath);
|
|
331
684
|
// Calculate content metrics
|
|
332
685
|
const contentBytes = Buffer.from(content).length;
|
|
333
|
-
const lineCount = content
|
|
686
|
+
const lineCount = countLines(content);
|
|
334
687
|
// Capture file extension and operation details in telemetry without capturing the file path
|
|
335
688
|
capture('server_write_file', {
|
|
336
689
|
fileExtension: fileExtension,
|
|
@@ -443,15 +796,14 @@ export async function getFileInfo(filePath) {
|
|
|
443
796
|
permissions: stats.mode.toString(8).slice(-3),
|
|
444
797
|
};
|
|
445
798
|
// For text files that aren't too large, also count lines
|
|
446
|
-
if (stats.isFile() && stats.size <
|
|
799
|
+
if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
447
800
|
try {
|
|
448
|
-
//
|
|
449
|
-
const {
|
|
450
|
-
const mimeType = getMimeType(validPath);
|
|
801
|
+
// Get MIME type information
|
|
802
|
+
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
451
803
|
// Only count lines for non-image, likely text files
|
|
452
|
-
if (!
|
|
804
|
+
if (!isImage) {
|
|
453
805
|
const content = await fs.readFile(validPath, 'utf8');
|
|
454
|
-
const lineCount = content
|
|
806
|
+
const lineCount = countLines(content);
|
|
455
807
|
info.lineCount = lineCount;
|
|
456
808
|
info.lastLine = lineCount - 1; // Zero-indexed last line
|
|
457
809
|
info.appendPosition = lineCount; // Position to append at end
|
|
@@ -464,5 +816,3 @@ export async function getFileInfo(filePath) {
|
|
|
464
816
|
}
|
|
465
817
|
return info;
|
|
466
818
|
}
|
|
467
|
-
// This function has been replaced with configManager.getConfig()
|
|
468
|
-
// Use get_config tool to retrieve allowedDirectories
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ServerResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Start a new process (renamed from execute_command)
|
|
4
|
+
* Includes early detection of process waiting for input
|
|
5
|
+
*/
|
|
6
|
+
export declare function startProcess(args: unknown): Promise<ServerResult>;
|
|
7
|
+
/**
|
|
8
|
+
* Read output from a running process (renamed from read_output)
|
|
9
|
+
* Includes early detection of process waiting for input
|
|
10
|
+
*/
|
|
11
|
+
export declare function readProcessOutput(args: unknown): Promise<ServerResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Interact with a running process (renamed from send_input)
|
|
14
|
+
* Automatically detects when process is ready and returns output
|
|
15
|
+
*/
|
|
16
|
+
export declare function interactWithProcess(args: unknown): Promise<ServerResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Force terminate a process
|
|
19
|
+
*/
|
|
20
|
+
export declare function forceTerminate(args: unknown): Promise<ServerResult>;
|
|
21
|
+
/**
|
|
22
|
+
* List active sessions
|
|
23
|
+
*/
|
|
24
|
+
export declare function listSessions(): Promise<ServerResult>;
|