@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
172 lines • 7.13 kB
JavaScript
import { promises as fs } from 'fs';
import path from 'path';
export class DocumentMemory {
configManager;
documentsPath;
documents;
constructor(configManager) {
this.configManager = configManager;
this.documentsPath = '';
this.documents = new Map();
}
async initialize() {
const storageManager = this.configManager.getStorageManager();
const location = await storageManager.getStorageLocation();
this.documentsPath = path.join(location.data, 'memory', 'documents.json');
// Ensure directory exists
await fs.mkdir(path.dirname(this.documentsPath), { recursive: true });
// Load existing document knowledge
await this.loadDocuments();
}
async updateKnowledge(context) {
this.documents.set(context.documentPath, context);
await this.saveDocuments();
}
async getDocumentContext(documentPath) {
return this.documents.get(documentPath);
}
async searchDocuments(query) {
const results = [];
const queryLower = query.toLowerCase();
for (const [, doc] of this.documents) {
let relevance = 0;
// Check summary
if (doc.summary.toLowerCase().includes(queryLower)) {
relevance += 0.4;
}
// Check key points
const keyPointMatch = doc.keyPoints.some(point => point.toLowerCase().includes(queryLower));
if (keyPointMatch) {
relevance += 0.3;
}
// Check topics
const topicMatch = doc.topics.some(topic => topic.toLowerCase().includes(queryLower));
if (topicMatch) {
relevance += 0.2;
}
// Check document path
if (doc.documentPath.toLowerCase().includes(queryLower)) {
relevance += 0.1;
}
if (relevance > 0.2) {
results.push(doc);
}
}
// Sort by relevance (importance and recency)
results.sort((a, b) => {
const aScore = this.getImportanceScore(a.importance) + this.getRecencyScore(a.lastUpdated);
const bScore = this.getImportanceScore(b.importance) + this.getRecencyScore(b.lastUpdated);
return bScore - aScore;
});
return results;
}
async getRelatedDocuments(documentPath) {
const targetDoc = this.documents.get(documentPath);
if (!targetDoc)
return [];
const related = [];
for (const [path, doc] of this.documents) {
if (path === documentPath)
continue;
let score = 0;
// Check if documents reference each other
if (targetDoc.relatedFiles.includes(path) || doc.relatedFiles.includes(documentPath)) {
score += 0.5;
}
// Check for shared topics
const sharedTopics = targetDoc.topics.filter(topic => doc.topics.includes(topic));
score += sharedTopics.length * 0.2;
// Check for similar key points
const similarPoints = targetDoc.keyPoints.filter(point => doc.keyPoints.some(otherPoint => this.calculateSimilarity(point, otherPoint) > 0.3));
score += similarPoints.length * 0.1;
if (score > 0.2) {
related.push({ doc, score });
}
}
return related
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(item => item.doc);
}
async generateTopics(content) {
// Simple topic extraction based on keywords
const topics = [];
const contentLower = content.toLowerCase();
const topicKeywords = {
'api': ['api', 'endpoint', 'rest', 'graphql', 'swagger'],
'authentication': ['auth', 'login', 'token', 'jwt', 'oauth'],
'database': ['database', 'sql', 'mongodb', 'postgres', 'schema'],
'testing': ['test', 'testing', 'jest', 'mocha', 'cypress'],
'deployment': ['deploy', 'deployment', 'docker', 'kubernetes', 'ci/cd'],
'frontend': ['react', 'vue', 'angular', 'frontend', 'ui', 'component'],
'backend': ['server', 'backend', 'express', 'node', 'api'],
'security': ['security', 'encryption', 'vulnerability', 'secure'],
'performance': ['performance', 'optimization', 'speed', 'cache'],
'configuration': ['config', 'configuration', 'settings', 'environment'],
};
for (const [topic, keywords] of Object.entries(topicKeywords)) {
if (keywords.some(keyword => contentLower.includes(keyword))) {
topics.push(topic);
}
}
return topics;
}
async getDocumentStats() {
const documents = Array.from(this.documents.values());
const byImportance = {};
const topicCounts = {};
documents.forEach(doc => {
byImportance[doc.importance] = (byImportance[doc.importance] || 0) + 1;
doc.topics.forEach(topic => {
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
});
});
const topTopics = Object.entries(topicCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([name, count]) => ({ name, count }));
const recentlyUpdated = documents
.sort((a, b) => new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime())
.slice(0, 5);
return {
totalDocuments: documents.length,
byImportance,
topTopics,
recentlyUpdated,
};
}
async loadDocuments() {
try {
const data = await fs.readFile(this.documentsPath, 'utf-8');
const documentsArray = JSON.parse(data);
this.documents.clear();
for (const doc of documentsArray) {
this.documents.set(doc.documentPath, doc);
}
}
catch (error) {
// File doesn't exist or is invalid, start with empty documents
this.documents.clear();
}
}
async saveDocuments() {
const documentsArray = Array.from(this.documents.values());
await fs.writeFile(this.documentsPath, JSON.stringify(documentsArray, null, 2));
}
getImportanceScore(importance) {
const scores = { low: 1, medium: 2, high: 3, critical: 4 };
return scores[importance] || 2;
}
getRecencyScore(timestamp) {
const ageInDays = (Date.now() - new Date(timestamp).getTime()) / (1000 * 60 * 60 * 24);
return Math.max(0, 1 - (ageInDays * 0.01)); // Decay over time
}
calculateSimilarity(text1, text2) {
const words1 = text1.toLowerCase().split(/\s+/);
const words2 = text2.toLowerCase().split(/\s+/);
const commonWords = words1.filter(word => words2.includes(word));
const totalWords = new Set([...words1, ...words2]).size;
return commonWords.length / totalWords;
}
}
//# sourceMappingURL=document-memory.js.map