UNPKG

lorehub

Version:

Capture and surface the collective wisdom of your codebase

165 lines 8.11 kB
import React from 'react'; import { render } from 'ink'; import { Database } from '../../db/database.js'; import { FactsView } from '../components/FactsView.js'; import { getDbPath } from '../utils/db-config.js'; export async function renderSearch(options) { const dbPath = getDbPath(); const db = new Database(dbPath); // Check if we can use interactive mode if (!process.stdin.isTTY) { // Non-interactive fallback try { const project = db.findProjectByPath(process.cwd()); let results = []; const projects = db.listProjects(); const currentProject = project; // Filter projects based on options let projectsToSearch = projects; if (options.currentProjectOnly) { if (!currentProject) { console.log('No LoreHub project found in current directory.'); return; } projectsToSearch = [currentProject]; } else if (options.projectPath) { const specificProject = db.findProjectByPath(options.projectPath); if (!specificProject) { console.log(`No LoreHub project found at path: ${options.projectPath}`); return; } projectsToSearch = [specificProject]; } // Search selected projects for (const proj of projectsToSearch) { let projectFacts; if (options.hybrid) { // Use hybrid search - combine keyword and semantic results const keywordResults = db.searchFacts(proj.id, options.query); const semanticResults = await db.semanticSearchLores(options.query, { realmId: proj.id, includeScore: true }); // Create a map to track unique facts and combine scores const factMap = new Map(); // Add keyword results with base score keywordResults.forEach((fact) => { factMap.set(fact.id, { ...fact, keywordScore: 1.0, // Binary: found or not semanticScore: 0, hybridScore: 0.3 // Initial score from keyword match }); }); // Add or update with semantic scores semanticResults.forEach((fact) => { const existing = factMap.get(fact.id); if (existing) { // Fact found in both searches - combine scores existing.semanticScore = fact.similarity || 0; existing.hybridScore = 0.3 * existing.keywordScore + 0.7 * existing.semanticScore; existing.similarity = existing.hybridScore; // For display } else { // Only found in semantic search factMap.set(fact.id, { ...fact, keywordScore: 0, semanticScore: fact.similarity || 0, hybridScore: 0.7 * (fact.similarity || 0), similarity: 0.7 * (fact.similarity || 0) // For display }); } }); // Convert map to array and sort by hybrid score projectFacts = Array.from(factMap.values()) .sort((a, b) => (b.hybridScore || 0) - (a.hybridScore || 0)); } else if (options.semantic) { // Use semantic search // Note: semanticSearchFacts expects distance threshold, not similarity // Don't pass threshold to get all results, we'll filter by similarity later projectFacts = await db.semanticSearchLores(options.query, { realmId: proj.id, includeScore: true }); } else { // Use traditional keyword search projectFacts = db.searchFacts(proj.id, options.query); } // Add project info to each fact for display results.push(...projectFacts.map(f => ({ ...f, projectName: proj.name, projectPath: proj.path, isCurrentProject: currentProject?.id === proj.id }))); } // Apply filters if (options.type) { results = results.filter(f => f.type === options.type); } if (options.province) { results = results.filter(f => f.provinces.includes(options.province)); } // Apply similarity threshold filter for semantic or hybrid search if ((options.semantic || options.hybrid) && options.threshold !== undefined) { const threshold = options.threshold; results = results.filter(f => f.similarity !== undefined && f.similarity >= threshold); } // Sort by current project first, then by creation date descending results.sort((a, b) => { // Prioritize current project if (a.isCurrentProject && !b.isCurrentProject) return -1; if (!a.isCurrentProject && b.isCurrentProject) return 1; // Then by date return b.createdAt.getTime() - a.createdAt.getTime(); }); if (results.length === 0) { console.log(`No facts found matching "${options.query}"`); if (options.type) console.log(`Filter: type = ${options.type}`); if (options.province) console.log(`Filter: province = ${options.province}`); } else { const searchMode = options.hybrid ? 'hybrid' : (options.semantic ? 'semantic' : 'keyword'); console.log(`\nFound ${results.length} fact${results.length === 1 ? '' : 's'} matching "${options.query}" (${searchMode} search):\n`); results.forEach((fact, index) => { const projectIndicator = fact.isCurrentProject ? ' ⭐' : ''; console.log(`${index + 1}. [${fact.type}] ${fact.content}`); console.log(` Project: ${fact.projectName}${projectIndicator} (${fact.projectPath})`); if (fact.why) { console.log(` Why: ${fact.why}`); } if ((options.semantic || options.hybrid) && fact.similarity !== undefined) { console.log(` Similarity: ${(fact.similarity * 100).toFixed(1)}%`); } console.log(` Confidence: ${fact.confidence}%`); console.log(` Created: ${fact.createdAt.toLocaleString()}`); if (fact.sigils.length > 0) { console.log(` Sigils: ${fact.sigils.join(', ')}`); } console.log(''); }); } } finally { db.close(); } return; } // Interactive mode with Ink const { waitUntilExit } = render(React.createElement(FactsView, { db: db, projectPath: process.cwd(), initialQuery: options.query, type: options.type, province: options.province, filterProjectPath: options.projectPath, currentProjectOnly: options.currentProjectOnly })); try { await waitUntilExit(); } finally { db.close(); } } //# sourceMappingURL=search.js.map