From a8c6ad52eec0f6ee3ec75e9014b83041977a54ed Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Fri, 12 Sep 2025 20:55:21 +0300 Subject: [PATCH 1/2] Fix searches with patterns in the string --- src/handlers/search-handlers.ts | 1 + src/search-manager.ts | 6 + src/server.ts | 16 ++ src/tools/schemas.ts | 1 + test/test-literal-search.js | 421 ++++++++++++++++++++++++++++++++ 5 files changed, 445 insertions(+) create mode 100644 test/test-literal-search.js diff --git a/src/handlers/search-handlers.ts b/src/handlers/search-handlers.ts index dd12aedd..4a4f6add 100644 --- a/src/handlers/search-handlers.ts +++ b/src/handlers/search-handlers.ts @@ -31,6 +31,7 @@ export async function handleStartSearch(args: unknown): Promise { contextLines: parsed.data.contextLines, timeout: parsed.data.timeout_ms, earlyTermination: parsed.data.earlyTermination, + literalSearch: parsed.data.literalSearch, }); const searchTypeText = parsed.data.searchType === 'content' ? 'content search' : 'file search'; diff --git a/src/search-manager.ts b/src/search-manager.ts index be5dd809..cb176ffa 100644 --- a/src/search-manager.ts +++ b/src/search-manager.ts @@ -37,6 +37,7 @@ export interface SearchSessionOptions { contextLines?: number; timeout?: number; earlyTermination?: boolean; // Stop search early when exact filename match is found + literalSearch?: boolean; // Force literal string matching (-F flag) instead of regex } /** @@ -312,6 +313,11 @@ export interface SearchSessionOptions { // Content search mode args.push('--json', '--line-number'); + // Add literal search support for content searches + if (options.literalSearch) { + args.push('-F'); // Fixed string matching (literal) + } + if (options.contextLines && options.contextLines > 0) { args.push('-C', options.contextLines.toString()); } diff --git a/src/server.ts b/src/server.ts index 34b67b49..3eff3201 100644 --- a/src/server.ts +++ b/src/server.ts @@ -312,8 +312,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { - searchType="files": Find files by name (pattern matches file names) - searchType="content": Search inside files for text patterns + PATTERN MATCHING MODES: + - Default (literalSearch=false): Patterns are treated as regular expressions + - Literal (literalSearch=true): Patterns are treated as exact strings + + WHEN TO USE literalSearch=true: + Use literal search when searching for code patterns with special characters: + - Function calls with parentheses and quotes + - Array access with brackets + - Object methods with dots and parentheses + - File paths with backslashes + - Any pattern containing: . * + ? ^ $ { } [ ] | \\ ( ) + IMPORTANT PARAMETERS: - pattern: What to search for (file names OR content text) + - literalSearch: Use exact string matching instead of regex (default: false) - filePattern: Optional filter to limit search to specific file types (e.g., "*.js", "package.json") - ignoreCase: Case-insensitive search (default: true). Works for both file names and content. - earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches) @@ -322,6 +335,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { - Find package.json files: searchType="files", pattern="package.json", filePattern="package.json" - Find all JS files: searchType="files", pattern="*.js" (or use filePattern="*.js") - Search for "TODO" in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts" + - Search for exact code: searchType="content", pattern="toast.error('test')", literalSearch=true + - Search for function calls: searchType="content", pattern="console.log()", literalSearch=true + - Regex pattern search: searchType="content", pattern="error.*test", literalSearch=false - Case-sensitive file search: searchType="files", pattern="README", ignoreCase=false - Case-insensitive file search: searchType="files", pattern="readme", ignoreCase=true - Find exact file, stop after first match: searchType="files", pattern="config.json", earlyTermination=true diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 80ce0da6..d5ad00f8 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -115,6 +115,7 @@ export const StartSearchArgsSchema = z.object({ contextLines: z.number().optional().default(5), timeout_ms: z.number().optional(), // Match process naming convention earlyTermination: z.boolean().optional(), // Stop search early when exact filename match is found (default: true for files, false for content) + literalSearch: z.boolean().optional().default(false), // Force literal string matching (-F flag) instead of regex }); export const GetMoreSearchResultsArgsSchema = z.object({ diff --git a/test/test-literal-search.js b/test/test-literal-search.js new file mode 100644 index 00000000..1b76f5b8 --- /dev/null +++ b/test/test-literal-search.js @@ -0,0 +1,421 @@ +/** + * Test for literal search functionality - testing regex vs literal string matching + */ + +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { handleStartSearch, handleGetMoreSearchResults, handleStopSearch } from '../dist/handlers/search-handlers.js'; +import { configManager } from '../dist/config-manager.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test directory for literal search tests +const LITERAL_SEARCH_TEST_DIR = path.join(__dirname, 'literal-search-test-files'); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m' +}; + +/** + * Helper function to wait for search completion and get all results + */ +async function searchAndWaitForCompletion(searchArgs, timeout = 10000) { + const result = await handleStartSearch(searchArgs); + + // Extract session ID from result + const sessionIdMatch = result.content[0].text.match(/Started .+ session: (.+)/); + if (!sessionIdMatch) { + throw new Error('Could not extract session ID from search result'); + } + const sessionId = sessionIdMatch[1]; + + try { + // Wait for completion by polling + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + const moreResults = await handleGetMoreSearchResults({ sessionId }); + + if (moreResults.content[0].text.includes('✅ Search completed')) { + return { initialResult: result, finalResult: moreResults, sessionId }; + } + + if (moreResults.content[0].text.includes('❌ ERROR')) { + throw new Error(`Search failed: ${moreResults.content[0].text}`); + } + + // Wait a bit before polling again + await new Promise(resolve => setTimeout(resolve, 100)); + } + + throw new Error('Search timed out'); + } finally { + // Always stop the search session to prevent hanging + try { + await handleStopSearch({ sessionId }); + } catch (e) { + // Ignore errors when stopping - session might already be completed + } + } +} + +/** + * Setup function to prepare literal search test environment + */ +async function setup() { + console.log(`${colors.blue}Setting up literal search tests...${colors.reset}`); + + // Save original config + const originalConfig = await configManager.getConfig(); + + // Set allowed directories to include test directory + await configManager.setValue('allowedDirectories', [LITERAL_SEARCH_TEST_DIR]); + + // Create test directory + await fs.mkdir(LITERAL_SEARCH_TEST_DIR, { recursive: true }); + + // Create test file with patterns that contain regex special characters + await fs.writeFile(path.join(LITERAL_SEARCH_TEST_DIR, 'code-patterns.js'), `// JavaScript file with common code patterns +// These patterns contain regex special characters that should be matched literally + +// Function calls with parentheses +toast.error("test"); +console.log("hello world"); +alert("message"); + +// Array access with brackets +array[0] +data[index] +items[key] + +// Object method calls with dots +obj.method() +user.getName() +config.getValue() + +// Template literals with backticks +const msg = \`Hello \${name}\`; +const query = \`SELECT * FROM users WHERE id = \${id}\`; + +// Regex special characters +const pattern = ".*"; +const wildcard = "test*"; +const question = "value?"; +const plus = "count++"; +const caret = "^start"; +const dollar = "end$"; +const pipe = "a|b"; +const backslash = "path\\\\to\\\\file"; + +// Complex patterns +if (condition && obj.method()) { + toast.error("Error occurred"); +} + +function validateEmail(email) { + return email.includes("@") && email.includes("."); +} +`); + + // Create another file with similar but different patterns + await fs.writeFile(path.join(LITERAL_SEARCH_TEST_DIR, 'similar-patterns.ts'), `// TypeScript file with similar patterns +interface Config { + getValue(): string; +} + +class Logger { + static error(message: string): void { + console.error(\`[ERROR] \${message}\`); + } + + static log(message: string): void { + console.log(message); + } +} + +// These should NOT match when searching for exact patterns +toast.errorHandler("test"); // Similar but different +console.logout("hello world"); // Similar but different +array.slice(0, 1); // Similar but different +`); + + console.log(`${colors.green}✓ Setup complete: Literal search test files created${colors.reset}`); + return originalConfig; +} + +/** + * Teardown function to clean up after tests + */ +async function teardown(originalConfig) { + console.log(`${colors.blue}Cleaning up literal search tests...${colors.reset}`); + + // Clean up test directory + await fs.rm(LITERAL_SEARCH_TEST_DIR, { force: true, recursive: true }); + + // Restore original config + await configManager.updateConfig(originalConfig); + + console.log(`${colors.green}✓ Teardown complete: Test files removed and config restored${colors.reset}`); +} + +/** + * Assert function for test validation + */ +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +/** + * Count occurrences of a pattern in text + */ +function countOccurrences(text, pattern) { + return (text.match(new RegExp(pattern, 'g')) || []).length; +} + +/** + * Test that literal search finds exact matches for patterns with special characters + * This test should FAIL initially since literalSearch parameter doesn't exist yet + */ +async function testLiteralSearchExactMatches() { + console.log(`${colors.yellow}Testing literal search for exact matches...${colors.reset}`); + + // Test 1: Search for exact function call with quotes and parentheses + const { finalResult: result1 } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'toast.error("test")', + searchType: 'content', + literalSearch: true // This parameter should be added + }); + + const text1 = result1.content[0].text; + + // Should find exactly 2 occurrences (one in each file: the exact match and in comment) + const exactMatches = countOccurrences(text1, 'toast\\.error\\("test"\\)'); + assert(exactMatches >= 1, `Should find exact matches for 'toast.error("test")', found: ${exactMatches}`); + + // Should NOT find the similar but different pattern + assert(!text1.includes('toast.errorHandler'), 'Should not match similar but different patterns'); + + console.log(`${colors.green}✓ Literal search exact matches test passed${colors.reset}`); +} + +/** + * Test that regex search works differently than literal search + */ +async function testRegexVsLiteralDifference() { + console.log(`${colors.yellow}Testing difference between regex and literal search...${colors.reset}`); + + // Test with regex (default behavior) - should interpret dots as wildcard + const { finalResult: regexResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'console.log', // Dot should match any character in regex mode + searchType: 'content', + literalSearch: false // Explicit regex mode + }); + + // Test with literal search - should match exact dots + const { finalResult: literalResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'console.log', // Dot should match literal dot only + searchType: 'content', + literalSearch: true + }); + + const regexText = regexResult.content[0].text; + const literalText = literalResult.content[0].text; + + // Both should find console.log, but regex might find more due to dot wildcard behavior + assert(regexText.includes('console.log'), 'Regex search should find console.log'); + assert(literalText.includes('console.log'), 'Literal search should find console.log'); + + console.log(`${colors.green}✓ Regex vs literal difference test passed${colors.reset}`); +} + +/** + * Test literal search with various special characters + */ +async function testSpecialCharactersLiteralSearch() { + console.log(`${colors.yellow}Testing literal search with various special characters...${colors.reset}`); + + const testPatterns = [ + 'array[0]', // Brackets + 'obj.method()', // Dots and parentheses + 'count++', // Plus signs + 'value?', // Question mark + 'pattern.*', // Dot and asterisk + '^start', // Caret + 'end$', // Dollar sign + 'a|b', // Pipe + 'path\\\\to\\\\file' // Backslashes + ]; + + for (const pattern of testPatterns) { + console.log(` Testing pattern: ${pattern}`); + + const { finalResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: pattern, + searchType: 'content', + literalSearch: true + }); + + const text = finalResult.content[0].text; + + // Should find the pattern or indicate no matches (both are valid for literal search) + const hasResults = text.includes(pattern) || text.includes('No matches found') || text.includes('Total results found: 0'); + assert(hasResults, `Should handle literal search for pattern '${pattern}' (found results or no matches message)`); + } + + console.log(`${colors.green}✓ Special characters literal search test passed${colors.reset}`); +} + +/** + * Test that literalSearch parameter defaults to false (maintains backward compatibility) + */ +async function testLiteralSearchDefault() { + console.log(`${colors.yellow}Testing that literalSearch defaults to false...${colors.reset}`); + + // Search without specifying literalSearch - should default to regex behavior + const { finalResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'console.log', + searchType: 'content' + // literalSearch not specified - should default to false + }); + + const text = finalResult.content[0].text; + + // Should work (either find matches or no matches, but not error) + const isValidResult = text.includes('console.log') || text.includes('No matches found') || text.includes('Total results found'); + assert(isValidResult, 'Should handle search with default literalSearch behavior'); + + console.log(`${colors.green}✓ Literal search default behavior test passed${colors.reset}`); +} + +/** + * Test the specific failing case that motivated this fix + */ +async function testOriginalFailingCase() { + console.log(`${colors.yellow}Testing the original failing case that motivated this fix...${colors.reset}`); + + // This was the original failing search: toast.error("test") + const { finalResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'toast.error("test")', + searchType: 'content', + literalSearch: true + }); + + const text = finalResult.content[0].text; + + // Should find the exact match + assert(text.includes('toast.error("test")') || text.includes('code-patterns.js'), + 'Should find the exact pattern that was originally failing'); + + // Verify it contains the file where we know the pattern exists + assert(text.includes('code-patterns.js'), 'Should find matches in code-patterns.js file'); + + console.log(`${colors.green}✓ Original failing case test passed${colors.reset}`); +} + +/** + * Test that demonstrates regex mode fails while literal mode succeeds + * This is the key test that shows the problem we solved + */ +async function testRegexFailureLiteralSuccess() { + console.log(`${colors.yellow}Testing that regex mode fails where literal mode succeeds...${colors.reset}`); + + // Test with regex mode (default) - should fail to find exact pattern due to regex interpretation + const { finalResult: regexResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'toast.error("test")', // This pattern has regex special chars + searchType: 'content', + literalSearch: false // Use regex mode (default) + }); + + // Test with literal mode - should succeed in finding exact pattern + const { finalResult: literalResult } = await searchAndWaitForCompletion({ + path: LITERAL_SEARCH_TEST_DIR, + pattern: 'toast.error("test")', // Same pattern + searchType: 'content', + literalSearch: true // Use literal mode + }); + + const regexText = regexResult.content[0].text; + const literalText = literalResult.content[0].text; + + // Regex mode should find few/no matches due to special character interpretation + const regexMatches = (regexText.match(/toast\.error\("test"\)/g) || []).length; + + // Literal mode should find the exact matches + const literalMatches = (literalText.match(/toast\.error\("test"\)/g) || []).length; + + console.log(` Regex mode found: ${regexMatches} matches`); + console.log(` Literal mode found: ${literalMatches} matches`); + + // The key assertion: literal should find more matches than regex + assert(literalMatches > regexMatches, + `Literal search should find more matches (${literalMatches}) than regex search (${regexMatches}) for patterns with special characters`); + + // Literal should find at least one match + assert(literalMatches >= 1, 'Literal search should find at least one exact match'); + + console.log(`${colors.green}✓ Regex failure vs literal success test passed${colors.reset}`); +} + +/** + * Main test runner function for literal search tests + */ +export async function testLiteralSearch() { + console.log(`${colors.blue}Starting literal search functionality tests...${colors.reset}`); + + let originalConfig; + + try { + // Setup + originalConfig = await setup(); + + // Run all literal search tests + await testLiteralSearchExactMatches(); + await testRegexVsLiteralDifference(); + await testSpecialCharactersLiteralSearch(); + await testLiteralSearchDefault(); + await testOriginalFailingCase(); + await testRegexFailureLiteralSuccess(); // NEW: Critical test showing the problem we solved + + console.log(`${colors.green}✅ All literal search tests passed!${colors.reset}`); + return true; + + } catch (error) { + console.error(`${colors.red}❌ Literal search test failed: ${error.message}${colors.reset}`); + console.error(error.stack); + throw error; + } finally { + // Cleanup + if (originalConfig) { + await teardown(originalConfig); + } + } +} + +// Export for use in run-all-tests.js +export default testLiteralSearch; + +// Run tests if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + testLiteralSearch().then(() => { + console.log('Literal search tests completed successfully.'); + process.exit(0); + }).catch(error => { + console.error('Literal search test execution failed:', error); + process.exit(1); + }); +} From 3767775405a4df8829b9afe8bdd32a486a094231 Mon Sep 17 00:00:00 2001 From: Eduard Ruzga Date: Fri, 12 Sep 2025 21:41:01 +0300 Subject: [PATCH 2/2] Prompt improvement --- src/server.ts | 52 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/server.ts b/src/server.ts index 3eff3201..dcc06fd7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -308,6 +308,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: ` Start a streaming search that can return results progressively. + SEARCH STRATEGY GUIDE: + Choose the right search type based on what the user is looking for: + + USE searchType="files" WHEN: + - User asks for specific files: "find package.json", "locate config files" + - Pattern looks like a filename: "*.js", "README.md", "test-*.tsx" + - User wants to find files by name/extension: "all TypeScript files", "Python scripts" + - Looking for configuration/setup files: ".env", "dockerfile", "tsconfig.json" + + USE searchType="content" WHEN: + - User asks about code/logic: "authentication logic", "error handling", "API calls" + - Looking for functions/variables: "getUserData function", "useState hook" + - Searching for text/comments: "TODO items", "FIXME comments", "documentation" + - Finding patterns in code: "console.log statements", "import statements" + - User describes functionality: "components that handle login", "files with database queries" + + WHEN UNSURE OR USER REQUEST IS AMBIGUOUS: + Run TWO searches in parallel - one for files and one for content: + + Example approach for ambiguous queries like "find authentication stuff": + 1. Start file search: searchType="files", pattern="auth" + 2. Simultaneously start content search: searchType="content", pattern="authentication" + 3. Present combined results: "Found 3 auth-related files and 8 files containing authentication code" + SEARCH TYPES: - searchType="files": Find files by name (pattern matches file names) - searchType="content": Search inside files for text patterns @@ -331,17 +355,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { - ignoreCase: Case-insensitive search (default: true). Works for both file names and content. - earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches) - EXAMPLES: - - Find package.json files: searchType="files", pattern="package.json", filePattern="package.json" - - Find all JS files: searchType="files", pattern="*.js" (or use filePattern="*.js") - - Search for "TODO" in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts" + DECISION EXAMPLES: + - "find package.json" → searchType="files", pattern="package.json" (specific file) + - "find authentication components" → searchType="content", pattern="authentication" (looking for functionality) + - "locate all React components" → searchType="files", pattern="*.tsx" or "*.jsx" (file pattern) + - "find TODO comments" → searchType="content", pattern="TODO" (text in files) + - "show me login files" → AMBIGUOUS → run both: files with "login" AND content with "login" + - "find config" → AMBIGUOUS → run both: config files AND files containing config code + + COMPREHENSIVE SEARCH EXAMPLES: + - Find package.json files: searchType="files", pattern="package.json" + - Find all JS files: searchType="files", pattern="*.js" + - Search for TODO in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts" - Search for exact code: searchType="content", pattern="toast.error('test')", literalSearch=true - - Search for function calls: searchType="content", pattern="console.log()", literalSearch=true - - Regex pattern search: searchType="content", pattern="error.*test", literalSearch=false - - Case-sensitive file search: searchType="files", pattern="README", ignoreCase=false - - Case-insensitive file search: searchType="files", pattern="readme", ignoreCase=true - - Find exact file, stop after first match: searchType="files", pattern="config.json", earlyTermination=true - - Find all matching files: searchType="files", pattern="test.js", earlyTermination=false + - Ambiguous request "find auth stuff": Run two searches: + 1. searchType="files", pattern="auth" + 2. searchType="content", pattern="authentication" + + PRO TIP: When user requests are ambiguous about whether they want files or content, + run both searches concurrently and combine results for comprehensive coverage. Unlike regular search tools, this starts a background search process and returns immediately with a session ID. Use get_more_search_results to get results as they