Skip to main content
Glama
relationships.ts9.21 kB
import type { ConversationSummary } from '../database/types.js'; /** * Relationship detection and similarity scoring algorithms */ export interface RelationshipScore { sharedFiles?: string[]; sharedFolders?: string[]; sharedLanguages?: string[]; sizeSimilarity?: number; temporalProximity?: number; } export interface RelatedConversation { composerId: string; relationshipScore: number; relationships: RelationshipScore; summary: string; scoreBreakdown?: Record<string, number>; } export interface RelationshipOptions { relationshipTypes: Array<'files' | 'folders' | 'languages' | 'size' | 'temporal'>; maxResults: number; minScore: number; includeScoreBreakdown: boolean; } /** * Find conversations related to a reference conversation */ export function findRelatedConversations( referenceSummary: ConversationSummary, allSummaries: ConversationSummary[], conversationIds: string[], options: RelationshipOptions ): RelatedConversation[] { const related: RelatedConversation[] = []; // Get reference conversation index for temporal calculations const referenceIndex = conversationIds.indexOf(referenceSummary.composerId); for (const summary of allSummaries) { // Skip the reference conversation itself if (summary.composerId === referenceSummary.composerId) { continue; } const relationships = calculateRelationships( referenceSummary, summary, conversationIds, referenceIndex, options.relationshipTypes ); const score = calculateCompositeScore(relationships, options.relationshipTypes); if (score >= options.minScore) { related.push({ composerId: summary.composerId, relationshipScore: score, relationships, summary: summary.firstMessage || 'No preview available', scoreBreakdown: options.includeScoreBreakdown ? calculateScoreBreakdown(relationships, options.relationshipTypes) : undefined }); } } // Sort by score and limit results return related .sort((a, b) => b.relationshipScore - a.relationshipScore) .slice(0, options.maxResults); } /** * Calculate relationships between two conversations */ function calculateRelationships( reference: ConversationSummary, candidate: ConversationSummary, conversationIds: string[], referenceIndex: number, relationshipTypes: string[] ): RelationshipScore { const relationships: RelationshipScore = {}; if (relationshipTypes.includes('files')) { relationships.sharedFiles = calculateSharedItems( reference.relevantFiles, candidate.relevantFiles ); } if (relationshipTypes.includes('folders')) { relationships.sharedFolders = calculateSharedItems( reference.attachedFolders, candidate.attachedFolders ); } if (relationshipTypes.includes('languages')) { // Extract languages from both conversations (would need code block data) // For now, we'll use a placeholder - this would be enhanced with actual language extraction relationships.sharedLanguages = []; } if (relationshipTypes.includes('size')) { relationships.sizeSimilarity = calculateSizeSimilarity( reference.conversationSize, candidate.conversationSize ); } if (relationshipTypes.includes('temporal')) { const candidateIndex = conversationIds.indexOf(candidate.composerId); relationships.temporalProximity = calculateTemporalProximity( referenceIndex, candidateIndex, conversationIds.length ); } return relationships; } /** * Calculate shared items between two arrays */ function calculateSharedItems(array1: string[], array2: string[]): string[] { const set1 = new Set(array1); return array2.filter(item => set1.has(item)); } /** * Calculate size similarity between two conversations */ function calculateSizeSimilarity(size1: number, size2: number): number { if (size1 === 0 && size2 === 0) return 1; if (size1 === 0 || size2 === 0) return 0; const maxSize = Math.max(size1, size2); const minSize = Math.min(size1, size2); return minSize / maxSize; } /** * Calculate temporal proximity based on ROWID distance */ function calculateTemporalProximity( index1: number, index2: number, totalConversations: number ): number { if (index1 === -1 || index2 === -1) return 0; const distance = Math.abs(index1 - index2); const maxDistance = totalConversations - 1; if (maxDistance === 0) return 1; // Closer conversations get higher scores return 1 - (distance / maxDistance); } /** * Calculate composite score from relationships */ function calculateCompositeScore( relationships: RelationshipScore, relationshipTypes: string[] ): number { let totalScore = 0; let weightSum = 0; // Define weights for different relationship types const weights = { files: 0.4, folders: 0.3, languages: 0.2, size: 0.05, temporal: 0.05 }; if (relationshipTypes.includes('files') && relationships.sharedFiles) { const score = Math.min(relationships.sharedFiles.length / 5, 1); // Cap at 5 shared files totalScore += score * weights.files; weightSum += weights.files; } if (relationshipTypes.includes('folders') && relationships.sharedFolders) { const score = Math.min(relationships.sharedFolders.length / 3, 1); // Cap at 3 shared folders totalScore += score * weights.folders; weightSum += weights.folders; } if (relationshipTypes.includes('languages') && relationships.sharedLanguages) { const score = Math.min(relationships.sharedLanguages.length / 3, 1); // Cap at 3 shared languages totalScore += score * weights.languages; weightSum += weights.languages; } if (relationshipTypes.includes('size') && relationships.sizeSimilarity !== undefined) { totalScore += relationships.sizeSimilarity * weights.size; weightSum += weights.size; } if (relationshipTypes.includes('temporal') && relationships.temporalProximity !== undefined) { totalScore += relationships.temporalProximity * weights.temporal; weightSum += weights.temporal; } return weightSum > 0 ? totalScore / weightSum : 0; } /** * Calculate individual score breakdown for debugging */ function calculateScoreBreakdown( relationships: RelationshipScore, relationshipTypes: string[] ): Record<string, number> { const breakdown: Record<string, number> = {}; if (relationshipTypes.includes('files') && relationships.sharedFiles) { breakdown.files = Math.min(relationships.sharedFiles.length / 5, 1); } if (relationshipTypes.includes('folders') && relationships.sharedFolders) { breakdown.folders = Math.min(relationships.sharedFolders.length / 3, 1); } if (relationshipTypes.includes('languages') && relationships.sharedLanguages) { breakdown.languages = Math.min(relationships.sharedLanguages.length / 3, 1); } if (relationshipTypes.includes('size') && relationships.sizeSimilarity !== undefined) { breakdown.size = relationships.sizeSimilarity; } if (relationshipTypes.includes('temporal') && relationships.temporalProximity !== undefined) { breakdown.temporal = relationships.temporalProximity; } return breakdown; } /** * Extract languages from code blocks in conversation data */ export function extractLanguagesFromCodeBlocks( codeBlocks: Array<{ language: string; code: string }> ): string[] { const languages = new Set<string>(); codeBlocks.forEach(block => { if (block.language && block.language.trim()) { languages.add(normalizeLanguage(block.language)); } }); return Array.from(languages); } /** * Normalize language names for consistency */ function normalizeLanguage(language: string): string { const normalized = language.toLowerCase().trim(); // Common language mappings const mappings: Record<string, string> = { 'js': 'javascript', 'ts': 'typescript', 'jsx': 'javascript', 'tsx': 'typescript', 'py': 'python', 'rb': 'ruby', 'sh': 'shell', 'bash': 'shell', 'zsh': 'shell', 'fish': 'shell', 'yml': 'yaml', 'md': 'markdown', 'dockerfile': 'docker' }; return mappings[normalized] || normalized; } /** * Calculate file overlap score between two conversations */ export function calculateFileOverlapScore(files1: string[], files2: string[]): number { if (files1.length === 0 && files2.length === 0) return 1; if (files1.length === 0 || files2.length === 0) return 0; const set1 = new Set(files1); const intersection = files2.filter(file => set1.has(file)); const union = new Set([...files1, ...files2]); return intersection.length / union.size; // Jaccard similarity } /** * Calculate folder overlap score between two conversations */ export function calculateFolderOverlapScore(folders1: string[], folders2: string[]): number { if (folders1.length === 0 && folders2.length === 0) return 1; if (folders1.length === 0 || folders2.length === 0) return 0; const set1 = new Set(folders1); const intersection = folders2.filter(folder => set1.has(folder)); const union = new Set([...folders1, ...folders2]); return intersection.length / union.size; // Jaccard similarity }

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/vltansky/cursor-conversations-mcp'

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