Skip to main content
Glama
conversation-tools.ts58.4 kB
import { z } from 'zod'; import { CursorDatabaseReader } from '../database/reader.js'; import { ConversationParser } from '../database/parser.js'; import type { ConversationFilters, ConversationSummary, ConversationSearchResult, BubbleMessage } from '../database/types.js'; import { detectCursorDatabasePath } from '../utils/database-utils.js'; // Input schema for list_conversations tool export const listConversationsSchema = z.object({ limit: z.number().min(1).max(1000).optional(), minLength: z.number().min(0).optional(), keywords: z.array(z.string()).optional(), hasCodeBlocks: z.boolean().optional(), format: z.enum(['legacy', 'modern', 'both']).optional(), includeEmpty: z.boolean().optional(), projectPath: z.string().optional(), filePattern: z.string().optional(), relevantFiles: z.array(z.string()).optional(), startDate: z.string().optional(), endDate: z.string().optional(), includeAiSummaries: z.boolean().optional().default(true) }); export type ListConversationsInput = z.infer<typeof listConversationsSchema>; // Output type for list_conversations tool export interface ListConversationsOutput { conversations: Array<{ composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; relevantFiles: string[]; attachedFolders: string[]; firstMessage?: string; title?: string; aiGeneratedSummary?: string; size: number; }>; totalFound: number; filters: { limit: number; minLength: number; format: string; hasCodeBlocks?: boolean; keywords?: string[]; projectPath?: string; filePattern?: string; relevantFiles?: string[]; includeAiSummaries?: boolean; }; } /** * List Cursor conversations with optional filters and ROWID-based ordering */ export async function listConversations(input: ListConversationsInput): Promise<ListConversationsOutput> { const validatedInput = listConversationsSchema.parse(input); const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath }); try { await reader.connect(); const filters: ConversationFilters = { minLength: validatedInput.minLength, format: validatedInput.format, hasCodeBlocks: validatedInput.hasCodeBlocks, keywords: validatedInput.keywords, projectPath: validatedInput.projectPath, filePattern: validatedInput.filePattern, relevantFiles: validatedInput.relevantFiles }; // Add date range filter if provided if (validatedInput.startDate || validatedInput.endDate) { const start = validatedInput.startDate ? new Date(validatedInput.startDate) : new Date('1970-01-01'); const end = validatedInput.endDate ? new Date(validatedInput.endDate) : new Date(); filters.dateRange = { start, end }; } const conversationIds = await reader.getConversationIds(filters); let limitedIds = conversationIds.slice(0, validatedInput.limit); // Apply date filtering if specified (post-query filtering due to unreliable timestamps) if (validatedInput.startDate || validatedInput.endDate) { const filteredIds = []; for (const composerId of limitedIds) { try { const conversation = await reader.getConversationById(composerId); if (!conversation) continue; const hasValidDate = checkConversationDateRange( conversation, validatedInput.startDate, validatedInput.endDate ); if (hasValidDate) { filteredIds.push(composerId); } } catch (error) { // Skip conversations that can't be processed continue; } } limitedIds = filteredIds; } const conversations = []; for (const composerId of limitedIds) { try { const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: true, maxFirstMessageLength: 150, includeTitle: true, includeAIGeneratedSummary: validatedInput.includeAiSummaries }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, title: summary.title, aiGeneratedSummary: summary.aiGeneratedSummary, size: summary.conversationSize }); } } catch (error) { console.error(`Failed to get summary for conversation ${composerId}:`, error); } } return { conversations, totalFound: conversationIds.length, filters: { limit: validatedInput.limit ?? 10, minLength: validatedInput.minLength ?? 100, format: validatedInput.format ?? 'both', hasCodeBlocks: validatedInput.hasCodeBlocks, keywords: validatedInput.keywords, projectPath: validatedInput.projectPath, filePattern: validatedInput.filePattern, relevantFiles: validatedInput.relevantFiles, includeAiSummaries: validatedInput.includeAiSummaries } }; } finally { // Always close the database connection reader.close(); } } // Input schema for get_conversation tool export const getConversationSchema = z.object({ conversationId: z.string().min(1), includeCodeBlocks: z.boolean().optional().default(true), includeFileReferences: z.boolean().optional().default(true), includeMetadata: z.boolean().optional().default(false), resolveBubbles: z.boolean().optional().default(true), summaryOnly: z.boolean().optional().default(false) }); export type GetConversationInput = z.infer<typeof getConversationSchema>; // Output type for get_conversation tool export interface GetConversationOutput { conversation: { composerId: string; format: 'legacy' | 'modern'; messageCount: number; title?: string; aiGeneratedSummary?: string; messages?: Array<{ type: number; text: string; bubbleId: string; relevantFiles?: string[]; attachedFolders?: string[]; codeBlocks?: Array<{ language: string; code: string; filename?: string; }>; }>; codeBlocks?: Array<{ language: string; code: string; filename?: string; }>; relevantFiles?: string[]; attachedFolders?: string[]; metadata?: { hasLoaded: boolean; storedSummary?: string; storedRichText?: string; size: number; }; } | null; } /** * Get a specific conversation by ID with full content */ export async function getConversation(input: GetConversationInput): Promise<GetConversationOutput> { // Validate input const validatedInput = getConversationSchema.parse(input); // Create database reader const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath }); try { // Connect to database await reader.connect(); // If summaryOnly is requested, return enhanced summary without full content if (validatedInput.summaryOnly) { const summary = await reader.getConversationSummary(validatedInput.conversationId, { includeTitle: true, includeAIGeneratedSummary: true, includeFirstMessage: true, includeLastMessage: true, maxFirstMessageLength: 200, maxLastMessageLength: 200 }); if (!summary) { return { conversation: null }; } return { conversation: { composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, title: summary.title, aiGeneratedSummary: summary.aiGeneratedSummary, relevantFiles: validatedInput.includeFileReferences ? summary.relevantFiles : undefined, attachedFolders: validatedInput.includeFileReferences ? summary.attachedFolders : undefined, metadata: validatedInput.includeMetadata ? { hasLoaded: true, storedSummary: summary.storedSummary, storedRichText: summary.storedRichText, size: summary.conversationSize } : undefined } }; } // Get conversation const conversation = await reader.getConversationById(validatedInput.conversationId); if (!conversation) { return { conversation: null }; } // Get conversation summary to extract title and AI summary const summary = await reader.getConversationSummary(validatedInput.conversationId, { includeTitle: true, includeAIGeneratedSummary: true }); // Determine format const format = conversation.hasOwnProperty('_v') ? 'modern' : 'legacy'; // Build response based on format if (format === 'legacy') { const legacyConv = conversation as any; const messages = legacyConv.conversation || []; // Extract data let allCodeBlocks: any[] = []; let allRelevantFiles: string[] = []; let allAttachedFolders: string[] = []; const processedMessages = messages.map((msg: any) => { if (validatedInput.includeCodeBlocks && msg.suggestedCodeBlocks) { allCodeBlocks.push(...msg.suggestedCodeBlocks); } if (validatedInput.includeFileReferences) { if (msg.relevantFiles) allRelevantFiles.push(...msg.relevantFiles); if (msg.attachedFoldersNew) allAttachedFolders.push(...msg.attachedFoldersNew); } return { type: msg.type, text: msg.text, bubbleId: msg.bubbleId, relevantFiles: validatedInput.includeFileReferences ? msg.relevantFiles : undefined, attachedFolders: validatedInput.includeFileReferences ? msg.attachedFoldersNew : undefined, codeBlocks: validatedInput.includeCodeBlocks ? msg.suggestedCodeBlocks : undefined }; }); allRelevantFiles = Array.from(new Set(allRelevantFiles)); allAttachedFolders = Array.from(new Set(allAttachedFolders)); return { conversation: { composerId: legacyConv.composerId, format: 'legacy', messageCount: messages.length, title: summary?.title, aiGeneratedSummary: summary?.aiGeneratedSummary, messages: processedMessages, codeBlocks: validatedInput.includeCodeBlocks ? allCodeBlocks : undefined, relevantFiles: validatedInput.includeFileReferences ? allRelevantFiles : undefined, attachedFolders: validatedInput.includeFileReferences ? allAttachedFolders : undefined, metadata: validatedInput.includeMetadata ? { hasLoaded: true, storedSummary: legacyConv.storedSummary, storedRichText: legacyConv.storedRichText, size: JSON.stringify(conversation).length } : undefined } }; } else { const modernConv = conversation as any; const headers = modernConv.fullConversationHeadersOnly || []; if (validatedInput.resolveBubbles) { const resolvedMessages = []; for (const header of headers.slice(0, 10)) { try { const bubbleMessage = await reader.getBubbleMessage(modernConv.composerId, header.bubbleId); if (bubbleMessage) { resolvedMessages.push({ type: header.type, text: bubbleMessage.text, bubbleId: header.bubbleId, relevantFiles: validatedInput.includeFileReferences ? bubbleMessage.relevantFiles : undefined, attachedFolders: validatedInput.includeFileReferences ? bubbleMessage.attachedFoldersNew : undefined, codeBlocks: validatedInput.includeCodeBlocks ? bubbleMessage.suggestedCodeBlocks : undefined }); } } catch (error) { console.error(`Failed to resolve bubble ${header.bubbleId}:`, error); } } return { conversation: { composerId: modernConv.composerId, format: 'modern', messageCount: headers.length, title: summary?.title, aiGeneratedSummary: summary?.aiGeneratedSummary, messages: resolvedMessages, metadata: validatedInput.includeMetadata ? { hasLoaded: true, storedSummary: modernConv.storedSummary, storedRichText: modernConv.storedRichText, size: JSON.stringify(conversation).length } : undefined } }; } else { return { conversation: { composerId: modernConv.composerId, format: 'modern', messageCount: headers.length, title: summary?.title, aiGeneratedSummary: summary?.aiGeneratedSummary, metadata: validatedInput.includeMetadata ? { hasLoaded: true, storedSummary: modernConv.storedSummary, storedRichText: modernConv.storedRichText, size: JSON.stringify(conversation).length } : undefined } }; } } } finally { // Always close the database connection reader.close(); } } // Input schema for get_conversation_summary tool export const getConversationSummarySchema = z.object({ conversationId: z.string().min(1), includeFirstMessage: z.boolean().optional().default(false), includeLastMessage: z.boolean().optional().default(false), maxFirstMessageLength: z.number().min(1).max(1000).optional().default(200), maxLastMessageLength: z.number().min(1).max(1000).optional().default(200), includeMetadata: z.boolean().optional().default(false) }); export type GetConversationSummaryInput = z.infer<typeof getConversationSummarySchema>; // Output type for get_conversation_summary tool export interface GetConversationSummaryOutput { summary: { composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; codeBlockCount?: number; conversationSize: number; firstMessage?: string; lastMessage?: string; storedSummary?: string; storedRichText?: string; relevantFiles?: string[]; attachedFolders?: string[]; metadata?: { totalCharacters: number; averageMessageLength: number; }; } | null; } /** * Get conversation summary with optional first/last message content */ export async function getConversationSummary(input: GetConversationSummaryInput): Promise<GetConversationSummaryOutput> { const validatedInput = getConversationSummarySchema.parse(input); const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath }); try { await reader.connect(); const summary = await reader.getConversationSummary(validatedInput.conversationId, { includeFirstMessage: validatedInput.includeFirstMessage, includeLastMessage: validatedInput.includeLastMessage, maxFirstMessageLength: validatedInput.maxFirstMessageLength, maxLastMessageLength: validatedInput.maxLastMessageLength }); if (!summary) { return { summary: null }; } return { summary: { composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, codeBlockCount: summary.codeBlockCount, conversationSize: summary.conversationSize, firstMessage: summary.firstMessage, lastMessage: summary.lastMessage, storedSummary: summary.storedSummary, storedRichText: summary.storedRichText, relevantFiles: summary.relevantFiles, attachedFolders: summary.attachedFolders, metadata: validatedInput.includeMetadata ? { totalCharacters: summary.conversationSize, averageMessageLength: Math.round(summary.conversationSize / summary.messageCount) } : undefined } }; } finally { reader.close(); } } // Input schema for search_conversations tool export const searchConversationsSchema = z.object({ // Simple query (existing - backward compatible) query: z.string().optional(), // Multi-keyword search keywords: z.array(z.string().min(1)).optional(), keywordOperator: z.enum(['AND', 'OR']).optional().default('OR'), // LIKE pattern search (database-level) likePattern: z.string().optional(), // Date filtering startDate: z.string().optional(), endDate: z.string().optional(), // Existing options includeCode: z.boolean().optional().default(true), contextLines: z.number().min(0).max(10).optional().default(2), maxResults: z.number().min(1).max(100).optional().default(10), searchBubbles: z.boolean().optional().default(true), searchType: z.enum(['all', 'summarization', 'code', 'files', 'project']).optional().default('all'), format: z.enum(['legacy', 'modern', 'both']).optional().default('both'), highlightMatches: z.boolean().optional().default(true), projectSearch: z.boolean().optional().default(false), fuzzyMatch: z.boolean().optional().default(false), includePartialPaths: z.boolean().optional().default(true), includeFileContent: z.boolean().optional().default(false), minRelevanceScore: z.number().min(0).max(1).optional().default(0.1), orderBy: z.enum(['relevance', 'recency']).optional().default('relevance') }).refine( (data) => { const hasSearchCriteria = (data.query && data.query.trim() !== '' && data.query.trim() !== '?') || data.keywords || data.likePattern; const hasDateFilter = data.startDate || data.endDate; const hasOtherFilters = data.searchType !== 'all'; return hasSearchCriteria || hasDateFilter || hasOtherFilters; }, { message: "At least one search criteria (query, keywords, likePattern), date filter (startDate, endDate), or search type filter must be provided" } ); export type SearchConversationsInput = z.infer<typeof searchConversationsSchema>; // Output type for search_conversations tool export interface SearchConversationsOutput { conversations: Array<{ composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; relevantFiles: string[]; attachedFolders: string[]; firstMessage?: string; title?: string; aiGeneratedSummary?: string; size: number; relevanceScore?: number; matchDetails?: { exactPathMatch: boolean; partialPathMatch: boolean; filePathMatch: boolean; fuzzyMatch: boolean; matchedPaths: string[]; matchedFiles: string[]; }; }>; totalResults: number; query: string; searchOptions: { includeCode: boolean; contextLines: number; maxResults: number; searchBubbles: boolean; searchType: 'all' | 'summarization' | 'code' | 'files' | 'project'; format: 'legacy' | 'modern' | 'both'; highlightMatches: boolean; projectSearch?: boolean; fuzzyMatch?: boolean; includePartialPaths?: boolean; includeFileContent?: boolean; minRelevanceScore?: number; orderBy?: 'relevance' | 'recency'; }; debugInfo?: { totalConversationsScanned: number; averageRelevanceScore: number; matchTypeDistribution: { exactPath: number; partialPath: number; filePath: number; fuzzy: number; }; }; } /** * Search conversations with enhanced multi-keyword and LIKE pattern support */ export async function searchConversations(input: SearchConversationsInput): Promise<SearchConversationsOutput> { const validatedInput = searchConversationsSchema.parse(input); const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath }); try { await reader.connect(); // Determine the search query for display purposes const displayQuery = validatedInput.query || (validatedInput.keywords ? validatedInput.keywords.join(` ${validatedInput.keywordOperator} `) : '') || validatedInput.likePattern || 'advanced search'; if (validatedInput.projectSearch && validatedInput.query) { // Handle project search (existing logic) const searchOptions = { fuzzyMatch: validatedInput.fuzzyMatch, includePartialPaths: validatedInput.includePartialPaths, includeFileContent: validatedInput.includeFileContent, minRelevanceScore: validatedInput.minRelevanceScore, orderBy: validatedInput.orderBy, limit: validatedInput.maxResults }; const conversationIds = await reader.getConversationIds({ format: validatedInput.format, projectPath: validatedInput.query }); const conversations = []; const matchTypeDistribution = { exactPath: 0, partialPath: 0, filePath: 0, fuzzy: 0 }; let totalConversationsScanned = 0; let totalRelevanceScore = 0; for (const composerId of conversationIds.slice(0, validatedInput.maxResults * 2)) { try { totalConversationsScanned++; const conversation = await reader.getConversationById(composerId); if (!conversation) continue; const format = conversation.hasOwnProperty('_v') ? 'modern' : 'legacy'; if (format === 'modern') { const modernConv = conversation as any; const headers = modernConv.fullConversationHeadersOnly || []; for (const header of headers.slice(0, 5)) { try { const bubbleMessage = await reader.getBubbleMessage(modernConv.composerId, header.bubbleId); if (bubbleMessage) { (conversation as any).resolvedMessages = (conversation as any).resolvedMessages || []; (conversation as any).resolvedMessages.push(bubbleMessage); } } catch (error) { continue; } } } const relevanceResult = calculateEnhancedProjectRelevance( conversation, validatedInput.query, { fuzzyMatch: validatedInput.fuzzyMatch || false, includePartialPaths: validatedInput.includePartialPaths || false, includeFileContent: validatedInput.includeFileContent || false } ); if (relevanceResult.score >= (validatedInput.minRelevanceScore || 0.1)) { const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: true, maxFirstMessageLength: 150 }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, size: summary.conversationSize, relevanceScore: relevanceResult.score, matchDetails: relevanceResult.details }); totalRelevanceScore += relevanceResult.score; if (relevanceResult.details.exactPathMatch) matchTypeDistribution.exactPath++; if (relevanceResult.details.partialPathMatch) matchTypeDistribution.partialPath++; if (relevanceResult.details.filePathMatch) matchTypeDistribution.filePath++; if (relevanceResult.details.fuzzyMatch) matchTypeDistribution.fuzzy++; } } } catch (error) { continue; } } if (validatedInput.orderBy === 'relevance') { conversations.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0)); } return { conversations: conversations.slice(0, validatedInput.maxResults), totalResults: conversations.length, query: displayQuery, searchOptions: { includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType, format: validatedInput.format, highlightMatches: validatedInput.highlightMatches, projectSearch: validatedInput.projectSearch, fuzzyMatch: validatedInput.fuzzyMatch, includePartialPaths: validatedInput.includePartialPaths, includeFileContent: validatedInput.includeFileContent, minRelevanceScore: validatedInput.minRelevanceScore, orderBy: validatedInput.orderBy }, debugInfo: { totalConversationsScanned, averageRelevanceScore: totalConversationsScanned > 0 ? totalRelevanceScore / totalConversationsScanned : 0, matchTypeDistribution } }; } else { const hasSearchCriteria = (validatedInput.query && validatedInput.query.trim() !== '' && validatedInput.query.trim() !== '?') || validatedInput.keywords || validatedInput.likePattern; if (!hasSearchCriteria && (validatedInput.startDate || validatedInput.endDate)) { // Date-only search: get all conversations and filter by date const allConversationIds = await reader.getConversationIds({ format: validatedInput.format }); const conversations = []; for (const composerId of allConversationIds.slice(0, validatedInput.maxResults * 2)) { try { const conversation = await reader.getConversationById(composerId); if (!conversation) continue; // Apply date filtering const hasValidDate = checkConversationDateRange( conversation, validatedInput.startDate, validatedInput.endDate ); if (!hasValidDate) continue; const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: true, maxFirstMessageLength: 150, includeTitle: true, includeAIGeneratedSummary: true }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, title: summary.title, aiGeneratedSummary: summary.aiGeneratedSummary, size: summary.conversationSize }); if (conversations.length >= validatedInput.maxResults) break; } } catch (error) { console.error(`Failed to process conversation ${composerId}:`, error); } } return { conversations, totalResults: conversations.length, query: displayQuery, searchOptions: { includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType, format: validatedInput.format, highlightMatches: validatedInput.highlightMatches } }; } // Handle enhanced search with keywords, LIKE patterns, or simple query const searchResults = await reader.searchConversationsEnhanced({ query: validatedInput.query, keywords: validatedInput.keywords, keywordOperator: validatedInput.keywordOperator, likePattern: validatedInput.likePattern, includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType === 'project' ? 'all' : validatedInput.searchType, format: validatedInput.format, startDate: validatedInput.startDate, endDate: validatedInput.endDate }); // Convert search results to conversation summaries for consistency const conversations = []; for (const result of searchResults) { try { // Apply date filtering if specified (post-query filtering due to unreliable timestamps) if (validatedInput.startDate || validatedInput.endDate) { const conversation = await reader.getConversationById(result.composerId); if (!conversation) continue; const hasValidDate = checkConversationDateRange( conversation, validatedInput.startDate, validatedInput.endDate ); if (!hasValidDate) continue; } const summary = await reader.getConversationSummary(result.composerId, { includeFirstMessage: true, maxFirstMessageLength: 150, includeTitle: true, includeAIGeneratedSummary: true }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, title: summary.title, aiGeneratedSummary: summary.aiGeneratedSummary, size: summary.conversationSize }); } } catch (error) { console.error(`Failed to get summary for conversation ${result.composerId}:`, error); } } return { conversations, totalResults: conversations.length, query: displayQuery, searchOptions: { includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType, format: validatedInput.format, highlightMatches: validatedInput.highlightMatches } }; } } finally { reader.close(); } } // Get bubble message tool schema and types export const getBubbleMessageSchema = z.object({ composerId: z.string().min(1).describe('The composer ID of the conversation containing the bubble message'), bubbleId: z.string().min(1).describe('The unique bubble ID of the message to retrieve'), includeMetadata: z.boolean().optional().default(false).describe('Include additional metadata about the bubble message'), includeCodeBlocks: z.boolean().optional().default(true).describe('Include code blocks in the response'), includeFileReferences: z.boolean().optional().default(true).describe('Include file references and attached folders'), resolveReferences: z.boolean().optional().default(false).describe('Attempt to resolve file references to actual content') }); export type GetBubbleMessageInput = z.infer<typeof getBubbleMessageSchema>; export interface GetBubbleMessageOutput { bubbleMessage: BubbleMessage | null; metadata?: { composerId: string; bubbleId: string; messageType: 'user' | 'assistant' | 'unknown'; hasCodeBlocks: boolean; codeBlockCount: number; hasFileReferences: boolean; fileReferenceCount: number; hasAttachedFolders: boolean; attachedFolderCount: number; messageLength: number; timestamp?: string; }; error?: string; } /** * Get a specific bubble message from a modern format conversation */ export async function getBubbleMessage(input: GetBubbleMessageInput): Promise<GetBubbleMessageOutput> { // Validate input const validatedInput = getBubbleMessageSchema.parse(input); // Create database reader const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath }); try { // Connect to database await reader.connect(); // Get the bubble message const bubbleMessage = await reader.getBubbleMessage(validatedInput.composerId, validatedInput.bubbleId); if (!bubbleMessage) { return { bubbleMessage: null, error: `Bubble message not found: ${validatedInput.bubbleId} in conversation ${validatedInput.composerId}` }; } // Build metadata if requested let metadata; if (validatedInput.includeMetadata) { const hasCodeBlocks = !!(bubbleMessage.suggestedCodeBlocks && bubbleMessage.suggestedCodeBlocks.length > 0); const hasFileReferences = !!(bubbleMessage.relevantFiles && bubbleMessage.relevantFiles.length > 0); const hasAttachedFolders = !!(bubbleMessage.attachedFoldersNew && bubbleMessage.attachedFoldersNew.length > 0); const messageType: 'user' | 'assistant' | 'unknown' = bubbleMessage.type === 0 ? 'user' : bubbleMessage.type === 1 ? 'assistant' : 'unknown'; metadata = { composerId: validatedInput.composerId, bubbleId: validatedInput.bubbleId, messageType, hasCodeBlocks, codeBlockCount: bubbleMessage.suggestedCodeBlocks?.length || 0, hasFileReferences, fileReferenceCount: bubbleMessage.relevantFiles?.length || 0, hasAttachedFolders, attachedFolderCount: bubbleMessage.attachedFoldersNew?.length || 0, messageLength: bubbleMessage.text.length, timestamp: bubbleMessage.timestamp }; } return { bubbleMessage, metadata }; } finally { // Always close the database connection reader.close(); } } // Input schema for get_recent_conversations tool export const getRecentConversationsSchema = z.object({ limit: z.number().min(1).max(100).optional().default(10), includeEmpty: z.boolean().optional().default(false), format: z.enum(['legacy', 'modern', 'both']).optional().default('both'), includeFirstMessage: z.boolean().optional().default(true), maxFirstMessageLength: z.number().min(10).max(500).optional().default(150), includeMetadata: z.boolean().optional().default(false) }); export type GetRecentConversationsInput = z.infer<typeof getRecentConversationsSchema>; // Output type for get_recent_conversations tool export interface GetRecentConversationsOutput { conversations: Array<{ composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; relevantFiles: string[]; attachedFolders: string[]; firstMessage?: string; size: number; metadata?: { hasLoaded: boolean; totalCharacters: number; averageMessageLength: number; codeBlockCount: number; fileReferenceCount: number; attachedFolderCount: number; }; }>; totalFound: number; requestedLimit: number; timestamp: string; } /** * Get recent Cursor conversations ordered by ROWID (most recent first) */ export async function getRecentConversations(input: GetRecentConversationsInput): Promise<GetRecentConversationsOutput> { // Validate input const validatedInput = getRecentConversationsSchema.parse(input); // Create database reader const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath, minConversationSize: validatedInput.includeEmpty ? 0 : 5000 }); try { // Connect to database await reader.connect(); // Build minimal filters for recent conversations const filters: ConversationFilters = { minLength: validatedInput.includeEmpty ? 0 : 5000, format: validatedInput.format }; // Get conversation IDs (already ordered by ROWID DESC) const conversationIds = await reader.getConversationIds(filters); // Limit results const limitedIds = conversationIds.slice(0, validatedInput.limit); // Get conversation summaries const conversations = []; for (const composerId of limitedIds) { try { const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: validatedInput.includeFirstMessage, maxFirstMessageLength: validatedInput.maxFirstMessageLength, includeFileList: true, includeCodeBlockCount: true }); if (summary) { const conversationData: any = { composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles, attachedFolders: summary.attachedFolders, firstMessage: summary.firstMessage, size: summary.conversationSize }; // Add metadata if requested if (validatedInput.includeMetadata) { conversationData.metadata = { hasLoaded: true, totalCharacters: summary.conversationSize, averageMessageLength: summary.messageCount > 0 ? Math.round(summary.conversationSize / summary.messageCount) : 0, codeBlockCount: summary.codeBlockCount || 0, fileReferenceCount: summary.relevantFiles.length, attachedFolderCount: summary.attachedFolders.length }; } conversations.push(conversationData); } } catch (error) { console.error(`Failed to get summary for conversation ${composerId}:`, error); // Continue with other conversations } } return { conversations, totalFound: conversationIds.length, requestedLimit: validatedInput.limit, timestamp: new Date().toISOString() }; } finally { // Always close the database connection reader.close(); } } // Input schema for get_conversations_by_project tool export const getConversationsByProjectSchema = z.object({ projectPath: z.string().min(1), filePattern: z.string().optional(), exactFilePath: z.string().optional(), orderBy: z.enum(['recency', 'relevance']).optional().default('recency'), limit: z.number().min(1).max(1000).optional().default(50), fuzzyMatch: z.boolean().optional().default(false) }); export type GetConversationsByProjectInput = z.infer<typeof getConversationsByProjectSchema>; // Output type for get_conversations_by_project tool export interface GetConversationsByProjectOutput { conversations: Array<{ composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; relevantFiles: string[]; attachedFolders: string[]; firstMessage?: string; size: number; relevanceScore?: number; }>; totalFound: number; filters: { projectPath: string; filePattern?: string; exactFilePath?: string; orderBy: string; limit: number; }; } /** * Get conversations filtered by project path, attached folders, or relevant files */ export async function getConversationsByProject(input: GetConversationsByProjectInput): Promise<GetConversationsByProjectOutput> { // Validate input const validatedInput = getConversationsByProjectSchema.parse(input); // Create database reader const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath, minConversationSize: 5000 // Default minimum size for project conversations }); try { // Connect to database await reader.connect(); // Get conversation IDs with project-specific filtering const conversationResults = await reader.getConversationIdsByProject( validatedInput.projectPath, { filePattern: validatedInput.filePattern, exactFilePath: validatedInput.exactFilePath, orderBy: validatedInput.orderBy, limit: validatedInput.limit, format: 'both', // Support both legacy and modern formats fuzzyMatch: validatedInput.fuzzyMatch } ); // Get conversation summaries const conversations = []; for (const result of conversationResults) { try { const summary = await reader.getConversationSummary(result.composerId, { includeFirstMessage: true, maxFirstMessageLength: 100, includeFileList: true, includeCodeBlockCount: true }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles, attachedFolders: summary.attachedFolders, firstMessage: summary.firstMessage, size: summary.conversationSize, relevanceScore: result.relevanceScore }); } } catch (error) { console.error(`Failed to get summary for conversation ${result.composerId}:`, error); // Continue with other conversations } } return { conversations, totalFound: conversationResults.length, filters: { projectPath: validatedInput.projectPath, filePattern: validatedInput.filePattern, exactFilePath: validatedInput.exactFilePath, orderBy: validatedInput.orderBy, limit: validatedInput.limit } }; } finally { // Always close the database connection reader.close(); } } // Input schema for search_conversations_by_project tool (improved project search) export const searchConversationsByProjectSchema = z.object({ projectQuery: z.string().min(1), fuzzyMatch: z.boolean().optional().default(true), includePartialPaths: z.boolean().optional().default(true), includeFileContent: z.boolean().optional().default(false), minRelevanceScore: z.number().min(0).max(10).optional().default(1), orderBy: z.enum(['relevance', 'recency']).optional().default('relevance'), limit: z.number().min(1).max(1000).optional().default(50), includeDebugInfo: z.boolean().optional().default(false) }); export type SearchConversationsByProjectInput = z.infer<typeof searchConversationsByProjectSchema>; // Output type for search_conversations_by_project tool export interface SearchConversationsByProjectOutput { conversations: Array<{ composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; relevantFiles: string[]; attachedFolders: string[]; firstMessage?: string; size: number; relevanceScore: number; matchDetails?: { exactPathMatch: boolean; partialPathMatch: boolean; filePathMatch: boolean; fuzzyMatch: boolean; matchedPaths: string[]; matchedFiles: string[]; }; }>; totalFound: number; searchQuery: string; searchOptions: { fuzzyMatch: boolean; includePartialPaths: boolean; includeFileContent: boolean; minRelevanceScore: number; orderBy: string; limit: number; }; debugInfo?: { totalConversationsScanned: number; averageRelevanceScore: number; matchTypeDistribution: { exactPath: number; partialPath: number; filePath: number; fuzzy: number; }; }; } /** * Enhanced project search with fuzzy matching and flexible path matching */ export async function searchConversationsByProject(input: SearchConversationsByProjectInput): Promise<SearchConversationsByProjectOutput> { // Validate input const validatedInput = searchConversationsByProjectSchema.parse(input); // Create database reader const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath, minConversationSize: 1000 // Lower threshold for broader search }); try { // Connect to database await reader.connect(); // Get all conversations for flexible searching const allConversationIds = await reader.getConversationIds({ format: 'both', minLength: 1000 }); const results: Array<{ composerId: string; format: 'legacy' | 'modern'; messageCount: number; hasCodeBlocks: boolean; relevantFiles: string[]; attachedFolders: string[]; firstMessage?: string; size: number; relevanceScore: number; matchDetails?: any; }> = []; let totalScanned = 0; let matchTypeDistribution = { exactPath: 0, partialPath: 0, filePath: 0, fuzzy: 0 }; // Process conversations in batches to avoid memory issues const batchSize = 100; for (let i = 0; i < allConversationIds.length; i += batchSize) { const batch = allConversationIds.slice(i, i + batchSize); for (const composerId of batch) { totalScanned++; try { const conversation = await reader.getConversationById(composerId); if (!conversation) continue; // For modern format conversations, we need to resolve bubble messages to get file paths let enrichedConversation = conversation as any; if (conversation.hasOwnProperty('_v')) { // Modern format - resolve bubble messages const headers = (conversation as any).fullConversationHeadersOnly || []; const bubbleMessages: any[] = []; // Resolve a few bubble messages to get file paths (limit to avoid performance issues) const maxBubblesToResolve = Math.min(headers.length, 10); for (let i = 0; i < maxBubblesToResolve; i++) { const header = headers[i]; try { const bubbleMessage = await reader.getBubbleMessage(composerId, header.bubbleId); if (bubbleMessage) { bubbleMessages.push(bubbleMessage); } } catch (error) { // Continue with other bubbles if one fails console.error(`Failed to resolve bubble ${header.bubbleId}:`, error); } } // Add resolved messages to the conversation object for matching enrichedConversation = { ...conversation, messages: bubbleMessages }; } const matchResult = calculateEnhancedProjectRelevance( enrichedConversation, validatedInput.projectQuery, { fuzzyMatch: validatedInput.fuzzyMatch, includePartialPaths: validatedInput.includePartialPaths, includeFileContent: validatedInput.includeFileContent } ); if (matchResult.score >= validatedInput.minRelevanceScore) { const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: true, maxFirstMessageLength: 100, includeFileList: true, includeCodeBlockCount: true }); if (summary) { // Update match type distribution if (matchResult.details.exactPathMatch) matchTypeDistribution.exactPath++; if (matchResult.details.partialPathMatch) matchTypeDistribution.partialPath++; if (matchResult.details.filePathMatch) matchTypeDistribution.filePath++; if (matchResult.details.fuzzyMatch) matchTypeDistribution.fuzzy++; results.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles, attachedFolders: summary.attachedFolders, firstMessage: summary.firstMessage, size: summary.conversationSize, relevanceScore: matchResult.score, matchDetails: validatedInput.includeDebugInfo ? matchResult.details : undefined }); } } } catch (error) { console.error(`Failed to process conversation ${composerId}:`, error); // Continue with other conversations } } } // Sort by relevance or recency if (validatedInput.orderBy === 'relevance') { results.sort((a, b) => b.relevanceScore - a.relevanceScore); } else { // For recency, we rely on the original ROWID order from getConversationIds // which is already in descending order (most recent first) } const limitedResults = results.slice(0, validatedInput.limit); const debugInfo = validatedInput.includeDebugInfo ? { totalConversationsScanned: totalScanned, averageRelevanceScore: results.length > 0 ? results.reduce((sum, r) => sum + r.relevanceScore, 0) / results.length : 0, matchTypeDistribution } : undefined; return { conversations: limitedResults, totalFound: results.length, searchQuery: validatedInput.projectQuery, searchOptions: { fuzzyMatch: validatedInput.fuzzyMatch, includePartialPaths: validatedInput.includePartialPaths, includeFileContent: validatedInput.includeFileContent, minRelevanceScore: validatedInput.minRelevanceScore, orderBy: validatedInput.orderBy, limit: validatedInput.limit }, debugInfo }; } finally { // Always close the database connection reader.close(); } } /** * Calculate enhanced project relevance with fuzzy matching and flexible path matching */ function calculateEnhancedProjectRelevance( conversation: any, projectQuery: string, options: { fuzzyMatch: boolean; includePartialPaths: boolean; includeFileContent: boolean; } ): { score: number; details: { exactPathMatch: boolean; partialPathMatch: boolean; filePathMatch: boolean; fuzzyMatch: boolean; matchedPaths: string[]; matchedFiles: string[]; }; } { let score = 0; const details = { exactPathMatch: false, partialPathMatch: false, filePathMatch: false, fuzzyMatch: false, matchedPaths: [] as string[], matchedFiles: [] as string[] }; const queryLower = projectQuery.toLowerCase(); const queryParts = queryLower.split(/[-_\s]+/); // Split on common separators // Helper function for fuzzy matching const fuzzyMatch = (text: string, query: string): number => { const textLower = text.toLowerCase(); // Exact match if (textLower.includes(query)) return 10; // Check if all query parts are present const allPartsPresent = queryParts.every(part => textLower.includes(part)); if (allPartsPresent) return 8; // Check for partial matches const partialMatches = queryParts.filter(part => textLower.includes(part)).length; if (partialMatches > 0) return (partialMatches / queryParts.length) * 6; // Levenshtein-like similarity for very fuzzy matching const similarity = calculateSimilarity(textLower, query); if (similarity > 0.6) return similarity * 4; return 0; }; // Helper function to process files and folders const processFiles = (files: string[], scoreMultiplier: number = 1) => { if (!files || !Array.isArray(files)) return; for (const file of files) { if (typeof file === 'string') { const fileName = file.split('/').pop() || file; const filePath = file.toLowerCase(); const fileNameLower = fileName.toLowerCase(); // Check if file path contains project query if (filePath.includes(queryLower)) { score += 10 * scoreMultiplier; details.filePathMatch = true; details.matchedFiles.push(file); } // Check file name else if (fileNameLower.includes(queryLower)) { score += 8 * scoreMultiplier; details.filePathMatch = true; details.matchedFiles.push(file); } // Fuzzy match on file paths else if (options.fuzzyMatch) { const fuzzyScore = Math.max( fuzzyMatch(file, queryLower), fuzzyMatch(fileName, queryLower) ); if (fuzzyScore > 0) { score += fuzzyScore * 0.5 * scoreMultiplier; // Lower weight for file matches details.fuzzyMatch = true; details.matchedFiles.push(file); } } } } }; const processFolders = (folders: string[], scoreMultiplier: number = 1) => { if (!folders || !Array.isArray(folders)) return; for (const folder of folders) { if (typeof folder === 'string') { const folderName = folder.split('/').pop() || folder; // Get last part of path const folderLower = folder.toLowerCase(); // Exact path match if (folderLower === queryLower || folderName.toLowerCase() === queryLower) { score += 20 * scoreMultiplier; details.exactPathMatch = true; details.matchedPaths.push(folder); } // Partial path match else if (options.includePartialPaths && (folderLower.includes(queryLower) || folderName.toLowerCase().includes(queryLower))) { score += 15 * scoreMultiplier; details.partialPathMatch = true; details.matchedPaths.push(folder); } // Fuzzy match else if (options.fuzzyMatch) { const fuzzyScore = Math.max( fuzzyMatch(folder, queryLower), fuzzyMatch(folderName, queryLower) ); if (fuzzyScore > 0) { score += fuzzyScore * scoreMultiplier; details.fuzzyMatch = true; details.matchedPaths.push(folder); } } } } }; // Check top-level attachedFoldersNew and relevantFiles (legacy format) processFolders(conversation.attachedFoldersNew); processFiles(conversation.relevantFiles); // Check legacy conversation messages if (conversation.conversation && Array.isArray(conversation.conversation)) { for (const message of conversation.conversation) { processFolders(message.attachedFoldersNew, 0.8); processFiles(message.relevantFiles, 0.8); // Check message content if enabled if (options.includeFileContent && message.text && typeof message.text === 'string') { const textLower = message.text.toLowerCase(); if (textLower.includes(queryLower)) { score += 2; // Lower weight for content matches } } } } // Check modern format messages (this is the key fix!) if (conversation.messages && Array.isArray(conversation.messages)) { for (const message of conversation.messages) { processFolders(message.attachedFolders, 0.8); processFiles(message.relevantFiles, 0.8); // Check message content if enabled if (options.includeFileContent && message.text && typeof message.text === 'string') { const textLower = message.text.toLowerCase(); if (textLower.includes(queryLower)) { score += 2; // Lower weight for content matches } } } } // Check modern format bubbles for additional context if (conversation._v && conversation.bubbles && Array.isArray(conversation.bubbles)) { for (const bubble of conversation.bubbles) { processFolders(bubble.attachedFoldersNew, 0.5); processFiles(bubble.relevantFiles, 0.5); } } return { score: Math.max(score, 0), details }; } /** * Calculate string similarity (simplified Levenshtein-based) */ function calculateSimilarity(str1: string, str2: string): number { const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) return 1.0; const editDistance = levenshteinDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } /** * Calculate Levenshtein distance between two strings */ function levenshteinDistance(str1: string, str2: string): number { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } /** * Check if a conversation falls within the specified date range */ function checkConversationDateRange(conversation: any, startDate?: string, endDate?: string): boolean { if (!startDate && !endDate) return true; const start = startDate ? new Date(startDate) : new Date('1970-01-01'); const end = endDate ? new Date(endDate) : new Date(); // Check if conversation is legacy or modern format const isLegacy = conversation.conversation && Array.isArray(conversation.conversation); if (isLegacy) { // Legacy format: check timestamps in conversation.conversation array for (const message of conversation.conversation) { if (message.timestamp) { const messageDate = new Date(message.timestamp); if (messageDate >= start && messageDate <= end) { return true; } } } } else { // Modern format: would need to resolve bubble messages to check timestamps // For now, return true to include all modern conversations when date filtering // since resolving all bubble messages would be too expensive return true; } // If no valid timestamps found, include the conversation return true; }

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