@wonderwhy-er/desktop-commander 0.1.35 → 0.1.37
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/LICENSE +2 -2
- package/README.md +88 -27
- package/dist/command-manager.js +1 -1
- package/dist/config-manager.d.ts +1 -0
- package/dist/config-manager.js +21 -4
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -3
- package/dist/error-handlers.js +1 -1
- package/dist/handlers/edit-search-handlers.d.ts +3 -1
- package/dist/handlers/edit-search-handlers.js +6 -12
- package/dist/handlers/filesystem-handlers.js +1 -1
- package/dist/index.js +1 -1
- package/dist/polyform-license-src/edit/edit.d.ts +15 -0
- package/dist/polyform-license-src/edit/edit.js +163 -0
- package/dist/polyform-license-src/edit/fuzzySearch.d.ts +30 -0
- package/dist/polyform-license-src/edit/fuzzySearch.js +121 -0
- package/dist/polyform-license-src/edit/handlers.d.ts +16 -0
- package/dist/polyform-license-src/edit/handlers.js +24 -0
- package/dist/polyform-license-src/edit/index.d.ts +12 -0
- package/dist/polyform-license-src/edit/index.js +13 -0
- package/dist/polyform-license-src/edit/schemas.d.ts +25 -0
- package/dist/polyform-license-src/edit/schemas.js +16 -0
- package/dist/polyform-license-src/index.d.ts +9 -0
- package/dist/polyform-license-src/index.js +10 -0
- package/dist/server.js +71 -43
- package/dist/setup-claude-server.js +549 -288
- package/dist/terminal-manager.js +4 -2
- package/dist/tools/edit.d.ts +8 -6
- package/dist/tools/edit.js +161 -34
- package/dist/tools/execute.js +2 -2
- package/dist/tools/filesystem.js +59 -10
- package/dist/tools/fuzzySearch.d.ts +22 -0
- package/dist/tools/fuzzySearch.js +113 -0
- package/dist/tools/pdf-reader.d.ts +13 -0
- package/dist/tools/pdf-reader.js +214 -0
- package/dist/tools/schemas.d.ts +12 -3
- package/dist/tools/schemas.js +5 -2
- package/dist/tools/search.js +5 -4
- package/dist/utils/capture.d.ts +15 -0
- package/dist/utils/capture.js +175 -0
- package/dist/utils/withTimeout.d.ts +11 -0
- package/dist/utils/withTimeout.js +52 -0
- package/dist/utils.d.ts +10 -1
- package/dist/utils.js +99 -26
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/terminal-manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
|
|
3
3
|
import { configManager } from './config-manager.js';
|
|
4
|
-
import { capture } from "./utils.js";
|
|
4
|
+
import { capture } from "./utils/capture.js";
|
|
5
5
|
export class TerminalManager {
|
|
6
6
|
constructor() {
|
|
7
7
|
this.sessions = new Map();
|
|
@@ -118,7 +118,9 @@ export class TerminalManager {
|
|
|
118
118
|
return true;
|
|
119
119
|
}
|
|
120
120
|
catch (error) {
|
|
121
|
-
|
|
121
|
+
// Convert error to string, handling both Error objects and other types
|
|
122
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
123
|
+
capture('server_request_error', { error: errorMessage, message: `Failed to terminate process ${pid}:` });
|
|
122
124
|
return false;
|
|
123
125
|
}
|
|
124
126
|
}
|
package/dist/tools/edit.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ interface SearchReplace {
|
|
|
3
3
|
search: string;
|
|
4
4
|
replace: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function performSearchReplace(filePath: string, block: SearchReplace): Promise<ServerResult>;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export declare function performSearchReplace(filePath: string, block: SearchReplace, expectedReplacements?: number): Promise<ServerResult>;
|
|
7
|
+
/**
|
|
8
|
+
* Handle edit_block command with enhanced functionality
|
|
9
|
+
* - Supports multiple replacements
|
|
10
|
+
* - Validates expected replacements count
|
|
11
|
+
* - Provides detailed error messages
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleEditBlock(args: unknown): Promise<ServerResult>;
|
|
12
14
|
export {};
|
package/dist/tools/edit.js
CHANGED
|
@@ -1,48 +1,175 @@
|
|
|
1
1
|
import { readFile, writeFile } from './filesystem.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
|
|
3
|
+
import { capture } from '../utils/capture.js';
|
|
4
|
+
import { EditBlockArgsSchema } from "./schemas.js";
|
|
5
|
+
import path from 'path';
|
|
6
|
+
/**
|
|
7
|
+
* Threshold for fuzzy matching - similarity must be at least this value to be considered
|
|
8
|
+
* (0-1 scale where 1 is perfect match and 0 is completely different)
|
|
9
|
+
*/
|
|
10
|
+
const FUZZY_THRESHOLD = 0.7;
|
|
11
|
+
export async function performSearchReplace(filePath, block, expectedReplacements = 1) {
|
|
12
|
+
// Check for empty search string to prevent infinite loops
|
|
13
|
+
if (block.search === "") {
|
|
14
|
+
return {
|
|
15
|
+
content: [{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: "Empty search strings are not allowed. Please provide a non-empty string to search for."
|
|
18
|
+
}],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Get file extension for telemetry using path module
|
|
22
|
+
const fileExtension = path.extname(filePath).toLowerCase();
|
|
23
|
+
// Capture file extension in telemetry without capturing the file path
|
|
24
|
+
capture('server_edit_block', { fileExtension: fileExtension });
|
|
25
|
+
// Read file as plain string
|
|
4
26
|
const { content } = await readFile(filePath);
|
|
5
27
|
// Make sure content is a string
|
|
6
28
|
if (typeof content !== 'string') {
|
|
7
29
|
throw new Error('Wrong content for file ' + filePath);
|
|
8
30
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
31
|
+
// First try exact match
|
|
32
|
+
let tempContent = content;
|
|
33
|
+
let count = 0;
|
|
34
|
+
let pos = tempContent.indexOf(block.search);
|
|
35
|
+
while (pos !== -1) {
|
|
36
|
+
count++;
|
|
37
|
+
pos = tempContent.indexOf(block.search, pos + 1);
|
|
38
|
+
}
|
|
39
|
+
// If exact match found and count matches expected replacements, proceed with exact replacement
|
|
40
|
+
if (count > 0 && count === expectedReplacements) {
|
|
41
|
+
// Replace all occurrences
|
|
42
|
+
let newContent = content;
|
|
43
|
+
// If we're only replacing one occurrence, replace it directly
|
|
44
|
+
if (expectedReplacements === 1) {
|
|
45
|
+
const searchIndex = newContent.indexOf(block.search);
|
|
46
|
+
newContent =
|
|
47
|
+
newContent.substring(0, searchIndex) +
|
|
48
|
+
block.replace +
|
|
49
|
+
newContent.substring(searchIndex + block.search.length);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Replace all occurrences using split and join for multiple replacements
|
|
53
|
+
newContent = newContent.split(block.search).join(block.replace);
|
|
54
|
+
}
|
|
55
|
+
await writeFile(filePath, newContent);
|
|
13
56
|
return {
|
|
14
|
-
content: [{
|
|
57
|
+
content: [{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `Successfully applied ${expectedReplacements} edit${expectedReplacements > 1 ? 's' : ''} to ${filePath}`
|
|
60
|
+
}],
|
|
15
61
|
};
|
|
16
62
|
}
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
block.replace +
|
|
20
|
-
content.substring(searchIndex + block.search.length);
|
|
21
|
-
await writeFile(filePath, newContent);
|
|
22
|
-
return {
|
|
23
|
-
content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export async function parseEditBlock(blockContent) {
|
|
27
|
-
const lines = blockContent.split('\n');
|
|
28
|
-
// First line should be the file path
|
|
29
|
-
const filePath = lines[0].trim();
|
|
30
|
-
// Find the markers
|
|
31
|
-
const searchStart = lines.indexOf('<<<<<<< SEARCH');
|
|
32
|
-
const divider = lines.indexOf('=======');
|
|
33
|
-
const replaceEnd = lines.indexOf('>>>>>>> REPLACE');
|
|
34
|
-
if (searchStart === -1 || divider === -1 || replaceEnd === -1) {
|
|
63
|
+
// If exact match found but count doesn't match expected, inform the user
|
|
64
|
+
if (count > 0 && count !== expectedReplacements) {
|
|
35
65
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
content: [{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `Expected ${expectedReplacements} occurrences but found ${count} in ${filePath}. ` +
|
|
69
|
+
`Double check and make sure you understand all occurencies and if you want to replace all ${count} occurrences, set expected_replacements to ${count}. ` +
|
|
70
|
+
`If there are many occurrancies and you want to change some of them and keep the rest. Do it one by one, by adding more lines around each occurrence.` +
|
|
71
|
+
`If you want to replace a specific occurrence, make your search string more unique by adding more lines around search string.`
|
|
72
|
+
}],
|
|
39
73
|
};
|
|
40
74
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
// If exact match not found, try fuzzy search
|
|
76
|
+
if (count === 0) {
|
|
77
|
+
// Track fuzzy search time
|
|
78
|
+
const startTime = performance.now();
|
|
79
|
+
// Perform fuzzy search
|
|
80
|
+
const fuzzyResult = recursiveFuzzyIndexOf(content, block.search);
|
|
81
|
+
const similarity = getSimilarityRatio(block.search, fuzzyResult.value);
|
|
82
|
+
// Calculate execution time in milliseconds
|
|
83
|
+
const executionTime = performance.now() - startTime;
|
|
84
|
+
// Check if the fuzzy match is "close enough"
|
|
85
|
+
if (similarity >= FUZZY_THRESHOLD) {
|
|
86
|
+
// Format differences for clearer output
|
|
87
|
+
const diff = highlightDifferences(block.search, fuzzyResult.value);
|
|
88
|
+
// Capture the fuzzy search event
|
|
89
|
+
capture('server_fuzzy_search_performed', {
|
|
90
|
+
similarity: similarity,
|
|
91
|
+
execution_time_ms: executionTime,
|
|
92
|
+
search_length: block.search.length,
|
|
93
|
+
file_size: content.length,
|
|
94
|
+
threshold: FUZZY_THRESHOLD,
|
|
95
|
+
found_text_length: fuzzyResult.value.length
|
|
96
|
+
});
|
|
97
|
+
// If we allow fuzzy matches, we would make the replacement here
|
|
98
|
+
// For now, we'll return a detailed message about the fuzzy match
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: `Exact match not found, but found a similar text with ${Math.round(similarity * 100)}% similarity (found in ${executionTime.toFixed(2)}ms):\n\n` +
|
|
103
|
+
`Differences:\n${diff}\n\n` +
|
|
104
|
+
`To replace this text, use the exact text found in the file.`
|
|
105
|
+
}],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// If the fuzzy match isn't close enough
|
|
110
|
+
// Still capture the fuzzy search event even for unsuccessful matches
|
|
111
|
+
capture('server_fuzzy_search_performed', {
|
|
112
|
+
similarity: similarity,
|
|
113
|
+
execution_time_ms: executionTime,
|
|
114
|
+
search_length: block.search.length,
|
|
115
|
+
file_size: content.length,
|
|
116
|
+
threshold: FUZZY_THRESHOLD,
|
|
117
|
+
found_text_length: fuzzyResult.value.length,
|
|
118
|
+
below_threshold: true
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
content: [{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: `Search content not found in ${filePath}. The closest match was "${fuzzyResult.value}" ` +
|
|
124
|
+
`with only ${Math.round(similarity * 100)}% similarity, which is below the ${Math.round(FUZZY_THRESHOLD * 100)}% threshold. ` +
|
|
125
|
+
`(Fuzzy search completed in ${executionTime.toFixed(2)}ms)`
|
|
126
|
+
}],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new Error("Unexpected error during search and replace operation.");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generates a character-level diff using standard {-removed-}{+added+} format
|
|
134
|
+
* @param expected The string that was searched for
|
|
135
|
+
* @param actual The string that was found
|
|
136
|
+
* @returns A formatted string showing character-level differences
|
|
137
|
+
*/
|
|
138
|
+
function highlightDifferences(expected, actual) {
|
|
139
|
+
// Implementation of a simplified character-level diff
|
|
140
|
+
// Find common prefix and suffix
|
|
141
|
+
let prefixLength = 0;
|
|
142
|
+
const minLength = Math.min(expected.length, actual.length);
|
|
143
|
+
// Determine common prefix length
|
|
144
|
+
while (prefixLength < minLength &&
|
|
145
|
+
expected[prefixLength] === actual[prefixLength]) {
|
|
146
|
+
prefixLength++;
|
|
147
|
+
}
|
|
148
|
+
// Determine common suffix length
|
|
149
|
+
let suffixLength = 0;
|
|
150
|
+
while (suffixLength < minLength - prefixLength &&
|
|
151
|
+
expected[expected.length - 1 - suffixLength] === actual[actual.length - 1 - suffixLength]) {
|
|
152
|
+
suffixLength++;
|
|
153
|
+
}
|
|
154
|
+
// Extract the common and different parts
|
|
155
|
+
const commonPrefix = expected.substring(0, prefixLength);
|
|
156
|
+
const commonSuffix = expected.substring(expected.length - suffixLength);
|
|
157
|
+
const expectedDiff = expected.substring(prefixLength, expected.length - suffixLength);
|
|
158
|
+
const actualDiff = actual.substring(prefixLength, actual.length - suffixLength);
|
|
159
|
+
// Format the output as a character-level diff
|
|
160
|
+
return `${commonPrefix}{-${expectedDiff}-}{+${actualDiff}+}${commonSuffix}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Handle edit_block command with enhanced functionality
|
|
164
|
+
* - Supports multiple replacements
|
|
165
|
+
* - Validates expected replacements count
|
|
166
|
+
* - Provides detailed error messages
|
|
167
|
+
*/
|
|
168
|
+
export async function handleEditBlock(args) {
|
|
169
|
+
const parsed = EditBlockArgsSchema.parse(args);
|
|
170
|
+
const searchReplace = {
|
|
171
|
+
search: parsed.old_string,
|
|
172
|
+
replace: parsed.new_string
|
|
47
173
|
};
|
|
174
|
+
return performSearchReplace(parsed.file_path, searchReplace, parsed.expected_replacements);
|
|
48
175
|
}
|
package/dist/tools/execute.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { terminalManager } from '../terminal-manager.js';
|
|
2
2
|
import { commandManager } from '../command-manager.js';
|
|
3
3
|
import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema } from './schemas.js';
|
|
4
|
-
import { capture } from "../utils.js";
|
|
4
|
+
import { capture } from "../utils/capture.js";
|
|
5
5
|
export async function executeCommand(args) {
|
|
6
6
|
const parsed = ExecuteCommandArgsSchema.safeParse(args);
|
|
7
7
|
if (!parsed.success) {
|
|
@@ -13,7 +13,7 @@ export async function executeCommand(args) {
|
|
|
13
13
|
}
|
|
14
14
|
try {
|
|
15
15
|
// Extract all commands for analytics while ensuring execution continues even if parsing fails
|
|
16
|
-
const commands = commandManager.extractCommands(parsed.data.command);
|
|
16
|
+
const commands = commandManager.extractCommands(parsed.data.command).join(', ');
|
|
17
17
|
capture('server_execute_command', {
|
|
18
18
|
command: commandManager.getBaseCommand(parsed.data.command), // Keep original for backward compatibility
|
|
19
19
|
commands: commands // Add the array of all identified commands
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,7 +2,8 @@ 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 { capture
|
|
5
|
+
import { capture } from '../utils/capture.js';
|
|
6
|
+
import { withTimeout } from '../utils/withTimeout.js';
|
|
6
7
|
import { configManager } from '../config-manager.js';
|
|
7
8
|
// Initialize allowed directories from configuration
|
|
8
9
|
async function getAllowedDirs() {
|
|
@@ -123,7 +124,11 @@ export async function validatePath(requestedPath) {
|
|
|
123
124
|
: path.resolve(process.cwd(), expandedPath);
|
|
124
125
|
// Check if path is allowed
|
|
125
126
|
if (!(await isPathAllowed(absolute))) {
|
|
126
|
-
|
|
127
|
+
capture('server_path_validation_error', {
|
|
128
|
+
error: 'Path not allowed',
|
|
129
|
+
allowedDirsCount: (await getAllowedDirs()).length
|
|
130
|
+
});
|
|
131
|
+
throw new Error(`Path not allowed: ${requestedPath}. Must be within one of these directories: ${(await getAllowedDirs()).join(', ')}`);
|
|
127
132
|
}
|
|
128
133
|
// Check if path exists
|
|
129
134
|
try {
|
|
@@ -143,9 +148,13 @@ export async function validatePath(requestedPath) {
|
|
|
143
148
|
}
|
|
144
149
|
};
|
|
145
150
|
// Execute with timeout
|
|
146
|
-
const result = await withTimeout(validationOperation(), PATH_VALIDATION_TIMEOUT, `Path validation for
|
|
151
|
+
const result = await withTimeout(validationOperation(), PATH_VALIDATION_TIMEOUT, `Path validation operation`, // Generic name for telemetry
|
|
152
|
+
null);
|
|
147
153
|
if (result === null) {
|
|
148
|
-
//
|
|
154
|
+
// Keep original path in error for AI but a generic message for telemetry
|
|
155
|
+
capture('server_path_validation_timeout', {
|
|
156
|
+
timeoutMs: PATH_VALIDATION_TIMEOUT
|
|
157
|
+
});
|
|
149
158
|
throw new Error(`Path validation failed for path: ${requestedPath}`);
|
|
150
159
|
}
|
|
151
160
|
return result;
|
|
@@ -206,22 +215,29 @@ export async function readFileFromDisk(filePath) {
|
|
|
206
215
|
// Import the MIME type utilities
|
|
207
216
|
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
208
217
|
const validPath = await validatePath(filePath);
|
|
218
|
+
// Get file extension for telemetry using path module consistently
|
|
219
|
+
const fileExtension = path.extname(validPath).toLowerCase();
|
|
209
220
|
// Check file size before attempting to read
|
|
210
221
|
try {
|
|
211
222
|
const stats = await fs.stat(validPath);
|
|
212
223
|
const MAX_SIZE = 100 * 1024; // 100KB limit
|
|
213
224
|
if (stats.size > MAX_SIZE) {
|
|
214
225
|
const message = `File too large (${(stats.size / 1024).toFixed(2)}KB > ${MAX_SIZE / 1024}KB limit)`;
|
|
226
|
+
// Capture file extension in telemetry without capturing the file path
|
|
227
|
+
capture('server_read_file_large', { fileExtension: fileExtension });
|
|
215
228
|
return {
|
|
216
229
|
content: message,
|
|
217
230
|
mimeType: 'text/plain',
|
|
218
231
|
isImage: false
|
|
219
232
|
};
|
|
220
233
|
}
|
|
234
|
+
// Capture file extension in telemetry without capturing the file path
|
|
235
|
+
capture('server_read_file', { fileExtension: fileExtension });
|
|
221
236
|
}
|
|
222
237
|
catch (error) {
|
|
223
238
|
console.error('error catch ' + error);
|
|
224
|
-
|
|
239
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
240
|
+
capture('server_read_file_error', { error: errorMessage, fileExtension: fileExtension });
|
|
225
241
|
// If we can't stat the file, continue anyway and let the read operation handle errors
|
|
226
242
|
//console.error(`Failed to stat file ${validPath}:`, error);
|
|
227
243
|
}
|
|
@@ -273,6 +289,10 @@ export async function readFile(filePath, isUrl) {
|
|
|
273
289
|
}
|
|
274
290
|
export async function writeFile(filePath, content) {
|
|
275
291
|
const validPath = await validatePath(filePath);
|
|
292
|
+
// Get file extension for telemetry
|
|
293
|
+
const fileExtension = path.extname(validPath).toLowerCase();
|
|
294
|
+
// Capture file extension in telemetry without capturing the file path
|
|
295
|
+
capture('server_write_file', { fileExtension: fileExtension });
|
|
276
296
|
await fs.writeFile(validPath, content, "utf-8");
|
|
277
297
|
}
|
|
278
298
|
export async function readMultipleFiles(paths) {
|
|
@@ -313,7 +333,19 @@ export async function moveFile(sourcePath, destinationPath) {
|
|
|
313
333
|
export async function searchFiles(rootPath, pattern) {
|
|
314
334
|
const results = [];
|
|
315
335
|
async function search(currentPath) {
|
|
316
|
-
|
|
336
|
+
let entries;
|
|
337
|
+
try {
|
|
338
|
+
entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
// Only sanitize for telemetry, not for the returned error
|
|
342
|
+
capture('server_search_files_error', {
|
|
343
|
+
errorType: error instanceof Error ? error.name : 'Unknown',
|
|
344
|
+
errorMessage: 'Error reading directory',
|
|
345
|
+
isReadDirError: true
|
|
346
|
+
});
|
|
347
|
+
return; // Skip this directory on error
|
|
348
|
+
}
|
|
317
349
|
for (const entry of entries) {
|
|
318
350
|
const fullPath = path.join(currentPath, entry.name);
|
|
319
351
|
try {
|
|
@@ -330,10 +362,27 @@ export async function searchFiles(rootPath, pattern) {
|
|
|
330
362
|
}
|
|
331
363
|
}
|
|
332
364
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
365
|
+
try {
|
|
366
|
+
// Validate root path before starting search
|
|
367
|
+
const validPath = await validatePath(rootPath);
|
|
368
|
+
await search(validPath);
|
|
369
|
+
// Log only the count of found files, not their paths
|
|
370
|
+
capture('server_search_files_complete', {
|
|
371
|
+
resultsCount: results.length,
|
|
372
|
+
patternLength: pattern.length
|
|
373
|
+
});
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
// For telemetry only - sanitize error info
|
|
378
|
+
capture('server_search_files_error', {
|
|
379
|
+
errorType: error instanceof Error ? error.name : 'Unknown',
|
|
380
|
+
errorMessage: 'Error with root path',
|
|
381
|
+
isRootPathError: true
|
|
382
|
+
});
|
|
383
|
+
// Re-throw the original error for the caller
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
337
386
|
}
|
|
338
387
|
export async function getFileInfo(filePath) {
|
|
339
388
|
const validPath = await validatePath(filePath);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively finds the closest match to a query string within text using fuzzy matching
|
|
3
|
+
* @param text The text to search within
|
|
4
|
+
* @param query The query string to find
|
|
5
|
+
* @param start Start index in the text (default: 0)
|
|
6
|
+
* @param end End index in the text (default: text.length)
|
|
7
|
+
* @param parentDistance Best distance found so far (default: Infinity)
|
|
8
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
9
|
+
*/
|
|
10
|
+
export declare function recursiveFuzzyIndexOf(text: string, query: string, start?: number, end?: number | null, parentDistance?: number, depth?: number): {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
value: string;
|
|
14
|
+
distance: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Calculates the similarity ratio between two strings
|
|
18
|
+
* @param a First string
|
|
19
|
+
* @param b Second string
|
|
20
|
+
* @returns Similarity ratio (0-1)
|
|
21
|
+
*/
|
|
22
|
+
export declare function getSimilarityRatio(a: string, b: string): number;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { distance } from 'fastest-levenshtein';
|
|
2
|
+
import { capture } from '../utils/capture.js';
|
|
3
|
+
/**
|
|
4
|
+
* Recursively finds the closest match to a query string within text using fuzzy matching
|
|
5
|
+
* @param text The text to search within
|
|
6
|
+
* @param query The query string to find
|
|
7
|
+
* @param start Start index in the text (default: 0)
|
|
8
|
+
* @param end End index in the text (default: text.length)
|
|
9
|
+
* @param parentDistance Best distance found so far (default: Infinity)
|
|
10
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
11
|
+
*/
|
|
12
|
+
export function recursiveFuzzyIndexOf(text, query, start = 0, end = null, parentDistance = Infinity, depth = 0) {
|
|
13
|
+
// For debugging and performance tracking purposes
|
|
14
|
+
if (depth === 0) {
|
|
15
|
+
const startTime = performance.now();
|
|
16
|
+
const result = recursiveFuzzyIndexOf(text, query, start, end, parentDistance, depth + 1);
|
|
17
|
+
const executionTime = performance.now() - startTime;
|
|
18
|
+
// Capture detailed metrics for the recursive search for in-depth analysis
|
|
19
|
+
capture('fuzzy_search_recursive_metrics', {
|
|
20
|
+
execution_time_ms: executionTime,
|
|
21
|
+
text_length: text.length,
|
|
22
|
+
query_length: query.length,
|
|
23
|
+
result_distance: result.distance
|
|
24
|
+
});
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
if (end === null)
|
|
28
|
+
end = text.length;
|
|
29
|
+
// For small text segments, use iterative approach
|
|
30
|
+
if (end - start <= 2 * query.length) {
|
|
31
|
+
return iterativeReduction(text, query, start, end, parentDistance);
|
|
32
|
+
}
|
|
33
|
+
let midPoint = start + Math.floor((end - start) / 2);
|
|
34
|
+
let leftEnd = Math.min(end, midPoint + query.length); // Include query length to cover overlaps
|
|
35
|
+
let rightStart = Math.max(start, midPoint - query.length); // Include query length to cover overlaps
|
|
36
|
+
// Calculate distance for current segments
|
|
37
|
+
let leftDistance = distance(text.substring(start, leftEnd), query);
|
|
38
|
+
let rightDistance = distance(text.substring(rightStart, end), query);
|
|
39
|
+
let bestDistance = Math.min(leftDistance, parentDistance, rightDistance);
|
|
40
|
+
// If parent distance is already the best, use iterative approach
|
|
41
|
+
if (parentDistance === bestDistance) {
|
|
42
|
+
return iterativeReduction(text, query, start, end, parentDistance);
|
|
43
|
+
}
|
|
44
|
+
// Recursively search the better half
|
|
45
|
+
if (leftDistance < rightDistance) {
|
|
46
|
+
return recursiveFuzzyIndexOf(text, query, start, leftEnd, bestDistance, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return recursiveFuzzyIndexOf(text, query, rightStart, end, bestDistance, depth + 1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Iteratively refines the best match by reducing the search area
|
|
54
|
+
* @param text The text to search within
|
|
55
|
+
* @param query The query string to find
|
|
56
|
+
* @param start Start index in the text
|
|
57
|
+
* @param end End index in the text
|
|
58
|
+
* @param parentDistance Best distance found so far
|
|
59
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
60
|
+
*/
|
|
61
|
+
function iterativeReduction(text, query, start, end, parentDistance) {
|
|
62
|
+
const startTime = performance.now();
|
|
63
|
+
let iterations = 0;
|
|
64
|
+
let bestDistance = parentDistance;
|
|
65
|
+
let bestStart = start;
|
|
66
|
+
let bestEnd = end;
|
|
67
|
+
// Improve start position
|
|
68
|
+
let nextDistance = distance(text.substring(bestStart + 1, bestEnd), query);
|
|
69
|
+
while (nextDistance < bestDistance) {
|
|
70
|
+
bestDistance = nextDistance;
|
|
71
|
+
bestStart++;
|
|
72
|
+
const smallerString = text.substring(bestStart + 1, bestEnd);
|
|
73
|
+
nextDistance = distance(smallerString, query);
|
|
74
|
+
iterations++;
|
|
75
|
+
}
|
|
76
|
+
// Improve end position
|
|
77
|
+
nextDistance = distance(text.substring(bestStart, bestEnd - 1), query);
|
|
78
|
+
while (nextDistance < bestDistance) {
|
|
79
|
+
bestDistance = nextDistance;
|
|
80
|
+
bestEnd--;
|
|
81
|
+
const smallerString = text.substring(bestStart, bestEnd - 1);
|
|
82
|
+
nextDistance = distance(smallerString, query);
|
|
83
|
+
iterations++;
|
|
84
|
+
}
|
|
85
|
+
const executionTime = performance.now() - startTime;
|
|
86
|
+
// Capture metrics for the iterative refinement phase
|
|
87
|
+
capture('fuzzy_search_iterative_metrics', {
|
|
88
|
+
execution_time_ms: executionTime,
|
|
89
|
+
iterations: iterations,
|
|
90
|
+
segment_length: end - start,
|
|
91
|
+
query_length: query.length,
|
|
92
|
+
final_distance: bestDistance
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
start: bestStart,
|
|
96
|
+
end: bestEnd,
|
|
97
|
+
value: text.substring(bestStart, bestEnd),
|
|
98
|
+
distance: bestDistance
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Calculates the similarity ratio between two strings
|
|
103
|
+
* @param a First string
|
|
104
|
+
* @param b Second string
|
|
105
|
+
* @returns Similarity ratio (0-1)
|
|
106
|
+
*/
|
|
107
|
+
export function getSimilarityRatio(a, b) {
|
|
108
|
+
const maxLength = Math.max(a.length, b.length);
|
|
109
|
+
if (maxLength === 0)
|
|
110
|
+
return 1; // Both strings are empty
|
|
111
|
+
const levenshteinDistance = distance(a, b);
|
|
112
|
+
return 1 - (levenshteinDistance / maxLength);
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FileResult } from './filesystem.js';
|
|
2
|
+
export declare enum PDFOutputFormat {
|
|
3
|
+
TEXT = "text",
|
|
4
|
+
MARKDOWN = "markdown",
|
|
5
|
+
HTML = "html"
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Reads a PDF file and returns its content in the specified format
|
|
9
|
+
* @param filePath Path to the PDF file
|
|
10
|
+
* @param outputFormat Desired output format (text, markdown, or html)
|
|
11
|
+
* @returns FileResult object with the parsed content
|
|
12
|
+
*/
|
|
13
|
+
export declare function readPDFFile(filePath: string, outputFormat?: PDFOutputFormat): Promise<FileResult>;
|