/ lib / github-analyzer.ts
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  }