@wonderwhy-er/desktop-commander 0.2.23 → 0.2.24
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 +14 -55
- package/dist/custom-stdio.d.ts +1 -0
- package/dist/custom-stdio.js +19 -0
- package/dist/handlers/filesystem-handlers.d.ts +4 -0
- package/dist/handlers/filesystem-handlers.js +120 -14
- package/dist/handlers/node-handlers.d.ts +6 -0
- package/dist/handlers/node-handlers.js +73 -0
- package/dist/index.js +5 -3
- package/dist/search-manager.d.ts +25 -0
- package/dist/search-manager.js +212 -0
- package/dist/server.js +160 -73
- package/dist/terminal-manager.d.ts +56 -2
- package/dist/terminal-manager.js +169 -13
- package/dist/tools/edit.d.ts +28 -4
- package/dist/tools/edit.js +87 -4
- package/dist/tools/filesystem.d.ts +23 -12
- package/dist/tools/filesystem.js +201 -416
- package/dist/tools/improved-process-tools.d.ts +2 -2
- package/dist/tools/improved-process-tools.js +244 -214
- package/dist/tools/mime-types.d.ts +1 -0
- package/dist/tools/mime-types.js +7 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +96 -0
- package/dist/tools/pdf/markdown.d.ts +7 -0
- package/dist/tools/pdf/markdown.js +37 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/schemas.d.ts +167 -12
- package/dist/tools/schemas.js +54 -5
- package/dist/types.d.ts +2 -1
- package/dist/utils/feature-flags.js +7 -4
- package/dist/utils/files/base.d.ts +167 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +416 -0
- package/dist/utils/files/factory.d.ts +40 -0
- package/dist/utils/files/factory.js +101 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +357 -0
- package/dist/utils/ripgrep-resolver.js +3 -2
- package/dist/utils/system-info.d.ts +5 -0
- package/dist/utils/system-info.js +71 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -3
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,66 +2,33 @@ 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';
|
|
7
|
-
import { isBinaryFile } from 'isbinaryfile';
|
|
8
5
|
import { capture } from '../utils/capture.js';
|
|
9
6
|
import { withTimeout } from '../utils/withTimeout.js';
|
|
10
7
|
import { configManager } from '../config-manager.js';
|
|
8
|
+
import { getFileHandler, TextFileHandler } from '../utils/files/index.js';
|
|
9
|
+
import { isPdfFile } from "./mime-types.js";
|
|
10
|
+
import { parsePdfToMarkdown, editPdf, parseMarkdownToPdf } from './pdf/index.js';
|
|
11
11
|
// CONSTANTS SECTION - Consolidate all timeouts and thresholds
|
|
12
12
|
const FILE_OPERATION_TIMEOUTS = {
|
|
13
13
|
PATH_VALIDATION: 10000, // 10 seconds
|
|
14
|
-
URL_FETCH: 30000, // 30 seconds
|
|
14
|
+
URL_FETCH: 30000, // 30 seconds
|
|
15
15
|
FILE_READ: 30000, // 30 seconds
|
|
16
16
|
};
|
|
17
17
|
const FILE_SIZE_LIMITS = {
|
|
18
|
-
LARGE_FILE_THRESHOLD: 10 * 1024 * 1024, // 10MB
|
|
19
18
|
LINE_COUNT_LIMIT: 10 * 1024 * 1024, // 10MB for line counting
|
|
20
19
|
};
|
|
21
|
-
const READ_PERFORMANCE_THRESHOLDS = {
|
|
22
|
-
SMALL_READ_THRESHOLD: 100, // For very small reads
|
|
23
|
-
DEEP_OFFSET_THRESHOLD: 1000, // For byte estimation
|
|
24
|
-
SAMPLE_SIZE: 10000, // Sample size for estimation
|
|
25
|
-
CHUNK_SIZE: 8192, // 8KB chunks for reverse reading
|
|
26
|
-
};
|
|
27
20
|
// UTILITY FUNCTIONS - Eliminate duplication
|
|
28
|
-
/**
|
|
29
|
-
* Count lines in text content efficiently
|
|
30
|
-
* @param content Text content to count lines in
|
|
31
|
-
* @returns Number of lines
|
|
32
|
-
*/
|
|
33
|
-
function countLines(content) {
|
|
34
|
-
return content.split('\n').length;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Count lines in a file efficiently (for files under size limit)
|
|
38
|
-
* @param filePath Path to the file
|
|
39
|
-
* @returns Line count or undefined if file too large/can't read
|
|
40
|
-
*/
|
|
41
|
-
async function getFileLineCount(filePath) {
|
|
42
|
-
try {
|
|
43
|
-
const stats = await fs.stat(filePath);
|
|
44
|
-
// Only count lines for reasonably sized files to avoid performance issues
|
|
45
|
-
if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
46
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
47
|
-
return countLines(content);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
// If we can't read the file, just return undefined
|
|
52
|
-
}
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
21
|
/**
|
|
56
22
|
* Get MIME type information for a file
|
|
57
23
|
* @param filePath Path to the file
|
|
58
24
|
* @returns Object with mimeType and isImage properties
|
|
59
25
|
*/
|
|
60
26
|
async function getMimeTypeInfo(filePath) {
|
|
61
|
-
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
27
|
+
const { getMimeType, isImageFile, isPdfFile } = await import('./mime-types.js');
|
|
62
28
|
const mimeType = getMimeType(filePath);
|
|
63
29
|
const isImage = isImageFile(mimeType);
|
|
64
|
-
|
|
30
|
+
const isPdf = isPdfFile(mimeType);
|
|
31
|
+
return { mimeType, isImage, isPdf };
|
|
65
32
|
}
|
|
66
33
|
/**
|
|
67
34
|
* Get file extension for telemetry purposes
|
|
@@ -79,20 +46,6 @@ async function getDefaultReadLength() {
|
|
|
79
46
|
const config = await configManager.getConfig();
|
|
80
47
|
return config.fileReadLineLimit ?? 1000; // Default to 1000 lines if not set
|
|
81
48
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Generate instructions for handling binary files
|
|
84
|
-
* @param filePath Path to the binary file
|
|
85
|
-
* @param mimeType MIME type of the file
|
|
86
|
-
* @returns Instruction message for the LLM
|
|
87
|
-
*/
|
|
88
|
-
function getBinaryFileInstructions(filePath, mimeType) {
|
|
89
|
-
const fileName = path.basename(filePath);
|
|
90
|
-
return `Cannot read binary file as text: ${fileName} (${mimeType})
|
|
91
|
-
|
|
92
|
-
Use start_process + interact_with_process to analyze binary files with appropriate tools (Node.js or Python libraries, command-line utilities, etc.).
|
|
93
|
-
|
|
94
|
-
The read_file tool only handles text files and images.`;
|
|
95
|
-
}
|
|
96
49
|
// Initialize allowed directories from configuration
|
|
97
50
|
async function getAllowedDirs() {
|
|
98
51
|
try {
|
|
@@ -266,19 +219,37 @@ export async function readFileFromUrl(url) {
|
|
|
266
219
|
if (!response.ok) {
|
|
267
220
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
268
221
|
}
|
|
269
|
-
// Get MIME type from Content-Type header
|
|
222
|
+
// Get MIME type from Content-Type header or infer from URL
|
|
270
223
|
const contentType = response.headers.get('content-type') || 'text/plain';
|
|
271
224
|
const isImage = isImageFile(contentType);
|
|
272
|
-
|
|
225
|
+
const isPdf = isPdfFile(contentType) || url.toLowerCase().endsWith('.pdf');
|
|
226
|
+
// NEW: Add PDF handling before image check
|
|
227
|
+
if (isPdf) {
|
|
228
|
+
// Use URL directly - pdfreader handles URL downloads internally
|
|
229
|
+
const pdfResult = await parsePdfToMarkdown(url);
|
|
230
|
+
return {
|
|
231
|
+
content: "",
|
|
232
|
+
mimeType: 'text/plain',
|
|
233
|
+
metadata: {
|
|
234
|
+
isImage: false,
|
|
235
|
+
isPdf: true,
|
|
236
|
+
author: pdfResult.metadata.author,
|
|
237
|
+
title: pdfResult.metadata.title,
|
|
238
|
+
totalPages: pdfResult.metadata.totalPages,
|
|
239
|
+
pages: pdfResult.pages
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
else if (isImage) {
|
|
273
244
|
// For images, convert to base64
|
|
274
245
|
const buffer = await response.arrayBuffer();
|
|
275
246
|
const content = Buffer.from(buffer).toString('base64');
|
|
276
|
-
return { content, mimeType: contentType, isImage };
|
|
247
|
+
return { content, mimeType: contentType, metadata: { isImage } };
|
|
277
248
|
}
|
|
278
249
|
else {
|
|
279
250
|
// For text content
|
|
280
251
|
const content = await response.text();
|
|
281
|
-
return { content, mimeType: contentType, isImage };
|
|
252
|
+
return { content, mimeType: contentType, metadata: { isImage } };
|
|
282
253
|
}
|
|
283
254
|
}
|
|
284
255
|
catch (error) {
|
|
@@ -291,269 +262,15 @@ export async function readFileFromUrl(url) {
|
|
|
291
262
|
throw new Error(errorMessage);
|
|
292
263
|
}
|
|
293
264
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Generate enhanced status message with total and remaining line information
|
|
296
|
-
* @param readLines Number of lines actually read
|
|
297
|
-
* @param offset Starting offset (line number)
|
|
298
|
-
* @param totalLines Total lines in the file (if available)
|
|
299
|
-
* @param isNegativeOffset Whether this is a tail operation
|
|
300
|
-
* @returns Enhanced status message string
|
|
301
|
-
*/
|
|
302
|
-
function generateEnhancedStatusMessage(readLines, offset, totalLines, isNegativeOffset = false) {
|
|
303
|
-
if (isNegativeOffset) {
|
|
304
|
-
// For tail operations (negative offset)
|
|
305
|
-
if (totalLines !== undefined) {
|
|
306
|
-
return `[Reading last ${readLines} lines (total: ${totalLines} lines)]`;
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
return `[Reading last ${readLines} lines]`;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
// For normal reads (positive offset)
|
|
314
|
-
if (totalLines !== undefined) {
|
|
315
|
-
const endLine = offset + readLines;
|
|
316
|
-
const remainingLines = Math.max(0, totalLines - endLine);
|
|
317
|
-
if (offset === 0) {
|
|
318
|
-
return `[Reading ${readLines} lines from start (total: ${totalLines} lines, ${remainingLines} remaining)]`;
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
return `[Reading ${readLines} lines from line ${offset} (total: ${totalLines} lines, ${remainingLines} remaining)]`;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
// Fallback when total lines unknown
|
|
326
|
-
if (offset === 0) {
|
|
327
|
-
return `[Reading ${readLines} lines from start]`;
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
return `[Reading ${readLines} lines from line ${offset}]`;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Read file content using smart positioning for optimal performance
|
|
337
|
-
* @param filePath Path to the file (already validated)
|
|
338
|
-
* @param offset Starting line number (negative for tail behavior)
|
|
339
|
-
* @param length Maximum number of lines to read
|
|
340
|
-
* @param mimeType MIME type of the file
|
|
341
|
-
* @param includeStatusMessage Whether to include status headers (default: true)
|
|
342
|
-
* @returns File result with content
|
|
343
|
-
*/
|
|
344
|
-
async function readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
345
|
-
const stats = await fs.stat(filePath);
|
|
346
|
-
const fileSize = stats.size;
|
|
347
|
-
// Check if the file is binary (but allow images to pass through)
|
|
348
|
-
const { isImage } = await getMimeTypeInfo(filePath);
|
|
349
|
-
if (!isImage) {
|
|
350
|
-
const isBinary = await isBinaryFile(filePath);
|
|
351
|
-
if (isBinary) {
|
|
352
|
-
// Return instructions instead of trying to read binary content
|
|
353
|
-
const instructions = getBinaryFileInstructions(filePath, mimeType);
|
|
354
|
-
throw new Error(instructions);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
// Get total line count for enhanced status messages (only for smaller files)
|
|
358
|
-
const totalLines = await getFileLineCount(filePath);
|
|
359
|
-
// For negative offsets (tail behavior), use reverse reading
|
|
360
|
-
if (offset < 0) {
|
|
361
|
-
const requestedLines = Math.abs(offset);
|
|
362
|
-
if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD && requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
|
|
363
|
-
// Use efficient reverse reading for large files with small tail requests
|
|
364
|
-
return await readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
// Use readline circular buffer for other cases
|
|
368
|
-
return await readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
// For positive offsets
|
|
372
|
-
else {
|
|
373
|
-
// For small files or reading from start, use simple readline
|
|
374
|
-
if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
|
|
375
|
-
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
376
|
-
}
|
|
377
|
-
// For large files with middle/end reads, try to estimate position
|
|
378
|
-
else {
|
|
379
|
-
// If seeking deep into file, try byte estimation
|
|
380
|
-
if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
|
|
381
|
-
return await readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Read last N lines efficiently by reading file backwards in chunks
|
|
391
|
-
*/
|
|
392
|
-
async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
393
|
-
const fd = await fs.open(filePath, 'r');
|
|
394
|
-
try {
|
|
395
|
-
const stats = await fd.stat();
|
|
396
|
-
const fileSize = stats.size;
|
|
397
|
-
let position = fileSize;
|
|
398
|
-
let lines = [];
|
|
399
|
-
let partialLine = '';
|
|
400
|
-
while (position > 0 && lines.length < n) {
|
|
401
|
-
const readSize = Math.min(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE, position);
|
|
402
|
-
position -= readSize;
|
|
403
|
-
const buffer = Buffer.alloc(readSize);
|
|
404
|
-
await fd.read(buffer, 0, readSize, position);
|
|
405
|
-
const chunk = buffer.toString('utf-8');
|
|
406
|
-
const text = chunk + partialLine;
|
|
407
|
-
const chunkLines = text.split('\n');
|
|
408
|
-
partialLine = chunkLines.shift() || '';
|
|
409
|
-
lines = chunkLines.concat(lines);
|
|
410
|
-
}
|
|
411
|
-
// Add the remaining partial line if we reached the beginning
|
|
412
|
-
if (position === 0 && partialLine) {
|
|
413
|
-
lines.unshift(partialLine);
|
|
414
|
-
}
|
|
415
|
-
const result = lines.slice(-n); // Get exactly n lines
|
|
416
|
-
const content = includeStatusMessage
|
|
417
|
-
? `${generateEnhancedStatusMessage(result.length, -n, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
418
|
-
: result.join('\n');
|
|
419
|
-
return { content, mimeType, isImage: false };
|
|
420
|
-
}
|
|
421
|
-
finally {
|
|
422
|
-
await fd.close();
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Read from end using readline with circular buffer
|
|
427
|
-
*/
|
|
428
|
-
async function readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
429
|
-
const rl = createInterface({
|
|
430
|
-
input: createReadStream(filePath),
|
|
431
|
-
crlfDelay: Infinity
|
|
432
|
-
});
|
|
433
|
-
const buffer = new Array(requestedLines);
|
|
434
|
-
let bufferIndex = 0;
|
|
435
|
-
let totalLines = 0;
|
|
436
|
-
for await (const line of rl) {
|
|
437
|
-
buffer[bufferIndex] = line;
|
|
438
|
-
bufferIndex = (bufferIndex + 1) % requestedLines;
|
|
439
|
-
totalLines++;
|
|
440
|
-
}
|
|
441
|
-
rl.close();
|
|
442
|
-
// Extract lines in correct order
|
|
443
|
-
let result;
|
|
444
|
-
if (totalLines >= requestedLines) {
|
|
445
|
-
result = [
|
|
446
|
-
...buffer.slice(bufferIndex),
|
|
447
|
-
...buffer.slice(0, bufferIndex)
|
|
448
|
-
].filter(line => line !== undefined);
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
result = buffer.slice(0, totalLines);
|
|
452
|
-
}
|
|
453
|
-
const content = includeStatusMessage
|
|
454
|
-
? `${generateEnhancedStatusMessage(result.length, -requestedLines, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
455
|
-
: result.join('\n');
|
|
456
|
-
return { content, mimeType, isImage: false };
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Read from start/middle using readline
|
|
460
|
-
*/
|
|
461
|
-
async function readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
462
|
-
const rl = createInterface({
|
|
463
|
-
input: createReadStream(filePath),
|
|
464
|
-
crlfDelay: Infinity
|
|
465
|
-
});
|
|
466
|
-
const result = [];
|
|
467
|
-
let lineNumber = 0;
|
|
468
|
-
for await (const line of rl) {
|
|
469
|
-
if (lineNumber >= offset && result.length < length) {
|
|
470
|
-
result.push(line);
|
|
471
|
-
}
|
|
472
|
-
if (result.length >= length)
|
|
473
|
-
break; // Early exit optimization
|
|
474
|
-
lineNumber++;
|
|
475
|
-
}
|
|
476
|
-
rl.close();
|
|
477
|
-
if (includeStatusMessage) {
|
|
478
|
-
const statusMessage = generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
|
|
479
|
-
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
480
|
-
return { content, mimeType, isImage: false };
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
const content = result.join('\n');
|
|
484
|
-
return { content, mimeType, isImage: false };
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Read from estimated byte position for very large files
|
|
489
|
-
*/
|
|
490
|
-
async function readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
491
|
-
// First, do a quick scan to estimate lines per byte
|
|
492
|
-
const rl = createInterface({
|
|
493
|
-
input: createReadStream(filePath),
|
|
494
|
-
crlfDelay: Infinity
|
|
495
|
-
});
|
|
496
|
-
let sampleLines = 0;
|
|
497
|
-
let bytesRead = 0;
|
|
498
|
-
for await (const line of rl) {
|
|
499
|
-
bytesRead += Buffer.byteLength(line, 'utf-8') + 1; // +1 for newline
|
|
500
|
-
sampleLines++;
|
|
501
|
-
if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
|
|
502
|
-
break;
|
|
503
|
-
}
|
|
504
|
-
rl.close();
|
|
505
|
-
if (sampleLines === 0) {
|
|
506
|
-
// Fallback to simple read
|
|
507
|
-
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines);
|
|
508
|
-
}
|
|
509
|
-
// Estimate average line length and seek position
|
|
510
|
-
const avgLineLength = bytesRead / sampleLines;
|
|
511
|
-
const estimatedBytePosition = Math.floor(offset * avgLineLength);
|
|
512
|
-
// Create a new stream starting from estimated position
|
|
513
|
-
const fd = await fs.open(filePath, 'r');
|
|
514
|
-
try {
|
|
515
|
-
const stats = await fd.stat();
|
|
516
|
-
const startPosition = Math.min(estimatedBytePosition, stats.size);
|
|
517
|
-
const stream = createReadStream(filePath, { start: startPosition });
|
|
518
|
-
const rl2 = createInterface({
|
|
519
|
-
input: stream,
|
|
520
|
-
crlfDelay: Infinity
|
|
521
|
-
});
|
|
522
|
-
const result = [];
|
|
523
|
-
let lineCount = 0;
|
|
524
|
-
let firstLineSkipped = false;
|
|
525
|
-
for await (const line of rl2) {
|
|
526
|
-
// Skip first potentially partial line if we didn't start at beginning
|
|
527
|
-
if (!firstLineSkipped && startPosition > 0) {
|
|
528
|
-
firstLineSkipped = true;
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (result.length < length) {
|
|
532
|
-
result.push(line);
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
lineCount++;
|
|
538
|
-
}
|
|
539
|
-
rl2.close();
|
|
540
|
-
const content = includeStatusMessage
|
|
541
|
-
? `${generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
|
|
542
|
-
: result.join('\n');
|
|
543
|
-
return { content, mimeType, isImage: false };
|
|
544
|
-
}
|
|
545
|
-
finally {
|
|
546
|
-
await fd.close();
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
265
|
/**
|
|
550
266
|
* Read file content from the local filesystem
|
|
551
267
|
* @param filePath Path to the file
|
|
552
|
-
* @param
|
|
553
|
-
* @param length Maximum number of lines to read (default: from config or 1000)
|
|
268
|
+
* @param options Read options (offset, length, sheet, range)
|
|
554
269
|
* @returns File content or file result with metadata
|
|
555
270
|
*/
|
|
556
|
-
export async function readFileFromDisk(filePath,
|
|
271
|
+
export async function readFileFromDisk(filePath, options) {
|
|
272
|
+
const { offset = 0, sheet, range } = options ?? {};
|
|
273
|
+
let { length } = options ?? {};
|
|
557
274
|
// Add validation for required parameters
|
|
558
275
|
if (!filePath || typeof filePath !== 'string') {
|
|
559
276
|
throw new Error('Invalid file path provided');
|
|
@@ -563,7 +280,7 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
563
280
|
length = await getDefaultReadLength();
|
|
564
281
|
}
|
|
565
282
|
const validPath = await validatePath(filePath);
|
|
566
|
-
// Get file extension for telemetry
|
|
283
|
+
// Get file extension for telemetry
|
|
567
284
|
const fileExtension = getFileExtension(validPath);
|
|
568
285
|
// Check file size before attempting to read
|
|
569
286
|
try {
|
|
@@ -582,37 +299,37 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
582
299
|
capture('server_read_file_error', { error: errorMessage, fileExtension: fileExtension });
|
|
583
300
|
// If we can't stat the file, continue anyway and let the read operation handle errors
|
|
584
301
|
}
|
|
585
|
-
// Detect the MIME type based on file extension
|
|
586
|
-
const { mimeType, isImage } = await getMimeTypeInfo(validPath);
|
|
587
302
|
// Use withTimeout to handle potential hangs
|
|
588
303
|
const readOperation = async () => {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
304
|
+
// Get appropriate handler for this file type (async - includes binary detection)
|
|
305
|
+
const handler = await getFileHandler(validPath);
|
|
306
|
+
// Use handler to read the file
|
|
307
|
+
const result = await handler.read(validPath, {
|
|
308
|
+
offset,
|
|
309
|
+
length,
|
|
310
|
+
sheet,
|
|
311
|
+
range,
|
|
312
|
+
includeStatusMessage: true
|
|
313
|
+
});
|
|
314
|
+
// Return with content as string
|
|
315
|
+
// For images: content is already base64-encoded string from handler
|
|
316
|
+
// For text: content may be string or Buffer, convert to UTF-8 string
|
|
317
|
+
let content;
|
|
318
|
+
if (typeof result.content === 'string') {
|
|
319
|
+
content = result.content;
|
|
320
|
+
}
|
|
321
|
+
else if (result.metadata?.isImage) {
|
|
322
|
+
// Image buffer should be base64 encoded, not UTF-8 converted
|
|
323
|
+
content = result.content.toString('base64');
|
|
595
324
|
}
|
|
596
325
|
else {
|
|
597
|
-
|
|
598
|
-
try {
|
|
599
|
-
return await readFileWithSmartPositioning(validPath, offset, length, mimeType, true);
|
|
600
|
-
}
|
|
601
|
-
catch (error) {
|
|
602
|
-
// If it's our binary file instruction error, return it as content
|
|
603
|
-
if (error instanceof Error && error.message.includes('Cannot read binary file as text:')) {
|
|
604
|
-
return { content: error.message, mimeType: 'text/plain', isImage: false };
|
|
605
|
-
}
|
|
606
|
-
// If UTF-8 reading fails for other reasons, also check if it's binary
|
|
607
|
-
const isBinary = await isBinaryFile(validPath);
|
|
608
|
-
if (isBinary) {
|
|
609
|
-
const instructions = getBinaryFileInstructions(validPath, mimeType);
|
|
610
|
-
return { content: instructions, mimeType: 'text/plain', isImage: false };
|
|
611
|
-
}
|
|
612
|
-
// Only if it's truly not binary, then we have a real UTF-8 reading error
|
|
613
|
-
throw error;
|
|
614
|
-
}
|
|
326
|
+
content = result.content.toString('utf8');
|
|
615
327
|
}
|
|
328
|
+
return {
|
|
329
|
+
content,
|
|
330
|
+
mimeType: result.mimeType,
|
|
331
|
+
metadata: result.metadata
|
|
332
|
+
};
|
|
616
333
|
};
|
|
617
334
|
// Execute with timeout
|
|
618
335
|
const result = await withTimeout(readOperation(), FILE_OPERATION_TIMEOUTS.FILE_READ, `Read file operation for ${filePath}`, null);
|
|
@@ -625,15 +342,14 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
625
342
|
/**
|
|
626
343
|
* Read a file from either the local filesystem or a URL
|
|
627
344
|
* @param filePath Path to the file or URL
|
|
628
|
-
* @param
|
|
629
|
-
* @param offset Starting line number to read from (default: 0)
|
|
630
|
-
* @param length Maximum number of lines to read (default: from config or 1000)
|
|
345
|
+
* @param options Read options (isUrl, offset, length, sheet, range)
|
|
631
346
|
* @returns File content or file result with metadata
|
|
632
347
|
*/
|
|
633
|
-
export async function readFile(filePath,
|
|
348
|
+
export async function readFile(filePath, options) {
|
|
349
|
+
const { isUrl, offset, length, sheet, range } = options ?? {};
|
|
634
350
|
return isUrl
|
|
635
351
|
? readFileFromUrl(filePath)
|
|
636
|
-
: readFileFromDisk(filePath, offset, length);
|
|
352
|
+
: readFileFromDisk(filePath, { offset, length, sheet, range });
|
|
637
353
|
}
|
|
638
354
|
/**
|
|
639
355
|
* Read file content without status messages for internal operations
|
|
@@ -667,56 +383,19 @@ export async function readFileInternal(filePath, offset = 0, length) {
|
|
|
667
383
|
return content;
|
|
668
384
|
}
|
|
669
385
|
// Handle offset/length by splitting on line boundaries while preserving line endings
|
|
670
|
-
const lines = splitLinesPreservingEndings(content);
|
|
386
|
+
const lines = TextFileHandler.splitLinesPreservingEndings(content);
|
|
671
387
|
// Apply offset and length
|
|
672
388
|
const selectedLines = lines.slice(offset, offset + length);
|
|
673
389
|
// Join back together (this preserves the original line endings)
|
|
674
390
|
return selectedLines.join('');
|
|
675
391
|
}
|
|
676
|
-
/**
|
|
677
|
-
* Split text into lines while preserving original line endings with each line
|
|
678
|
-
* @param content The text content to split
|
|
679
|
-
* @returns Array of lines, each including its original line ending
|
|
680
|
-
*/
|
|
681
|
-
function splitLinesPreservingEndings(content) {
|
|
682
|
-
if (!content)
|
|
683
|
-
return [''];
|
|
684
|
-
const lines = [];
|
|
685
|
-
let currentLine = '';
|
|
686
|
-
for (let i = 0; i < content.length; i++) {
|
|
687
|
-
const char = content[i];
|
|
688
|
-
currentLine += char;
|
|
689
|
-
// Check for line ending patterns
|
|
690
|
-
if (char === '\n') {
|
|
691
|
-
// LF or end of CRLF
|
|
692
|
-
lines.push(currentLine);
|
|
693
|
-
currentLine = '';
|
|
694
|
-
}
|
|
695
|
-
else if (char === '\r') {
|
|
696
|
-
// Could be CR or start of CRLF
|
|
697
|
-
if (i + 1 < content.length && content[i + 1] === '\n') {
|
|
698
|
-
// It's CRLF, include the \n as well
|
|
699
|
-
currentLine += content[i + 1];
|
|
700
|
-
i++; // Skip the \n in next iteration
|
|
701
|
-
}
|
|
702
|
-
// Either way, we have a complete line
|
|
703
|
-
lines.push(currentLine);
|
|
704
|
-
currentLine = '';
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
// Handle any remaining content (file not ending with line ending)
|
|
708
|
-
if (currentLine) {
|
|
709
|
-
lines.push(currentLine);
|
|
710
|
-
}
|
|
711
|
-
return lines;
|
|
712
|
-
}
|
|
713
392
|
export async function writeFile(filePath, content, mode = 'rewrite') {
|
|
714
393
|
const validPath = await validatePath(filePath);
|
|
715
394
|
// Get file extension for telemetry
|
|
716
395
|
const fileExtension = getFileExtension(validPath);
|
|
717
396
|
// Calculate content metrics
|
|
718
397
|
const contentBytes = Buffer.from(content).length;
|
|
719
|
-
const lineCount = countLines(content);
|
|
398
|
+
const lineCount = TextFileHandler.countLines(content);
|
|
720
399
|
// Capture file extension and operation details in telemetry without capturing the file path
|
|
721
400
|
capture('server_write_file', {
|
|
722
401
|
fileExtension: fileExtension,
|
|
@@ -724,24 +403,41 @@ export async function writeFile(filePath, content, mode = 'rewrite') {
|
|
|
724
403
|
contentBytes: contentBytes,
|
|
725
404
|
lineCount: lineCount
|
|
726
405
|
});
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
else {
|
|
732
|
-
await fs.writeFile(validPath, content);
|
|
733
|
-
}
|
|
406
|
+
// Get appropriate handler for this file type (async - includes binary detection)
|
|
407
|
+
const handler = await getFileHandler(validPath);
|
|
408
|
+
// Use handler to write the file
|
|
409
|
+
await handler.write(validPath, content, mode);
|
|
734
410
|
}
|
|
735
411
|
export async function readMultipleFiles(paths) {
|
|
736
412
|
return Promise.all(paths.map(async (filePath) => {
|
|
737
413
|
try {
|
|
738
414
|
const validPath = await validatePath(filePath);
|
|
739
415
|
const fileResult = await readFile(validPath);
|
|
416
|
+
// Handle content conversion properly for images vs text
|
|
417
|
+
let content;
|
|
418
|
+
if (typeof fileResult.content === 'string') {
|
|
419
|
+
content = fileResult.content;
|
|
420
|
+
}
|
|
421
|
+
else if (fileResult.metadata?.isImage) {
|
|
422
|
+
content = fileResult.content.toString('base64');
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
content = fileResult.content.toString('utf8');
|
|
426
|
+
}
|
|
740
427
|
return {
|
|
741
428
|
path: filePath,
|
|
742
|
-
content
|
|
743
|
-
mimeType:
|
|
744
|
-
isImage:
|
|
429
|
+
content,
|
|
430
|
+
mimeType: fileResult.mimeType,
|
|
431
|
+
isImage: fileResult.metadata?.isImage ?? false,
|
|
432
|
+
isPdf: fileResult.metadata?.isPdf ?? false,
|
|
433
|
+
payload: fileResult.metadata?.isPdf ? {
|
|
434
|
+
metadata: {
|
|
435
|
+
author: fileResult.metadata.author,
|
|
436
|
+
title: fileResult.metadata.title,
|
|
437
|
+
totalPages: fileResult.metadata.totalPages ?? 0
|
|
438
|
+
},
|
|
439
|
+
pages: fileResult.metadata.pages ?? []
|
|
440
|
+
} : undefined
|
|
745
441
|
};
|
|
746
442
|
}
|
|
747
443
|
catch (error) {
|
|
@@ -923,9 +619,9 @@ async function searchFilesNodeJS(rootPath, pattern) {
|
|
|
923
619
|
}
|
|
924
620
|
export async function getFileInfo(filePath) {
|
|
925
621
|
const validPath = await validatePath(filePath);
|
|
622
|
+
// Get fs.stat as a fallback for any missing fields
|
|
926
623
|
const stats = await fs.stat(validPath);
|
|
927
|
-
|
|
928
|
-
const info = {
|
|
624
|
+
const fallbackInfo = {
|
|
929
625
|
size: stats.size,
|
|
930
626
|
created: stats.birthtime,
|
|
931
627
|
modified: stats.mtime,
|
|
@@ -933,25 +629,114 @@ export async function getFileInfo(filePath) {
|
|
|
933
629
|
isDirectory: stats.isDirectory(),
|
|
934
630
|
isFile: stats.isFile(),
|
|
935
631
|
permissions: stats.mode.toString(8).slice(-3),
|
|
632
|
+
fileType: 'text',
|
|
633
|
+
metadata: undefined,
|
|
936
634
|
};
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
635
|
+
// Get appropriate handler for this file type (async - includes binary detection)
|
|
636
|
+
const handler = await getFileHandler(validPath);
|
|
637
|
+
// Use handler to get file info, with fallback
|
|
638
|
+
let fileInfo;
|
|
639
|
+
try {
|
|
640
|
+
fileInfo = await handler.getInfo(validPath);
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
// If handler fails, use fallback stats
|
|
644
|
+
fileInfo = fallbackInfo;
|
|
645
|
+
}
|
|
646
|
+
// Convert to legacy format (for backward compatibility)
|
|
647
|
+
// Use handler values with fallback to fs.stat values for any missing fields
|
|
648
|
+
const info = {
|
|
649
|
+
size: fileInfo.size ?? fallbackInfo.size,
|
|
650
|
+
created: fileInfo.created ?? fallbackInfo.created,
|
|
651
|
+
modified: fileInfo.modified ?? fallbackInfo.modified,
|
|
652
|
+
accessed: fileInfo.accessed ?? fallbackInfo.accessed,
|
|
653
|
+
isDirectory: fileInfo.isDirectory ?? fallbackInfo.isDirectory,
|
|
654
|
+
isFile: fileInfo.isFile ?? fallbackInfo.isFile,
|
|
655
|
+
permissions: fileInfo.permissions ?? fallbackInfo.permissions,
|
|
656
|
+
fileType: fileInfo.fileType ?? fallbackInfo.fileType,
|
|
657
|
+
};
|
|
658
|
+
// Add type-specific metadata from file handler
|
|
659
|
+
if (fileInfo.metadata) {
|
|
660
|
+
// For text files
|
|
661
|
+
if (fileInfo.metadata.lineCount !== undefined) {
|
|
662
|
+
info.lineCount = fileInfo.metadata.lineCount;
|
|
663
|
+
info.lastLine = fileInfo.metadata.lineCount - 1;
|
|
664
|
+
info.appendPosition = fileInfo.metadata.lineCount;
|
|
665
|
+
}
|
|
666
|
+
// For Excel files
|
|
667
|
+
if (fileInfo.metadata.sheets) {
|
|
668
|
+
info.sheets = fileInfo.metadata.sheets;
|
|
669
|
+
info.isExcelFile = true;
|
|
670
|
+
}
|
|
671
|
+
// For images
|
|
672
|
+
if (fileInfo.metadata.isImage) {
|
|
673
|
+
info.isImage = true;
|
|
674
|
+
}
|
|
675
|
+
// For PDF files
|
|
676
|
+
if (fileInfo.metadata.isPdf) {
|
|
677
|
+
info.isPdf = true;
|
|
678
|
+
info.totalPages = fileInfo.metadata.totalPages;
|
|
679
|
+
if (fileInfo.metadata.title)
|
|
680
|
+
info.title = fileInfo.metadata.title;
|
|
681
|
+
if (fileInfo.metadata.author)
|
|
682
|
+
info.author = fileInfo.metadata.author;
|
|
683
|
+
}
|
|
684
|
+
// For binary files
|
|
685
|
+
if (fileInfo.metadata.isBinary) {
|
|
686
|
+
info.isBinary = true;
|
|
954
687
|
}
|
|
955
688
|
}
|
|
956
689
|
return info;
|
|
957
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Write content to a PDF file.
|
|
693
|
+
* Can create a new PDF from Markdown string, or modify an existing PDF using operations.
|
|
694
|
+
*
|
|
695
|
+
* @param filePath Path to the output PDF file
|
|
696
|
+
* @param content Markdown string (for creation) or array of operations (for modification)
|
|
697
|
+
* @param options Options for PDF generation or modification. For modification, can include `sourcePdf`.
|
|
698
|
+
*/
|
|
699
|
+
export async function writePdf(filePath, content, outputPath, options = {}) {
|
|
700
|
+
const validPath = await validatePath(filePath);
|
|
701
|
+
const fileExtension = getFileExtension(validPath);
|
|
702
|
+
if (typeof content === 'string') {
|
|
703
|
+
// --- PDF CREATION MODE ---
|
|
704
|
+
capture('server_write_pdf', {
|
|
705
|
+
fileExtension: fileExtension,
|
|
706
|
+
contentLength: content.length,
|
|
707
|
+
mode: 'create'
|
|
708
|
+
});
|
|
709
|
+
const pdfBuffer = await parseMarkdownToPdf(content, options);
|
|
710
|
+
// Use outputPath if provided, otherwise overwrite input file
|
|
711
|
+
const targetPath = outputPath ? await validatePath(outputPath) : validPath;
|
|
712
|
+
await fs.writeFile(targetPath, pdfBuffer);
|
|
713
|
+
}
|
|
714
|
+
else if (Array.isArray(content)) {
|
|
715
|
+
// Use outputPath if provided, otherwise overwrite input file
|
|
716
|
+
const targetPath = outputPath ? await validatePath(outputPath) : validPath;
|
|
717
|
+
const operations = [];
|
|
718
|
+
// Validate paths in operations
|
|
719
|
+
for (const o of content) {
|
|
720
|
+
if (o.type === 'insert') {
|
|
721
|
+
if (o.sourcePdfPath) {
|
|
722
|
+
o.sourcePdfPath = await validatePath(o.sourcePdfPath);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
operations.push(o);
|
|
726
|
+
}
|
|
727
|
+
capture('server_write_pdf', {
|
|
728
|
+
fileExtension: fileExtension,
|
|
729
|
+
operationCount: operations.length,
|
|
730
|
+
mode: 'modify',
|
|
731
|
+
deleteCount: operations.filter(op => op.type === 'delete').length,
|
|
732
|
+
insertCount: operations.filter(op => op.type === 'insert').length
|
|
733
|
+
});
|
|
734
|
+
// Perform the PDF editing
|
|
735
|
+
const modifiedPdfBuffer = await editPdf(validPath, operations);
|
|
736
|
+
// Write the modified PDF to the output path
|
|
737
|
+
await fs.writeFile(targetPath, modifiedPdfBuffer);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
throw new Error('Invalid content type for writePdf. Expected string (markdown) or array of operations.');
|
|
741
|
+
}
|
|
742
|
+
}
|