context-rag
Version:
Get relevant project context for AI agents to save 90% of tokens. Lightweight CLI tool for semantic search on project codebases.
217 lines (181 loc) • 6.74 kB
JavaScript
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
class ContextService {
constructor(config) {
this.config = config;
this.contextPath = '.project';
this.contextTypes = [
'architecture',
'golden-path',
'design-principles',
'constraints',
'requirements',
'api-specs',
'data-models'
];
}
async detectProjectContext() {
if (!fs.existsSync(this.contextPath)) {
return null;
}
const contextFiles = [];
try {
const entries = fs.readdirSync(this.contextPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.endsWith('.md')) {
const contextType = this.identifyContextType(entry.name);
const filePath = path.join(this.contextPath, entry.name);
contextFiles.push({
type: contextType,
name: entry.name,
path: filePath,
size: fs.statSync(filePath).size
});
}
}
if (contextFiles.length > 0) {
console.log(chalk.blue(`🎯 Detected project context with ${contextFiles.length} files`));
return {
path: this.contextPath,
files: contextFiles,
totalFiles: contextFiles.length
};
}
} catch (error) {
console.warn(chalk.yellow(`⚠️ Could not scan context directory: ${error.message}`));
}
return null;
}
identifyContextType(filename) {
const name = filename.toLowerCase().replace('.md', '');
for (const type of this.contextTypes) {
if (name.includes(type) || name.includes(type.replace('-', '_'))) {
return type;
}
}
// Try to infer from common patterns
if (name.includes('arch')) return 'architecture';
if (name.includes('golden') || name.includes('path')) return 'golden-path';
if (name.includes('design') || name.includes('principle')) return 'design-principles';
if (name.includes('constraint') || name.includes('limit')) return 'constraints';
if (name.includes('requirement') || name.includes('spec')) return 'requirements';
if (name.includes('api') || name.includes('endpoint')) return 'api-specs';
if (name.includes('data') || name.includes('model') || name.includes('schema')) return 'data-models';
return 'general';
}
async indexContextFiles(indexer) {
const contextInfo = await this.detectProjectContext();
if (!contextInfo) {
return null;
}
console.log(chalk.blue('🎯 Indexing project context files...'));
const contextChunks = [];
let totalChunks = 0;
for (const contextFile of contextInfo.files) {
try {
const content = fs.readFileSync(contextFile.path, 'utf8');
const chunks = indexer.chunkContent(content);
const crypto = require('crypto');
const fileHash = crypto.createHash('sha256').update(content).digest('hex');
const stats = fs.statSync(contextFile.path);
chunks.forEach((chunk, index) => {
contextChunks.push({
file_path: contextFile.path,
content: chunk,
chunk_index: index,
file_hash: fileHash,
modified_time: stats.mtime.getTime(),
context_type: contextFile.type,
is_context: true,
priority: this.getContextPriority(contextFile.type)
});
totalChunks++;
});
console.log(chalk.gray(` 📄 ${contextFile.name} (${contextFile.type}): ${chunks.length} chunks`));
} catch (error) {
console.warn(chalk.yellow(`⚠️ Could not process ${contextFile.path}: ${error.message}`));
}
}
console.log(chalk.green(`✅ Indexed ${totalChunks} context chunks from ${contextInfo.files.length} files`));
return {
chunks: contextChunks,
metadata: {
context_path: contextInfo.path,
context_files: contextInfo.files.length,
total_chunks: totalChunks,
indexed_at: new Date().toISOString()
}
};
}
getContextPriority(contextType) {
// Higher priority means more relevant for general queries
const priorities = {
'architecture': 10,
'golden-path': 9,
'design-principles': 8,
'requirements': 7,
'constraints': 6,
'api-specs': 5,
'data-models': 4,
'general': 3
};
return priorities[contextType] || 3;
}
async searchContext(query, allResults, maxContextResults = 3) {
// Filter results to prioritize context files
const contextResults = allResults.filter(result => result.is_context);
const regularResults = allResults.filter(result => !result.is_context);
if (contextResults.length === 0) {
return allResults; // No context available, return regular results
}
// Sort context results by priority and similarity
contextResults.sort((a, b) => {
const priorityDiff = (b.priority || 0) - (a.priority || 0);
if (priorityDiff !== 0) return priorityDiff;
return b.similarity - a.similarity;
});
// Take top context results and mix with regular results
const topContextResults = contextResults.slice(0, maxContextResults);
const remainingSlots = Math.max(0, allResults.length - topContextResults.length);
const topRegularResults = regularResults.slice(0, remainingSlots);
// Combine and sort by similarity
const combinedResults = [...topContextResults, ...topRegularResults];
combinedResults.sort((a, b) => b.similarity - a.similarity);
return combinedResults;
}
async enhanceSearchResults(results) {
// Add context type information to results
return results.map(result => {
if (result.is_context) {
return {
...result,
source_type: 'project-context',
context_category: result.context_type,
priority_score: result.priority || 0
};
} else {
return {
...result,
source_type: 'project-file',
context_category: 'code',
priority_score: 0
};
}
});
}
async generateContextSummary(contextResults) {
if (contextResults.length === 0) {
return null;
}
const contextTypes = [...new Set(contextResults.map(r => r.context_type))];
const summary = {
total_context_results: contextResults.length,
context_types_found: contextTypes,
top_context_type: contextTypes[0],
context_coverage: contextTypes.length / this.contextTypes.length
};
return summary;
}
}
module.exports = { ContextService };