@claude-vector/cli
Version:
CLI for Claude-integrated vector search
275 lines (238 loc) • 10.4 kB
JavaScript
/**
* 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)}%)`);
}