import {octokit} from './octokit' import {Bot} from './bot' import {type Prompts} from './prompts' import {getTokenCount} from './tokenizer' import {als} from './context' import {Inputs} from './inputs' import {type Options} from './options' import {PRIX_BRANDING, PLAN_BRANDING} from './commenter' let info = console.log let warning = console.warn export const context: any = new Proxy({} as any, { get(target, prop) { return (als.getStore()?.probotContext as any)[prop] } }) export const repo: any = new Proxy({} as any, { get(target, prop) { return (als.getStore()?.repo as any)[prop] } }) const ISSUE_COMMAND_TAG = '@prix' const MAX_API_CALLS = 100 const MAX_FILES_RETURNED = 200 let apiCallCount = 0 export const handleIssueComment = async ( heavyBot: Bot, options: Options, prompts: Prompts ) => { const inputs: Inputs = new Inputs() if (context.name !== 'issue_comment') { warning(`Skipped: ${context.name} is not an issue_comment event`) return } if (!context.payload) { warning(`Skipped: issue_comment event is missing payload`) return } if (!context.payload.repository) { warning(`Skipped: issue_comment event is missing repository`) return } const comment = context.payload.comment if (comment == null) { warning(`Skipped: issue_comment event is missing comment`) return } const issue = context.payload.issue if (issue == null) { warning(`Skipped: issue_comment event is missing issue`) return } if (context.payload.action !== 'created') { warning(`Skipped: issue_comment event is not created`) return } const isBotComment = comment.user?.type === 'Bot' if (isBotComment) { info(`Skipped: issue_comment event is from the bot itself`) return } if (!comment.body.includes(ISSUE_COMMAND_TAG)) { info(`Skipped: comment does not mention @prix`) return } if (!repo?.owner || !repo?.repo) { warning(`Skipped: repo context is missing`) return } const issueNumber = issue.number if (!issueNumber || typeof issueNumber !== 'number') { warning(`Skipped: issue number is invalid`) return } const issueTitle = issue.title || '' const issueBody = issue.body || '' const commentBody = comment.body || '' const commentUser = comment.user?.login || 'unknown' if (issueTitle.length > 500) { await postIssueComment( issueNumber, `@${commentUser} Issue title is too long. Please shorten it to under 500 characters.` ) return } if (issueBody.length > 10000) { await postIssueComment( issueNumber, `@${commentUser} Issue description is too long. Please simplify it to under 10,000 characters.` ) return } info(`Processing issue comment for issue #${issueNumber} from @${commentUser}`) inputs.title = issueTitle inputs.description = issueBody inputs.comment = `${commentUser}: ${commentBody}` const taskDescription = extractTaskFromComment(commentBody) inputs.taskDescription = taskDescription apiCallCount = 0 const counter = { fileCount: 0 } const codebaseStructure = await fetchCodebaseStructure( repo.owner, repo.repo, () => counter.fileCount >= MAX_FILES_RETURNED, counter ) inputs.codebaseStructure = codebaseStructure if (counter.fileCount > MAX_FILES_RETURNED) { info(`Codebase structure truncated: ${counter.fileCount} files found, limit is ${MAX_FILES_RETURNED}`) } const tokens = getTokenCount(prompts.renderIssueAnalysis(inputs)) if (tokens > options.heavyTokenLimits.requestTokens) { await postIssueComment( issueNumber, `@${commentUser} The issue content is too large to process. Please simplify or split the issue.` ) return } const [analysis] = await heavyBot.chat( prompts.renderIssueAnalysis(inputs), {} ) if (!analysis || analysis.trim().length === 0) { warning(`Empty analysis received for issue #${issueNumber}`) return } const response = formatIssueResponse(commentUser, analysis) await postIssueComment(issueNumber, response) info(`Successfully processed issue comment for issue #${issueNumber}`) } const EXCLUDED_DIRS = new Set([ 'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '__pycache__', 'venv', '.venv', 'target', 'bin', 'obj', '.idea', '.vscode', 'vendor', 'packages' ]) async function fetchCodebaseStructure( owner: string, repoName: string, shouldStop: () => boolean, counter?: { fileCount: number } ): Promise { try { const structure = await fetchDirectoryTree(owner, repoName, '', 3, shouldStop, 0, counter) return structure || 'Unable to fetch codebase structure' } catch (error) { warning(`Failed to fetch codebase structure: ${error}`) return 'Codebase structure unavailable' } } async function fetchDirectoryTree( owner: string, repo: string, path: string, depth: number, shouldStop: () => boolean, currentDepth = 0, counter?: { fileCount: number } ): Promise { if (currentDepth >= depth) { return '' } if (apiCallCount >= MAX_API_CALLS) { return '' } if (shouldStop()) { return '' } try { apiCallCount++ const response = await octokit.repos.getContent({ owner, repo, path, ref: context.payload.repository?.default_branch || 'main' }) const items = Array.isArray(response.data) ? response.data : [response.data] let tree = '' for (const item of items) { if (shouldStop()) { break } if (item.type === 'dir' && !EXCLUDED_DIRS.has(item.name)) { tree += `${' '.repeat(currentDepth)}📁 ${item.name}/\n` const subTree = await fetchDirectoryTree( owner, repo, item.path, depth, shouldStop, currentDepth + 1, counter ) tree += subTree } else if (item.type === 'file') { const ext = item.name.split('.').pop() || '' if (isRelevantExtension(ext)) { tree += `${' '.repeat(currentDepth)}📄 ${item.name}\n` if (counter) { counter.fileCount++ } } } } return tree } catch (error) { return '' } } function isRelevantExtension(ext: string): boolean { const relevantExts = new Set([ 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'py', 'rb', 'go', 'rs', 'java', 'kt', 'scala', 'cpp', 'c', 'h', 'hpp', 'cs', 'swift', 'json', 'yaml', 'yml', 'toml', 'xml', 'md', 'txt', 'sh', 'bash', 'zsh', 'css', 'scss', 'sass', 'less', 'html', 'htm', 'vue', 'svelte', 'sql', 'graphql', 'proto', 'dockerfile', 'env', 'gitignore', 'editorconfig' ]) return relevantExts.has(ext.toLowerCase()) } function extractTaskFromComment(commentBody: string): string { const mentionPattern = /@prix\s*(.*)$/i const match = commentBody.match(mentionPattern) return match ? match[1].trim() : commentBody } async function postIssueComment( issueNumber: number, body: string ): Promise { try { await octokit.issues.createComment({ owner: repo.owner, repo: repo.repo, issue_number: issueNumber, body }) } catch (error) { warning(`Failed to post issue comment: ${error}`) } } function formatIssueResponse(user: string, analysis: string): string { return `## @${user} Issue Analysis ${analysis}${PLAN_BRANDING}` }