import { NextRequest, NextResponse } from 'next/server'; import { adminAuth, adminDb } from '@/lib/firebase/admin'; export async function POST(request: NextRequest) { try { // Verify authentication using API key const authHeader = request.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json( { error: 'Unauthorized - Missing API key' }, { status: 401 } ); } const apiKey = authHeader.substring(7); // Look up user by API key const apiKeysSnapshot = await adminDb .collection('apiKeys') .where('key', '==', apiKey) .where('isActive', '==', true) .limit(1) .get(); if (apiKeysSnapshot.empty) { return NextResponse.json( { error: 'Invalid API key' }, { status: 401 } ); } const apiKeyDoc = apiKeysSnapshot.docs[0]; const apiKeyData = apiKeyDoc.data(); const userId = apiKeyData.userId; if (!userId) { return NextResponse.json( { error: 'API key not associated with user' }, { status: 401 } ); } // Parse request body const body = await request.json(); const { projectId, workspacePath, githubUrl, conversations } = body; if (!projectId || !conversations) { return NextResponse.json( { error: 'Missing required fields: projectId, conversations' }, { status: 400 } ); } // Verify user has access to the project const projectRef = adminDb.collection('projects').doc(projectId); const projectDoc = await projectRef.get(); if (!projectDoc.exists) { return NextResponse.json( { error: 'Project not found' }, { status: 404 } ); } const projectData = projectDoc.data(); if (projectData?.userId !== userId) { return NextResponse.json( { error: 'Access denied to this project' }, { status: 403 } ); } // Process and store conversations const { composers, workspaceFiles, totalGenerations } = conversations; let conversationCount = 0; let totalMessagesWritten = 0; // Determine filtering keywords based on project context // TODO: Make this configurable per project const projectKeywords = ['vibn', 'project', 'extension', 'collector', 'cursor-monitor']; const excludeKeywords = ['nhl', 'hockey', 'market', 'transaction']; // Store each composer (chat session) as a separate document for (const composer of composers || []) { if (composer.type !== 'head') continue; // Only process head composers const conversationId = `cursor-${composer.composerId}`; const conversationRef = adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .doc(conversationId); const name = composer.name || 'Untitled Conversation'; const nameLower = name.toLowerCase(); // Simple relevance scoring let relevanceScore = 0; // Check for project keywords in name for (const keyword of projectKeywords) { if (nameLower.includes(keyword)) { relevanceScore += 2; } } // Penalize for exclude keywords for (const keyword of excludeKeywords) { if (nameLower.includes(keyword)) { relevanceScore -= 3; } } // Check if name mentions files from this workspace if (workspaceFiles && Array.isArray(workspaceFiles)) { for (const file of workspaceFiles) { if (nameLower.includes(file.toLowerCase())) { relevanceScore += 1; } } } // Count messages let messageCount = 0; if (composer.bubbles && Array.isArray(composer.bubbles)) { messageCount = composer.bubbles.length; } const conversationData = { userId, projectId, conversationId, composerId: composer.composerId, name, createdAt: new Date(composer.createdAt).toISOString(), lastUpdatedAt: new Date(composer.lastUpdatedAt).toISOString(), unifiedMode: composer.unifiedMode || false, forceMode: composer.forceMode || false, workspacePath, githubUrl: githubUrl || null, importedAt: new Date().toISOString(), relevanceScore, // For filtering messageCount, metadata: { source: 'cursor-monitor-extension', composerType: composer.type, } }; // Write conversation document first await conversationRef.set(conversationData); // Store messages in chunks to avoid Firestore batch limit (500 operations) if (composer.bubbles && Array.isArray(composer.bubbles)) { const BATCH_SIZE = 400; // Leave room for overhead for (let i = 0; i < composer.bubbles.length; i += BATCH_SIZE) { const batch = adminDb.batch(); const chunk = composer.bubbles.slice(i, i + BATCH_SIZE); for (const bubble of chunk) { const messageRef = conversationRef .collection('messages') .doc(bubble.bubbleId); batch.set(messageRef, { bubbleId: bubble.bubbleId, type: bubble.type, // 1 = user, 2 = AI role: bubble.type === 1 ? 'user' : bubble.type === 2 ? 'assistant' : 'unknown', text: bubble.text || '', createdAt: bubble.createdAt, requestId: bubble.requestId, attachedFiles: bubble.attachedFiles || [] }); } await batch.commit(); totalMessagesWritten += chunk.length; console.log(`✅ Wrote ${chunk.length} messages (${i + chunk.length}/${composer.bubbles.length}) for ${name}`); } } conversationCount++; } // Store workspace metadata for reference const workspaceMetaRef = adminDb .collection('projects') .doc(projectId) .collection('cursorData') .doc('workspace-meta'); await workspaceMetaRef.set({ workspacePath, githubUrl, workspaceFiles: workspaceFiles || [], totalGenerations: totalGenerations || 0, importedAt: new Date().toISOString(), lastBatchImportedAt: new Date().toISOString(), }, { merge: true }); console.log(`✅ Imported ${conversationCount} conversations to project ${projectId}`); const workspaceFilesCount = conversations.workspaceFiles?.length || workspaceFiles?.length || 0; const generationsCount = conversations.totalGenerations || totalGenerations || 0; return NextResponse.json({ success: true, conversationCount, totalMessages: totalMessagesWritten, workspaceFilesCount, totalGenerations: generationsCount, message: `Successfully imported ${conversationCount} conversations with ${totalMessagesWritten} messages` }); } catch (error) { console.error('Error importing Cursor conversations:', error); return NextResponse.json( { error: 'Failed to import conversations', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } }