@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.
Files changed (47) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +88 -27
  3. package/dist/command-manager.js +1 -1
  4. package/dist/config-manager.d.ts +1 -0
  5. package/dist/config-manager.js +21 -4
  6. package/dist/config.d.ts +2 -2
  7. package/dist/config.js +2 -3
  8. package/dist/error-handlers.js +1 -1
  9. package/dist/handlers/edit-search-handlers.d.ts +3 -1
  10. package/dist/handlers/edit-search-handlers.js +6 -12
  11. package/dist/handlers/filesystem-handlers.js +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/polyform-license-src/edit/edit.d.ts +15 -0
  14. package/dist/polyform-license-src/edit/edit.js +163 -0
  15. package/dist/polyform-license-src/edit/fuzzySearch.d.ts +30 -0
  16. package/dist/polyform-license-src/edit/fuzzySearch.js +121 -0
  17. package/dist/polyform-license-src/edit/handlers.d.ts +16 -0
  18. package/dist/polyform-license-src/edit/handlers.js +24 -0
  19. package/dist/polyform-license-src/edit/index.d.ts +12 -0
  20. package/dist/polyform-license-src/edit/index.js +13 -0
  21. package/dist/polyform-license-src/edit/schemas.d.ts +25 -0
  22. package/dist/polyform-license-src/edit/schemas.js +16 -0
  23. package/dist/polyform-license-src/index.d.ts +9 -0
  24. package/dist/polyform-license-src/index.js +10 -0
  25. package/dist/server.js +71 -43
  26. package/dist/setup-claude-server.js +549 -288
  27. package/dist/terminal-manager.js +4 -2
  28. package/dist/tools/edit.d.ts +8 -6
  29. package/dist/tools/edit.js +161 -34
  30. package/dist/tools/execute.js +2 -2
  31. package/dist/tools/filesystem.js +59 -10
  32. package/dist/tools/fuzzySearch.d.ts +22 -0
  33. package/dist/tools/fuzzySearch.js +113 -0
  34. package/dist/tools/pdf-reader.d.ts +13 -0
  35. package/dist/tools/pdf-reader.js +214 -0
  36. package/dist/tools/schemas.d.ts +12 -3
  37. package/dist/tools/schemas.js +5 -2
  38. package/dist/tools/search.js +5 -4
  39. package/dist/utils/capture.d.ts +15 -0
  40. package/dist/utils/capture.js +175 -0
  41. package/dist/utils/withTimeout.d.ts +11 -0
  42. package/dist/utils/withTimeout.js +52 -0
  43. package/dist/utils.d.ts +10 -1
  44. package/dist/utils.js +99 -26
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +2 -2
@@ -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
- capture('server_request_error', { error: error, message: `Failed to terminate process ${pid}:` });
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
  }
@@ -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
- export declare function parseEditBlock(blockContent: string): Promise<{
8
- filePath: string;
9
- searchReplace: SearchReplace;
10
- error?: string;
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 {};
@@ -1,48 +1,175 @@
1
1
  import { readFile, writeFile } from './filesystem.js';
2
- export async function performSearchReplace(filePath, block) {
3
- // Read file as plain string (don't pass true to get just the string)
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
- // Find first occurrence
11
- const searchIndex = content.indexOf(block.search);
12
- if (searchIndex === -1) {
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: [{ type: "text", text: `Search content not found in ${filePath}.` }],
57
+ content: [{
58
+ type: "text",
59
+ text: `Successfully applied ${expectedReplacements} edit${expectedReplacements > 1 ? 's' : ''} to ${filePath}`
60
+ }],
15
61
  };
16
62
  }
17
- // Replace content
18
- const newContent = content.substring(0, searchIndex) +
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
- filePath: '',
37
- searchReplace: { search: '', replace: '' },
38
- error: 'Invalid edit block format - missing markers'
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
- // Extract search and replace content
42
- const search = lines.slice(searchStart + 1, divider).join('\n');
43
- const replace = lines.slice(divider + 1, replaceEnd).join('\n');
44
- return {
45
- filePath,
46
- searchReplace: { search, replace }
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
  }
@@ -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
@@ -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, withTimeout } from '../utils.js';
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
- throw (`Path not allowed: ${requestedPath}. Must be within one of these directories: ${(await getAllowedDirs()).join(', ')}`);
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 ${requestedPath}`, null);
151
+ const result = await withTimeout(validationOperation(), PATH_VALIDATION_TIMEOUT, `Path validation operation`, // Generic name for telemetry
152
+ null);
147
153
  if (result === null) {
148
- // Return a path with an error indicator instead of throwing
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
- capture('server_read_file_error', { error: error });
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
- const entries = await fs.readdir(currentPath, { withFileTypes: true });
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
- // if path not exist, it will throw an error
334
- const validPath = await validatePath(rootPath);
335
- await search(validPath);
336
- return results;
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>;