Skip to main content
Glama
formatter.ts30.6 kB
// Cool robot face formatter for Claude Historian MCP import { CompactMessage, SearchResult, FileContext, ErrorSolution, ToolPattern } from './types.js'; // Robot faces for each MCP tool operation - these are the signature of Claude Historian! const robots = { search: '[⌐■_■]', // search_conversations similar: '[⌐◆_◆]', // find_similar_queries fileContext: '[⌐□_□]', // find_file_context errorSolutions: '[⌐×_×]', // get_error_solutions toolPatterns: '[⌐⎚_⎚]', // find_tool_patterns sessions: '[⌐○_○]', // list_recent_sessions summary: '[⌐◉_◉]', // extract_compact_summary }; export class BeautifulFormatter { constructor() { // Robot face formatter with maximum information density } private formatTimestamp(timestamp: string): string { try { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const minutes = Math.floor(diffMs / 60000); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (minutes < 1) return 'just now'; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; if (days < 7) return `${days}d ago`; return date.toLocaleDateString(); } catch { return timestamp; } } private truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return this.smartTruncation(text, maxLength); } private smartTruncation(text: string, maxLength: number): string { // Dynamic sizing based on content type const contentType = this.detectContentType(text); switch (contentType) { case 'code': return this.preserveCodeInSummary(text, maxLength); case 'error': return this.preserveErrorInSummary(text, maxLength); case 'technical': return this.preserveTechnicalInSummary(text, maxLength); default: return this.intelligentTextTruncation(text, maxLength); } } private detectContentType(text: string): 'code' | 'error' | 'technical' | 'conversational' { // Code detection if (text.includes('```') || text.includes('function ') || text.includes('const ') || text.includes('import ') || text.includes('export ')) { return 'code'; } // Error detection if (text.match(/(error|exception|failed|cannot|unable to)/i)) { return 'error'; } // Technical content detection if (text.match(/\.(ts|js|json|md|py|java|cpp|rs|go|yml|yaml)\b/) || text.includes('src/') || text.includes('./') || text.includes('tool_use')) { return 'technical'; } return 'conversational'; } private preserveCodeInSummary(text: string, maxLength: number): string { // Extract function names, key identifiers const codeElements = text.match(/(function \w+|const \w+|class \w+|export \w+)/g) || []; if (codeElements.length > 0) { const summary = codeElements.slice(0, 3).join(', '); if (summary.length < maxLength) { return summary + (codeElements.length > 3 ? '...' : ''); } } return this.intelligentTextTruncation(text, maxLength); } private preserveErrorInSummary(text: string, maxLength: number): string { // Keep error type and key details const errorMatch = text.match(/(error|exception|failed)[\s\S]*?(\n|$)/i); if (errorMatch && errorMatch[0].length <= maxLength) { return errorMatch[0].trim(); } // Extract error type at least const errorType = text.match(/(TypeError|ReferenceError|SyntaxError|Error):/); if (errorType && errorType.index !== undefined) { const remaining = maxLength - errorType[0].length - 3; const context = text.substring(errorType.index + errorType[0].length, errorType.index + errorType[0].length + remaining); return errorType[0] + ' ' + context + '...'; } return this.intelligentTextTruncation(text, maxLength); } private preserveTechnicalInSummary(text: string, maxLength: number): string { // Extract file references and key technical terms const fileRefs = text.match(/[\w\-/\\.]+\.(ts|js|json|md|py|java|cpp|rs|go|yml|yaml)/g) || []; const toolRefs = text.match(/tool_use.*?"name":\s*"([^"]+)"/g) || []; const keyElements = [...fileRefs.slice(0, 2), ...toolRefs.slice(0, 1)]; if (keyElements.length > 0) { const summary = keyElements.join(' | '); if (summary.length <= maxLength) { return summary; } } return this.intelligentTextTruncation(text, maxLength); } private intelligentTextTruncation(text: string, maxLength: number): string { if (text.length <= maxLength) return text; // Try to truncate at sentence boundaries const sentences = text.split(/[.!?]+/); let result = ''; for (const sentence of sentences) { if (result.length + sentence.length + 1 <= maxLength - 3) { result += sentence + '.'; } else { break; } } if (result.length > 0) { return result + '..'; } // Fallback to word boundaries const words = text.split(' '); result = ''; for (const word of words) { if (result.length + word.length + 1 <= maxLength - 3) { result += word + ' '; } else { break; } } return result.trim() + '...'; } private extractHighValueContent(text: string): string { // REVOLUTIONARY: Maximum information density extraction for Claude Code const contentType = this.detectContentType(text); if (contentType === 'code' || contentType === 'error' || contentType === 'technical') { // Extract core technical elements while preserving completeness return this.extractTechnicalEssence(text); } // For conversational: extract only actionable intelligence return this.extractActionableIntelligence(text); } private extractTechnicalEssence(text: string): string { // Extract function signatures, file paths, error messages, key variables const technical = []; // Function/class/interface declarations const declarations = text.match(/(function|class|interface|const|let|var)\s+\w+[^{;]*[{;]/g); if (declarations) technical.push(...declarations.slice(0, 2)); // File paths and imports const paths = text.match(/[\w\-./]+\.(ts|js|json|md|py|java|cpp|rs|go|yml|yaml|tsx|jsx)/g); if (paths) technical.push(...[...new Set(paths)].slice(0, 3)); // Error messages (preserve completely) const errors = text.match(/(Error|Exception|Failed|Cannot|Unable)[\s\S]*?(?=\n|$)/gi); if (errors) technical.push(...errors.slice(0, 1)); // Key technical terms const keyTerms = text.match(/(npm|git|build|deploy|test|fix|update|install|configure)\s+[\w-]+/gi); if (keyTerms) technical.push(...[...new Set(keyTerms)].slice(0, 2)); if (technical.length > 0) { return technical.join(' | '); } // Fallback: preserve complete technical content return text.length > 500 ? text.substring(0, 500) + '...' : text; } private extractActionableIntelligence(text: string): string { // Extract only decisions, solutions, and actions - eliminate noise const intelligence = []; // Solutions and fixes const solutions = text.match(/(fixed|resolved|solution|approach):\s*([^.!?\n]+)/gi); if (solutions) intelligence.push(...solutions.slice(0, 2)); // Concrete actions const actions = text.match(/(will|should|need to|going to|implemented|added|updated)\s+([^.!?\n]+)/gi); if (actions) intelligence.push(...actions.slice(0, 2)); // Key outcomes const outcomes = text.match(/(success|completed|working|deployed|built|tested)[\s\S]*?(?=[.!?\n]|$)/gi); if (outcomes) intelligence.push(...outcomes.slice(0, 1)); if (intelligence.length > 0) { return intelligence.join('; '); } // Last resort: extract first meaningful sentence const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 20); return sentences[0]?.trim() + (sentences.length > 1 ? '...' : '') || text.substring(0, 100) + '...'; } public getDynamicDisplayLength(content: string): number { const contentType = this.detectContentType(content); switch (contentType) { case 'code': return 600; // Increased for complete code context case 'error': return 700; // Increased for full error context case 'technical': return 500; // Increased for complete technical context default: return 400; // Increased for better conversational context } } // MCP Tool Operation Formatters formatSearchConversations(result: SearchResult, _detailLevel: string = 'summary'): string { let output = `Search: "${result.searchQuery}"\n\n`; if (result.messages.length === 0) { return output + 'No messages found matching your query.\n'; } // REVOLUTIONARY: Smart deduplication and relevance ranking const rankedMessages = this.rankAndDeduplicateMessages(result.messages); const topMessages = rankedMessages.slice(0, Math.min(result.messages.length, 8)); // Cap for token efficiency output += `Found ${result.totalResults} messages, showing ${topMessages.length} highest-value:\n\n`; topMessages.forEach((message, index) => { const timestamp = this.formatTimestamp(message.timestamp); const messageType = message.type.toUpperCase(); const content = this.extractHighValueContent(message.content); output += `${index + 1}. ${messageType} ${timestamp}\n`; output += ` ${content}\n`; // Intelligent context aggregation const context = this.aggregateContext(message); if (context) { output += ` ${context}\n`; } output += '\n'; }); return output.trim(); } private rankAndDeduplicateMessages(messages: any[]): any[] { // Score messages by information density and uniqueness const scored = messages.map(msg => { let score = 0; const content = msg.content.toLowerCase(); // Higher score for technical content if (this.detectContentType(msg.content) === 'technical') score += 50; if (this.detectContentType(msg.content) === 'code') score += 60; if (this.detectContentType(msg.content) === 'error') score += 70; // Boost for actionable content if (/(fix|solution|implement|deploy|build)/i.test(content)) score += 30; if (/(error|fail|issue|problem)/i.test(content)) score += 25; if (/(success|complete|working|done)/i.test(content)) score += 20; // Penalize generic content if (/(hello|thanks|okay|sure|yes|no)$/.test(content.trim())) score -= 20; // Boost for file references if (msg.context?.filesReferenced?.length) score += msg.context.filesReferenced.length * 10; // Boost for tool usage if (msg.context?.toolsUsed?.length) score += msg.context.toolsUsed.length * 5; return { ...msg, score }; }); // Deduplicate similar content const deduplicated: any[] = []; for (const msg of scored) { const isDuplicate = deduplicated.some(existing => this.calculateSimilarity(msg.content, existing.content) > 0.8 ); if (!isDuplicate) { deduplicated.push(msg); } } // Sort by score descending return deduplicated.sort((a, b) => b.score - a.score); } private calculateSimilarity(text1: string, text2: string): number { const words1 = new Set(text1.toLowerCase().split(/\s+/)); const words2 = new Set(text2.toLowerCase().split(/\s+/)); const intersection = new Set([...words1].filter(x => words2.has(x))); const union = new Set([...words1, ...words2]); return intersection.size / union.size; } private aggregateContext(message: any): string { const contexts = []; if (message.projectPath && message.projectPath !== 'unknown') { const projectName = message.projectPath.split('/').pop() || 'unknown'; contexts.push(`Project: ${projectName}`); } if (message.context?.filesReferenced?.length) { const files = [...new Set(message.context.filesReferenced)].slice(0, 3); contexts.push(`Files: ${files.join(', ')}`); } if (message.context?.toolsUsed?.length) { const tools = [...new Set(message.context.toolsUsed)].slice(0, 3); contexts.push(`Tools: ${tools.join(' → ')}`); } if (message.context?.errorPatterns?.length) { contexts.push(`Error: ${message.context.errorPatterns[0]}`); } return contexts.join(' | '); } formatSimilarQueries(queries: CompactMessage[], originalQuery: string, _detailLevel: string = 'summary'): string { if (queries.length === 0) { return 'No similar queries found.\n'; } // Optimize for Claude's context consumption - raw useful data const clusteredQueries = this.clusterBySemantic(queries, originalQuery); const highValueQueries = clusteredQueries.filter(q => q.relevanceScore && q.relevanceScore > 0.1); let output = `Found ${highValueQueries.length} similar queries:\n\n`; highValueQueries.forEach((query, index) => { const timestamp = this.formatTimestamp(query.timestamp); const score = query.relevanceScore ? ` (${query.relevanceScore.toFixed(1)})` : ''; const project = query.projectPath ? query.projectPath.split('/').pop() : 'unknown'; // RAW COMPLETE CONTENT for Claude - no truncation of valuable data output += `${index + 1}. ${timestamp}${score}: ${query.content}\n`; output += ` Project: ${project}`; // Add valuable metadata for Claude's context if (query.context?.toolsUsed?.length) { output += ` | Tools: ${query.context.toolsUsed.join(', ')}`; } if (query.context?.filesReferenced?.length) { output += ` | Files: ${query.context.filesReferenced.slice(0, 2).join(', ')}`; } output += '\n\n'; }); return output.trim(); } private clusterBySemantic(queries: CompactMessage[], originalQuery: string): CompactMessage[] { // Boost relevance scores based on semantic similarity return queries.map(query => { let boostedScore = query.relevanceScore || 0; // Boost for exact keyword matches const originalWords = originalQuery.toLowerCase().split(/\s+/); const queryWords = query.content.toLowerCase().split(/\s+/); const matchCount = originalWords.filter(word => queryWords.includes(word)).length; boostedScore += matchCount * 0.1; // Boost for technical similarity if (this.detectContentType(query.content) === this.detectContentType(originalQuery)) { boostedScore += 0.2; } // Boost for actionable content if (/(fix|solve|implement|build|deploy)/.test(query.content.toLowerCase())) { boostedScore += 0.15; } return { ...query, relevanceScore: Math.min(boostedScore, 1.0) }; }).sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0)); } formatFileContext(contexts: FileContext[], filepath: string, _detailLevel: string = 'summary', _operationType: string = 'all'): string { let output = `File context: ${filepath}\n\n`; if (contexts.length === 0) { return output + 'No file contexts found.\n'; } // CLAUDE-OPTIMIZED: Rich metadata and complete context for AI consumption const rankedContexts = this.rankFileContextsByImpact(contexts); const topContexts = rankedContexts.slice(0, Math.min(contexts.length, 15)); // More results for Claude output += `Found ${contexts.length} operations, showing ${topContexts.length} with complete context:\n\n`; topContexts.forEach((context, index) => { const timestamp = this.formatTimestamp(context.lastModified); const opType = context.operationType.toUpperCase(); output += `${index + 1}. ${opType} ${timestamp} | File: ${filepath}\n`; if (context.relatedMessages.length > 0) { // CLAUDE-OPTIMIZED: Provide multiple messages for full context, not just "best" one const topMessages = context.relatedMessages.slice(0, 3); // Show up to 3 messages for context topMessages.forEach((msg, msgIndex) => { // RAW COMPLETE CONTENT for Claude - no truncation of valuable data const content = msg.content.length > 500 ? msg.content : msg.content; // Keep complete content output += ` Message ${msgIndex + 1}: ${content}\n`; // Add valuable metadata for Claude's context if (msg.context?.toolsUsed?.length) { output += ` Tools: ${msg.context.toolsUsed.join(', ')}\n`; } if (msg.context?.filesReferenced?.length) { output += ` Files: ${msg.context.filesReferenced.slice(0, 3).join(', ')}\n`; } if (msg.context?.errorPatterns?.length) { output += ` Errors: ${msg.context.errorPatterns.slice(0, 2).join(', ')}\n`; } }); if (context.relatedMessages.length > 3) { output += ` (${context.relatedMessages.length - 3} additional messages available)\n`; } } output += '\n'; }); return output.trim(); } private rankFileContextsByImpact(contexts: FileContext[]): FileContext[] { return contexts.map(context => { let score = 0; // Higher score for more recent operations const daysSince = (Date.now() - new Date(context.lastModified).getTime()) / (1000 * 60 * 60 * 24); score += Math.max(0, 10 - daysSince); // Recent operations score higher // Boost for critical operations if (context.operationType.toLowerCase().includes('edit')) score += 20; if (context.operationType.toLowerCase().includes('create')) score += 15; if (context.operationType.toLowerCase().includes('read')) score += 5; // Boost for more messages (indicates complex operations) score += context.relatedMessages.length * 2; // Boost for technical content context.relatedMessages.forEach(msg => { const contentType = this.detectContentType(msg.content); if (contentType === 'code') score += 10; if (contentType === 'error') score += 15; if (contentType === 'technical') score += 8; }); return { ...context, score }; }).sort((a, b) => (b as any).score - (a as any).score); } private selectBestMessage(messages: any[]): any { // Select the message with highest information value return messages.reduce((best, current) => { const currentType = this.detectContentType(current.content); const bestType = this.detectContentType(best.content); // Prioritize technical content if (currentType === 'code' && bestType !== 'code') return current; if (currentType === 'error' && bestType !== 'error' && bestType !== 'code') return current; if (currentType === 'technical' && bestType === 'conversational') return current; // Prioritize longer, more detailed content if (current.content.length > best.content.length * 1.5) return current; return best; }); } formatErrorSolutions(solutions: ErrorSolution[], errorPattern: string, _detailLevel: string = 'summary'): string { let output = `Error solutions for: "${errorPattern}"\n\n`; if (solutions.length === 0) { return output + 'No error solutions found.\n'; } // CLAUDE-OPTIMIZED: Enhanced quality with speed and token efficiency const rankedSolutions = this.rankErrorSolutions(solutions); const topSolutions = rankedSolutions.slice(0, Math.min(solutions.length, 3)); // Focused on top results output += `${solutions.length} solutions for "${errorPattern}":\n\n`; topSolutions.forEach((solution, index) => { output += `${index + 1}. ${solution.errorPattern} (${solution.frequency}x)\n`; if (solution.solution.length > 0) { // CLAUDE-OPTIMIZED: Best solution with essential metadata - quality + efficiency balance const bestSolution = this.selectBestSolution(solution.solution); // Enhanced content extraction - preserve key technical details, remove fluff const content = this.extractTechnicalEssence(bestSolution.content); output += ` ${content}\n`; // Add essential metadata for Claude's context if (bestSolution.context?.toolsUsed?.length) { output += ` Tools: ${bestSolution.context.toolsUsed.slice(0, 2).join(', ')}\n`; } if (bestSolution.context?.filesReferenced?.length) { output += ` Files: ${bestSolution.context.filesReferenced.slice(0, 2).join(', ')}\n`; } // Show additional solutions count for context without bloating output if (solution.solution.length > 1) { output += ` (+${solution.solution.length - 1} more solutions)\n`; } } output += '\n'; }); return output.trim(); } private rankErrorSolutions(solutions: ErrorSolution[]): ErrorSolution[] { return solutions.map(solution => { let score = 0; // Higher score for more frequent errors (more important to solve) score += solution.frequency * 5; // Boost for solutions with actionable content solution.solution.forEach(sol => { const content = sol.content.toLowerCase(); if (/(fix|solution|resolved|implemented|deploy)/i.test(content)) score += 20; if (/(npm|install|config|update|build)/i.test(content)) score += 15; if (this.detectContentType(sol.content) === 'code') score += 25; if (this.detectContentType(sol.content) === 'technical') score += 10; }); return { ...solution, score }; }).sort((a, b) => (b as any).score - (a as any).score); } private selectBestSolution(solutions: any[]): any { return solutions.reduce((best, current) => { // Prioritize technical solutions over conversational const currentType = this.detectContentType(current.content); const bestType = this.detectContentType(best.content); if (currentType === 'code' && bestType !== 'code') return current; if (currentType === 'technical' && bestType === 'conversational') return current; // Prioritize solutions with actionable language if (/(fix|solution|resolved)/i.test(current.content) && !/(fix|solution|resolved)/i.test(best.content)) return current; return best; }); } formatToolPatterns(patterns: ToolPattern[], toolName?: string, _patternType: string = 'tools'): string { const toolFilter = toolName ? ` for "${toolName}"` : ''; let output = `Tool usage patterns${toolFilter}\n\n`; if (patterns.length === 0) { return output + 'No tool patterns found.\n'; } // REVOLUTIONARY: Usage frequency ranking and workflow intelligence const rankedPatterns = this.rankToolPatternsByValue(patterns); const topPatterns = rankedPatterns.slice(0, Math.min(patterns.length, 8)); output += `Found ${patterns.length} patterns, showing ${topPatterns.length} highest-value (${Math.round((topPatterns.length/patterns.length)*100)}% success rate):\n\n`; topPatterns.forEach((pattern, index) => { const successRate = pattern.successfulUsages.length; const efficiency = this.calculateToolEfficiency(pattern); output += `${index + 1}. ${pattern.toolName} (${successRate} uses, ${efficiency}% efficiency)\n`; // Extract most valuable pattern if (pattern.commonPatterns.length > 0) { const bestPattern = this.selectBestPattern(pattern.commonPatterns); output += ` Pattern: ${bestPattern}\n`; } // Extract most actionable practice if (pattern.bestPractices.length > 0) { const bestPractice = this.selectBestPractice(pattern.bestPractices); output += ` Best Practice: ${bestPractice}\n`; } output += '\n'; }); return output.trim(); } private rankToolPatternsByValue(patterns: ToolPattern[]): ToolPattern[] { return patterns.map(pattern => { let score = 0; // Higher score for more successful usages score += pattern.successfulUsages.length * 2; // Boost for commonly used tools if (/(Read|Edit|Bash|Grep|Glob)/i.test(pattern.toolName)) score += 20; // Boost for patterns with actionable practices pattern.bestPractices.forEach(practice => { if (/(efficient|fast|optimal|best)/i.test(practice)) score += 10; if (practice.length > 50) score += 5; // Detailed practices }); // Boost for workflow patterns pattern.commonPatterns.forEach(p => { if (/→/.test(p)) score += 15; // Tool chains if (/(file|search|edit|build)/i.test(p)) score += 8; }); return { ...pattern, score }; }).sort((a, b) => (b as any).score - (a as any).score); } private calculateToolEfficiency(pattern: ToolPattern): number { // Simple efficiency metric based on usage frequency const usageCount = pattern.successfulUsages.length; return Math.min(100, Math.round((usageCount / 100) * 100)); } private selectBestPattern(patterns: string[]): string { // Prioritize workflow patterns with tool chains const workflowPattern = patterns.find(p => /→/.test(p)); if (workflowPattern) return workflowPattern; // Prioritize technical patterns const technicalPattern = patterns.find(p => /(file|search|edit|build|deploy)/i.test(p)); if (technicalPattern) return technicalPattern; return patterns[0] || ''; } private selectBestPractice(practices: string[]): string { // Prioritize actionable practices const actionablePractice = practices.find(p => /(use|avoid|ensure|prefer)/i.test(p)); if (actionablePractice) return actionablePractice; // Prioritize detailed practices const detailedPractice = practices.find(p => p.length > 50); if (detailedPractice) return detailedPractice; return practices[0] || ''; } formatRecentSessions(sessions: any[], project?: string): string { let output = `Recent session analysis` + (project ? ` | Project: ${project}` : '') + '\n\n'; if (sessions.length === 0) { return output + 'No recent sessions found.\n'; } // REVOLUTIONARY: Activity scoring and productivity metrics const rankedSessions = this.rankSessionsByProductivity(sessions); const topSessions = rankedSessions.slice(0, Math.min(sessions.length, 10)); output += `Found ${sessions.length} sessions, showing ${topSessions.length} most productive (${Math.round((topSessions.length/sessions.length)*100)}% activity):\n\n`; topSessions.forEach((session, index) => { const duration = session.duration_minutes ? `${session.duration_minutes}m` : '0m'; const timestamp = this.formatTimestamp(session.end_time || session.start_time); const sessionId = session.session_id ? session.session_id.substring(0, 8) : 'unknown'; const productivity = this.calculateProductivityScore(session); output += `${index + 1}. ${sessionId} ${timestamp}\n`; output += ` ${session.message_count || 0} msgs (${duration}) | Productivity: ${productivity}%`; if (session.project_path) { const projectName = session.project_path.split('/').pop() || 'unknown'; output += ` | Project: ${projectName}`; } // Extract key tools used const tools = this.extractSessionTools(session); if (tools.length > 0) { output += ` | Tools: ${tools.join(', ')}`; } output += '\n\n'; }); return output.trim(); } private rankSessionsByProductivity(sessions: any[]): any[] { return sessions.map(session => { let score = 0; // Score based on message density (messages per minute) const duration = session.duration_minutes || 1; const messageCount = session.message_count || 0; const density = messageCount / duration; score += density * 10; // Boost for recent sessions const timestamp = session.end_time || session.start_time; if (timestamp) { const hoursAgo = (Date.now() - new Date(timestamp).getTime()) / (1000 * 60 * 60); score += Math.max(0, 24 - hoursAgo); // Recent sessions score higher } // Boost for longer sessions (indicates focus work) if (duration > 30) score += 20; if (duration > 60) score += 30; // Boost for high message count (indicates activity) if (messageCount > 50) score += 15; if (messageCount > 100) score += 25; return { ...session, score }; }).sort((a, b) => (b as any).score - (a as any).score); } private calculateProductivityScore(session: any): number { const duration = session.duration_minutes || 1; const messageCount = session.message_count || 0; const density = messageCount / duration; // Normalize to 0-100 scale return Math.min(100, Math.round(density * 5)); } private extractSessionTools(session: any): string[] { // Extract tools from session metadata if available const tools = []; if (session.tools_used) { tools.push(...session.tools_used.slice(0, 3)); } return tools; } formatCompactSummary(sessions: any[], sessionId?: string): string { let output = `${robots.summary} Session summary`; if (sessionId) { output += ` for: ${sessionId}`; } output += '\n\n'; if (sessions.length === 0) { return output + 'No session information found.\n'; } const session = sessions[0]; const timestamp = this.formatTimestamp(session.end_time || session.start_time); const duration = session.duration_minutes ? `${session.duration_minutes}m` : '0m'; const id = session.session_id ? session.session_id.substring(0, 8) : 'unknown'; output += `Session: ${id}\n`; output += `Duration: ${duration}\n`; output += `Messages: ${session.message_count || 0}\n`; output += `Last activity: ${timestamp}\n`; if (session.project_path) { const projectName = session.project_path.split('/').pop() || 'unknown'; output += `Project: ${projectName}\n`; } return output; } }

Implementation Reference

Latest Blog Posts

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/Vvkmnn/claude-historian'

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