11import { execSync } from "child_process" ;
2- import OpenAI from "openai" ;
32import fs from "fs" ;
43
5- const client = new OpenAI ( { apiKey : process . env . OPENAI_API_KEY } ) ;
4+ // Support multiple AI providers
5+ const provider = process . env . AI_PROVIDER || "claude" ; // claude, openai, or gemini
66const task = process . env . TASK || "Improve code quality" ;
77const requester = process . env . REQUESTER || "unknown" ;
8+ const webhookUrl = process . env . SLACK_WEBHOOK_URL ;
9+ const runId = process . env . GITHUB_RUN_ID ;
10+
11+ // Initialize AI client based on provider
12+ let aiClient = null ;
13+ let aiModel = null ;
14+
15+ if ( provider === "claude" ) {
16+ // Claude (Anthropic) - FREE tier available!
17+ const { Anthropic } = await import ( "@anthropic-ai/sdk" ) ;
18+ const apiKey = process . env . ANTHROPIC_API_KEY || process . env . CLAUDE_API_KEY ;
19+ if ( ! apiKey ) {
20+ console . error ( "[AGENT] ANTHROPIC_API_KEY or CLAUDE_API_KEY not set" ) ;
21+ process . exit ( 1 ) ;
22+ }
23+ aiClient = new Anthropic ( { apiKey } ) ;
24+ aiModel = process . env . CLAUDE_MODEL || "claude-3-5-sonnet-20241022" ;
25+ console . log ( `[AGENT] Using Claude (Anthropic) with model: ${ aiModel } ` ) ;
26+ } else if ( provider === "openai" ) {
27+ // OpenAI (original)
28+ const { default : OpenAI } = await import ( "openai" ) ;
29+ const apiKey = process . env . OPENAI_API_KEY ;
30+ if ( ! apiKey ) {
31+ console . error ( "[AGENT] OPENAI_API_KEY not set" ) ;
32+ process . exit ( 1 ) ;
33+ }
34+ aiClient = new OpenAI ( { apiKey } ) ;
35+ aiModel = process . env . OPENAI_MODEL || "gpt-4o" ;
36+ console . log ( `[AGENT] Using OpenAI with model: ${ aiModel } ` ) ;
37+ } else if ( provider === "gemini" ) {
38+ // Google Gemini - FREE tier available!
39+ const { GoogleGenerativeAI } = await import ( "@google/generative-ai" ) ;
40+ const apiKey = process . env . GEMINI_API_KEY || process . env . GOOGLE_API_KEY ;
41+ if ( ! apiKey ) {
42+ console . error ( "[AGENT] GEMINI_API_KEY or GOOGLE_API_KEY not set" ) ;
43+ process . exit ( 1 ) ;
44+ }
45+ const genAI = new GoogleGenerativeAI ( apiKey ) ;
46+ aiModel = process . env . GEMINI_MODEL || "gemini-1.5-pro" ;
47+ aiClient = genAI . getGenerativeModel ( { model : aiModel } ) ;
48+ console . log ( `[AGENT] Using Google Gemini with model: ${ aiModel } ` ) ;
49+ } else {
50+ console . error ( `[AGENT] Unknown provider: ${ provider } . Use: claude, openai, or gemini` ) ;
51+ process . exit ( 1 ) ;
52+ }
853
954function sh ( cmd ) {
1055 return execSync ( cmd , { encoding : "utf8" , stdio : "pipe" } ) ;
1156}
1257
58+ // Send error notification to Slack
59+ async function notifySlackError ( errorMessage , errorType = "Unknown error" ) {
60+ if ( ! webhookUrl ) {
61+ console . log ( "[AGENT] No SLACK_WEBHOOK_URL configured, skipping notification" ) ;
62+ return ;
63+ }
64+
65+ const errorEmoji = errorType . includes ( "quota" ) ? "💳" : "❌" ;
66+ const message = {
67+ text : `${ errorEmoji } AICODE Agent Failed` ,
68+ blocks : [
69+ {
70+ type : "section" ,
71+ text : {
72+ type : "mrkdwn" ,
73+ text : `${ errorEmoji } *AICODE Agent Failed*\n\n*Task:* ${ task } \n*Requested by:* ${ requester } \n*Error:* ${ errorType } \n\n${ errorMessage } `
74+ }
75+ } ,
76+ {
77+ type : "section" ,
78+ text : {
79+ type : "mrkdwn" ,
80+ text : `<https://github.com/htilly/SlackONOS/actions/runs/${ runId } |View GitHub Actions logs>`
81+ }
82+ }
83+ ]
84+ } ;
85+
86+ try {
87+ const response = await fetch ( webhookUrl , {
88+ method : "POST" ,
89+ headers : { "Content-Type" : "application/json" } ,
90+ body : JSON . stringify ( message )
91+ } ) ;
92+ if ( ! response . ok ) {
93+ console . error ( `[AGENT] Failed to send Slack notification: ${ response . status } ` ) ;
94+ } else {
95+ console . log ( "[AGENT] Error notification sent to Slack" ) ;
96+ }
97+ } catch ( err ) {
98+ console . error ( `[AGENT] Error sending Slack notification: ${ err . message } ` ) ;
99+ }
100+ }
101+
102+ // Handle errors and exit gracefully
103+ async function handleError ( error , errorType = "Unknown error" ) {
104+ let errorMessage = error . message || String ( error ) ;
105+
106+ // Format quota errors more clearly
107+ if ( error . code === "insufficient_quota" || error . type === "insufficient_quota" ) {
108+ errorType = `${ provider . toUpperCase ( ) } API Quota Exceeded` ;
109+ if ( provider === "claude" ) {
110+ errorMessage = "You exceeded your Anthropic API quota. Check billing at https://console.anthropic.com/" ;
111+ } else if ( provider === "openai" ) {
112+ errorMessage = "You exceeded your OpenAI API quota. Check billing at https://platform.openai.com/account/billing" ;
113+ } else {
114+ errorMessage = "You exceeded your API quota. Please check your plan and billing details." ;
115+ }
116+ } else if ( error . status === 429 || error . statusCode === 429 ) {
117+ errorType = `${ provider . toUpperCase ( ) } API Rate Limit` ;
118+ errorMessage = "API rate limit exceeded. Please try again later." ;
119+ } else if ( error . status === 401 || error . statusCode === 401 ) {
120+ errorType = `${ provider . toUpperCase ( ) } API Authentication Failed` ;
121+ if ( provider === "claude" ) {
122+ errorMessage = "Invalid Anthropic API key. Please check your GitHub secrets (ANTHROPIC_API_KEY or CLAUDE_API_KEY)." ;
123+ } else if ( provider === "openai" ) {
124+ errorMessage = "Invalid OpenAI API key. Please check your GitHub secrets (OPENAI_API_KEY)." ;
125+ } else {
126+ errorMessage = "Invalid API key. Please check your GitHub secrets." ;
127+ }
128+ }
129+
130+ console . error ( `[AGENT] ${ errorType } : ${ errorMessage } ` ) ;
131+
132+ // Send notification to Slack
133+ await notifySlackError ( errorMessage , errorType ) ;
134+
135+ process . exit ( 1 ) ;
136+ }
137+
13138console . log ( `[AGENT] Starting AI code agent for task: ${ task } ` ) ;
14139console . log ( `[AGENT] Requested by: ${ requester } ` ) ;
15140
@@ -56,14 +181,40 @@ Generate a safe, focused code change as a unified git diff. The diff will be app
56181
57182Remember: Output ONLY the git diff, no explanations, no markdown code blocks, just the raw diff.` ;
58183
59- console . log ( "[AGENT] Calling OpenAI API..." ) ;
60- const res = await client . chat . completions . create ( {
61- model : "gpt-4o" ,
62- temperature : 0.2 ,
63- messages : [ { role : "user" , content : prompt } ] ,
64- } ) ;
184+ // Call AI provider with unified interface
185+ async function callAI ( promptText ) {
186+ if ( provider === "claude" ) {
187+ const response = await aiClient . messages . create ( {
188+ model : aiModel ,
189+ max_tokens : 4096 ,
190+ temperature : 0.2 ,
191+ messages : [ { role : "user" , content : promptText } ] ,
192+ } ) ;
193+ return response . content [ 0 ] . text ;
194+ } else if ( provider === "openai" ) {
195+ const response = await aiClient . chat . completions . create ( {
196+ model : aiModel ,
197+ temperature : 0.2 ,
198+ messages : [ { role : "user" , content : promptText } ] ,
199+ } ) ;
200+ return response . choices [ 0 ] . message . content ;
201+ } else if ( provider === "gemini" ) {
202+ const result = await aiClient . generateContent ( {
203+ contents : [ { role : "user" , parts : [ { text : promptText } ] } ] ,
204+ generationConfig : { temperature : 0.2 } ,
205+ } ) ;
206+ return result . response . text ( ) ;
207+ }
208+ }
65209
66- const output = res . choices [ 0 ] . message . content ;
210+ console . log ( `[AGENT] Calling ${ provider . toUpperCase ( ) } API...` ) ;
211+ let output ;
212+ try {
213+ output = await callAI ( prompt ) ;
214+ } catch ( error ) {
215+ await handleError ( error , `${ provider . toUpperCase ( ) } API Error` ) ;
216+ return ; // Exit already handled
217+ }
67218
68219// Extract diff from potential markdown code blocks
69220let diff = output ;
@@ -77,10 +228,9 @@ if (output.includes("```")) {
77228
78229// Validate diff format
79230if ( ! diff . includes ( "diff --git" ) ) {
80- console . error ( "[AGENT] Model did not return a valid diff" ) ;
81- console . error ( "[AGENT] Output was:" ) ;
82- console . error ( output ) ;
83- process . exit ( 1 ) ;
231+ const errorMsg = `Model did not return a valid diff. Output was:\n\`\`\`\n${ output . substring ( 0 , 500 ) } \n\`\`\`` ;
232+ await handleError ( new Error ( errorMsg ) , "Invalid Diff Format" ) ;
233+ return ;
84234}
85235
86236// Safety check: Ensure we're not touching forbidden files
@@ -94,16 +244,22 @@ const forbiddenPatterns = [
94244
95245for ( const pattern of forbiddenPatterns ) {
96246 if ( pattern . test ( diff ) ) {
97- console . error ( `[AGENT] SAFETY VIOLATION: Attempted to modify forbidden file matching ${ pattern } ` ) ;
98- process . exit ( 1 ) ;
247+ await handleError (
248+ new Error ( `Attempted to modify forbidden file matching ${ pattern } ` ) ,
249+ "Safety Violation"
250+ ) ;
251+ return ;
99252 }
100253}
101254
102255// Count lines changed
103256const linesChanged = ( diff . match ( / ^ [ + - ] [ ^ + - ] / gm) || [ ] ) . length ;
104257if ( linesChanged > 300 ) {
105- console . error ( `[AGENT] SAFETY VIOLATION: Too many lines changed (${ linesChanged } > 300)` ) ;
106- process . exit ( 1 ) ;
258+ await handleError (
259+ new Error ( `Too many lines changed (${ linesChanged } > 300). Maximum allowed is 300 lines.` ) ,
260+ "Safety Violation"
261+ ) ;
262+ return ;
107263}
108264
109265console . log ( `[AGENT] Generated diff with ${ linesChanged } lines changed` ) ;
@@ -115,10 +271,9 @@ try {
115271 sh ( "git apply /tmp/aicode.patch" ) ;
116272 console . log ( "[AGENT] Patch applied successfully" ) ;
117273} catch ( err ) {
118- console . error ( "[AGENT] Failed to apply patch:" , err . message ) ;
119- console . error ( "[AGENT] Diff was:" ) ;
120- console . error ( diff ) ;
121- process . exit ( 1 ) ;
274+ const errorMsg = `Failed to apply patch: ${ err . message } \n\nDiff preview:\n\`\`\`\n${ diff . substring ( 0 , 500 ) } \n\`\`\`` ;
275+ await handleError ( new Error ( errorMsg ) , "Patch Application Failed" ) ;
276+ return ;
122277}
123278
124279// Show diff for logs
0 commit comments