claude-client.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /**
  2. * Claude Agent SDK Client for Issue Automation
  3. */
  4. import { query } from '@anthropic-ai/claude-agent-sdk';
  5. import { writeFileSync, readFileSync, existsSync } 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. * Read the CLAUDE.md memory file if it exists
  16. */
  17. private readMemoryFile(): string {
  18. const memoryFilePath = join(this.config.workingDirectory, 'CLAUDE.md');
  19. if (existsSync(memoryFilePath)) {
  20. try {
  21. const content = readFileSync(memoryFilePath, 'utf-8');
  22. return content;
  23. } catch (error) {
  24. console.warn(`Warning: Could not read CLAUDE.md: ${error}`);
  25. return '';
  26. }
  27. }
  28. return '';
  29. }
  30. /**
  31. * Build the prompt for Claude based on issue data
  32. */
  33. private buildPrompt(issue: IssueData): string {
  34. const commentContext = issue.commentBody
  35. ? `A new comment by ${issue.pusher}:\n${issue.commentBody}\n\n`
  36. : '';
  37. const memoryContent = this.readMemoryFile();
  38. const memorySection = memoryContent
  39. ? `\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`
  40. : '';
  41. // Parse labels if provided
  42. const labels = issue.labels ? issue.labels.split(',').filter(l => l.trim()) : [];
  43. const hasQuestionLabel = labels.includes('question');
  44. const hasDocumentationLabel = labels.includes('documentation');
  45. const hasBugLabel = labels.includes('bug');
  46. const hasEnhancementLabel = labels.includes('enhancement');
  47. // Build label-specific instructions
  48. let labelInstructions = '';
  49. if (labels.length > 0) {
  50. labelInstructions = `\n=== ISSUE LABELS AND BEHAVIOR ===\n\n`;
  51. labelInstructions += `This issue has the following labels: ${labels.join(', ')}\n\n`;
  52. if (hasQuestionLabel) {
  53. labelInstructions += `⚠️ QUESTION LABEL DETECTED:\n`;
  54. labelInstructions += `- This issue is labeled as "question"\n`;
  55. labelInstructions += `- DO NOT implement any code changes or features\n`;
  56. labelInstructions += `- Your role is to provide information, guidance, and answers only\n`;
  57. labelInstructions += `- You may read code to understand context, but do not modify files\n`;
  58. labelInstructions += `- Respond with helpful explanations, suggestions, or clarifications\n\n`;
  59. }
  60. if (hasDocumentationLabel) {
  61. labelInstructions += `📚 DOCUMENTATION LABEL DETECTED:\n`;
  62. labelInstructions += `- This issue is about documentation\n`;
  63. labelInstructions += `- Focus on creating, updating, or improving documentation files\n`;
  64. labelInstructions += `- Acceptable changes: README.md, CLAUDE.md, other .md files, code comments\n`;
  65. labelInstructions += `- Avoid implementation changes unless specifically requested for documentation examples\n\n`;
  66. }
  67. if (hasBugLabel) {
  68. labelInstructions += `🐛 BUG LABEL DETECTED:\n`;
  69. labelInstructions += `- This issue reports a bug or defect\n`;
  70. labelInstructions += `- Focus on identifying and fixing the root cause\n`;
  71. labelInstructions += `- Implement minimal changes necessary to resolve the bug\n`;
  72. labelInstructions += `- Add tests if appropriate to prevent regression\n\n`;
  73. }
  74. if (hasEnhancementLabel) {
  75. labelInstructions += `✨ ENHANCEMENT LABEL DETECTED:\n`;
  76. labelInstructions += `- This issue requests a new feature or improvement\n`;
  77. labelInstructions += `- You may implement code changes as requested\n`;
  78. labelInstructions += `- Follow existing code patterns and architecture\n`;
  79. labelInstructions += `- Consider backward compatibility\n\n`;
  80. }
  81. }
  82. return `IMPORTANT: Issue #${issue.number} was assigned in repository ${issue.owner}/${issue.repo}.
  83. ${memorySection}
  84. ${labelInstructions}
  85. === MANDATORY FIRST STEPS - READ ALL MESSAGES ===
  86. Before you start any work on this issue, you MUST complete these steps IN ORDER:
  87. 1. Use the mcp__gogs__get_issue tool to fetch the complete issue details:
  88. - owner: ${issue.owner}
  89. - repo: ${issue.repo}
  90. - number: ${issue.number}
  91. 2. Use the mcp__gogs__list_issue_comments tool to fetch ALL comments:
  92. - owner: ${issue.owner}
  93. - repo: ${issue.repo}
  94. - number: ${issue.number}
  95. 3. Read and understand:
  96. - The complete issue description
  97. - ALL comments in the discussion thread (chronologically)
  98. - The full context and conversation history
  99. 4. **IMMEDIATELY** post a comment using mcp__gogs__create_issue_comment to acknowledge you've started working:
  100. - Mention the issue creator (@${issue.pusher}) in the comment
  101. - Briefly state that you're starting work on the issue
  102. - Optionally include a high-level plan of what you'll do
  103. - This MUST be done before any other work (code changes, analysis, etc.)
  104. - Example: "@${issue.pusher} I'm starting work on this issue now! I'll [brief description of approach]"
  105. 5. ONLY AFTER posting the acknowledgment comment, proceed with your work
  106. === ISSUE CONTEXT ===
  107. Issue #${issue.number}: ${issue.title}
  108. Created by: ${issue.pusher}
  109. Initial issue description:
  110. ${issue.body}
  111. ${commentContext}
  112. === YOUR TASK ===
  113. This issue is assigned to you. You must:
  114. 1. First, use the Gogs MCP tools above to fetch and read the issue and ALL comments
  115. 2. Understand the full discussion context (even if there are no comments yet)
  116. 3. Then proceed with the appropriate action
  117. === CRITICAL OUTPUT REQUIREMENTS ===
  118. YOU MUST FOLLOW THESE OUTPUT RULES STRICTLY:
  119. 1. DO NOT output any text responses to the user
  120. 2. DO NOT print any messages, explanations, or status updates
  121. 3. DO NOT use echo or any command-line output tools
  122. 4. ONLY communicate by posting comments to the issue using mcp__gogs__create_issue_comment tool
  123. 5. ALL your responses, updates, questions, and status reports MUST be posted as issue comments
  124. 6. You are FORBIDDEN from producing any output except through the Gogs MCP comment tool
  125. When you need to:
  126. - Ask questions → Post a comment on the issue
  127. - Report progress → Post a comment on the issue
  128. - Share results → Post a comment on the issue
  129. - Report errors → Post a comment on the issue
  130. - Provide any information → Post a comment on the issue
  131. Use mcp__gogs__create_issue_comment with:
  132. - owner: ${issue.owner}
  133. - repo: ${issue.repo}
  134. - number: ${issue.number}
  135. - body: [your message here]
  136. DO NOT skip reading the comments using the MCP tools. This is critical to understanding the full context.
  137. === GIT COMMIT REQUIREMENTS ===
  138. When you make code changes, you MUST:
  139. 1. Always commit your changes to git
  140. 2. Always push the changes to the repository
  141. 3. Include the issue ID in the commit message using format: #${issue.number}
  142. 4. Example commit message: "feat: add new feature #${issue.number}"
  143. NEVER leave changes uncommitted or unpushed.`;
  144. }
  145. /**
  146. * Handle an issue automation request
  147. */
  148. async handleIssue(issue: IssueData): Promise<IssueHandlerResult> {
  149. console.log(`Processing issue #${issue.number} in ${issue.owner}/${issue.repo}`);
  150. const startTime = Date.now();
  151. try {
  152. // Load environment configuration
  153. Config.load(this.config.workingDirectory);
  154. // Generate MCP config (with owner/repo for per-repository config support)
  155. const mcpConfig = Config.generateMcpConfig(this.config.workingDirectory, issue.owner, issue.repo);
  156. const mcpConfigPath = join(this.config.workingDirectory, '.mcp-gogs.json');
  157. writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
  158. console.log(`Generated MCP config at: ${mcpConfigPath}`);
  159. console.log(`Working directory: ${this.config.workingDirectory}`);
  160. // Build the prompt
  161. const prompt = this.buildPrompt(issue);
  162. // Create a Map to store MCP servers
  163. const mcpServers: Record<string, any> = {};
  164. mcpConfig.mcpServers && Object.keys(mcpConfig.mcpServers).forEach(key => {
  165. mcpServers[key] = mcpConfig.mcpServers[key];
  166. });
  167. console.log('Starting Claude agent session...');
  168. // Log query arguments
  169. console.log('\n=== QUERY ARGUMENTS ===');
  170. console.log('Working directory:', this.config.workingDirectory);
  171. console.log('Allowed tools count:', this.config.allowedTools.length);
  172. console.log('Allowed tools:', JSON.stringify(this.config.allowedTools, null, 2));
  173. console.log('MCP servers:', Object.keys(mcpServers));
  174. console.log('Skip permissions:', this.config.dangerouslySkipPermissions || true);
  175. console.log('Prompt length:', prompt.length, 'characters');
  176. console.log('=======================\n');
  177. // Create query with options
  178. const queryInstance = query({
  179. prompt,
  180. options: {
  181. cwd: this.config.workingDirectory,
  182. mcpServers,
  183. allowedTools: this.config.allowedTools,
  184. allowDangerouslySkipPermissions: this.config.dangerouslySkipPermissions || true
  185. }
  186. });
  187. // Iterate through messages
  188. let sessionId: string | undefined;
  189. let totalCost = 0;
  190. let messageCount = 0;
  191. console.log('\n=== CLAUDE OUTPUT ===\n');
  192. for await (const message of queryInstance) {
  193. messageCount++;
  194. // Extract session ID and cost information
  195. if ('sessionId' in message) {
  196. sessionId = message.sessionId as string;
  197. }
  198. if ('cost' in message && typeof message.cost === 'number') {
  199. totalCost = message.cost;
  200. }
  201. // Log message details
  202. console.log(`\n--- Message ${messageCount}: ${message.type} ---`);
  203. // Log all message properties for debugging
  204. const messageKeys = Object.keys(message);
  205. console.log('Available properties:', messageKeys.join(', '));
  206. // Try to extract and log content
  207. if ('content' in message) {
  208. const content = (message as any).content;
  209. if (typeof content === 'string') {
  210. console.log('Content (string):', content);
  211. } else if (Array.isArray(content)) {
  212. console.log('Content (array):', JSON.stringify(content, null, 2));
  213. } else if (content) {
  214. console.log('Content (object):', JSON.stringify(content, null, 2));
  215. }
  216. }
  217. // Log tool use if present
  218. if ('toolUse' in message) {
  219. console.log('Tool use:', JSON.stringify((message as any).toolUse, null, 2));
  220. }
  221. // Log result if present
  222. if ('result' in message) {
  223. console.log('Result:', JSON.stringify((message as any).result, null, 2));
  224. }
  225. // Log full message for unknown types
  226. if (message.type === 'stream_event') {
  227. console.log('Stream event:', JSON.stringify(message, null, 2));
  228. }
  229. }
  230. console.log('\n=== END CLAUDE OUTPUT ===\n');
  231. const duration = Date.now() - startTime;
  232. console.log('Claude agent session completed');
  233. console.log(`Total messages: ${messageCount}`);
  234. console.log(`Total cost: $${totalCost}`);
  235. console.log(`Duration: ${duration}ms`);
  236. return {
  237. success: true,
  238. sessionId,
  239. cost: totalCost,
  240. duration,
  241. output: `Processed ${messageCount} messages`
  242. };
  243. } catch (error) {
  244. console.error('Error handling issue:', error);
  245. return {
  246. success: false,
  247. error: error instanceof Error ? error.message : String(error),
  248. duration: Date.now() - startTime
  249. };
  250. }
  251. }
  252. }