/** * Claude Agent SDK Client for Issue Automation */ import { query } from '@anthropic-ai/claude-agent-sdk'; import { writeFileSync, readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { ClaudeClientConfig, IssueData, IssueHandlerResult } from './types.js'; import { Config } from './config.js'; export class ClaudeClient { private config: ClaudeClientConfig; constructor(config: ClaudeClientConfig) { this.config = config; } /** * Read the CLAUDE.md memory file if it exists */ private readMemoryFile(): string { const memoryFilePath = join(this.config.workingDirectory, 'CLAUDE.md'); if (existsSync(memoryFilePath)) { try { const content = readFileSync(memoryFilePath, 'utf-8'); return content; } catch (error) { console.warn(`Warning: Could not read CLAUDE.md: ${error}`); return ''; } } return ''; } /** * Build the prompt for Claude based on issue data */ private buildPrompt(issue: IssueData): string { const commentContext = issue.commentBody ? `A new comment by ${issue.pusher}:\n${issue.commentBody}\n\n` : ''; const memoryContent = this.readMemoryFile(); const memorySection = memoryContent ? `\n=== PROJECT MEMORY (CLAUDE.md) ===\n\nThe following is the project memory file that contains important context about this repository.\nYou MUST read and understand this information as it contains project-specific guidelines, architecture details, and development patterns.\n\n${memoryContent}\n\n` : ''; // Parse labels if provided const labels = issue.labels ? issue.labels.split(',').filter(l => l.trim()) : []; const hasQuestionLabel = labels.includes('question'); const hasDocumentationLabel = labels.includes('documentation'); const hasBugLabel = labels.includes('bug'); const hasEnhancementLabel = labels.includes('enhancement'); // Build label-specific instructions let labelInstructions = ''; if (labels.length > 0) { labelInstructions = `\n=== ISSUE LABELS AND BEHAVIOR ===\n\n`; labelInstructions += `This issue has the following labels: ${labels.join(', ')}\n\n`; if (hasQuestionLabel) { labelInstructions += `⚠️ QUESTION LABEL DETECTED:\n`; labelInstructions += `- This issue is labeled as "question"\n`; labelInstructions += `- DO NOT implement any code changes or features\n`; labelInstructions += `- Your role is to provide information, guidance, and answers only\n`; labelInstructions += `- You may read code to understand context, but do not modify files\n`; labelInstructions += `- Respond with helpful explanations, suggestions, or clarifications\n\n`; } if (hasDocumentationLabel) { labelInstructions += `📚 DOCUMENTATION LABEL DETECTED:\n`; labelInstructions += `- This issue is about documentation\n`; labelInstructions += `- Focus on creating, updating, or improving documentation files\n`; labelInstructions += `- Acceptable changes: README.md, CLAUDE.md, other .md files, code comments\n`; labelInstructions += `- Avoid implementation changes unless specifically requested for documentation examples\n\n`; } if (hasBugLabel) { labelInstructions += `🐛 BUG LABEL DETECTED:\n`; labelInstructions += `- This issue reports a bug or defect\n`; labelInstructions += `- Focus on identifying and fixing the root cause\n`; labelInstructions += `- Implement minimal changes necessary to resolve the bug\n`; labelInstructions += `- Add tests if appropriate to prevent regression\n\n`; } if (hasEnhancementLabel) { labelInstructions += `✨ ENHANCEMENT LABEL DETECTED:\n`; labelInstructions += `- This issue requests a new feature or improvement\n`; labelInstructions += `- You may implement code changes as requested\n`; labelInstructions += `- Follow existing code patterns and architecture\n`; labelInstructions += `- Consider backward compatibility\n\n`; } } return `IMPORTANT: Issue #${issue.number} was assigned in repository ${issue.owner}/${issue.repo}. ${memorySection} ${labelInstructions} === MANDATORY FIRST STEPS - READ ALL MESSAGES === Before you start any work on this issue, you MUST complete these steps IN ORDER: 1. Use the mcp__gogs__get_issue tool to fetch the complete issue details: - owner: ${issue.owner} - repo: ${issue.repo} - number: ${issue.number} 2. Use the mcp__gogs__list_issue_comments tool to fetch ALL comments: - owner: ${issue.owner} - repo: ${issue.repo} - number: ${issue.number} 3. Read and understand: - The complete issue description - ALL comments in the discussion thread (chronologically) - The full context and conversation history 4. **IMMEDIATELY** post a comment using mcp__gogs__create_issue_comment to acknowledge you've started working: - Mention the issue creator (@${issue.pusher}) in the comment - Briefly state that you're starting work on the issue - Optionally include a high-level plan of what you'll do - This MUST be done before any other work (code changes, analysis, etc.) - Example: "@${issue.pusher} I'm starting work on this issue now! I'll [brief description of approach]" 5. ONLY AFTER posting the acknowledgment comment, proceed with your work === ISSUE CONTEXT === Issue #${issue.number}: ${issue.title} Created by: ${issue.pusher} Initial issue description: ${issue.body} ${commentContext} === YOUR TASK === This issue is assigned to you. You must: 1. First, use the Gogs MCP tools above to fetch and read the issue and ALL comments 2. Understand the full discussion context (even if there are no comments yet) 3. Then proceed with the appropriate action === CRITICAL OUTPUT REQUIREMENTS === YOU MUST FOLLOW THESE OUTPUT RULES STRICTLY: 1. DO NOT output any text responses to the user 2. DO NOT print any messages, explanations, or status updates 3. DO NOT use echo or any command-line output tools 4. ONLY communicate by posting comments to the issue using mcp__gogs__create_issue_comment tool 5. ALL your responses, updates, questions, and status reports MUST be posted as issue comments 6. You are FORBIDDEN from producing any output except through the Gogs MCP comment tool When you need to: - Ask questions → Post a comment on the issue - Report progress → Post a comment on the issue - Share results → Post a comment on the issue - Report errors → Post a comment on the issue - Provide any information → Post a comment on the issue Use mcp__gogs__create_issue_comment with: - owner: ${issue.owner} - repo: ${issue.repo} - number: ${issue.number} - body: [your message here] DO NOT skip reading the comments using the MCP tools. This is critical to understanding the full context. === GIT COMMIT REQUIREMENTS === When you make code changes, you MUST: 1. Always commit your changes to git 2. Always push the changes to the repository 3. Include the issue ID in the commit message using format: #${issue.number} 4. Example commit message: "feat: add new feature #${issue.number}" NEVER leave changes uncommitted or unpushed.`; } /** * Handle an issue automation request */ async handleIssue(issue: IssueData): Promise { console.log(`Processing issue #${issue.number} in ${issue.owner}/${issue.repo}`); const startTime = Date.now(); try { // Load environment configuration Config.load(this.config.workingDirectory); // Generate MCP config (with owner/repo for per-repository config support) const mcpConfig = Config.generateMcpConfig(this.config.workingDirectory, issue.owner, issue.repo); const mcpConfigPath = join(this.config.workingDirectory, '.mcp-gogs.json'); writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2)); console.log(`Generated MCP config at: ${mcpConfigPath}`); console.log(`Working directory: ${this.config.workingDirectory}`); // Build the prompt const prompt = this.buildPrompt(issue); // Create a Map to store MCP servers const mcpServers: Record = {}; mcpConfig.mcpServers && Object.keys(mcpConfig.mcpServers).forEach(key => { mcpServers[key] = mcpConfig.mcpServers[key]; }); console.log('Starting Claude agent session...'); // Log query arguments console.log('\n=== QUERY ARGUMENTS ==='); console.log('Working directory:', this.config.workingDirectory); console.log('Allowed tools count:', this.config.allowedTools.length); console.log('Allowed tools:', JSON.stringify(this.config.allowedTools, null, 2)); console.log('MCP servers:', Object.keys(mcpServers)); console.log('Skip permissions:', this.config.dangerouslySkipPermissions || true); console.log('Prompt length:', prompt.length, 'characters'); console.log('=======================\n'); // Create query with options const queryInstance = query({ prompt, options: { cwd: this.config.workingDirectory, mcpServers, allowedTools: this.config.allowedTools, allowDangerouslySkipPermissions: this.config.dangerouslySkipPermissions || true } }); // Iterate through messages let sessionId: string | undefined; let totalCost = 0; let messageCount = 0; console.log('\n=== CLAUDE OUTPUT ===\n'); for await (const message of queryInstance) { messageCount++; // Extract session ID and cost information if ('sessionId' in message) { sessionId = message.sessionId as string; } if ('cost' in message && typeof message.cost === 'number') { totalCost = message.cost; } // Log message details console.log(`\n--- Message ${messageCount}: ${message.type} ---`); // Log all message properties for debugging const messageKeys = Object.keys(message); console.log('Available properties:', messageKeys.join(', ')); // Try to extract and log content if ('content' in message) { const content = (message as any).content; if (typeof content === 'string') { console.log('Content (string):', content); } else if (Array.isArray(content)) { console.log('Content (array):', JSON.stringify(content, null, 2)); } else if (content) { console.log('Content (object):', JSON.stringify(content, null, 2)); } } // Log tool use if present if ('toolUse' in message) { console.log('Tool use:', JSON.stringify((message as any).toolUse, null, 2)); } // Log result if present if ('result' in message) { console.log('Result:', JSON.stringify((message as any).result, null, 2)); } // Log full message for unknown types if (message.type === 'stream_event') { console.log('Stream event:', JSON.stringify(message, null, 2)); } } console.log('\n=== END CLAUDE OUTPUT ===\n'); const duration = Date.now() - startTime; console.log('Claude agent session completed'); console.log(`Total messages: ${messageCount}`); console.log(`Total cost: $${totalCost}`); console.log(`Duration: ${duration}ms`); return { success: true, sessionId, cost: totalCost, duration, output: `Processed ${messageCount} messages` }; } catch (error) { console.error('Error handling issue:', error); return { success: false, error: error instanceof Error ? error.message : String(error), duration: Date.now() - startTime }; } } }