230 lines
7.1 KiB
TypeScript
230 lines
7.1 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|
|
|