github-analyzer.ts
1 // lib/github-analyzer.ts 2 // Shared GitHub fetch + analyze function — used by SSE pipeline and /api/analyze-github 3 4 import { callClaude } from '@/lib/claude'; 5 import { 6 buildGitHubAnalysisPrompt, 7 GITHUB_ANALYSIS_FALLBACK, 8 type GitHubAnalysis, 9 type GitHubUserData, 10 type GitHubRepoData, 11 } from '@/lib/prompts/github-analysis'; 12 import { logger } from '@/lib/logger'; 13 14 export interface AnalyzeGitHubOptions { 15 githubUrl: string; 16 targetRole: string; 17 language?: string; 18 } 19 20 /** 21 * Fetch GitHub profile + repos and analyze with Claude. 22 * Returns null on any failure (GitHub 404, rate limit, etc.) — never throws. 23 */ 24 export async function analyzeGitHubProfile( 25 options: AnalyzeGitHubOptions 26 ): Promise<GitHubAnalysis | null> { 27 try { 28 const { githubUrl, targetRole, language } = options; 29 30 // Extract username from URL 31 const usernameMatch = githubUrl.match(/github\.com\/([a-zA-Z0-9-]+)/); 32 if (!usernameMatch) { 33 logger.warn('github_analyzer.invalid_url', { githubUrl }); 34 return null; 35 } 36 const username = usernameMatch[1]; 37 38 // Fetch GitHub API (no auth, 60 req/hr) 39 logger.debug('github_analyzer.fetching', { username }); 40 41 const [userRes, reposRes] = await Promise.all([ 42 fetch(`https://api.github.com/users/${username}`, { 43 headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'GapZero' }, 44 }), 45 fetch(`https://api.github.com/users/${username}/repos?sort=updated&per_page=30`, { 46 headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'GapZero' }, 47 }), 48 ]); 49 50 if (!userRes.ok) { 51 logger.warn('github_analyzer.user_fetch_failed', { username, status: userRes.status }); 52 return null; 53 } 54 55 const user: GitHubUserData = await userRes.json(); 56 const repos: GitHubRepoData[] = reposRes.ok ? await reposRes.json() : []; 57 58 // Call Claude 59 logger.debug('github_analyzer.analyzing', { username, repoCount: repos.length, targetRole }); 60 61 const prompt = buildGitHubAnalysisPrompt({ user, repos, targetRole, language }); 62 const analysis = await callClaude<GitHubAnalysis>({ 63 ...prompt, 64 maxTokens: 4096, 65 temperature: 0.3, 66 fallback: GITHUB_ANALYSIS_FALLBACK, 67 }); 68 69 logger.debug('github_analyzer.done', { 70 username, 71 strengths: analysis.strengths?.length || 0, 72 improvements: analysis.improvements?.length || 0, 73 }); 74 75 return analysis; 76 } catch (e) { 77 logger.warn('github_analyzer.failed', { error: e instanceof Error ? e.message : String(e) }); 78 return null; 79 } 80 }