@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.
Files changed (59) hide show
  1. package/README.md +14 -55
  2. package/dist/custom-stdio.d.ts +1 -0
  3. package/dist/custom-stdio.js +19 -0
  4. package/dist/handlers/filesystem-handlers.d.ts +4 -0
  5. package/dist/handlers/filesystem-handlers.js +120 -14
  6. package/dist/handlers/node-handlers.d.ts +6 -0
  7. package/dist/handlers/node-handlers.js +73 -0
  8. package/dist/index.js +5 -3
  9. package/dist/search-manager.d.ts +25 -0
  10. package/dist/search-manager.js +212 -0
  11. package/dist/server.js +160 -73
  12. package/dist/terminal-manager.d.ts +56 -2
  13. package/dist/terminal-manager.js +169 -13
  14. package/dist/tools/edit.d.ts +28 -4
  15. package/dist/tools/edit.js +87 -4
  16. package/dist/tools/filesystem.d.ts +23 -12
  17. package/dist/tools/filesystem.js +201 -416
  18. package/dist/tools/improved-process-tools.d.ts +2 -2
  19. package/dist/tools/improved-process-tools.js +244 -214
  20. package/dist/tools/mime-types.d.ts +1 -0
  21. package/dist/tools/mime-types.js +7 -0
  22. package/dist/tools/pdf/extract-images.d.ts +34 -0
  23. package/dist/tools/pdf/extract-images.js +132 -0
  24. package/dist/tools/pdf/index.d.ts +6 -0
  25. package/dist/tools/pdf/index.js +3 -0
  26. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  27. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  28. package/dist/tools/pdf/manipulations.d.ts +13 -0
  29. package/dist/tools/pdf/manipulations.js +96 -0
  30. package/dist/tools/pdf/markdown.d.ts +7 -0
  31. package/dist/tools/pdf/markdown.js +37 -0
  32. package/dist/tools/pdf/utils.d.ts +12 -0
  33. package/dist/tools/pdf/utils.js +34 -0
  34. package/dist/tools/schemas.d.ts +167 -12
  35. package/dist/tools/schemas.js +54 -5
  36. package/dist/types.d.ts +2 -1
  37. package/dist/utils/feature-flags.js +7 -4
  38. package/dist/utils/files/base.d.ts +167 -0
  39. package/dist/utils/files/base.js +5 -0
  40. package/dist/utils/files/binary.d.ts +21 -0
  41. package/dist/utils/files/binary.js +65 -0
  42. package/dist/utils/files/excel.d.ts +24 -0
  43. package/dist/utils/files/excel.js +416 -0
  44. package/dist/utils/files/factory.d.ts +40 -0
  45. package/dist/utils/files/factory.js +101 -0
  46. package/dist/utils/files/image.d.ts +21 -0
  47. package/dist/utils/files/image.js +78 -0
  48. package/dist/utils/files/index.d.ts +10 -0
  49. package/dist/utils/files/index.js +13 -0
  50. package/dist/utils/files/pdf.d.ts +32 -0
  51. package/dist/utils/files/pdf.js +142 -0
  52. package/dist/utils/files/text.d.ts +63 -0
  53. package/dist/utils/files/text.js +357 -0
  54. package/dist/utils/ripgrep-resolver.js +3 -2
  55. package/dist/utils/system-info.d.ts +5 -0
  56. package/dist/utils/system-info.js +71 -3
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +14 -3
@@ -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
- return { mimeType, isImage };
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
- if (isImage) {
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 offset Starting line number to read from (default: 0)
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, offset = 0, length) {
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 using path module consistently
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
- if (isImage) {
590
- // For image files, read as Buffer and convert to base64
591
- // Images are always read in full, ignoring offset and length
592
- const buffer = await fs.readFile(validPath);
593
- const content = buffer.toString('base64');
594
- return { content, mimeType, isImage };
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
- // For all other files, use smart positioning approach
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 isUrl Whether the path is a URL
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, isUrl, offset, length) {
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
- // Use different fs methods based on mode
728
- if (mode === 'append') {
729
- await fs.appendFile(validPath, content);
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: typeof fileResult === 'string' ? fileResult : fileResult.content,
743
- mimeType: typeof fileResult === 'string' ? "text/plain" : fileResult.mimeType,
744
- isImage: typeof fileResult === 'string' ? false : fileResult.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
- // Basic file info
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
- // For text files that aren't too large, also count lines
938
- if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
939
- try {
940
- // Get MIME type information
941
- const { mimeType, isImage } = await getMimeTypeInfo(validPath);
942
- // Only count lines for non-image, likely text files
943
- if (!isImage) {
944
- const content = await fs.readFile(validPath, 'utf8');
945
- const lineCount = countLines(content);
946
- info.lineCount = lineCount;
947
- info.lastLine = lineCount - 1; // Zero-indexed last line
948
- info.appendPosition = lineCount; // Position to append at end
949
- }
950
- }
951
- catch (error) {
952
- // If reading fails, just skip the line count
953
- // This could happen for binary files or very large files
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
+ }