UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

597 lines 24.2 kB
import { randomUUID } from 'crypto'; import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js'; /** * Store a new memory */ const storeMemoryTool = createTool({ name: 'store_memory', description: 'Store information in the project memory for future reference and context', category: 'memory-management', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The content to store in memory', minLength: 1, maxLength: 10000 }, type: { type: 'string', enum: ['code', 'documentation', 'decision', 'learning', 'context', 'insight'], description: 'Type of memory being stored' }, title: { type: 'string', description: 'Optional title for the memory', maxLength: 200 }, tags: { type: 'array', items: { type: 'string', maxLength: 50 }, description: 'Tags for categorizing and searching', maxItems: 20 }, metadata: { type: 'object', description: 'Additional structured metadata', additionalProperties: true }, importance: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], default: 'medium', description: 'Importance level of this memory' }, category: { type: 'string', description: 'Category for grouping related memories', maxLength: 100 }, source: { type: 'string', description: 'Source of the information (file, meeting, etc.)', maxLength: 500 }, relatedIds: { type: 'array', items: { type: 'string', pattern: '^[a-zA-Z0-9-]+$' }, description: 'IDs of related memories', maxItems: 10 } }, required: ['content', 'type'], additionalProperties: false }, async execute(input, context) { try { const memoryId = randomUUID(); const now = Date.now(); // Validate related memory IDs if provided if (input.relatedIds && input.relatedIds.length > 0) { for (const relatedId of input.relatedIds) { const relatedCheck = await context.db.get('SELECT id FROM memories WHERE id = ? AND project_id = ?', [relatedId, context.projectId || 'default']); if (!relatedCheck.success || !relatedCheck.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `Related memory not found: ${relatedId}`, category: 'validation' }); } } } // Generate a title if not provided const title = input.title || generateTitleFromContent(input.content, input.type); // Insert memory into database const result = await context.db.run(`INSERT INTO memories (id, title, content, type, tags, metadata, importance, category, source, project_id, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ memoryId, title, input.content, input.type, JSON.stringify(input.tags || []), JSON.stringify(input.metadata || {}), input.importance || 'medium', input.category || null, input.source || null, context.projectId || 'default', context.userId || 'system', now, now ]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to store memory', details: { error: result.error }, category: 'system' }); } // Create relationships if related IDs provided if (input.relatedIds && input.relatedIds.length > 0) { for (const relatedId of input.relatedIds) { await context.db.run('INSERT INTO memory_relationships (id, memory_id, related_memory_id, link_type, created_at) VALUES (?, ?, ?, ?, ?)', [randomUUID(), memoryId, relatedId, 'related', now]); } } return createSuccessResult({ memory: { id: memoryId, title, type: input.type, importance: input.importance || 'medium', category: input.category || null, tags: input.tags || [], relatedCount: input.relatedIds?.length || 0, createdAt: new Date(now).toISOString() }, message: `Memory "${title}" stored successfully`, searchable: true, relatedMemories: input.relatedIds || [] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to store memory: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Search memories */ const searchMemoryTool = createTool({ name: 'search_memory', description: 'Search stored memories using keywords, filters, and metadata', category: 'memory-management', readOnly: true, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query for finding relevant memories', minLength: 1, maxLength: 500 }, type: { type: 'string', enum: ['code', 'documentation', 'decision', 'learning', 'context', 'insight'], description: 'Filter by memory type' }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by specific tags', maxItems: 10 }, importance: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Filter by importance level' }, category: { type: 'string', description: 'Filter by category', maxLength: 100 }, dateRange: { type: 'object', properties: { from: { type: 'string', format: 'date' }, to: { type: 'string', format: 'date' } }, description: 'Filter by date range' }, limit: { type: 'integer', description: 'Maximum number of results', minimum: 1, maximum: 100, default: 20 }, includeContent: { type: 'boolean', description: 'Include full content in results', default: false } }, required: ['query'], additionalProperties: false }, async execute(input, context) { try { // Build the search query let sql = ` SELECT m.*, (CASE WHEN m.title LIKE ? THEN 10 WHEN m.content LIKE ? THEN 5 WHEN m.tags LIKE ? THEN 3 ELSE 1 END) as relevance_score FROM memories m WHERE m.project_id = ? AND (m.title LIKE ? OR m.content LIKE ? OR m.tags LIKE ? OR m.category LIKE ?) `; const searchPattern = `%${input.query}%`; const params = [ searchPattern, // title relevance searchPattern, // content relevance searchPattern, // tags relevance context.projectId || 'default', searchPattern, // title search searchPattern, // content search searchPattern, // tags search searchPattern // category search ]; // Add filters if (input.type) { sql += ' AND m.type = ?'; params.push(input.type); } if (input.importance) { sql += ' AND m.importance = ?'; params.push(input.importance); } if (input.category) { sql += ' AND m.category = ?'; params.push(input.category); } if (input.tags && input.tags.length > 0) { const tagConditions = input.tags.map(() => 'm.tags LIKE ?').join(' AND '); sql += ` AND (${tagConditions})`; input.tags.forEach(tag => params.push(`%"${tag}"%`)); } if (input.dateRange) { if (input.dateRange.from) { sql += ' AND m.created_at >= ?'; params.push(new Date(input.dateRange.from).getTime().toString()); } if (input.dateRange.to) { sql += ' AND m.created_at <= ?'; params.push(new Date(input.dateRange.to).getTime().toString()); } } sql += ' ORDER BY relevance_score DESC, m.importance DESC, m.created_at DESC LIMIT ?'; params.push((input.limit || 20).toString()); const result = await context.db.query(sql, params); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to search memories', details: { error: result.error }, category: 'system' }); } const memories = (result.data || []).map((memory) => { const tags = JSON.parse(memory.tags || '[]'); const metadata = JSON.parse(memory.metadata || '{}'); const memoryData = { id: memory.id, title: memory.title, type: memory.type, importance: memory.importance, category: memory.category, tags, source: memory.source, relevanceScore: memory.relevance_score, createdBy: memory.created_by, createdAt: new Date(memory.created_at).toISOString(), updatedAt: new Date(memory.updated_at).toISOString() }; if (input.includeContent) { memoryData.content = memory.content; memoryData.metadata = metadata; } else { // Provide a content preview memoryData.contentPreview = memory.content.length > 200 ? memory.content.substring(0, 200) + '...' : memory.content; } return memoryData; }); return createSuccessResult({ memories, count: memories.length, query: input.query, filters: { type: input.type || null, importance: input.importance || null, category: input.category || null, tags: input.tags || [], dateRange: input.dateRange || null }, hasMore: memories.length === (input.limit || 20), searchTips: memories.length === 0 ? [ 'Try using broader search terms', 'Check your filters - they might be too restrictive', 'Search in different memory types', 'Consider searching by tags or categories' ] : undefined }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to search memories: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Get specific memory by ID */ const getMemoryTool = createTool({ name: 'get_memory', description: 'Retrieve a specific memory by its ID with full content and relationships', category: 'memory-management', readOnly: true, inputSchema: { type: 'object', properties: { memoryId: { type: 'string', description: 'Memory ID to retrieve', pattern: '^[a-zA-Z0-9-]+$' } }, required: ['memoryId'], additionalProperties: false }, async execute(input, context) { try { // Get the memory const memoryResult = await context.db.get('SELECT * FROM memories WHERE id = ? AND project_id = ?', [input.memoryId, context.projectId || 'default']); if (!memoryResult.success || !memoryResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: 'Memory not found', details: { memoryId: input.memoryId }, category: 'validation' }); } const memory = memoryResult.data; // Get related memories const relatedResult = await context.db.query(`SELECT r.link_type, r.description, r.created_at as linked_at, rm.id, rm.title, rm.type, rm.importance FROM memory_relationships r JOIN memories rm ON (r.related_memory_id = rm.id OR r.memory_id = rm.id) WHERE (r.memory_id = ? OR r.related_memory_id = ?) AND rm.id != ?`, [input.memoryId, input.memoryId, input.memoryId]); const relatedMemories = (relatedResult.data || []).map((rel) => ({ id: rel.id, title: rel.title, type: rel.type, importance: rel.importance, linkType: rel.link_type, description: rel.description, linkedAt: new Date(rel.linked_at).toISOString() })); return createSuccessResult({ memory: { id: memory.id, title: memory.title, content: memory.content, type: memory.type, importance: memory.importance, category: memory.category, tags: JSON.parse(memory.tags || '[]'), metadata: JSON.parse(memory.metadata || '{}'), source: memory.source, createdBy: memory.created_by, createdAt: new Date(memory.created_at).toISOString(), updatedAt: new Date(memory.updated_at).toISOString() }, relatedMemories, statistics: { relatedCount: relatedMemories.length, contentLength: memory.content.length, tagCount: JSON.parse(memory.tags || '[]').length } }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to get memory: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Get memory statistics and insights */ const getMemoryStatsTool = createTool({ name: 'get_memory_stats', description: 'Get statistics and insights about stored memories', category: 'memory-management', readOnly: true, inputSchema: { type: 'object', properties: { timeframe: { type: 'string', enum: ['24h', '7d', '30d', '90d', 'all'], default: '30d', description: 'Time period for statistics' }, type: { type: 'string', enum: ['code', 'documentation', 'decision', 'learning', 'context', 'insight'], description: 'Filter statistics by memory type' } }, additionalProperties: false }, async execute(input, context) { try { // Calculate time threshold const now = Date.now(); let timeThreshold = 0; if (input.timeframe !== 'all') { switch (input.timeframe) { case '24h': timeThreshold = now - (24 * 60 * 60 * 1000); break; case '7d': timeThreshold = now - (7 * 24 * 60 * 60 * 1000); break; case '30d': timeThreshold = now - (30 * 24 * 60 * 60 * 1000); break; case '90d': timeThreshold = now - (90 * 24 * 60 * 60 * 1000); break; } } let whereClause = 'WHERE project_id = ?'; const params = [context.projectId || 'default']; if (timeThreshold > 0) { whereClause += ' AND created_at >= ?'; params.push(timeThreshold.toString()); } if (input.type) { whereClause += ' AND type = ?'; params.push(input.type); } // Get basic statistics const totalResult = await context.db.get(`SELECT COUNT(*) as total FROM memories ${whereClause}`, params); // Get type distribution const typeResult = await context.db.query(`SELECT type, COUNT(*) as count FROM memories ${whereClause} GROUP BY type`, params); // Get importance distribution const importanceResult = await context.db.query(`SELECT importance, COUNT(*) as count FROM memories ${whereClause} GROUP BY importance`, params); // Get category distribution const categoryResult = await context.db.query(`SELECT category, COUNT(*) as count FROM memories ${whereClause} AND category IS NOT NULL GROUP BY category ORDER BY count DESC LIMIT 10`, params); // Get recent activity const recentResult = await context.db.query(`SELECT DATE(created_at/1000, 'unixepoch') as date, COUNT(*) as count FROM memories ${whereClause} GROUP BY DATE(created_at/1000, 'unixepoch') ORDER BY date DESC LIMIT 30`, params); // Get top creators const creatorsResult = await context.db.query(`SELECT created_by, COUNT(*) as count FROM memories ${whereClause} AND created_by IS NOT NULL GROUP BY created_by ORDER BY count DESC LIMIT 10`, params); const total = totalResult.data?.total || 0; const typeDistribution = (typeResult.data || []).reduce((acc, row) => { acc[row.type] = row.count; return acc; }, {}); const importanceDistribution = (importanceResult.data || []).reduce((acc, row) => { acc[row.importance] = row.count; return acc; }, {}); const topCategories = (categoryResult.data || []).map((row) => ({ category: row.category, count: row.count })); const dailyActivity = (recentResult.data || []).map((row) => ({ date: row.date, count: row.count })); const topCreators = (creatorsResult.data || []).map((row) => ({ user: row.created_by, count: row.count })); return createSuccessResult({ timeframe: input.timeframe || '30d', type: input.type || null, overview: { totalMemories: total, averagePerDay: dailyActivity.length > 0 ? Math.round((total / dailyActivity.length) * 100) / 100 : 0 }, distribution: { byType: typeDistribution, byImportance: importanceDistribution, topCategories, topCreators }, activity: { dailyActivity: dailyActivity.slice(0, 7), // Last 7 days trend: calculateTrend(dailyActivity) }, insights: generateInsights(total, typeDistribution, importanceDistribution, dailyActivity) }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to get memory statistics: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Helper functions */ function generateTitleFromContent(content, type) { const words = content.split(/\s+/).slice(0, 8).join(' '); const truncated = words.length > 50 ? words.substring(0, 47) + '...' : words; const typePrefix = type.charAt(0).toUpperCase() + type.slice(1); return `${typePrefix}: ${truncated}`; } function calculateTrend(dailyActivity) { if (dailyActivity.length < 2) return 'stable'; const recent = dailyActivity.slice(0, 3).reduce((sum, day) => sum + day.count, 0); const older = dailyActivity.slice(3, 6).reduce((sum, day) => sum + day.count, 0); if (recent > older * 1.2) return 'increasing'; if (recent < older * 0.8) return 'decreasing'; return 'stable'; } function generateInsights(total, typeDistribution, importanceDistribution, dailyActivity) { const insights = []; if (total === 0) { insights.push('No memories stored yet - start capturing important information'); return insights; } if (total < 10) { insights.push('Getting started with memory collection - consider storing more context'); } else if (total > 100) { insights.push('Rich memory collection - good knowledge base building'); } const topType = Object.entries(typeDistribution).reduce((a, b) => typeDistribution[a[0]] > typeDistribution[b[0]] ? a : b)?.[0]; if (topType) { insights.push(`Most common memory type: ${topType}`); } const criticalCount = importanceDistribution.critical || 0; if (criticalCount > 0) { insights.push(`${criticalCount} critical memories stored`); } if (dailyActivity.length > 0) { const avgDaily = dailyActivity.reduce((sum, day) => sum + day.count, 0) / dailyActivity.length; if (avgDaily > 2) { insights.push('High memory activity - good knowledge capture'); } else if (avgDaily < 0.5) { insights.push('Low recent activity - consider storing more insights'); } } return insights; } /** * Setup memory management tools */ export async function setupMemoryManagementTools() { return { module: 'memory-management', tools: [ storeMemoryTool, searchMemoryTool, getMemoryTool, getMemoryStatsTool ] }; } //# sourceMappingURL=tools.js.map