UNPKG

@claude-vector/cli

Version:

CLI for Claude-integrated vector search

275 lines (238 loc) 10.4 kB
/** * Search command - Search for code, patterns, or solutions */ import chalk from 'chalk'; import ora from 'ora'; import { VectorSearchEngine, ProjectAdapter } from '@claude-vector/core'; import { SessionManager, QueryOptimizer } from '@claude-vector/claude-tools'; import { existsSync, promises as fs } from 'fs'; import { join } from 'path'; import path from 'path'; import { SmartClaude } from './smart-search.js'; export async function searchCommand(query, options) { const spinner = ora('Initializing search...').start(); try { // Load environment variables (same as ccvector) const smartSearch = new SmartClaude(); smartSearch.loadEnvironmentVariables(); // Check if initialized - try multiple index locations let indexPath = join(process.cwd(), '.claude-code-index'); if (!existsSync(join(indexPath, 'embeddings.json'))) { // Try fallback path indexPath = join(process.cwd(), '.claude-vector-index'); if (!existsSync(join(indexPath, 'embeddings.json'))) { spinner.fail('Project not indexed'); console.log(chalk.yellow('\n⚠️ No search index found')); console.log(chalk.gray('Run'), chalk.cyan('claude-search index'), chalk.gray('to build the index')); process.exit(1); } } // Load configuration const adapter = new ProjectAdapter(process.cwd()); const config = await adapter.getConfig(); // Initialize search engine const engine = new VectorSearchEngine({ searchThreshold: options.threshold || config.search?.threshold || 0.4, maxResults: options.limit || config.search?.maxResults || 10, cacheEnabled: options.cache !== false && config.cache?.enabled !== false, openaiApiKey: process.env.OPENAI_API_KEY }); // Load index spinner.text = 'Loading search index...'; await engine.loadIndex( join(indexPath, 'embeddings.json'), join(indexPath, 'chunks.json') ); const stats = engine.getStats(); spinner.succeed(`Loaded ${stats.totalChunks} chunks`); // Initialize session manager for search optimization let sessionManager = null; let queryOptimizer = null; try { sessionManager = new SessionManager(); queryOptimizer = new QueryOptimizer(); } catch (error) { // Enhanced features are optional } // Optimize query if session is active and optimization is enabled let optimizedQuery = query; if (sessionManager && queryOptimizer && (options.optimize || options.auto)) { try { const session = await sessionManager.getCurrentSessionStatus(); if (session) { spinner.text = 'Optimizing search query...'; optimizedQuery = await queryOptimizer.optimizeQuery(query, { taskType: session.taskType, previousSearches: [] }); if (optimizedQuery !== query) { console.log(chalk.dim(`\n💡 Optimized query: "${optimizedQuery}"`)); } } } catch (error) { // Fallback to original query optimizedQuery = query; } } // Perform search with dynamic threshold adjustment spinner.start(`Searching for "${optimizedQuery}"...`); const startTime = Date.now(); let results = await engine.search(optimizedQuery, { threshold: options.threshold, maxResults: options.limit, type: options.type, noCache: options.cache === false }); // Dynamic threshold adjustment if no results found if (results.results.length === 0 && !options.threshold) { console.log(chalk.dim('\n🔄 No results found with default threshold. Adjusting...')); const thresholds = [0.3, 0.25, 0.2]; for (const threshold of thresholds) { spinner.text = `Trying with lower threshold (${threshold})...`; results = await engine.search(optimizedQuery, { threshold: threshold, maxResults: options.limit, type: options.type, noCache: options.cache === false }); if (results.results.length > 0) { console.log(chalk.green(`\n✅ Found ${results.results.length} results with threshold: ${threshold}`)); break; } } } const searchTime = Date.now() - startTime; spinner.succeed(`Found ${results.totalMatches} matches in ${searchTime}ms`); // Output results if (options.json) { console.log(JSON.stringify(results, null, 2)); } else { displayResults(results, options); } // Save to session and integrate with context if using enhanced tools if (config.features?.persistence && sessionManager) { try { const session = await sessionManager.getCurrentSessionStatus(); if (session) { // Add search activity await sessionManager.addActivity('search', { originalQuery: query, optimizedQuery, resultsCount: results.results.length, totalMatches: results.totalMatches, searchTime, taskType: session.taskType }); // Auto-add relevant results to context if requested if (options.addToContext || options.auto) { spinner.start('Adding relevant results to context...'); try { await sessionManager.search(optimizedQuery, { maxResults: Math.min(results.results.length, 5), autoAddToContext: true }); spinner.succeed('Relevant results added to context'); } catch (contextError) { spinner.warn('Could not add results to context'); } } // Record feedback for query optimization learning if (queryOptimizer && optimizedQuery !== query) { await queryOptimizer.recordFeedback(optimizedQuery, { useful: results.totalMatches > 0, resultIds: results.results.slice(0, 3).map(r => r.chunk?.file), taskType: session.taskType }); } } } catch (error) { // Session saving is optional, don't fail the search } } // Save last search results for feedback/open commands try { let currentTaskType = 'general'; // Get task type from current session if available if (sessionManager) { try { const currentSession = await sessionManager.getCurrentSessionStatus(); if (currentSession) { currentTaskType = currentSession.taskType || 'general'; } } catch (err) { // Fallback to general if session check fails } } const lastSearchData = { query: optimizedQuery || query, originalQuery: query, results: results.results, totalMatches: results.totalMatches, timestamp: new Date().toISOString(), taskType: currentTaskType }; await fs.writeFile( path.join(process.cwd(), '.claude-last-search.json'), JSON.stringify(lastSearchData, null, 2) ); } catch (error) { // Don't fail if we can't save results if (process.env.DEBUG) { console.error('Failed to save search results:', error); } } // 正常終了 process.exit(0); } catch (error) { spinner.fail(`Search failed: ${error.message}`); process.exit(1); } } function displayResults(results, options) { console.log(chalk.bold(`\n🔍 Search Results for "${results.query}"\n`)); if (results.results.length === 0) { console.log(chalk.yellow('No results found')); console.log(chalk.gray('\nTry:')); console.log(chalk.gray('• Search for specific function or class names')); console.log(chalk.gray('• Use multiple keywords:'), chalk.cyan('"supabase auth"')); console.log(chalk.gray('• Lower the threshold:'), chalk.cyan('--threshold 0.2')); console.log(chalk.gray('• Search will auto-adjust threshold if needed')); console.log(chalk.gray('\nExamples:')); console.log(chalk.gray('•'), chalk.cyan('claude-search search "createClient"')); console.log(chalk.gray('•'), chalk.cyan('claude-search search "authentication user login"')); console.log(chalk.gray('•'), chalk.cyan('claude-search search "checkAuth" --threshold 0.3')); return; } results.results.forEach((result, index) => { const { chunk, score } = result; const relevance = getRelevanceLabel(score); // チャンクのファイル情報を取得(トップレベルまたはmetadata内) const filePath = chunk.file || chunk.metadata?.file || 'Unknown file'; const startLine = chunk.startLine || chunk.metadata?.startLine || chunk.metadata?.line || '?'; const endLine = chunk.endLine || chunk.metadata?.endLine || '?'; const chunkType = chunk.type || chunk.metadata?.type || chunk.metadata?.semanticType || 'code'; // ファイル名のみを表示(フルパスの場合) const fileName = filePath.includes('/') ? filePath.split('/').pop() : filePath; console.log(chalk.bold(`${index + 1}. ${fileName}`)); console.log(chalk.gray(` Lines ${startLine}-${endLine} | ${chunkType} | ${relevance}`)); // Show preview const preview = chunk.content.split('\n').slice(0, 3).join('\n'); const truncated = preview.length < chunk.content.length; console.log(chalk.dim(` ${preview}${truncated ? '...' : ''}`)); console.log(); }); // Show summary console.log(chalk.gray('─'.repeat(50))); console.log(chalk.gray(`Showing ${results.results.length} of ${results.totalMatches} results`)); console.log(chalk.gray(`Search time: ${results.searchTime}ms`)); console.log(chalk.gray(`Threshold: ${results.config.threshold}`)); if (results.totalMatches > results.results.length) { console.log(chalk.cyan(`\nTip: Use --limit ${results.totalMatches} to see all results`)); } } function getRelevanceLabel(score) { if (score >= 0.9) return chalk.green(`★★★ (${(score * 100).toFixed(1)}%)`); if (score >= 0.8) return chalk.yellow(`★★☆ (${(score * 100).toFixed(1)}%)`); if (score >= 0.7) return chalk.gray(`★☆☆ (${(score * 100).toFixed(1)}%)`); return chalk.dim(`☆☆☆ (${(score * 100).toFixed(1)}%)`); }