UNPKG

@codai/memorai-core

Version:

Simplified advanced memory engine - no tiers, just powerful semantic search with persistence

417 lines (416 loc) 16.2 kB
import { Octokit } from '@octokit/rest'; import { logger } from '../utils/logger.js'; export class GitHubIntegration { constructor(config) { this.config = config; this.octokit = new Octokit({ auth: config.token, }); } /** * Extract code context from repository files */ async extractCodeContext() { logger.info('Extracting code context from GitHub repository', { owner: this.config.owner, repo: this.config.repo, }); const contexts = []; const branches = this.config.branches || ['main', 'master']; for (const branch of branches) { try { const { data: tree } = await this.octokit.rest.git.getTree({ owner: this.config.owner, repo: this.config.repo, tree_sha: branch, recursive: 'true', }); const files = tree.tree.filter((item) => item.type === 'blob' && this.shouldProcessFile(item.path || '')); for (const file of files.slice(0, 50)) { // Limit to prevent rate limits try { const context = await this.extractFileContext(file.path, branch); if (context) { contexts.push(context); } } catch (error) { logger.warn('Failed to extract context from file', { file: file.path, error: error instanceof Error ? error.message : 'Unknown error', }); } } } catch (error) { logger.warn('Failed to process branch', { branch, error: error instanceof Error ? error.message : 'Unknown error', }); } } logger.info('Code context extraction completed', { filesProcessed: contexts.length, }); return contexts; } /** * Extract context from repository issues */ async extractIssueContext() { logger.info('Extracting issue context from GitHub repository'); const contexts = []; try { const { data: issues } = await this.octokit.rest.issues.listForRepo({ owner: this.config.owner, repo: this.config.repo, state: 'all', per_page: 100, }); for (const issue of issues) { if (!issue.pull_request) { // Skip pull requests const context = await this.extractSingleIssueContext(issue.number); contexts.push(context); } } } catch (error) { logger.error('Failed to extract issue context', { error: error instanceof Error ? error.message : 'Unknown error', }); } logger.info('Issue context extraction completed', { issuesProcessed: contexts.length, }); return contexts; } /** * Extract context from pull requests */ async extractPullRequestContext() { logger.info('Extracting pull request context from GitHub repository'); const contexts = []; try { const { data: pulls } = await this.octokit.rest.pulls.list({ owner: this.config.owner, repo: this.config.repo, state: 'all', per_page: 100, }); for (const pull of pulls) { try { const context = await this.extractSinglePullRequestContext(pull.number); contexts.push(context); } catch (error) { logger.warn('Failed to extract PR context', { prNumber: pull.number, error: error instanceof Error ? error.message : 'Unknown error', }); } } } catch (error) { logger.error('Failed to extract pull request context', { error: error instanceof Error ? error.message : 'Unknown error', }); } logger.info('Pull request context extraction completed', { pullRequestsProcessed: contexts.length, }); return contexts; } async extractFileContext(filePath, branch) { try { const { data: fileData } = await this.octokit.rest.repos.getContent({ owner: this.config.owner, repo: this.config.repo, path: filePath, ref: branch, }); if ('content' in fileData && fileData.content) { const content = Buffer.from(fileData.content, 'base64').toString('utf8'); // Get commit info for the file const { data: commits } = await this.octokit.rest.repos.listCommits({ owner: this.config.owner, repo: this.config.repo, path: filePath, per_page: 1, }); const lastCommit = commits[0]; return { filePath, content, language: this.detectLanguage(filePath), lastModified: new Date(lastCommit?.commit.committer?.date || Date.now()), author: lastCommit?.commit.author?.name || 'Unknown', commitHash: lastCommit?.sha || '', commitMessage: lastCommit?.commit.message || '', functions: this.extractFunctions(content, this.detectLanguage(filePath)), classes: this.extractClasses(content, this.detectLanguage(filePath)), imports: this.extractImports(content, this.detectLanguage(filePath)), }; } } catch (error) { logger.warn('Failed to extract file context', { filePath, error: error instanceof Error ? error.message : 'Unknown error', }); } return null; } async extractSingleIssueContext(issueNumber) { const { data: issue } = await this.octokit.rest.issues.get({ owner: this.config.owner, repo: this.config.repo, issue_number: issueNumber, }); const { data: comments } = await this.octokit.rest.issues.listComments({ owner: this.config.owner, repo: this.config.repo, issue_number: issueNumber, }); return { number: issue.number, title: issue.title, body: issue.body || '', state: issue.state, labels: issue.labels.map((label) => typeof label === 'string' ? label : label.name || ''), assignees: issue.assignees?.map((assignee) => assignee.login) || [], createdAt: new Date(issue.created_at), updatedAt: new Date(issue.updated_at), comments: comments.map((comment) => ({ author: comment.user?.login || 'Unknown', body: comment.body || '', createdAt: new Date(comment.created_at), })), }; } async extractSinglePullRequestContext(prNumber) { const { data: pr } = await this.octokit.rest.pulls.get({ owner: this.config.owner, repo: this.config.repo, pull_number: prNumber, }); const { data: files } = await this.octokit.rest.pulls.listFiles({ owner: this.config.owner, repo: this.config.repo, pull_number: prNumber, }); const { data: reviews } = await this.octokit.rest.pulls.listReviews({ owner: this.config.owner, repo: this.config.repo, pull_number: prNumber, }); return { number: pr.number, title: pr.title, body: pr.body || '', state: pr.state, head: pr.head.ref, base: pr.base.ref, author: pr.user?.login || 'Unknown', reviewers: reviews.map((review) => review.user?.login || 'Unknown'), changedFiles: files.map((file) => ({ filename: file.filename, status: file.status, additions: file.additions, deletions: file.deletions, })), createdAt: new Date(pr.created_at), updatedAt: new Date(pr.updated_at), }; } shouldProcessFile(filePath) { // Check file extensions if (this.config.fileExtensions) { const ext = filePath.split('.').pop()?.toLowerCase(); if (!ext || !this.config.fileExtensions.includes(ext)) { return false; } } // Check include paths if (this.config.includePaths) { const included = this.config.includePaths.some(path => filePath.startsWith(path)); if (!included) return false; } // Check exclude paths if (this.config.excludePaths) { const excluded = this.config.excludePaths.some(path => filePath.startsWith(path)); if (excluded) return false; } return true; } detectLanguage(filePath) { const ext = filePath.split('.').pop()?.toLowerCase(); const languageMap = { js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript', py: 'python', java: 'java', cpp: 'cpp', c: 'c', cs: 'csharp', go: 'go', rs: 'rust', php: 'php', rb: 'ruby', swift: 'swift', kt: 'kotlin', scala: 'scala', sh: 'bash', sql: 'sql', md: 'markdown', json: 'json', yaml: 'yaml', yml: 'yaml', xml: 'xml', html: 'html', css: 'css', scss: 'scss', sass: 'sass', }; return languageMap[ext || ''] || 'text'; } extractFunctions(content, language) { const functions = []; try { switch (language) { case 'javascript': case 'typescript': // Match function declarations and arrow functions const jsPattern = /(?:function\s+(\w+)|(\w+)\s*(?::\s*\w+)?\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*{)|(\w+)\s*\([^)]*\)\s*{)/g; let jsMatch; while ((jsMatch = jsPattern.exec(content)) !== null) { functions.push(jsMatch[1] || jsMatch[2] || jsMatch[3]); } break; case 'python': const pyPattern = /def\s+(\w+)\s*\(/g; let pyMatch; while ((pyMatch = pyPattern.exec(content)) !== null) { functions.push(pyMatch[1]); } break; // Add more language patterns as needed } } catch (error) { logger.warn('Failed to extract functions', { language, error }); } return functions; } extractClasses(content, language) { const classes = []; try { switch (language) { case 'javascript': case 'typescript': const jsPattern = /class\s+(\w+)/g; let jsMatch; while ((jsMatch = jsPattern.exec(content)) !== null) { classes.push(jsMatch[1]); } break; case 'python': const pyPattern = /class\s+(\w+)/g; let pyMatch; while ((pyMatch = pyPattern.exec(content)) !== null) { classes.push(pyMatch[1]); } break; // Add more language patterns as needed } } catch (error) { logger.warn('Failed to extract classes', { language, error }); } return classes; } extractImports(content, language) { const imports = []; try { switch (language) { case 'javascript': case 'typescript': const jsPattern = /import\s+.*?from\s+['"]([^'"]+)['"]/g; let jsMatch; while ((jsMatch = jsPattern.exec(content)) !== null) { imports.push(jsMatch[1]); } break; case 'python': const pyPattern = /(?:from\s+(\S+)\s+)?import\s+([^#\n]+)/g; let pyMatch; while ((pyMatch = pyPattern.exec(content)) !== null) { imports.push(pyMatch[1] || pyMatch[2]); } break; // Add more language patterns as needed } } catch (error) { logger.warn('Failed to extract imports', { language, error }); } return imports; } /** * Generate memory entries from extracted contexts */ generateCodeMemories(contexts) { return contexts.map(context => ({ content: `File: ${context.filePath}\n\nLanguage: ${context.language}\n\nFunctions: ${context.functions.join(', ')}\n\nClasses: ${context.classes.join(', ')}\n\nImports: ${context.imports.join(', ')}\n\nRecent changes by ${context.author}: ${context.commitMessage}`, type: 'fact', metadata: { sourceType: 'github', filePath: context.filePath, language: context.language, author: context.author, commitHash: context.commitHash, lastModified: context.lastModified, functions: context.functions, classes: context.classes, imports: context.imports, }, })); } generateIssueMemories(contexts) { return contexts.map(context => ({ content: `Issue #${context.number}: ${context.title}\n\n${context.body}\n\nLabels: ${context.labels.join(', ')}\nState: ${context.state}\nAssignees: ${context.assignees.join(', ')}\n\nComments:\n${context.comments.map(c => `${c.author}: ${c.body}`).join('\n\n')}`, type: 'task', metadata: { sourceType: 'github', issueNumber: context.number, state: context.state, labels: context.labels, assignees: context.assignees, createdAt: context.createdAt, updatedAt: context.updatedAt, }, })); } generatePullRequestMemories(contexts) { return contexts.map(context => ({ content: `PR #${context.number}: ${context.title}\n\n${context.body}\n\nBranch: ${context.head} → ${context.base}\nAuthor: ${context.author}\nReviewers: ${context.reviewers.join(', ')}\n\nChanged files:\n${context.changedFiles.map(f => `${f.filename} (${f.status}: +${f.additions}/-${f.deletions})`).join('\n')}`, type: 'procedure', metadata: { sourceType: 'github', prNumber: context.number, state: context.state, head: context.head, base: context.base, author: context.author, reviewers: context.reviewers, changedFiles: context.changedFiles, createdAt: context.createdAt, updatedAt: context.updatedAt, }, })); } }