UNPKG

@vibe-kit/grok-cli

Version:

An open-source AI agent that brings the power of Grok directly into your terminal.

302 lines 11.4 kB
import { spawn } from "child_process"; import { ConfirmationService } from "../utils/confirmation-service.js"; import fs from "fs-extra"; import * as path from "path"; export class SearchTool { confirmationService = ConfirmationService.getInstance(); currentDirectory = process.cwd(); /** * Unified search method that can search for text content or find files */ async search(query, options = {}) { try { const searchType = options.searchType || "both"; const results = []; // Search for text content if requested if (searchType === "text" || searchType === "both") { const textResults = await this.executeRipgrep(query, options); results.push(...textResults.map((r) => ({ type: "text", file: r.file, line: r.line, column: r.column, text: r.text, match: r.match, }))); } // Search for files if requested if (searchType === "files" || searchType === "both") { const fileResults = await this.findFilesByPattern(query, options); results.push(...fileResults.map((r) => ({ type: "file", file: r.path, score: r.score, }))); } if (results.length === 0) { return { success: true, output: `No results found for "${query}"`, }; } const formattedOutput = this.formatUnifiedResults(results, query, searchType); return { success: true, output: formattedOutput, }; } catch (error) { return { success: false, error: `Search error: ${error.message}`, }; } } /** * Execute ripgrep command with specified options */ async executeRipgrep(query, options) { return new Promise((resolve, reject) => { const args = [ "--json", "--with-filename", "--line-number", "--column", "--no-heading", "--color=never", ]; // Add case sensitivity if (!options.caseSensitive) { args.push("--ignore-case"); } // Add whole word matching if (options.wholeWord) { args.push("--word-regexp"); } // Add regex mode if (!options.regex) { args.push("--fixed-strings"); } // Add max results limit if (options.maxResults) { args.push("--max-count", options.maxResults.toString()); } // Add file type filters if (options.fileTypes) { options.fileTypes.forEach((type) => { args.push("--type", type); }); } // Add include pattern if (options.includePattern) { args.push("--glob", options.includePattern); } // Add exclude pattern if (options.excludePattern) { args.push("--glob", `!${options.excludePattern}`); } // Add exclude files if (options.excludeFiles) { options.excludeFiles.forEach((file) => { args.push("--glob", `!${file}`); }); } // Respect gitignore and common ignore patterns args.push("--no-require-git", "--follow", "--glob", "!.git/**", "--glob", "!node_modules/**", "--glob", "!.DS_Store", "--glob", "!*.log"); // Add query and search directory args.push(query, this.currentDirectory); const rg = spawn("rg", args); let output = ""; let errorOutput = ""; rg.stdout.on("data", (data) => { output += data.toString(); }); rg.stderr.on("data", (data) => { errorOutput += data.toString(); }); rg.on("close", (code) => { if (code === 0 || code === 1) { // 0 = found, 1 = not found const results = this.parseRipgrepOutput(output); resolve(results); } else { reject(new Error(`Ripgrep failed with code ${code}: ${errorOutput}`)); } }); rg.on("error", (error) => { reject(error); }); }); } /** * Parse ripgrep JSON output into SearchResult objects */ parseRipgrepOutput(output) { const results = []; const lines = output .trim() .split("\n") .filter((line) => line.length > 0); for (const line of lines) { try { const parsed = JSON.parse(line); if (parsed.type === "match") { const data = parsed.data; results.push({ file: data.path.text, line: data.line_number, column: data.submatches[0]?.start || 0, text: data.lines.text.trim(), match: data.submatches[0]?.match?.text || "", }); } } catch (e) { // Skip invalid JSON lines continue; } } return results; } /** * Find files by pattern using a simple file walking approach */ async findFilesByPattern(pattern, options) { const files = []; const maxResults = options.maxResults || 50; const searchPattern = pattern.toLowerCase(); const walkDir = async (dir, depth = 0) => { if (depth > 10 || files.length >= maxResults) return; // Prevent infinite recursion and limit results try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (files.length >= maxResults) break; const fullPath = path.join(dir, entry.name); const relativePath = path.relative(this.currentDirectory, fullPath); // Skip hidden files unless explicitly included if (!options.includeHidden && entry.name.startsWith(".")) { continue; } // Skip common directories if (entry.isDirectory() && [ "node_modules", ".git", ".svn", ".hg", "dist", "build", ".next", ".cache", ].includes(entry.name)) { continue; } // Apply exclude pattern if (options.excludePattern && relativePath.includes(options.excludePattern)) { continue; } if (entry.isFile()) { const score = this.calculateFileScore(entry.name, relativePath, searchPattern); if (score > 0) { files.push({ path: relativePath, name: entry.name, score, }); } } else if (entry.isDirectory()) { await walkDir(fullPath, depth + 1); } } } catch (error) { // Skip directories we can't read } }; await walkDir(this.currentDirectory); // Sort by score (descending) and return top results return files.sort((a, b) => b.score - a.score).slice(0, maxResults); } /** * Calculate fuzzy match score for file names */ calculateFileScore(fileName, filePath, pattern) { const lowerFileName = fileName.toLowerCase(); const lowerFilePath = filePath.toLowerCase(); // Exact matches get highest score if (lowerFileName === pattern) return 100; if (lowerFileName.includes(pattern)) return 80; // Path matches get medium score if (lowerFilePath.includes(pattern)) return 60; // Fuzzy matching - check if all characters of pattern exist in order let patternIndex = 0; for (let i = 0; i < lowerFileName.length && patternIndex < pattern.length; i++) { if (lowerFileName[i] === pattern[patternIndex]) { patternIndex++; } } if (patternIndex === pattern.length) { // All characters found in order - score based on how close they are return Math.max(10, 40 - (fileName.length - pattern.length)); } return 0; } /** * Format unified search results for display */ formatUnifiedResults(results, query, searchType) { if (results.length === 0) { return `No results found for "${query}"`; } let output = `Search results for "${query}":\n`; // Separate text and file results const textResults = results.filter((r) => r.type === "text"); const fileResults = results.filter((r) => r.type === "file"); // Show all unique files (from both text matches and file matches) const allFiles = new Set(); // Add files from text results textResults.forEach((result) => { allFiles.add(result.file); }); // Add files from file search results fileResults.forEach((result) => { allFiles.add(result.file); }); const fileList = Array.from(allFiles); const displayLimit = 8; // Show files in compact format fileList.slice(0, displayLimit).forEach((file) => { // Count matches in this file for text results const matchCount = textResults.filter((r) => r.file === file).length; const matchIndicator = matchCount > 0 ? ` (${matchCount} matches)` : ""; output += ` ${file}${matchIndicator}\n`; }); // Show "+X more" if there are additional results if (fileList.length > displayLimit) { const remaining = fileList.length - displayLimit; output += ` ... +${remaining} more\n`; } return output.trim(); } /** * Update current working directory */ setCurrentDirectory(directory) { this.currentDirectory = directory; } /** * Get current working directory */ getCurrentDirectory() { return this.currentDirectory; } } //# sourceMappingURL=search.js.map