mcp-repl
Version:
MCP REPL with code execution and semantic code search
1,102 lines (930 loc) • 34.9 kB
JavaScript
// Pure JavaScript implementation of code indexing and vector search
// This implementation avoids native dependencies for better Windows compatibility
// Force use of WASM backend to avoid onnxruntime-node dependency
process.env.TFJS_BACKEND = 'wasm';
import fs from 'fs/promises';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import path from 'path';
import { pipeline, env } from '@xenova/transformers';
// Configure transformers to use WASM backend
env.backends.onnx.wasm.numThreads = 1;
// Configuration constants
const INDEX_DIR = './code_search_index';
const DEFAULT_MODEL = 'Xenova/all-MiniLM-L6-v2';
const DEFAULT_DIM = 384; // Dimension size for the chosen model
const DEFAULT_EXTS = ['js', 'ts'];
const DEFAULT_IGNORES = ['node_modules'];
const INDEX_FILE = 'code_index.json';
const VECTOR_INDEX_FILE = 'vector_index.json';
// Global state
let embedder;
let codeChunks = [];
let chunkIds = [];
let isInitialized = false;
// Helper to calculate cosine similarity between two vectors
function cosineSimilarity(vecA, vecB) {
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
return dotProduct / (magnitudeA * magnitudeB);
}
// Parse .gitignore file and get ignore patterns
function parseGitignore(rootDir) {
const gitignorePath = path.join(rootDir, '.gitignore');
const patterns = [];
if (existsSync(gitignorePath)) {
try {
const content = readFileSync(gitignorePath, 'utf8');
const lines = content.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
// Skip empty lines and comments
if (trimmedLine && !trimmedLine.startsWith('#')) {
patterns.push(trimmedLine);
}
}
} catch (error) {
// Silently handle errors
}
}
return patterns;
}
// Check if a path should be ignored based on .gitignore patterns
function shouldIgnorePath(filePath, ignorePatterns, rootDir) {
// Normalize the path relative to the root directory
const normalizedPath = path.relative(rootDir, filePath);
// Ensure consistent path separators (use forward slashes for matching)
const standardPath = normalizedPath.replace(/\\/g, '/');
for (const pattern of ignorePatterns) {
// Handle exact matches
if (standardPath === pattern || standardPath === pattern.replace(/\/$/, '')) {
return true;
}
// Handle directory wildcards (e.g., dir/**)
if (pattern.endsWith('/**') && standardPath.startsWith(pattern.slice(0, -2))) {
return true;
}
// Handle file wildcards (e.g., *.log)
if (pattern.startsWith('*.') && standardPath.endsWith(pattern.slice(1))) {
return true;
}
// Handle simple directory patterns (e.g., node_modules/)
if (pattern.endsWith('/') && standardPath.startsWith(pattern)) {
return true;
}
// Handle direct file matches
if (standardPath === pattern) {
return true;
}
}
return false;
}
// Initialize the in-memory index and embedding model
export async function initialize(indexDir = INDEX_DIR) {
if (isInitialized) return true;
try {
// Create index directory if it doesn't exist
if (!existsSync(indexDir)) {
mkdirSync(indexDir, { recursive: true });
}
// Initialize embedding model with WASM backend
try {
embedder = await pipeline('feature-extraction', DEFAULT_MODEL, {
env: {
backends: {
onnx: {
wasm: {
numThreads: 1
}
}
}
}
});
} catch (modelError) {
// Fallback to a model that works well with Xenova
embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
env: {
backends: {
onnx: {
wasm: {
numThreads: 1
}
}
}
}
});
}
// Load existing index if available
const indexPath = path.join(indexDir, INDEX_FILE);
if (existsSync(indexPath)) {
try {
const data = readFileSync(indexPath, 'utf8');
codeChunks = JSON.parse(data);
chunkIds = codeChunks.map(chunk => chunk.id);
} catch (error) {
codeChunks = [];
chunkIds = [];
}
} else {
codeChunks = [];
chunkIds = [];
}
isInitialized = true;
return true;
} catch (error) {
return false;
}
}
// Gather files for indexing
export async function gatherFiles(dir, exts = DEFAULT_EXTS, ignores = DEFAULT_IGNORES) {
const results = [];
try {
// Parse .gitignore in the root directory
const gitignorePatterns = parseGitignore(dir);
const allIgnores = [...ignores, ...gitignorePatterns];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
// Check if the path should be ignored based on standard ignores or .gitignore
const isIgnoredByPattern = ignores.some(p => full.includes(p));
const isIgnoredByGitignore = shouldIgnorePath(full, gitignorePatterns, dir);
if (isIgnoredByPattern || isIgnoredByGitignore) {
continue;
}
if (entry.isDirectory()) {
const subDirFiles = await gatherFiles(full, exts, ignores);
results.push(...subDirFiles);
} else if (exts.includes(path.extname(entry.name).slice(1))) {
results.push(full);
}
}
} catch (error) {
// Silently handle errors
}
return results;
}
// Extract comment above an element
function extractDocComment(content, position) {
let docComment = '';
const linesBefore = content.substring(0, position).split('\n');
let i = linesBefore.length - 1;
while (i >= 0 && i >= linesBefore.length - 5) {
const line = linesBefore[i].trim();
if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
docComment = line.replace(/^\/\/|\*\/|\*|\/\*\*?/g, '').trim() + ' ' + docComment;
} else if (line === '') {
i--;
continue;
} else {
break;
}
i--;
}
return docComment.trim();
}
// Extract parameters from function or method signature
function extractParameters(signature) {
const paramMatch = signature.match(/\((.*?)\)/);
if (!paramMatch || !paramMatch[1]) return [];
const paramString = paramMatch[1].trim();
if (!paramString) return [];
return paramString.split(',')
.map(param => {
// Handle destructuring or complex params
const cleanParam = param.trim().replace(/[{}[\]]/g, '');
const parts = cleanParam.split('='); // Handle default values
const nameWithType = parts[0].trim();
// Try to separate type annotations (for TypeScript)
const typeSplit = nameWithType.split(':');
const name = typeSplit[0].trim();
const type = typeSplit.length > 1 ? typeSplit[1].trim() : '';
return { name, type };
})
.filter(p => p.name && p.name !== '');
}
// Extract return type from function signature (TypeScript)
function extractReturnType(signature, code) {
// Check for TypeScript return type annotation
const returnTypeMatch = signature.match(/\)(?:\s*:\s*([^{]+))?/);
if (returnTypeMatch && returnTypeMatch[1]) {
return returnTypeMatch[1].trim();
}
// Try to infer from return statements
const returnMatches = code.match(/return\s+([^;]+)/g);
if (returnMatches && returnMatches.length > 0) {
// Just indicate there are returns but don't try to infer type
return 'inferred';
}
return '';
}
// Extract exported status
function isExported(content, position) {
const linesBefore = content.substring(0, position).split('\n');
const currentLine = linesBefore[linesBefore.length - 1];
return currentLine.includes('export ');
}
// Extract code structure from a file
export async function extractChunks(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const stat = await fs.stat(filePath);
const chunks = [];
const fileName = path.basename(filePath);
const fileScope = {
id: Buffer.from(`file-${filePath}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16),
type: 'file',
name: fileName,
path: filePath,
children: [],
exports: []
};
// Map of element IDs to their relationship data
const relationships = new Map();
// Extract functions
const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*{/g;
let funcMatch;
while ((funcMatch = funcRegex.exec(content)) !== null) {
const funcName = funcMatch[1];
const funcStart = funcMatch.index;
// Find the function end
let openBraces = 1;
let funcEnd = funcStart + funcMatch[0].length;
for (let i = funcEnd; i < content.length; i++) {
if (content[i] === '{') openBraces++;
else if (content[i] === '}') openBraces--;
if (openBraces === 0) {
funcEnd = i + 1;
break;
}
}
const funcCode = content.substring(funcStart, funcEnd);
const lines = funcCode.split('\n').length;
const startPos = content.substring(0, funcStart).split('\n').length - 1;
const endPos = startPos + lines - 1;
const docComment = extractDocComment(content, funcStart);
const isExportedFunc = isExported(content, funcStart);
const parameters = extractParameters(funcMatch[0]);
const returnType = extractReturnType(funcMatch[0], funcCode);
const funcChunk = {
id: Buffer.from(`function-${funcName}-${filePath}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16),
type: 'function',
name: funcName,
qualifiedName: funcName,
file: filePath,
startLine: startPos,
endLine: endPos,
lines,
code: funcCode,
mtime: stat.mtimeMs,
doc: docComment,
isExported: isExportedFunc,
parameters,
returnType,
complexity: calculateComplexity(funcCode)
};
chunks.push(funcChunk);
fileScope.children.push(funcChunk.id);
if (isExportedFunc) {
fileScope.exports.push(funcChunk.id);
}
// Store relationships
relationships.set(funcChunk.id, {
calls: extractFunctionCalls(funcCode),
dependencies: extractDependencies(funcCode)
});
}
// Extract classes
const classRegex = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?\s*{/g;
let classMatch;
while ((classMatch = classRegex.exec(content)) !== null) {
const className = classMatch[1];
const extendedClass = classMatch[2] || null;
const classStart = classMatch.index;
// Find the class end
let openBraces = 1;
let classEnd = classStart + classMatch[0].length;
for (let i = classEnd; i < content.length; i++) {
if (content[i] === '{') openBraces++;
else if (content[i] === '}') openBraces--;
if (openBraces === 0) {
classEnd = i + 1;
break;
}
}
const classCode = content.substring(classStart, classEnd);
const lines = classCode.split('\n').length;
const startPos = content.substring(0, classStart).split('\n').length - 1;
const endPos = startPos + lines - 1;
const docComment = extractDocComment(content, classStart);
const isExportedClass = isExported(content, classStart);
const classChunk = {
id: Buffer.from(`class-${className}-${filePath}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16),
type: 'class',
name: className,
qualifiedName: className,
parentClass: extendedClass,
file: filePath,
startLine: startPos,
endLine: endPos,
lines,
code: classCode,
mtime: stat.mtimeMs,
doc: docComment,
isExported: isExportedClass,
methods: [],
properties: []
};
chunks.push(classChunk);
fileScope.children.push(classChunk.id);
if (isExportedClass) {
fileScope.exports.push(classChunk.id);
}
// Extract class methods
const methodRegex = /(?:async\s+)?(?:static\s+)?(?:get|set)?\s*(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*{/g;
let methodMatch;
const methodIds = [];
while ((methodMatch = methodRegex.exec(classCode)) !== null) {
const methodName = methodMatch[1];
// Skip constructor and private methods
if (methodName === 'constructor' || !methodName.match(/^[a-zA-Z]/) || methodName.startsWith('_')) continue;
const methodStart = classStart + methodMatch.index;
// Find the method end
let openBraces = 1;
let methodEnd = methodStart + methodMatch[0].length;
for (let i = methodEnd; i < classEnd; i++) {
if (content[i] === '{') openBraces++;
else if (content[i] === '}') openBraces--;
if (openBraces === 0) {
methodEnd = i + 1;
break;
}
// Don't go past the class boundary
if (i >= classEnd - 1) {
methodEnd = classEnd - 1;
break;
}
}
const methodCode = content.substring(methodStart, methodEnd);
const methodLines = methodCode.split('\n').length;
const methodStartPos = content.substring(0, methodStart).split('\n').length - 1;
const methodEndPos = methodStartPos + methodLines - 1;
const methodDocComment = extractDocComment(content, methodStart);
const parameters = extractParameters(methodMatch[0]);
const returnType = extractReturnType(methodMatch[0], methodCode);
const isStatic = methodMatch[0].includes('static ');
const methodId = Buffer.from(`method-${className}-${methodName}-${filePath}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
methodIds.push(methodId);
const methodChunk = {
id: methodId,
type: 'method',
name: methodName,
qualifiedName: `${className}.${methodName}`,
parentClass: className,
parentClassId: classChunk.id,
file: filePath,
startLine: methodStartPos,
endLine: methodEndPos,
lines: methodLines,
code: methodCode,
mtime: stat.mtimeMs,
doc: methodDocComment,
parameters,
returnType,
isStatic,
complexity: calculateComplexity(methodCode)
};
chunks.push(methodChunk);
classChunk.methods.push(methodId);
// Store relationships
relationships.set(methodId, {
calls: extractFunctionCalls(methodCode),
dependencies: extractDependencies(methodCode)
});
}
// Extract class properties
const propertyRegex = /(?:static\s+)?(?:readonly\s+)?(\w+)\s*(?::\s*([^;=]+))?\s*(?:=|;)/g;
let propertyMatch;
while ((propertyMatch = propertyRegex.exec(classCode)) !== null) {
const propName = propertyMatch[1];
// Skip private properties
if (propName.startsWith('_') || !propName.match(/^[a-zA-Z]/)) continue;
const propStart = classStart + propertyMatch.index;
let propEnd = propStart + propertyMatch[0].length;
// Find property end (could be a complex assignment)
if (content[propEnd - 1] !== ';') {
for (let i = propEnd; i < classEnd; i++) {
if (content[i] === ';') {
propEnd = i + 1;
break;
}
}
}
const propCode = content.substring(propStart, propEnd);
const propLines = propCode.split('\n').length;
const propStartPos = content.substring(0, propStart).split('\n').length - 1;
const propEndPos = propStartPos + propLines - 1;
const propType = propertyMatch[2] ? propertyMatch[2].trim() : '';
const isStatic = propertyMatch[0].includes('static ');
const propId = Buffer.from(`property-${className}-${propName}-${filePath}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
const propChunk = {
id: propId,
type: 'property',
name: propName,
qualifiedName: `${className}.${propName}`,
parentClass: className,
parentClassId: classChunk.id,
file: filePath,
startLine: propStartPos,
endLine: propEndPos,
lines: propLines,
code: propCode,
mtime: stat.mtimeMs,
propertyType: propType,
isStatic
};
chunks.push(propChunk);
classChunk.properties.push(propId);
}
// Add inheritance relationships
if (extendedClass) {
relationships.set(classChunk.id, {
inheritsFrom: extendedClass,
methods: methodIds
});
}
}
// Extract imports/exports
const importExportRegex = /(import|export)[\s\S]+?;/g;
let importMatch;
while ((importMatch = importExportRegex.exec(content)) !== null) {
const code = importMatch[0];
const type = importMatch[1] === 'import' ? 'import' : 'export';
const lines = code.split('\n').length;
const startPos = content.substring(0, importMatch.index).split('\n').length - 1;
const endPos = startPos + lines - 1;
// Extract imported/exported elements and module
let modulePath = '';
let elements = [];
if (type === 'import') {
const moduleMatch = code.match(/from\s+['"]([^'"]+)['"]/);
if (moduleMatch) {
modulePath = moduleMatch[1];
}
const elementsMatch = code.match(/{\s*([^}]+)\s*}/);
if (elementsMatch) {
elements = elementsMatch[1].split(',').map(e => e.trim());
} else {
// Default import
const defaultMatch = code.match(/import\s+(\w+)/);
if (defaultMatch) {
elements = [defaultMatch[1] + ' (default)'];
}
}
} else {
// Export
const namedExport = code.match(/{\s*([^}]+)\s*}/);
if (namedExport) {
elements = namedExport[1].split(',').map(e => e.trim());
} else {
const defaultExport = code.match(/export\s+default\s+(\w+)/);
if (defaultExport) {
elements = [defaultExport[1] + ' (default)'];
}
}
}
const chunk = {
id: Buffer.from(`${type}-${importMatch.index}-${filePath}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16),
type,
qualifiedName: code.trim(),
file: filePath,
startLine: startPos,
endLine: endPos,
lines,
code,
mtime: stat.mtimeMs,
doc: '',
modulePath,
elements
};
chunks.push(chunk);
if (type === 'import') {
// Add dependency relationships
relationships.set(chunk.id, {
dependsOn: modulePath,
imports: elements
});
}
}
// Add file metadata chunk as the first item
const fileChunk = {
id: fileScope.id,
type: 'file',
name: fileName,
qualifiedName: filePath,
file: filePath,
startLine: 0,
endLine: content.split('\n').length - 1,
lines: content.split('\n').length,
code: content.substring(0, Math.min(150, content.length)) + '...',
mtime: stat.mtimeMs,
doc: extractFileHeader(content),
children: fileScope.children,
exports: fileScope.exports
};
chunks.unshift(fileChunk);
// Add relationships data to chunks
for (const chunk of chunks) {
if (relationships.has(chunk.id)) {
chunk.relationships = relationships.get(chunk.id);
}
}
return chunks;
} catch (error) {
return [];
}
}
// Calculate code complexity (simplified)
function calculateComplexity(code) {
let complexity = 1; // Base complexity
// Count control flow statements
const controlFlow = (code.match(/if|else|for|while|switch|case|catch|try|return|throw/g) || []).length;
complexity += controlFlow * 0.5;
// Count logical operators
const logicalOps = (code.match(/&&|\|\|/g) || []).length;
complexity += logicalOps * 0.3;
return parseFloat(complexity.toFixed(1));
}
// Extract function calls from code
function extractFunctionCalls(code) {
const calls = [];
const callRegex = /(\w+)\s*\(/g;
let callMatch;
while ((callMatch = callRegex.exec(code)) !== null) {
const calledFunc = callMatch[1];
// Filter out common keywords that can appear before parentheses
if (!['if', 'for', 'while', 'switch', 'catch', 'function'].includes(calledFunc)) {
calls.push(calledFunc);
}
}
return [...new Set(calls)]; // Remove duplicates
}
// Extract dependencies from code
function extractDependencies(code) {
// Simple regex to find variable usage
const dependencies = [];
const varRegex = /(\b\w+\b)(?!\s*\(|:)/g;
let varMatch;
while ((varMatch = varRegex.exec(code)) !== null) {
const varName = varMatch[1];
// Filter out keywords and common primitives
if (!['let', 'const', 'var', 'function', 'class', 'if', 'else', 'return',
'true', 'false', 'null', 'undefined', 'this', 'super'].includes(varName)) {
dependencies.push(varName);
}
}
return [...new Set(dependencies)]; // Remove duplicates
}
// Extract file header comments
function extractFileHeader(content) {
const headerLines = [];
const lines = content.split('\n');
for (let i = 0; i < Math.min(10, lines.length); i++) {
const line = lines[i].trim();
if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
headerLines.push(line.replace(/^\/\/|\*\/|\*|\/\*\*?/g, '').trim());
} else if (headerLines.length > 0 && line === '') {
continue;
} else if (headerLines.length > 0) {
break;
}
}
return headerLines.join(' ');
}
// Create text representation of a chunk for embedding
function createEmbeddingText(chunk) {
const parts = [];
parts.push(`${chunk.type}: ${chunk.name || chunk.qualifiedName || ''}`);
if (chunk.doc) {
parts.push(`Documentation: ${chunk.doc}`);
}
if (chunk.parentClass) {
parts.push(`In class: ${chunk.parentClass}`);
}
// Add structural info
if (chunk.parameters) {
const paramText = chunk.parameters
.map(p => p.type ? `${p.name}: ${p.type}` : p.name)
.join(', ');
parts.push(`Parameters: ${paramText}`);
}
if (chunk.returnType) {
parts.push(`Returns: ${chunk.returnType}`);
}
if (chunk.complexity) {
parts.push(`Complexity: ${chunk.complexity}`);
}
if (chunk.isExported) {
parts.push('Exported: true');
}
if (chunk.relationships) {
if (chunk.relationships.calls && chunk.relationships.calls.length > 0) {
parts.push(`Calls: ${chunk.relationships.calls.join(', ')}`);
}
if (chunk.relationships.inheritsFrom) {
parts.push(`Inherits from: ${chunk.relationships.inheritsFrom}`);
}
}
if (chunk.code) {
// Clean up code to focus on semantics
const cleanCode = chunk.code
.replace(/[{};,=()[\]]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
parts.push(`Code: ${cleanCode}`);
}
return parts.join(' ');
}
// Generate embedding for a text
async function generateEmbedding(text) {
try {
if (!embedder) {
return null;
}
const output = await embedder(text, {
pooling: 'mean',
normalize: true
});
return Array.from(output.data);
} catch (error) {
return null;
}
}
// Save index to disk
async function saveIndex(indexDir = INDEX_DIR) {
try {
// Save code chunks
const indexPath = path.join(indexDir, INDEX_FILE);
await fs.writeFile(indexPath, JSON.stringify(codeChunks));
return true;
} catch (error) {
return false;
}
}
// Synchronize the index with the file system
export async function syncIndex(folders, exts = DEFAULT_EXTS, ignores = DEFAULT_IGNORES) {
if (!isInitialized) {
await initialize();
}
// Gather all files
const files = [];
for (const folder of folders) {
const folderFiles = await gatherFiles(folder, exts, ignores);
files.push(...folderFiles);
}
// Process files and extract chunks
let newChunksCount = 0;
const allNewChunks = [];
const updatedChunkIds = new Set();
for (const file of files) {
try {
const fileChunks = await extractChunks(file);
for (const chunk of fileChunks) {
updatedChunkIds.add(chunk.id);
// Check if chunk exists with the same mtime
const existingIndex = chunkIds.indexOf(chunk.id);
if (existingIndex !== -1 && codeChunks[existingIndex].mtime === chunk.mtime) {
continue;
}
// Generate embedding for the chunk
const text = createEmbeddingText(chunk);
chunk.embedding = await generateEmbedding(text);
allNewChunks.push(chunk);
newChunksCount++;
}
} catch (error) {
// Silently handle errors
}
}
// Find chunks to delete (chunks not in updated files)
const chunksToDelete = codeChunks.filter(chunk => !updatedChunkIds.has(chunk.id));
// Update the in-memory index
if (allNewChunks.length > 0 || chunksToDelete.length > 0) {
// Remove deleted chunks
for (const chunk of chunksToDelete) {
const index = chunkIds.indexOf(chunk.id);
if (index !== -1) {
// Remove from code chunks array
codeChunks.splice(index, 1);
chunkIds.splice(index, 1);
}
}
// Add new chunks
for (const chunk of allNewChunks) {
const existingIndex = chunkIds.indexOf(chunk.id);
if (existingIndex !== -1) {
// Update existing chunk
codeChunks[existingIndex] = chunk;
} else {
// Add new chunk
codeChunks.push(chunk);
chunkIds.push(chunk.id);
}
}
// Save the updated index
await saveIndex();
}
return {
total: codeChunks.length,
new: newChunksCount,
deleted: chunksToDelete.length
};
}
// Calculate text match score based on keyword presence
function textMatchScore(query, chunk) {
// Normalize query and text
const normalizedQuery = query.toLowerCase();
const normalizedCode = chunk.code ? chunk.code.toLowerCase() : '';
const normalizedName = chunk.name ? chunk.name.toLowerCase() : '';
const normalizedQualifiedName = chunk.qualifiedName ? chunk.qualifiedName.toLowerCase() : '';
const normalizedDoc = chunk.doc ? chunk.doc.toLowerCase() : '';
let score = 0;
// Check exact matches in name (highest weight)
if (normalizedName === normalizedQuery || normalizedQualifiedName === normalizedQuery) {
score += 1.0;
}
// Check if name contains query
else if (normalizedName.includes(normalizedQuery) || normalizedQualifiedName.includes(normalizedQuery)) {
score += 0.8;
}
// Check if doc contains query
if (normalizedDoc.includes(normalizedQuery)) {
score += 0.5;
}
// Check if code contains query
if (normalizedCode.includes(normalizedQuery)) {
score += 0.3;
}
// Check individual words
const queryWords = normalizedQuery.split(/\s+/);
for (const word of queryWords) {
if (word.length < 3) continue; // Skip very short words
if (normalizedName.includes(word) || normalizedQualifiedName.includes(word)) {
score += 0.2;
}
if (normalizedDoc.includes(word)) {
score += 0.1;
}
if (normalizedCode.includes(word)) {
score += 0.05;
}
}
return Math.min(score, 1.0); // Cap at 1.0
}
// Query the index
export async function queryIndex(query, topK = 8) {
if (!isInitialized) {
await initialize();
}
try {
if (codeChunks.length === 0) {
return [];
}
// Generate embedding for the query
const queryEmbedding = await generateEmbedding(query);
let scoredResults = [];
// If we have a valid embedding, do vector search
if (queryEmbedding) {
try {
// Score all chunks using vector similarity
scoredResults = codeChunks.map(chunk => {
// Calculate vector similarity if chunk has embedding
let similarityScore = 0;
if (chunk.embedding) {
similarityScore = cosineSimilarity(queryEmbedding, chunk.embedding);
}
// Get text match score
const textScore = textMatchScore(query, chunk);
// Combine scores (70% vector similarity, 30% text matching)
const combinedScore = (similarityScore * 0.7) + (textScore * 0.3);
return {
score: combinedScore,
chunk
};
});
} catch (error) {
// Fall back to text-based search
scoredResults = codeChunks.map(chunk => ({
score: textMatchScore(query, chunk),
chunk
}));
}
} else {
// Use text-based search only
scoredResults = codeChunks.map(chunk => ({
score: textMatchScore(query, chunk),
chunk
}));
}
// Filter out zero scores and sort by score descending
const filteredResults = scoredResults
.filter(result => result.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, topK);
// Format results for display
const results = filteredResults.map(result => {
const chunk = result.chunk;
// Base result structure
const formattedResult = {
score: parseFloat(result.score.toFixed(3)),
file: chunk.file,
startLine: chunk.startLine + 1,
endLine: chunk.endLine + 1,
type: chunk.type,
name: chunk.name || '',
qualifiedName: chunk.qualifiedName || '',
lines: chunk.lines,
doc: chunk.doc || '',
code: chunk.code ? (chunk.code.length > 140 ?
chunk.code.replace(/\s+/g, ' ').slice(0, 140) + '...' :
chunk.code.replace(/\s+/g, ' ')
) : ''
};
// Add structural metadata based on chunk type
switch (chunk.type) {
case 'file':
formattedResult.structure = {
childCount: chunk.children ? chunk.children.length : 0,
exportCount: chunk.exports ? chunk.exports.length : 0
};
break;
case 'function':
formattedResult.structure = {
isExported: chunk.isExported || false,
complexity: chunk.complexity || 1,
parameters: chunk.parameters || [],
returnType: chunk.returnType || '',
calls: chunk.relationships?.calls || []
};
break;
case 'class':
formattedResult.structure = {
isExported: chunk.isExported || false,
methodCount: chunk.methods ? chunk.methods.length : 0,
propertyCount: chunk.properties ? chunk.properties.length : 0,
parentClass: chunk.parentClass || null,
inheritsFrom: chunk.relationships?.inheritsFrom || null
};
break;
case 'method':
formattedResult.structure = {
parentClass: chunk.parentClass || '',
isStatic: chunk.isStatic || false,
complexity: chunk.complexity || 1,
parameters: chunk.parameters || [],
returnType: chunk.returnType || '',
calls: chunk.relationships?.calls || []
};
break;
case 'property':
formattedResult.structure = {
parentClass: chunk.parentClass || '',
isStatic: chunk.isStatic || false,
propertyType: chunk.propertyType || ''
};
break;
case 'import':
formattedResult.structure = {
modulePath: chunk.modulePath || '',
importedElements: chunk.elements || []
};
break;
case 'export':
formattedResult.structure = {
exportedElements: chunk.elements || []
};
break;
}
// Add relationships data if available
if (chunk.relationships) {
formattedResult.relationships = {};
if (chunk.relationships.calls && chunk.relationships.calls.length > 0) {
formattedResult.relationships.calls = chunk.relationships.calls;
}
if (chunk.relationships.dependencies && chunk.relationships.dependencies.length > 0) {
formattedResult.relationships.dependencies = chunk.relationships.dependencies;
}
if (chunk.relationships.inheritsFrom) {
formattedResult.relationships.inheritsFrom = chunk.relationships.inheritsFrom;
}
if (chunk.relationships.dependsOn) {
formattedResult.relationships.dependsOn = chunk.relationships.dependsOn;
}
}
return formattedResult;
});
return results;
} catch (error) {
return [];
}
}