Skip to main content
Glama
videoCodeAnalyzer.ts16.3 kB
import { LoomClient } from './loomClient' import type { VideoAnalysisService } from './videoAnalysis' import { execSync } from 'child_process' import * as fs from 'fs' import * as path from 'path' export interface CodeContext { files: string[] components: string[] routes: string[] apis: string[] errors: string[] keywords: string[] } export interface VideoCodeAnalysis { videoId: string videoTitle: string codeContext: CodeContext relevantFiles: FileMatch[] suggestedFiles: string[] analysisNotes: string[] confidenceScore: number } export interface FileMatch { path: string relevance: number reason: string snippets?: CodeSnippet[] } export interface CodeSnippet { line: number content: string context: string } export class VideoCodeAnalyzer { constructor( private loomClient: LoomClient, private videoAnalysisService: VideoAnalysisService, private codebasePath: string ) {} async analyzeVideoWithCodebase(videoUrl: string): Promise<VideoCodeAnalysis> { // Extract video ID from URL const videoId = LoomClient.extractVideoId(videoUrl) if (!videoId) { throw new Error(`Invalid Loom URL: ${videoUrl}`) } // Create LoomVideo object const loomVideo = { url: videoUrl, videoId: videoId, source: 'direct' as const, } // Get video analysis const videoAnalysis = await this.videoAnalysisService.analyzeVideo(loomVideo, { includeTranscript: true, detectAccounts: true, }) // Extract code context from video const codeContext = await this.extractCodeContext(videoAnalysis) // Search codebase for relevant files const relevantFiles = await this.findRelevantFiles(codeContext) // Generate suggested files based on patterns const suggestedFiles = await this.suggestAdditionalFiles(codeContext, relevantFiles) // Calculate confidence score const confidenceScore = this.calculateConfidence(relevantFiles, codeContext) // Generate analysis notes const analysisNotes = this.generateAnalysisNotes(videoAnalysis, codeContext, relevantFiles) return { videoId: videoAnalysis.videoId, videoTitle: videoAnalysis.metadata?.name || 'Untitled', codeContext, relevantFiles, suggestedFiles, analysisNotes, confidenceScore, } } private async extractCodeContext(videoAnalysis: any): Promise<CodeContext> { const context: CodeContext = { files: [], components: [], routes: [], apis: [], errors: [], keywords: [], } // Extract from video title and description if (videoAnalysis.metadata?.name) { context.keywords.push(...this.extractKeywords(videoAnalysis.metadata.name)) } // Extract from transcript if available if (videoAnalysis.transcript?.segments) { for (const segment of videoAnalysis.transcript.segments) { const text = segment.text.toLowerCase() // Look for file mentions const fileMatches = text.match(/[\w-]+\.(js|jsx|ts|tsx|css|scss|json|md)/g) if (fileMatches) { context.files.push(...fileMatches) } // Look for component names (capitalized words) const componentMatches = text.match( /\b[A-Z][a-zA-Z]+(?:Component|Page|Modal|Form|Button|List|View)?\b/g ) if (componentMatches) { context.components.push(...componentMatches) } // Look for routes/URLs const routeMatches = text.match(/\/[\w-/]+/g) if (routeMatches) { context.routes.push(...routeMatches) } // Look for API endpoints const apiMatches = text.match( /(?:api|endpoint|fetch|post|get|put|delete)\s+[\w-/]+/g ) if (apiMatches) { context.apis.push(...apiMatches) } // Look for error messages const errorMatches = text.match( /(?:error|exception|failed|cannot|unable|invalid)[\s\w]+/g ) if (errorMatches) { context.errors.push(...errorMatches) } } } // Extract from visual elements (UI text, error messages, etc.) if (videoAnalysis.visualElements) { context.keywords.push(...(videoAnalysis.visualElements.uiText || [])) context.errors.push(...(videoAnalysis.visualElements.errorMessages || [])) } // Deduplicate context.files = [...new Set(context.files)] context.components = [...new Set(context.components)] context.routes = [...new Set(context.routes)] context.apis = [...new Set(context.apis)] context.errors = [...new Set(context.errors)] context.keywords = [...new Set(context.keywords)] return context } private async findRelevantFiles(context: CodeContext): Promise<FileMatch[]> { const matches: FileMatch[] = [] // Search for exact file matches for (const file of context.files) { const foundFiles = this.searchFiles(file) for (const foundFile of foundFiles) { matches.push({ path: foundFile, relevance: 1.0, reason: `Exact file name match: ${file}`, snippets: await this.extractRelevantSnippets(foundFile, context), }) } } // Search for components for (const component of context.components) { const componentFiles = this.searchFiles(`*${component}*`) for (const file of componentFiles) { if (!matches.some((m) => m.path === file)) { matches.push({ path: file, relevance: 0.8, reason: `Component name match: ${component}`, snippets: await this.extractRelevantSnippets(file, context), }) } } } // Search for routes in router files if (context.routes.length > 0) { const routerFiles = this.searchInFiles(context.routes.join('|')) for (const file of routerFiles) { if (!matches.some((m) => m.path === file)) { matches.push({ path: file, relevance: 0.7, reason: `Route definition found`, snippets: await this.extractRelevantSnippets(file, context), }) } } } // Search for API endpoints if (context.apis.length > 0) { const apiFiles = this.searchInFiles(context.apis.join('|')) for (const file of apiFiles) { if (!matches.some((m) => m.path === file)) { matches.push({ path: file, relevance: 0.7, reason: `API endpoint found`, snippets: await this.extractRelevantSnippets(file, context), }) } } } // Search for error messages if (context.errors.length > 0) { const errorKeywords = context.errors .map((e) => e.split(' ').slice(0, 3).join(' ')) .filter((e) => e.length > 5) if (errorKeywords.length > 0) { const errorFiles = this.searchInFiles(errorKeywords.join('|')) for (const file of errorFiles) { if (!matches.some((m) => m.path === file)) { matches.push({ path: file, relevance: 0.6, reason: `Error message found`, snippets: await this.extractRelevantSnippets(file, context), }) } } } } // Sort by relevance return matches.sort((a, b) => b.relevance - a.relevance) } private searchFiles(pattern: string): string[] { try { const result = execSync( `find ${this.codebasePath} -type f -name "${pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -20`, { encoding: 'utf8' } ) return result.split('\n').filter((f) => f.length > 0) } catch { return [] } } private searchInFiles(pattern: string): string[] { try { // Use ripgrep if available, otherwise fall back to grep const command = this.isCommandAvailable('rg') ? `rg -l "${pattern}" ${this.codebasePath} --type-add 'web:*.{js,jsx,ts,tsx}' -t web --max-count 1 | head -20` : `grep -r -l "${pattern}" ${this.codebasePath} --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" | head -20` const result = execSync(command, { encoding: 'utf8' }) return result.split('\n').filter((f) => f.length > 0) } catch { return [] } } private isCommandAvailable(command: string): boolean { try { execSync(`which ${command}`, { encoding: 'utf8' }) return true } catch { return false } } private async extractRelevantSnippets( filePath: string, context: CodeContext ): Promise<CodeSnippet[]> { const snippets: CodeSnippet[] = [] try { const content = fs.readFileSync(filePath, 'utf8') const lines = content.split('\n') // Search for relevant lines const searchTerms = [ ...context.components, ...context.routes, ...context.apis.map((a) => a.split(' ').pop() || ''), ...context.errors.map((e) => e.split(' ').slice(0, 3).join(' ')), ...context.keywords, ].filter((t) => t && t.length > 2) for (let i = 0; i < lines.length; i++) { const line = lines[i] const lineLower = line.toLowerCase() for (const term of searchTerms) { if (lineLower.includes(term.toLowerCase())) { snippets.push({ line: i + 1, content: line.trim(), context: term, }) break } } } } catch (error) { // File read error } return snippets.slice(0, 10) // Limit to 10 most relevant snippets } private async suggestAdditionalFiles( context: CodeContext, foundFiles: FileMatch[] ): Promise<string[]> { const suggestions: Set<string> = new Set() // Suggest test files for found components for (const file of foundFiles) { if (file.path.match(/\.(jsx?|tsx?)$/)) { const testFile = file.path.replace(/\.(jsx?|tsx?)$/, '.test.$1') if (fs.existsSync(testFile)) { suggestions.add(testFile) } } } // Suggest style files for (const component of context.components) { const stylePatterns = [ `${component}.css`, `${component}.scss`, `${component}.module.css`, `${component}.module.scss`, ] for (const pattern of stylePatterns) { const files = this.searchFiles(pattern) files.forEach((f) => suggestions.add(f)) } } // Suggest API route handlers based on routes for (const route of context.routes) { const apiFiles = this.searchInFiles(`"${route}"`) apiFiles.forEach((f) => suggestions.add(f)) } return Array.from(suggestions).filter((f) => !foundFiles.some((fm) => fm.path === f)) } private calculateConfidence(files: FileMatch[], context: CodeContext): number { let score = 0 let factors = 0 // Factor 1: File matches if (files.length > 0) { score += Math.min(files.length / 5, 1) * 0.3 factors++ } // Factor 2: High relevance matches const highRelevance = files.filter((f) => f.relevance >= 0.8).length if (highRelevance > 0) { score += Math.min(highRelevance / 3, 1) * 0.3 factors++ } // Factor 3: Component matches if (context.components.length > 0) { score += Math.min(context.components.length / 5, 1) * 0.2 factors++ } // Factor 4: Error message matches if (context.errors.length > 0 && files.some((f) => f.reason.includes('Error'))) { score += 0.2 factors++ } return factors > 0 ? score / factors : 0 } private generateAnalysisNotes( videoAnalysis: any, context: CodeContext, files: FileMatch[] ): string[] { const notes: string[] = [] // Video summary if (videoAnalysis.metadata?.name) { notes.push( `Video: "${videoAnalysis.metadata.name}" (${this.formatDuration(videoAnalysis.metadata.video_properties?.duration)})` ) } // Key findings if (context.components.length > 0) { notes.push(`Components identified: ${context.components.slice(0, 5).join(', ')}`) } if (context.routes.length > 0) { notes.push(`Routes/pages shown: ${context.routes.slice(0, 5).join(', ')}`) } if (context.errors.length > 0) { notes.push(`Errors/issues: ${context.errors.slice(0, 3).join('; ')}`) } // File recommendations if (files.length > 0) { notes.push( `Most relevant files: ${files .slice(0, 3) .map((f) => path.basename(f.path)) .join(', ')}` ) } // Confidence assessment const confidence = this.calculateConfidence(files, context) if (confidence > 0.7) { notes.push('High confidence match - strong correlation between video and code') } else if (confidence > 0.4) { notes.push('Medium confidence - some relevant files found') } else { notes.push('Low confidence - limited code correlation found') } return notes } private extractKeywords(text: string): string[] { // Extract meaningful keywords from text const words = text .toLowerCase() .replace(/[^a-z0-9\s-]/g, ' ') .split(/\s+/) .filter((w) => w.length > 3) .filter( (w) => ![ 'this', 'that', 'with', 'from', 'have', 'been', 'were', 'what', 'when', 'where', ].includes(w) ) return [...new Set(words)] } private formatDuration(seconds?: number): string { if (!seconds) return 'unknown duration' const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } }

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/currentspace/shortcut_mcp'

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