| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- /**
- * 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<IssueHandlerResult> {
- 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<string, any> = {};
- 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
- };
- }
- }
- }
|