@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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF File Handler
|
|
3
|
+
* Implements FileHandler interface for PDF documents
|
|
4
|
+
*/
|
|
5
|
+
import { FileHandler, FileResult, FileInfo, ReadOptions, EditResult } from './base.js';
|
|
6
|
+
/**
|
|
7
|
+
* File handler for PDF documents
|
|
8
|
+
* Extracts text and images, supports page-based pagination
|
|
9
|
+
*/
|
|
10
|
+
export declare class PdfFileHandler implements FileHandler {
|
|
11
|
+
private readonly extensions;
|
|
12
|
+
/**
|
|
13
|
+
* Check if this handler can handle the given file
|
|
14
|
+
*/
|
|
15
|
+
canHandle(path: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Read PDF content - extracts text as markdown with images
|
|
18
|
+
*/
|
|
19
|
+
read(path: string, options?: ReadOptions): Promise<FileResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Write PDF - creates from markdown or operations
|
|
22
|
+
*/
|
|
23
|
+
write(path: string, content: any, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Edit PDF by range/operations
|
|
26
|
+
*/
|
|
27
|
+
editRange(path: string, range: string, content: any, options?: Record<string, any>): Promise<EditResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Get PDF file information
|
|
30
|
+
*/
|
|
31
|
+
getInfo(path: string): Promise<FileInfo>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF File Handler
|
|
3
|
+
* Implements FileHandler interface for PDF documents
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import { parsePdfToMarkdown, parseMarkdownToPdf, editPdf } from '../../tools/pdf/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* File handler for PDF documents
|
|
9
|
+
* Extracts text and images, supports page-based pagination
|
|
10
|
+
*/
|
|
11
|
+
export class PdfFileHandler {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.extensions = ['.pdf'];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if this handler can handle the given file
|
|
17
|
+
*/
|
|
18
|
+
canHandle(path) {
|
|
19
|
+
const ext = path.toLowerCase();
|
|
20
|
+
return this.extensions.some(e => ext.endsWith(e));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read PDF content - extracts text as markdown with images
|
|
24
|
+
*/
|
|
25
|
+
async read(path, options) {
|
|
26
|
+
const { offset = 0, length } = options ?? {};
|
|
27
|
+
try {
|
|
28
|
+
// Use existing PDF parser
|
|
29
|
+
// Ensure we pass a valid PageRange or number array
|
|
30
|
+
// If length is undefined, we assume "rest of file" which requires careful handling.
|
|
31
|
+
// If length is defined, we pass { offset, length }.
|
|
32
|
+
// If neither, we pass empty array (all pages).
|
|
33
|
+
// Note: offset defaults to 0 if undefined.
|
|
34
|
+
let range;
|
|
35
|
+
if (length !== undefined) {
|
|
36
|
+
range = { offset, length };
|
|
37
|
+
}
|
|
38
|
+
else if (offset > 0) {
|
|
39
|
+
// If offset provided but no length, try to read reasonable amount or all?
|
|
40
|
+
// PageRange requires length. Let's assume 0 means "all" or use a large number?
|
|
41
|
+
// Looking at pdf2md implementation, it uses generatePageNumbers(offset, length, total).
|
|
42
|
+
// We'll pass 0 for length to imply "rest" if supported, or just undefined length if valid.
|
|
43
|
+
// But typescript requires length.
|
|
44
|
+
range = { offset, length: 0 };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
range = [];
|
|
48
|
+
}
|
|
49
|
+
const pdfResult = await parsePdfToMarkdown(path, range);
|
|
50
|
+
return {
|
|
51
|
+
content: '', // Main content is in metadata.pages
|
|
52
|
+
mimeType: 'application/pdf',
|
|
53
|
+
metadata: {
|
|
54
|
+
isPdf: true,
|
|
55
|
+
author: pdfResult.metadata.author,
|
|
56
|
+
title: pdfResult.metadata.title,
|
|
57
|
+
totalPages: pdfResult.metadata.totalPages,
|
|
58
|
+
pages: pdfResult.pages
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
+
return {
|
|
65
|
+
content: `Error reading PDF: ${errorMessage}`,
|
|
66
|
+
mimeType: 'text/plain',
|
|
67
|
+
metadata: {
|
|
68
|
+
error: true,
|
|
69
|
+
errorMessage
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Write PDF - creates from markdown or operations
|
|
76
|
+
*/
|
|
77
|
+
async write(path, content, mode) {
|
|
78
|
+
// If content is string, treat as markdown to convert
|
|
79
|
+
if (typeof content === 'string') {
|
|
80
|
+
await parseMarkdownToPdf(content, path);
|
|
81
|
+
}
|
|
82
|
+
else if (Array.isArray(content)) {
|
|
83
|
+
// Array of operations - use editPdf
|
|
84
|
+
const resultBuffer = await editPdf(path, content);
|
|
85
|
+
await fs.writeFile(path, resultBuffer);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
throw new Error('PDF write requires markdown string or array of operations');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Edit PDF by range/operations
|
|
93
|
+
*/
|
|
94
|
+
async editRange(path, range, content, options) {
|
|
95
|
+
try {
|
|
96
|
+
// For PDF, range editing isn't directly supported
|
|
97
|
+
// Could interpret range as page numbers in future
|
|
98
|
+
const resultBuffer = await editPdf(path, content);
|
|
99
|
+
await fs.writeFile(options?.outputPath || path, resultBuffer);
|
|
100
|
+
return { success: true, editsApplied: 1 };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
editsApplied: 0,
|
|
107
|
+
errors: [{ location: range, error: errorMessage }]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get PDF file information
|
|
113
|
+
*/
|
|
114
|
+
async getInfo(path) {
|
|
115
|
+
const stats = await fs.stat(path);
|
|
116
|
+
// Get basic PDF metadata
|
|
117
|
+
let metadata = { isPdf: true };
|
|
118
|
+
try {
|
|
119
|
+
const pdfResult = await parsePdfToMarkdown(path, { offset: 0, length: 0 }); // Just metadata
|
|
120
|
+
metadata = {
|
|
121
|
+
isPdf: true,
|
|
122
|
+
title: pdfResult.metadata.title,
|
|
123
|
+
author: pdfResult.metadata.author,
|
|
124
|
+
totalPages: pdfResult.metadata.totalPages
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// If we can't parse, just return basic info
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
size: stats.size,
|
|
132
|
+
created: stats.birthtime,
|
|
133
|
+
modified: stats.mtime,
|
|
134
|
+
accessed: stats.atime,
|
|
135
|
+
isDirectory: false,
|
|
136
|
+
isFile: true,
|
|
137
|
+
permissions: (stats.mode & 0o777).toString(8),
|
|
138
|
+
fileType: 'binary',
|
|
139
|
+
metadata
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text file handler
|
|
3
|
+
* Handles reading, writing, and editing text files
|
|
4
|
+
*
|
|
5
|
+
* Binary detection is handled at the factory level (factory.ts) using isBinaryFile.
|
|
6
|
+
* This handler only receives files that have been confirmed as text.
|
|
7
|
+
*
|
|
8
|
+
* TECHNICAL DEBT:
|
|
9
|
+
* This handler is missing editRange() - text search/replace logic currently lives in
|
|
10
|
+
* src/tools/edit.ts (performSearchReplace function) instead of here.
|
|
11
|
+
*
|
|
12
|
+
* For architectural consistency with ExcelFileHandler.editRange(), the fuzzy
|
|
13
|
+
* search/replace logic should be moved here. See comment in src/tools/edit.ts.
|
|
14
|
+
*/
|
|
15
|
+
import { FileHandler, ReadOptions, FileResult, FileInfo } from './base.js';
|
|
16
|
+
/**
|
|
17
|
+
* Text file handler implementation
|
|
18
|
+
* Binary detection is done at the factory level - this handler assumes file is text
|
|
19
|
+
*/
|
|
20
|
+
export declare class TextFileHandler implements FileHandler {
|
|
21
|
+
canHandle(_path: string): boolean;
|
|
22
|
+
read(filePath: string, options?: ReadOptions): Promise<FileResult>;
|
|
23
|
+
write(path: string, content: string, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
24
|
+
getInfo(path: string): Promise<FileInfo>;
|
|
25
|
+
/**
|
|
26
|
+
* Count lines in text content
|
|
27
|
+
* Made static and public for use by other modules (e.g., writeFile telemetry in filesystem.ts)
|
|
28
|
+
*/
|
|
29
|
+
static countLines(content: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* Get file line count (for files under size limit)
|
|
32
|
+
*/
|
|
33
|
+
private getFileLineCount;
|
|
34
|
+
/**
|
|
35
|
+
* Generate enhanced status message
|
|
36
|
+
*/
|
|
37
|
+
private generateEnhancedStatusMessage;
|
|
38
|
+
/**
|
|
39
|
+
* Split text into lines while preserving line endings
|
|
40
|
+
* Made static and public for use by other modules (e.g., readFileInternal in filesystem.ts)
|
|
41
|
+
*/
|
|
42
|
+
static splitLinesPreservingEndings(content: string): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Read file with smart positioning for optimal performance
|
|
45
|
+
*/
|
|
46
|
+
private readFileWithSmartPositioning;
|
|
47
|
+
/**
|
|
48
|
+
* Read last N lines efficiently by reading file backwards
|
|
49
|
+
*/
|
|
50
|
+
private readLastNLinesReverse;
|
|
51
|
+
/**
|
|
52
|
+
* Read from end using readline with circular buffer
|
|
53
|
+
*/
|
|
54
|
+
private readFromEndWithReadline;
|
|
55
|
+
/**
|
|
56
|
+
* Read from start/middle using readline
|
|
57
|
+
*/
|
|
58
|
+
private readFromStartWithReadline;
|
|
59
|
+
/**
|
|
60
|
+
* Read from estimated byte position for very large files
|
|
61
|
+
*/
|
|
62
|
+
private readFromEstimatedPosition;
|
|
63
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text file handler
|
|
3
|
+
* Handles reading, writing, and editing text files
|
|
4
|
+
*
|
|
5
|
+
* Binary detection is handled at the factory level (factory.ts) using isBinaryFile.
|
|
6
|
+
* This handler only receives files that have been confirmed as text.
|
|
7
|
+
*
|
|
8
|
+
* TECHNICAL DEBT:
|
|
9
|
+
* This handler is missing editRange() - text search/replace logic currently lives in
|
|
10
|
+
* src/tools/edit.ts (performSearchReplace function) instead of here.
|
|
11
|
+
*
|
|
12
|
+
* For architectural consistency with ExcelFileHandler.editRange(), the fuzzy
|
|
13
|
+
* search/replace logic should be moved here. See comment in src/tools/edit.ts.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "fs/promises";
|
|
16
|
+
import { createReadStream } from 'fs';
|
|
17
|
+
import { createInterface } from 'readline';
|
|
18
|
+
// TODO: Centralize these constants with filesystem.ts to avoid silent drift
|
|
19
|
+
// These duplicate concepts from filesystem.ts and should be moved to a shared
|
|
20
|
+
// constants module (e.g., src/utils/files/constants.ts) during reorganization
|
|
21
|
+
const FILE_SIZE_LIMITS = {
|
|
22
|
+
LARGE_FILE_THRESHOLD: 10 * 1024 * 1024, // 10MB
|
|
23
|
+
LINE_COUNT_LIMIT: 10 * 1024 * 1024, // 10MB for line counting
|
|
24
|
+
};
|
|
25
|
+
const READ_PERFORMANCE_THRESHOLDS = {
|
|
26
|
+
SMALL_READ_THRESHOLD: 100, // For very small reads
|
|
27
|
+
DEEP_OFFSET_THRESHOLD: 1000, // For byte estimation
|
|
28
|
+
SAMPLE_SIZE: 10000, // Sample size for estimation
|
|
29
|
+
CHUNK_SIZE: 8192, // 8KB chunks for reverse reading
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Text file handler implementation
|
|
33
|
+
* Binary detection is done at the factory level - this handler assumes file is text
|
|
34
|
+
*/
|
|
35
|
+
export class TextFileHandler {
|
|
36
|
+
canHandle(_path) {
|
|
37
|
+
// Text handler accepts all files that pass the factory's binary check
|
|
38
|
+
// The factory routes binary files to BinaryFileHandler before reaching here
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
async read(filePath, options) {
|
|
42
|
+
const offset = options?.offset ?? 0;
|
|
43
|
+
const length = options?.length ?? 1000; // Default from config
|
|
44
|
+
const includeStatusMessage = options?.includeStatusMessage ?? true;
|
|
45
|
+
// Binary detection is done at factory level - just read as text
|
|
46
|
+
return this.readFileWithSmartPositioning(filePath, offset, length, 'text/plain', includeStatusMessage);
|
|
47
|
+
}
|
|
48
|
+
async write(path, content, mode = 'rewrite') {
|
|
49
|
+
if (mode === 'append') {
|
|
50
|
+
await fs.appendFile(path, content);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await fs.writeFile(path, content);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async getInfo(path) {
|
|
57
|
+
const stats = await fs.stat(path);
|
|
58
|
+
const info = {
|
|
59
|
+
size: stats.size,
|
|
60
|
+
created: stats.birthtime,
|
|
61
|
+
modified: stats.mtime,
|
|
62
|
+
accessed: stats.atime,
|
|
63
|
+
isDirectory: stats.isDirectory(),
|
|
64
|
+
isFile: stats.isFile(),
|
|
65
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
66
|
+
fileType: 'text',
|
|
67
|
+
metadata: {}
|
|
68
|
+
};
|
|
69
|
+
// For text files that aren't too large, count lines
|
|
70
|
+
if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
71
|
+
try {
|
|
72
|
+
const content = await fs.readFile(path, 'utf8');
|
|
73
|
+
const lineCount = TextFileHandler.countLines(content);
|
|
74
|
+
info.metadata.lineCount = lineCount;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// If reading fails, skip line count
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return info;
|
|
81
|
+
}
|
|
82
|
+
// ========================================================================
|
|
83
|
+
// Private Helper Methods (extracted from filesystem.ts)
|
|
84
|
+
// ========================================================================
|
|
85
|
+
/**
|
|
86
|
+
* Count lines in text content
|
|
87
|
+
* Made static and public for use by other modules (e.g., writeFile telemetry in filesystem.ts)
|
|
88
|
+
*/
|
|
89
|
+
static countLines(content) {
|
|
90
|
+
return content.split('\n').length;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get file line count (for files under size limit)
|
|
94
|
+
*/
|
|
95
|
+
async getFileLineCount(filePath) {
|
|
96
|
+
try {
|
|
97
|
+
const stats = await fs.stat(filePath);
|
|
98
|
+
if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
99
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
100
|
+
return TextFileHandler.countLines(content);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// If we can't read the file, return undefined
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Generate enhanced status message
|
|
110
|
+
*/
|
|
111
|
+
generateEnhancedStatusMessage(readLines, offset, totalLines, isNegativeOffset = false) {
|
|
112
|
+
if (isNegativeOffset) {
|
|
113
|
+
if (totalLines !== undefined) {
|
|
114
|
+
return `[Reading last ${readLines} lines (total: ${totalLines} lines)]`;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return `[Reading last ${readLines} lines]`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
if (totalLines !== undefined) {
|
|
122
|
+
const endLine = offset + readLines;
|
|
123
|
+
const remainingLines = Math.max(0, totalLines - endLine);
|
|
124
|
+
if (offset === 0) {
|
|
125
|
+
return `[Reading ${readLines} lines from start (total: ${totalLines} lines, ${remainingLines} remaining)]`;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
return `[Reading ${readLines} lines from line ${offset} (total: ${totalLines} lines, ${remainingLines} remaining)]`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (offset === 0) {
|
|
133
|
+
return `[Reading ${readLines} lines from start]`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return `[Reading ${readLines} lines from line ${offset}]`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Split text into lines while preserving line endings
|
|
143
|
+
* Made static and public for use by other modules (e.g., readFileInternal in filesystem.ts)
|
|
144
|
+
*/
|
|
145
|
+
static splitLinesPreservingEndings(content) {
|
|
146
|
+
if (!content)
|
|
147
|
+
return [''];
|
|
148
|
+
const lines = [];
|
|
149
|
+
let currentLine = '';
|
|
150
|
+
for (let i = 0; i < content.length; i++) {
|
|
151
|
+
const char = content[i];
|
|
152
|
+
currentLine += char;
|
|
153
|
+
if (char === '\n') {
|
|
154
|
+
lines.push(currentLine);
|
|
155
|
+
currentLine = '';
|
|
156
|
+
}
|
|
157
|
+
else if (char === '\r') {
|
|
158
|
+
if (i + 1 < content.length && content[i + 1] === '\n') {
|
|
159
|
+
currentLine += content[i + 1];
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
lines.push(currentLine);
|
|
163
|
+
currentLine = '';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (currentLine) {
|
|
167
|
+
lines.push(currentLine);
|
|
168
|
+
}
|
|
169
|
+
return lines;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Read file with smart positioning for optimal performance
|
|
173
|
+
*/
|
|
174
|
+
async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
175
|
+
const stats = await fs.stat(filePath);
|
|
176
|
+
const fileSize = stats.size;
|
|
177
|
+
const totalLines = await this.getFileLineCount(filePath);
|
|
178
|
+
// For negative offsets (tail behavior), use reverse reading
|
|
179
|
+
if (offset < 0) {
|
|
180
|
+
const requestedLines = Math.abs(offset);
|
|
181
|
+
if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD &&
|
|
182
|
+
requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
|
|
183
|
+
return await this.readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return await this.readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// For positive offsets
|
|
190
|
+
else {
|
|
191
|
+
if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
|
|
192
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
|
|
196
|
+
return await this.readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Read last N lines efficiently by reading file backwards
|
|
206
|
+
*/
|
|
207
|
+
async readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
208
|
+
const fd = await fs.open(filePath, 'r');
|
|
209
|
+
try {
|
|
210
|
+
const stats = await fd.stat();
|
|
211
|
+
const fileSize = stats.size;
|
|
212
|
+
let position = fileSize;
|
|
213
|
+
let lines = [];
|
|
214
|
+
let partialLine = '';
|
|
215
|
+
while (position > 0 && lines.length < n) {
|
|
216
|
+
const readSize = Math.min(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE, position);
|
|
217
|
+
position -= readSize;
|
|
218
|
+
const buffer = Buffer.alloc(readSize);
|
|
219
|
+
await fd.read(buffer, 0, readSize, position);
|
|
220
|
+
const chunk = buffer.toString('utf-8');
|
|
221
|
+
const text = chunk + partialLine;
|
|
222
|
+
const chunkLines = text.split('\n');
|
|
223
|
+
partialLine = chunkLines.shift() || '';
|
|
224
|
+
lines = chunkLines.concat(lines);
|
|
225
|
+
}
|
|
226
|
+
if (position === 0 && partialLine) {
|
|
227
|
+
lines.unshift(partialLine);
|
|
228
|
+
}
|
|
229
|
+
const result = lines.slice(-n);
|
|
230
|
+
const content = includeStatusMessage
|
|
231
|
+
? `${this.generateEnhancedStatusMessage(result.length, -n, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
232
|
+
: result.join('\n');
|
|
233
|
+
return { content, mimeType, metadata: {} };
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await fd.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Read from end using readline with circular buffer
|
|
241
|
+
*/
|
|
242
|
+
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
243
|
+
const rl = createInterface({
|
|
244
|
+
input: createReadStream(filePath),
|
|
245
|
+
crlfDelay: Infinity
|
|
246
|
+
});
|
|
247
|
+
const buffer = new Array(requestedLines);
|
|
248
|
+
let bufferIndex = 0;
|
|
249
|
+
let totalLines = 0;
|
|
250
|
+
for await (const line of rl) {
|
|
251
|
+
buffer[bufferIndex] = line;
|
|
252
|
+
bufferIndex = (bufferIndex + 1) % requestedLines;
|
|
253
|
+
totalLines++;
|
|
254
|
+
}
|
|
255
|
+
rl.close();
|
|
256
|
+
let result;
|
|
257
|
+
if (totalLines >= requestedLines) {
|
|
258
|
+
result = [
|
|
259
|
+
...buffer.slice(bufferIndex),
|
|
260
|
+
...buffer.slice(0, bufferIndex)
|
|
261
|
+
].filter(line => line !== undefined);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
result = buffer.slice(0, totalLines);
|
|
265
|
+
}
|
|
266
|
+
const content = includeStatusMessage
|
|
267
|
+
? `${this.generateEnhancedStatusMessage(result.length, -requestedLines, fileTotalLines, true)}\n\n${result.join('\n')}`
|
|
268
|
+
: result.join('\n');
|
|
269
|
+
return { content, mimeType, metadata: {} };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Read from start/middle using readline
|
|
273
|
+
*/
|
|
274
|
+
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
275
|
+
const rl = createInterface({
|
|
276
|
+
input: createReadStream(filePath),
|
|
277
|
+
crlfDelay: Infinity
|
|
278
|
+
});
|
|
279
|
+
const result = [];
|
|
280
|
+
let lineNumber = 0;
|
|
281
|
+
for await (const line of rl) {
|
|
282
|
+
if (lineNumber >= offset && result.length < length) {
|
|
283
|
+
result.push(line);
|
|
284
|
+
}
|
|
285
|
+
if (result.length >= length)
|
|
286
|
+
break;
|
|
287
|
+
lineNumber++;
|
|
288
|
+
}
|
|
289
|
+
rl.close();
|
|
290
|
+
if (includeStatusMessage) {
|
|
291
|
+
const statusMessage = this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
|
|
292
|
+
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
293
|
+
return { content, mimeType, metadata: {} };
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const content = result.join('\n');
|
|
297
|
+
return { content, mimeType, metadata: {} };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Read from estimated byte position for very large files
|
|
302
|
+
*/
|
|
303
|
+
async readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
304
|
+
// First, do a quick scan to estimate lines per byte
|
|
305
|
+
const rl = createInterface({
|
|
306
|
+
input: createReadStream(filePath),
|
|
307
|
+
crlfDelay: Infinity
|
|
308
|
+
});
|
|
309
|
+
let sampleLines = 0;
|
|
310
|
+
let bytesRead = 0;
|
|
311
|
+
for await (const line of rl) {
|
|
312
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
|
|
313
|
+
sampleLines++;
|
|
314
|
+
if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
rl.close();
|
|
318
|
+
if (sampleLines === 0) {
|
|
319
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines);
|
|
320
|
+
}
|
|
321
|
+
// Estimate position
|
|
322
|
+
const avgLineLength = bytesRead / sampleLines;
|
|
323
|
+
const estimatedBytePosition = Math.floor(offset * avgLineLength);
|
|
324
|
+
const fd = await fs.open(filePath, 'r');
|
|
325
|
+
try {
|
|
326
|
+
const stats = await fd.stat();
|
|
327
|
+
const startPosition = Math.min(estimatedBytePosition, stats.size);
|
|
328
|
+
const stream = createReadStream(filePath, { start: startPosition });
|
|
329
|
+
const rl2 = createInterface({
|
|
330
|
+
input: stream,
|
|
331
|
+
crlfDelay: Infinity
|
|
332
|
+
});
|
|
333
|
+
const result = [];
|
|
334
|
+
let firstLineSkipped = false;
|
|
335
|
+
for await (const line of rl2) {
|
|
336
|
+
if (!firstLineSkipped && startPosition > 0) {
|
|
337
|
+
firstLineSkipped = true;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (result.length < length) {
|
|
341
|
+
result.push(line);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
rl2.close();
|
|
348
|
+
const content = includeStatusMessage
|
|
349
|
+
? `${this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
|
|
350
|
+
: result.join('\n');
|
|
351
|
+
return { content, mimeType, metadata: {} };
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
await fd.close();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -31,10 +31,11 @@ export async function getRipgrepPath() {
|
|
|
31
31
|
catch (e) {
|
|
32
32
|
// @vscode/ripgrep import or binary resolution failed, continue to fallbacks
|
|
33
33
|
}
|
|
34
|
-
// Strategy 2: Try system ripgrep using 'which'
|
|
34
|
+
// Strategy 2: Try system ripgrep using 'which' (Unix) or 'where' (Windows)
|
|
35
35
|
try {
|
|
36
36
|
const systemRg = process.platform === 'win32' ? 'rg.exe' : 'rg';
|
|
37
|
-
const
|
|
37
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
38
|
+
const result = execSync(`${whichCmd} ${systemRg}`, { encoding: 'utf-8' }).trim().split(/\r?\n/)[0];
|
|
38
39
|
if (result && existsSync(result)) {
|
|
39
40
|
cachedRgPath = result;
|
|
40
41
|
return result;
|