claude-client.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * Claude Agent SDK Client for Issue Automation
  3. */
  4. import { query } from '@anthropic-ai/claude-agent-sdk';
  5. import { writeFileSync } from 'fs';
  6. import { join } from 'path';
  7. import { ClaudeClientConfig, IssueData, IssueHandlerResult } from './types.js';
  8. import { Config } from './config.js';
  9. export class ClaudeClient {
  10. private config: ClaudeClientConfig;
  11. constructor(config: ClaudeClientConfig) {
  12. this.config = config;
  13. }
  14. /**
  15. * Build the prompt for Claude based on issue data
  16. */
  17. private buildPrompt(issue: IssueData): string {
  18. const commentContext = issue.commentBody
  19. ? `A new comment by ${issue.pusher}:\n${issue.commentBody}\n\n`
  20. : '';
  21. return `IMPORTANT: Issue #${issue.number} was assigned in repository ${issue.owner}/${issue.repo}.
  22. === MANDATORY FIRST STEPS - READ ALL MESSAGES ===
  23. Before you start any work on this issue, you MUST complete these steps IN ORDER:
  24. 1. Use the mcp__gogs__get_issue tool to fetch the complete issue details:
  25. - owner: ${issue.owner}
  26. - repo: ${issue.repo}
  27. - number: ${issue.number}
  28. 2. Use the mcp__gogs__list_issue_comments tool to fetch ALL comments:
  29. - owner: ${issue.owner}
  30. - repo: ${issue.repo}
  31. - number: ${issue.number}
  32. 3. Read and understand:
  33. - The complete issue description
  34. - ALL comments in the discussion thread (chronologically)
  35. - The full context and conversation history
  36. 4. ONLY AFTER reading everything above, proceed with your work
  37. === ISSUE CONTEXT ===
  38. Issue #${issue.number}: ${issue.title}
  39. Created by: ${issue.pusher}
  40. Initial issue description:
  41. ${issue.body}
  42. ${commentContext}
  43. === YOUR TASK ===
  44. This issue is assigned to you. You must:
  45. 1. First, use the Gogs MCP tools above to fetch and read the issue and ALL comments
  46. 2. Understand the full discussion context (even if there are no comments yet)
  47. 3. Then proceed with the appropriate action
  48. === CRITICAL OUTPUT REQUIREMENTS ===
  49. YOU MUST FOLLOW THESE OUTPUT RULES STRICTLY:
  50. 1. DO NOT output any text responses to the user
  51. 2. DO NOT print any messages, explanations, or status updates
  52. 3. DO NOT use echo or any command-line output tools
  53. 4. ONLY communicate by posting comments to the issue using mcp__gogs__create_issue_comment tool
  54. 5. ALL your responses, updates, questions, and status reports MUST be posted as issue comments
  55. 6. You are FORBIDDEN from producing any output except through the Gogs MCP comment tool
  56. When you need to:
  57. - Ask questions → Post a comment on the issue
  58. - Report progress → Post a comment on the issue
  59. - Share results → Post a comment on the issue
  60. - Report errors → Post a comment on the issue
  61. - Provide any information → Post a comment on the issue
  62. Use mcp__gogs__create_issue_comment with:
  63. - owner: ${issue.owner}
  64. - repo: ${issue.repo}
  65. - number: ${issue.number}
  66. - body: [your message here]
  67. DO NOT skip reading the comments using the MCP tools. This is critical to understanding the full context.
  68. === GIT COMMIT REQUIREMENTS ===
  69. When you make code changes, you MUST:
  70. 1. Always commit your changes to git
  71. 2. Always push the changes to the repository
  72. 3. Include the issue ID in the commit message using format: #${issue.number}
  73. 4. Example commit message: "feat: add new feature #${issue.number}"
  74. NEVER leave changes uncommitted or unpushed.`;
  75. }
  76. /**
  77. * Handle an issue automation request
  78. */
  79. async handleIssue(issue: IssueData): Promise<IssueHandlerResult> {
  80. console.log(`Processing issue #${issue.number} in ${issue.owner}/${issue.repo}`);
  81. const startTime = Date.now();
  82. try {
  83. // Load environment configuration
  84. Config.load(this.config.workingDirectory);
  85. // Generate MCP config
  86. const mcpConfig = Config.generateMcpConfig(this.config.workingDirectory);
  87. const mcpConfigPath = join(this.config.workingDirectory, '.mcp-gogs.json');
  88. writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
  89. console.log(`Generated MCP config at: ${mcpConfigPath}`);
  90. console.log(`Working directory: ${this.config.workingDirectory}`);
  91. // Build the prompt
  92. const prompt = this.buildPrompt(issue);
  93. // Create a Map to store MCP servers
  94. const mcpServers: Record<string, any> = {};
  95. mcpConfig.mcpServers && Object.keys(mcpConfig.mcpServers).forEach(key => {
  96. mcpServers[key] = mcpConfig.mcpServers[key];
  97. });
  98. console.log('Starting Claude agent session...');
  99. // Create query with options
  100. const queryInstance = query({
  101. prompt,
  102. options: {
  103. cwd: this.config.workingDirectory,
  104. mcpServers,
  105. allowedTools: this.config.allowedTools,
  106. allowDangerouslySkipPermissions: this.config.dangerouslySkipPermissions || true
  107. }
  108. });
  109. // Iterate through messages
  110. let sessionId: string | undefined;
  111. let totalCost = 0;
  112. let messageCount = 0;
  113. for await (const message of queryInstance) {
  114. messageCount++;
  115. // Extract session ID and cost information
  116. if ('sessionId' in message) {
  117. sessionId = message.sessionId as string;
  118. }
  119. if ('cost' in message && typeof message.cost === 'number') {
  120. totalCost = message.cost;
  121. }
  122. // Log progress
  123. if (message.type === 'assistant' || message.type === 'result') {
  124. console.log(`Message ${messageCount}: ${message.type}`);
  125. }
  126. }
  127. const duration = Date.now() - startTime;
  128. console.log('Claude agent session completed');
  129. console.log(`Total messages: ${messageCount}`);
  130. console.log(`Total cost: $${totalCost}`);
  131. console.log(`Duration: ${duration}ms`);
  132. return {
  133. success: true,
  134. sessionId,
  135. cost: totalCost,
  136. duration,
  137. output: `Processed ${messageCount} messages`
  138. };
  139. } catch (error) {
  140. console.error('Error handling issue:', error);
  141. return {
  142. success: false,
  143. error: error instanceof Error ? error.message : String(error),
  144. duration: Date.now() - startTime
  145. };
  146. }
  147. }
  148. }