Skip to main content
Glama

meMCP - Memory-Enhanced Model Context Protocol

MIT License
23
2
HookManager.js14.8 kB
import { promises as fs } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; export class HookManager { constructor(processor) { this.processor = processor; this.sessionDir = join(homedir(), '.mcp_sequential_thinking'); this.currentSessionFile = join(this.sessionDir, 'current_session.json'); this.hooks = new Map(); this.debounceTimeout = null; this.debounceDelay = 2000; this.sessionData = []; this.lastUserPrompt = null; this.slashCommandProcessor = null; this.initialized = false; } setSlashCommandProcessor(processor) { this.slashCommandProcessor = processor; } async initialize() { try { await this.ensureSessionDirectory(); await this.loadCurrentSession(); await this.setupHooks(); this.initialized = true; console.log('HookManager initialized with sequential thinking hooks'); } catch (error) { console.error('Failed to initialize HookManager:', error); throw error; } } async ensureSessionDirectory() { try { await fs.mkdir(this.sessionDir, { recursive: true }); } catch (error) { console.error('Failed to create session directory:', error); throw error; } } async loadCurrentSession() { try { const data = await fs.readFile(this.currentSessionFile, 'utf-8'); this.sessionData = JSON.parse(data); } catch (error) { this.sessionData = []; } } async saveCurrentSession() { try { await fs.writeFile( this.currentSessionFile, JSON.stringify(this.sessionData, null, 2) ); } catch (error) { console.error('Failed to save current session:', error); } } async setupHooks() { this.registerHook('post-tool-use', this.handlePostToolUse.bind(this)); this.registerHook('user-prompt-submit', this.handleUserPromptSubmit.bind(this)); this.registerHook('session-stop', this.handleSessionStop.bind(this)); this.registerHook('sequential-thinking', this.handleSequentialThinking.bind(this)); process.on('SIGINT', () => this.handleSessionStop()); process.on('SIGTERM', () => this.handleSessionStop()); } registerHook(hookName, handler) { if (!this.hooks.has(hookName)) { this.hooks.set(hookName, []); } this.hooks.get(hookName).push(handler); } async triggerHook(hookName, data) { if (!this.hooks.has(hookName)) return; const handlers = this.hooks.get(hookName); for (const handler of handlers) { try { await handler(data); } catch (error) { console.error(`Error in hook ${hookName}:`, error); } } } async handlePostToolUse(data) { if (!this.shouldProcessToolUse(data)) return; const context = this.extractContext(data); // Add working directory context context.workingDirectory = data.workingDirectory || process.cwd(); // Correlate with the latest user prompt if available if (this.lastUserPrompt) { context.userPromptCorrelation = { prompt: this.lastUserPrompt.prompt, promptTimestamp: this.lastUserPrompt.timestamp, timeSincePrompt: new Date().getTime() - new Date(this.lastUserPrompt.timestamp).getTime(), hasSlashCommand: this.lastUserPrompt.context.hasSlashCommand, }; } const sessionEntry = { type: 'tool_use', toolName: data.toolName, arguments: data.arguments, result: data.result, timestamp: new Date().toISOString(), context, }; this.sessionData.push(sessionEntry); await this.saveCurrentSession(); this.debouncedProcess(); } shouldProcessToolUse(data) { const relevantTools = [ 'Edit', 'MultiEdit', 'Write', 'TodoWrite', 'Bash', 'Grep', 'Read' ]; return relevantTools.includes(data.toolName); } extractContext(data) { const context = { toolName: data.toolName, timestamp: new Date().toISOString(), }; if (data.arguments) { if (data.arguments.file_path) { context.filePath = data.arguments.file_path; context.fileType = this.inferFileType(data.arguments.file_path); } if (data.arguments.command) { context.command = data.arguments.command; } if (data.arguments.pattern) { context.searchPattern = data.arguments.pattern; } } if (data.result && typeof data.result === 'string') { context.hasOutput = data.result.length > 0; context.outputLength = data.result.length; if (data.result.includes('error') || data.result.includes('Error')) { context.hasError = true; } } return context; } inferFileType(filePath) { const extension = filePath.split('.').pop()?.toLowerCase(); const typeMap = { js: 'javascript', ts: 'typescript', jsx: 'react', tsx: 'react-typescript', py: 'python', java: 'java', cpp: 'cpp', c: 'c', cs: 'csharp', go: 'go', rs: 'rust', php: 'php', rb: 'ruby', html: 'html', css: 'css', scss: 'sass', json: 'json', yaml: 'yaml', yml: 'yaml', md: 'markdown', sql: 'sql', }; return typeMap[extension] || 'unknown'; } async handleUserPromptSubmit(data) { const prompt = data.prompt || data.content; const hasSlashCommand = prompt?.startsWith('/'); const sessionEntry = { type: 'user_prompt_submit', prompt, workingDirectory: data.workingDirectory || process.cwd(), timestamp: new Date().toISOString(), context: { promptLength: (prompt || '').length, hasSlashCommand, workingDirectory: data.workingDirectory || process.cwd(), }, }; // Process slash commands if available if (hasSlashCommand && this.slashCommandProcessor) { try { const slashResult = await this.slashCommandProcessor.processSlashCommand(prompt); sessionEntry.slashCommandResult = slashResult; // Log slash command execution console.log(`Slash command executed: ${prompt.split(' ')[0]} - Success: ${slashResult.success}`); } catch (error) { sessionEntry.slashCommandError = error.message; console.error(`Slash command failed: ${prompt.split(' ')[0]} - ${error.message}`); } } this.sessionData.push(sessionEntry); await this.saveCurrentSession(); // Store the latest prompt for correlation with subsequent tool uses this.lastUserPrompt = sessionEntry; } async handleSequentialThinking(data) { const sessionEntry = { type: 'sequential_thinking', content: data.content, context: data.context || {}, timestamp: new Date().toISOString(), }; this.sessionData.push(sessionEntry); await this.saveCurrentSession(); this.debouncedProcess(); } debouncedProcess() { if (this.debounceTimeout) { clearTimeout(this.debounceTimeout); } this.debounceTimeout = setTimeout(() => { this.processRecentSession(); }, this.debounceDelay); } async processRecentSession() { if (this.sessionData.length === 0) return; try { const recentData = this.sessionData.slice(-10); const context = this.analyzeSessionContext(recentData); await this.processor.queueProcessing(recentData, context); console.log(`Processed ${recentData.length} session entries`); } catch (error) { console.error('Error processing recent session:', error); } } analyzeSessionContext(sessionData) { const context = { sessionLength: sessionData.length, timespan: this.calculateTimespan(sessionData), dominantFileType: this.findDominantFileType(sessionData), toolsUsed: this.extractToolsUsed(sessionData), hasErrors: this.hasErrors(sessionData), workflowPattern: this.identifyWorkflowPattern(sessionData), projectContext: this.extractProjectContext(sessionData), userPromptContext: this.analyzeUserPrompts(sessionData), }; return context; } calculateTimespan(sessionData) { if (sessionData.length < 2) return 0; const first = new Date(sessionData[0].timestamp); const last = new Date(sessionData[sessionData.length - 1].timestamp); return last.getTime() - first.getTime(); } findDominantFileType(sessionData) { const typeCounts = {}; for (const entry of sessionData) { if (entry.context?.fileType) { typeCounts[entry.context.fileType] = (typeCounts[entry.context.fileType] || 0) + 1; } } return Object.entries(typeCounts) .sort(([, a], [, b]) => b - a)[0]?.[0] || 'unknown'; } extractToolsUsed(sessionData) { return [...new Set(sessionData.map(entry => entry.toolName || entry.type))]; } hasErrors(sessionData) { return sessionData.some(entry => entry.context?.hasError); } identifyWorkflowPattern(sessionData) { const tools = sessionData.map(entry => entry.toolName || entry.type); if (tools.includes('Read') && tools.includes('Edit')) { return 'read_edit_cycle'; } if (tools.includes('Grep') && tools.includes('Edit')) { return 'search_modify'; } if (tools.includes('Bash') && tools.includes('Edit')) { return 'test_driven'; } if (tools.filter(t => t === 'Edit').length > 3) { return 'iterative_development'; } return 'general'; } async handleSessionStop() { if (this.sessionData.length === 0) return; try { console.log('Processing complete session before shutdown...'); if (this.debounceTimeout) { clearTimeout(this.debounceTimeout); } const fullContext = this.analyzeSessionContext(this.sessionData); fullContext.sessionComplete = true; await this.processor.processSequentialThinking(this.sessionData, fullContext); await this.archiveSession(); console.log(`Session processed: ${this.sessionData.length} entries`); } catch (error) { console.error('Error processing session on stop:', error); } } async archiveSession() { if (this.sessionData.length === 0) return; try { const archiveFileName = `session_${Date.now()}.json`; const archivePath = join(this.sessionDir, 'archives', archiveFileName); await fs.mkdir(join(this.sessionDir, 'archives'), { recursive: true }); const archiveData = { sessionData: this.sessionData, archived: new Date().toISOString(), summary: this.analyzeSessionContext(this.sessionData), }; await fs.writeFile(archivePath, JSON.stringify(archiveData, null, 2)); this.sessionData = []; await this.saveCurrentSession(); await this.cleanupOldArchives(); } catch (error) { console.error('Failed to archive session:', error); } } async cleanupOldArchives() { try { const archivesDir = join(this.sessionDir, 'archives'); const files = await fs.readdir(archivesDir).catch(() => []); const archiveFiles = files .filter(file => file.startsWith('session_') && file.endsWith('.json')) .map(file => ({ name: file, path: join(archivesDir, file), timestamp: parseInt(file.replace('session_', '').replace('.json', ''), 10), })) .sort((a, b) => b.timestamp - a.timestamp); const maxArchives = 50; if (archiveFiles.length > maxArchives) { const filesToDelete = archiveFiles.slice(maxArchives); for (const file of filesToDelete) { await fs.unlink(file.path); } } } catch (error) { console.warn('Failed to cleanup old archives:', error); } } extractProjectContext(sessionData) { const workingDirectories = new Set(); const projects = []; for (const entry of sessionData) { if (entry.context?.workingDirectory) { workingDirectories.add(entry.context.workingDirectory); } } // Identify project types based on directory names and patterns workingDirectories.forEach(dir => { const segments = dir.split('/').filter(Boolean); const projectName = segments[segments.length - 1]; projects.push({ directory: dir, projectName, isGitRepo: dir.includes('.git') || segments.some(s => s.startsWith('.')), estimatedType: this.inferProjectType(dir), }); }); return { workingDirectories: Array.from(workingDirectories), projectCount: workingDirectories.size, dominantProject: projects.length > 0 ? projects[0] : null, projects, }; } analyzeUserPrompts(sessionData) { const prompts = sessionData.filter(entry => entry.type === 'user_prompt_submit'); const slashCommands = prompts.filter(p => p.context?.hasSlashCommand); return { totalPrompts: prompts.length, slashCommandCount: slashCommands.length, averagePromptLength: prompts.length > 0 ? prompts.reduce((sum, p) => sum + (p.context?.promptLength || 0), 0) / prompts.length : 0, recentSlashCommands: slashCommands.slice(-3).map(p => p.prompt?.split(' ')[0]), }; } inferProjectType(directory) { const dir = directory.toLowerCase(); if (dir.includes('node_modules') || dir.includes('package.json')) return 'nodejs'; if (dir.includes('python') || dir.includes('.py')) return 'python'; if (dir.includes('java') || dir.includes('.java')) return 'java'; if (dir.includes('react') || dir.includes('next')) return 'react'; if (dir.includes('vue')) return 'vue'; if (dir.includes('angular')) return 'angular'; if (dir.includes('rust') || dir.includes('.rs')) return 'rust'; if (dir.includes('go') || dir.includes('.go')) return 'go'; if (dir.includes('docker')) return 'docker'; if (dir.includes('k8s') || dir.includes('kubernetes')) return 'kubernetes'; return 'unknown'; } async getSessionStats() { return { currentSessionEntries: this.sessionData.length, sessionStarted: this.sessionData.length > 0 ? this.sessionData[0].timestamp : null, lastActivity: this.sessionData.length > 0 ? this.sessionData[this.sessionData.length - 1].timestamp : null, context: this.sessionData.length > 0 ? this.analyzeSessionContext(this.sessionData) : null, }; } async shutdown() { if (this.initialized) { await this.handleSessionStop(); console.log('HookManager shut down successfully'); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/meMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server