codeflow-hook 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,13 @@ import ora from 'ora';
6
6
  import axios from 'axios';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
- import { execSync } from 'child_process';
9
+ import os from 'os'; // Make sure os is imported
10
10
  import readline from 'readline';
11
+ import { indexProject } from './rag.js';
12
+ import { orchestrateReview } from './agents.js';
13
+
14
+ // Export for use in agents module
15
+ export { callAIProvider };
11
16
 
12
17
  const program = new Command();
13
18
 
@@ -25,94 +30,168 @@ program
25
30
  .option('-u, --url <url>', 'Custom API URL (optional)')
26
31
  .option('-m, --model <model>', 'AI model name (optional - uses provider default)')
27
32
  .action(async (options) => {
28
- const configDir = path.join(process.env.HOME, '.codeflow-hook');
33
+ // Use USERPROFILE on Windows instead of HOME which might be undefined
34
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
35
+ const configDir = path.join(homeDir, '.codeflow-hook');
29
36
  if (!fs.existsSync(configDir)) {
30
37
  fs.mkdirSync(configDir, { recursive: true });
31
38
  }
32
39
 
33
40
  const configPath = path.join(configDir, 'config.json');
41
+
42
+ // Configuration cascade: Check for project-level config first, then fall back to global
43
+ const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
44
+ const projectConfig = fs.existsSync(projectConfigPath) ? JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')) : {};
45
+
34
46
  const existingConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
35
47
 
36
- // Initialize config with existing values, then override with CLI options
48
+ // Show config cascade message
49
+ if (Object.keys(projectConfig).length > 0) {
50
+ console.log(chalk.blue('📁 Using configuration cascade (project → global):'));
51
+ console.log(chalk.gray(` Project config: ${projectConfigPath}`));
52
+ if (fs.existsSync(configPath)) {
53
+ console.log(chalk.gray(` Global config: ${configPath}`));
54
+ }
55
+ }
56
+
57
+ // Determine what the new provider and API key should be
58
+ const requestedProvider = (options.provider || existingConfig.provider || 'gemini').toLowerCase();
59
+ const requestedApiKey = options.key || existingConfig.apiKey;
60
+
61
+ // FIX: Explicit boolean coercion to prevent || operator from returning non-boolean values
62
+ // Step by step to ensure proper boolean evaluation
63
+ const hasNewKey = !!options.key;
64
+ const isFirstTimeSetup = !existingConfig.provider && !existingConfig.apiKey;
65
+ const shouldValidate = hasNewKey || isFirstTimeSetup;
66
+
67
+ console.log('VALIDATION DEBUG - provider:', requestedProvider, 'key_exists:', !!options.key, 'shouldValidate:', shouldValidate);
68
+
69
+ if (shouldValidate && requestedApiKey) {
70
+ console.log(chalk.blue('🔐 Validating API key for provider:', requestedProvider));
71
+ const validationSpinner = ora('Checking key permissions...').start();
72
+
73
+ try {
74
+ await validateApiKey(requestedProvider, requestedApiKey);
75
+ validationSpinner.succeed('API key validated');
76
+ } catch (error) {
77
+ validationSpinner.fail('Validation failed');
78
+ console.error(chalk.red(`❌ ${error.message}`));
79
+ console.error(chalk.red(`💡 Make sure you're using a valid ${requestedProvider.toUpperCase()} API key for the ${requestedProvider} provider.`));
80
+ process.exit(1);
81
+ }
82
+ } else {
83
+ console.log(chalk.yellow('⚠️ Validation skipped - no API key validation needed'));
84
+ }
85
+
86
+ // Initialize config with existing values, then override with verified CLI options
37
87
  const config = {
38
- provider: options.provider || existingConfig.provider || 'gemini',
39
- apiKey: options.key || existingConfig.apiKey,
88
+ provider: requestedProvider,
89
+ apiKey: requestedApiKey,
40
90
  apiUrl: options.url || existingConfig.apiUrl,
41
91
  model: options.model || existingConfig.model
42
92
  };
43
93
 
44
- // Set default API URL and model if not provided by options or existing config
94
+ // Interactive model selection if model is not explicitly provided
95
+ if (!options.model) {
96
+ // FIX: More robust logic for when to fetch new models
97
+ const isNewProvider = existingConfig.provider && (existingConfig.provider.toLowerCase() !== config.provider.toLowerCase());
98
+ const hasNewKey = !!options.key;
99
+
100
+ const shouldFetchModels = !existingConfig.model || isNewProvider || hasNewKey;
101
+
102
+ if (shouldFetchModels) {
103
+ console.log(chalk.blue('🔍 Fetching available models...'));
104
+
105
+ const modelSpinner = ora('Contacting API...').start();
106
+ try {
107
+ const models = await fetchModels(config.provider, config.apiKey);
108
+ modelSpinner.succeed('Models fetched successfully');
109
+
110
+ console.log(chalk.blue('Available models:'));
111
+ models.forEach((model, index) => {
112
+ console.log(chalk.gray(` ${index + 1}. ${model}`));
113
+ });
114
+
115
+ const rl = readline.createInterface({
116
+ input: process.stdin,
117
+ output: process.stdout
118
+ });
119
+
120
+ const selectedModel = await new Promise(resolve => {
121
+ rl.question(chalk.blue(`Enter the model name (or number): `), (input) => {
122
+ rl.close();
123
+ resolve(input.trim());
124
+ });
125
+ });
126
+
127
+ // Check if user entered a number
128
+ const index = parseInt(selectedModel) - 1;
129
+ if (!isNaN(index) && index >= 0 && index < models.length) {
130
+ config.model = models[index];
131
+ } else {
132
+ config.model = selectedModel;
133
+ }
134
+
135
+ console.log(chalk.green(`✓ Selected model: ${config.model}`));
136
+
137
+ } catch (error) {
138
+ modelSpinner.fail('Failed to fetch models');
139
+ console.error(chalk.red(`❌ API Error: ${error.message}`));
140
+ // Fallback to hardcoded list if API call fails
141
+ const fallbackModels = getFallbackModels(config.provider);
142
+
143
+ console.log(chalk.yellow('⚠️ Using fallback model list:'));
144
+ fallbackModels.forEach((model, index) => {
145
+ console.log(chalk.gray(` ${index + 1}. ${model}`));
146
+ });
147
+
148
+ const rl = readline.createInterface({
149
+ input: process.stdin,
150
+ output: process.stdout
151
+ });
152
+
153
+ const selectedModel = await new Promise(resolve => {
154
+ rl.question(chalk.blue(`Select a model (name or number): `), (input) => {
155
+ rl.close();
156
+ resolve(input.trim());
157
+ });
158
+ });
159
+
160
+ const index = parseInt(selectedModel) - 1;
161
+ if (!isNaN(index) && index >= 0 && index < fallbackModels.length) {
162
+ config.model = fallbackModels[index];
163
+ } else {
164
+ config.model = selectedModel;
165
+ }
166
+
167
+ console.log(chalk.green(`✓ Selected fallback model: ${config.model}`));
168
+ }
169
+ } else {
170
+ // Reuse existing model and config
171
+ console.log(chalk.blue(`📚 Using existing configuration (provider: ${existingConfig.provider}, model: ${existingConfig.model})`));
172
+ }
173
+ } else {
174
+ // User explicitly provided model
175
+ console.log(chalk.green(`✓ Using specified model: ${config.model}`));
176
+ }
177
+
178
+ // Set default API URL if not provided by options or existing config
45
179
  if (!config.apiUrl) {
46
180
  switch (config.provider) {
47
181
  case 'openai':
48
182
  config.apiUrl = 'https://api.openai.com/v1/chat/completions';
49
- config.model = config.model || 'gpt-4'; // Default model for OpenAI
50
183
  break;
51
184
  case 'claude':
52
185
  config.apiUrl = 'https://api.anthropic.com/v1/messages';
53
- config.model = config.model || 'claude-3-sonnet-20240229'; // Default model for Claude
54
186
  break;
55
187
  case 'gemini':
56
188
  default:
57
- // Updated Gemini API URL to v1
58
- config.apiUrl = 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent';
59
- // Default model for Gemini
60
- config.model = config.model || 'gemini-pro'; // Using 'gemini-pro' as a common default
189
+ // Updated Gemini API URL to v1 - using a base URL
190
+ config.apiUrl = 'https://generativelanguage.googleapis.com/v1/models';
61
191
  break;
62
192
  }
63
193
  }
64
194
 
65
- // Interactive model selection if model is not explicitly provided via CLI option
66
- // AND if the model is not already set (either from existing config or default)
67
- if (!options.model && !config.model) {
68
- try {
69
- switch (config.provider) {
70
- case 'gemini':
71
- const geminiModels = ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest', 'gemini-pro'];
72
- const geminiRl = readline.createInterface({
73
- input: process.stdin,
74
- output: process.stdout
75
- });
76
- config.model = await new Promise(resolve => {
77
- geminiRl.question(chalk.blue(`Select a Gemini model (${geminiModels.join(', ')}): `), (model) => {
78
- geminiRl.close();
79
- resolve(model || config.model); // Use input or current model if empty
80
- });
81
- });
82
- break;
83
- case 'openai':
84
- const openaiModels = ['gpt-4o', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'];
85
- const openaiRl = readline.createInterface({
86
- input: process.stdin,
87
- output: process.stdout
88
- });
89
- config.model = await new Promise(resolve => {
90
- openaiRl.question(chalk.blue(`Select an OpenAI model (${openaiModels.join(', ')}): `), (model) => {
91
- openaiRl.close();
92
- resolve(model || config.model);
93
- });
94
- });
95
- break;
96
- case 'claude':
97
- const claudeModels = ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'];
98
- const claudeRl = readline.createInterface({
99
- input: process.stdin,
100
- output: process.stdout
101
- });
102
- config.model = await new Promise(resolve => {
103
- claudeRl.question(chalk.blue(`Select a Claude model (${claudeModels.join(', ')}): `), (model) => {
104
- claudeRl.close();
105
- resolve(model || config.model);
106
- });
107
- });
108
- break;
109
- }
110
- } catch (error) {
111
- console.error(chalk.red(`Error selecting model: ${error.message}`));
112
- process.exit(1); // Exit if model selection fails
113
- }
114
- }
115
-
116
195
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
117
196
  console.log(chalk.green(`✅ Configuration saved for ${config.provider} provider`));
118
197
  if (config.model) {
@@ -120,6 +199,45 @@ program
120
199
  }
121
200
  });
122
201
 
202
+ // Index project knowledge base for RAG
203
+ program
204
+ .command('index')
205
+ .description('Index project files for Retrieval-Augmented Generation (RAG)')
206
+ .option('-d, --dry-run', 'Show what files would be indexed without actually indexing')
207
+ .action(async (options) => {
208
+ try {
209
+ const configPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
210
+
211
+ if (!fs.existsSync(configPath)) {
212
+ console.log(chalk.red('No configuration found. Run: codeflow-hook config -k <api-key>'));
213
+ process.exit(1);
214
+ }
215
+
216
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
217
+ const spinner = ora('Indexing project knowledge base...').start();
218
+
219
+ if (options.dryRun) {
220
+ spinner.stop();
221
+ console.log(chalk.blue('🔍 Dry run mode - files to be indexed:'));
222
+ const { findKeyFiles } = await import('./rag.js');
223
+ const keyFiles = await findKeyFiles(process.cwd());
224
+ keyFiles.forEach(file => console.log(chalk.gray(` - ${file}`)));
225
+ console.log(chalk.green(`📊 Total files to index: ${keyFiles.length}`));
226
+ return;
227
+ }
228
+
229
+ const result = await indexProject(config);
230
+
231
+ spinner.succeed('Knowledge base indexing complete');
232
+ console.log(chalk.green(`✅ Indexed ${result.indexedFiles} files with ${result.totalChunks} chunks`));
233
+ console.log(chalk.blue('📁 Knowledge base stored in: .codeflow/index/'));
234
+
235
+ } catch (error) {
236
+ console.error(chalk.red(`❌ Indexing failed: ${error.message}`));
237
+ process.exit(1);
238
+ }
239
+ });
240
+
123
241
  // Install git hooks
124
242
  program
125
243
  .command('install')
@@ -127,63 +245,45 @@ program
127
245
  .option('--hooks-dir <dir>', 'Custom hooks directory', '.git/hooks')
128
246
  .action(async (options) => {
129
247
  const spinner = ora('Installing git hooks...').start();
130
-
131
248
  try {
132
249
  const hooksDir = path.resolve(options.hooksDir);
133
250
  if (!fs.existsSync(hooksDir)) {
134
251
  fs.mkdirSync(hooksDir, { recursive: true });
135
252
  }
136
253
 
137
- // Create pre-commit hook
254
+ // CHANGE 1: The git hooks are modified to PIPE the diff content via stdin
138
255
  const preCommitHook = `#!/usr/bin/env bash
139
256
  # Codeflow pre-commit hook
140
- # Auto-generated by codeflow-hook CLI
141
-
142
257
  set -e
143
-
144
258
  echo "🔬 Running Codeflow AI Code Analysis..."
145
-
146
- # Get staged changes
147
259
  STAGED_DIFF=$(git diff --cached --no-color)
148
-
149
260
  if [ -z "$STAGED_DIFF" ]; then
150
261
  echo "ℹ️ No staged changes to analyze"
151
262
  exit 0
152
263
  fi
153
-
154
- # Run AI analysis
155
- npx codeflow-hook analyze-diff "$STAGED_DIFF"
264
+ # Use stdin to avoid "command line too long" error
265
+ echo "$STAGED_DIFF" | npx codeflow-hook analyze-diff
156
266
  `;
157
267
 
158
268
  fs.writeFileSync(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
159
269
 
160
- // Create pre-push hook (enhanced version)
161
270
  const prePushHook = `#!/usr/bin/env bash
162
271
  # Codeflow pre-push hook
163
- # Auto-generated by codeflow-hook CLI
164
-
165
272
  set -e
166
-
167
273
  echo "🚀 Running Codeflow CI/CD simulation..."
168
-
169
- # Run tests if available
170
274
  if [ -f "package.json" ]; then
171
275
  echo "🧪 Running tests..."
172
276
  npm test || (echo "❌ Tests failed" && exit 1)
173
277
  fi
174
-
175
- # Get staged changes for AI analysis
176
278
  STAGED_DIFF=$(git diff --cached --no-color)
177
-
178
279
  if [ -n "$STAGED_DIFF" ]; then
179
280
  echo "🔬 Running AI Code Review..."
180
- npx codeflow-hook analyze-diff "$STAGED_DIFF" || exit 1
281
+ # Use stdin to avoid "command line too long" error
282
+ echo "$STAGED_DIFF" | npx codeflow-hook analyze-diff || exit 1
181
283
  fi
182
-
183
284
  echo "✅ All checks passed!"
184
285
  exit 0
185
286
  `;
186
-
187
287
  fs.writeFileSync(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
188
288
 
189
289
  spinner.succeed('Git hooks installed successfully');
@@ -198,14 +298,26 @@ exit 0
198
298
  }
199
299
  });
200
300
 
201
- // Analyze diff with configured AI provider
301
+ // Analyze diff with specialized AI agents
202
302
  program
203
303
  .command('analyze-diff')
204
- .description('Analyze git diff with configured AI provider')
205
- .argument('<diff>', 'Git diff content')
206
- .action(async (diff) => {
304
+ .description('Analyze git diff using specialized AI agents (RAG-enhanced)')
305
+ .argument('[diff]', 'Git diff content')
306
+ .option('--legacy', 'Use legacy monolithic analysis instead of agentic workflow')
307
+ .option('--no-rag', 'Disable RAG context retrieval')
308
+ .action(async (diff, options) => {
207
309
  try {
208
- const configPath = path.join(process.env.HOME, '.codeflow-hook', 'config.json');
310
+ // Read diff content from stdin or argument
311
+ let diffContent = diff;
312
+ if (!diffContent) {
313
+ const chunks = [];
314
+ for await (const chunk of process.stdin) {
315
+ chunks.push(chunk);
316
+ }
317
+ diffContent = Buffer.concat(chunks).toString('utf8');
318
+ }
319
+
320
+ const configPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
209
321
 
210
322
  if (!fs.existsSync(configPath)) {
211
323
  console.log(chalk.red('No configuration found. Run: codeflow-hook config -k <api-key>'));
@@ -214,27 +326,51 @@ program
214
326
 
215
327
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
216
328
 
217
- if (diff.trim() === '') {
329
+ if (diffContent.trim() === '') {
218
330
  console.log(chalk.gray('ℹ️ No changes to analyze'));
219
331
  return;
220
332
  }
221
333
 
222
- const spinner = ora(`Analyzing code with ${config.provider}...`).start();
223
- const prompt = generateCodeReviewPrompt(diff);
334
+ // Legacy mode: use original monolithic analysis
335
+ if (options.legacy) {
336
+ const spinner = ora(`Analyzing code with ${config.provider}...`).start();
337
+ const prompt = generateCodeReviewPrompt(diffContent);
338
+
339
+ let result;
340
+ try {
341
+ result = await callAIProvider(config, prompt);
342
+ } catch (error) {
343
+ spinner.fail('Analysis failed');
344
+ console.error(chalk.red(`AI API Error: ${error.message}`));
345
+ process.exit(1);
346
+ }
347
+
348
+ spinner.succeed('Analysis complete');
349
+ displayAnalysisResults(result);
350
+ return;
351
+ }
352
+
353
+ // Agentic workflow mode
354
+ const spinner = ora(`Running specialized code review agents...`).start();
224
355
 
225
- let result;
356
+ let results;
226
357
  try {
227
- result = await callAIProvider(config, prompt);
358
+ if (options.rag === false) {
359
+ // Force no RAG context
360
+ const { orchestrateReviewWithoutRAG } = await import('./agents.js');
361
+ results = await orchestrateReviewWithoutRAG(diffContent, config);
362
+ } else {
363
+ // Use RAG-enabled workflow
364
+ results = await orchestrateReview(diffContent, config);
365
+ }
228
366
  } catch (error) {
229
367
  spinner.fail('Analysis failed');
230
- console.error(chalk.red(`AI API Error: ${error.message}`));
368
+ console.error(chalk.red(`Agent analysis failed: ${error.message}`));
231
369
  process.exit(1);
232
370
  }
233
371
 
234
- spinner.succeed('Analysis complete');
235
-
236
- // Parse and display results
237
- displayAnalysisResults(result);
372
+ spinner.succeed('Agentic analysis complete');
373
+ displayAgenticResults(results);
238
374
 
239
375
  } catch (error) {
240
376
  console.log(chalk.red(`Configuration error: ${error.message}`));
@@ -250,12 +386,27 @@ program
250
386
  console.log(chalk.blue('🔍 Codeflow Hook Status'));
251
387
  console.log();
252
388
 
253
- // Check configuration
254
- const configPath = path.join(process.env.HOME, '.codeflow-hook', 'config.json');
255
- if (fs.existsSync(configPath)) {
256
- console.log(chalk.green('✅ Configuration: Found'));
389
+ // Check configuration cascade
390
+ const globalConfigPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
391
+ const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
392
+
393
+ const hasGlobalConfig = fs.existsSync(globalConfigPath);
394
+ const hasProjectConfig = fs.existsSync(projectConfigPath);
395
+
396
+ if (hasProjectConfig) {
397
+ console.log(chalk.green('✅ Project Configuration: Found (.codeflowrc.json)'));
257
398
  } else {
258
- console.log(chalk.red(' Configuration: Not found (run: codeflow-hook config)'));
399
+ console.log(chalk.gray('ℹ️ Project Configuration: Not found'));
400
+ }
401
+
402
+ if (hasGlobalConfig) {
403
+ console.log(chalk.green('✅ Global Configuration: Found'));
404
+ } else {
405
+ console.log(chalk.red('❌ Global Configuration: Not found (run: codeflow-hook config)'));
406
+ }
407
+
408
+ if (!hasGlobalConfig && !hasProjectConfig) {
409
+ console.log(chalk.red('❌ No configuration found. Run: codeflow-hook config -k <api-key>'));
259
410
  }
260
411
 
261
412
  // Check git hooks
@@ -274,8 +425,111 @@ program
274
425
  } else {
275
426
  console.log(chalk.red('❌ Git Hook (pre-push): Not installed'));
276
427
  }
428
+
429
+ console.log();
430
+ console.log(chalk.blue('💡 Tips:'));
431
+ console.log(chalk.gray(' • Create .codeflowrc.json in project root for project-specific settings'));
432
+ console.log(chalk.gray(' • Large diffs (>20KB) will prompt for confirmation to avoid high costs'));
433
+ console.log(chalk.gray(' • Run "codeflow-hook config -h" for configuration options'));
277
434
  });
278
435
 
436
+
437
+ async function validateApiKey(provider, apiKey) {
438
+ console.log(`DEBUG: validateApiKey called for provider: ${provider}`);
439
+ if (!apiKey) {
440
+ throw new Error('No API key provided');
441
+ }
442
+
443
+ try {
444
+ console.log(`DEBUG: Making API call for ${provider} validation...`);
445
+ switch (provider) {
446
+ case 'gemini':
447
+ // Test Gemini API key by making a simple models list request
448
+ const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
449
+ console.log(`DEBUG: Gemini URL: ${geminiUrl.substring(0, 80)}...`);
450
+ await axios.get(geminiUrl);
451
+ console.log(`DEBUG: Gemini API call succeeded`);
452
+ break;
453
+ case 'openai':
454
+ const openaiUrl = 'https://api.openai.com/v1/models';
455
+ console.log(`DEBUG: OpenAI URL: ${openaiUrl} with Bearer token`);
456
+ await axios.get(openaiUrl, {
457
+ headers: { 'Authorization': `Bearer ${apiKey}` }
458
+ });
459
+ console.log(`DEBUG: OpenAI API call succeeded`);
460
+ break;
461
+ case 'claude':
462
+ // Test Claude API key with a minimal request (Anthropic doesn't have models endpoint, so we use a very basic check)
463
+ console.log(`DEBUG: Claude validation call`);
464
+ try {
465
+ await axios.post('https://api.anthropic.com/v1/messages', {
466
+ model: 'claude-3-haiku-20240307',
467
+ max_tokens: 1,
468
+ messages: [{ role: 'user', content: 'Test' }]
469
+ }, {
470
+ headers: {
471
+ 'x-api-key': apiKey,
472
+ 'anthropic-version': '2023-06-01',
473
+ 'Content-Type': 'application/json'
474
+ }
475
+ });
476
+ } catch (claudeError) {
477
+ console.log(`DEBUG: Claude error status: ${claudeError.response?.status}`);
478
+ if (claudeError.response?.status === 401) {
479
+ throw new Error('Invalid Claude API key');
480
+ }
481
+ // If it's a different error (like rate limit), we'll allow it through
482
+ }
483
+ break;
484
+ default:
485
+ throw new Error(`Unsupported provider: ${provider}`);
486
+ }
487
+ } catch (error) {
488
+ console.log(`DEBUG: Validation failed with error: ${error.message}`);
489
+ console.log(`DEBUG: Error response status: ${error.response?.status}`);
490
+ if (error.response?.status === 401) {
491
+ throw new Error(`Invalid ${provider} API key`);
492
+ } else if (error.response?.status === 403) {
493
+ throw new Error(`API key lacks permissions for ${provider}`);
494
+ } else if (error.response?.status === 429) {
495
+ throw new Error(`API rate limit exceeded. Please try again later.`);
496
+ } else {
497
+ throw new Error(`${provider} API is currently unavailable: ${error.message}`);
498
+ }
499
+ }
500
+ }
501
+
502
+ async function fetchModels(provider, apiKey) {
503
+ switch (provider) {
504
+ case 'gemini':
505
+ const geminiResponse = await axios.get(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
506
+ return geminiResponse.data.models.map(m => m.name.replace('models/', ''));
507
+ case 'openai':
508
+ const openaiResponse = await axios.get('https://api.openai.com/v1/models', {
509
+ headers: { 'Authorization': `Bearer ${apiKey}` }
510
+ });
511
+ return openaiResponse.data.data.map(m => m.id);
512
+ case 'claude':
513
+ console.log(chalk.yellow("Claude does not support dynamic model fetching. Using a standard list."));
514
+ return getFallbackModels('claude');
515
+ default:
516
+ return [];
517
+ }
518
+ }
519
+
520
+ function getFallbackModels(provider) {
521
+ switch (provider) {
522
+ case 'gemini':
523
+ return ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest', 'gemini-pro'];
524
+ case 'openai':
525
+ return ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'];
526
+ case 'claude':
527
+ return ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'];
528
+ default:
529
+ return [];
530
+ }
531
+ }
532
+
279
533
  function generateCodeReviewPrompt(diff) {
280
534
  return `You are "Codeflow", a world-class AI software engineering assistant acting as a Principal Engineer. Your mission is to perform a rigorous and constructive code review on the provided code changes.
281
535
 
@@ -330,7 +584,8 @@ async function callGemini(config, prompt) {
330
584
  }
331
585
  };
332
586
 
333
- const response = await axios.post(`${config.apiUrl}?key=${config.apiKey}`, payload, {
587
+ const url = `${config.apiUrl}/${config.model}:generateContent?key=${config.apiKey}`;
588
+ const response = await axios.post(url, payload, {
334
589
  headers: {
335
590
  'Content-Type': 'application/json'
336
591
  }
@@ -406,4 +661,78 @@ function displayAnalysisResults(result) {
406
661
  console.log();
407
662
  }
408
663
 
409
- program.parse();
664
+ function displayAgenticResults(results) {
665
+ if (!results || results.length === 0) {
666
+ console.log(chalk.green('✅ No issues found in the analysis.'));
667
+ return;
668
+ }
669
+
670
+ // Group results by file and type
671
+ const groupedResults = {};
672
+ const summaryStats = { security: 0, architecture: 0, maintainability: 0 };
673
+
674
+ for (const result of results) {
675
+ const key = `${result.file}:${result.scopeType}`;
676
+ if (!groupedResults[key]) {
677
+ groupedResults[key] = [];
678
+ }
679
+ groupedResults[key].push(result);
680
+
681
+ // Count by severity for summary
682
+ summaryStats[result.type.toLowerCase()]++;
683
+ }
684
+
685
+ // Display summary stats
686
+ console.log(chalk.blue('📊 Code Review Summary:'));
687
+ console.log(` 🔒 Security issues: ${summaryStats.security}`);
688
+ console.log(` 🏗️ Architecture issues: ${summaryStats.architecture}`);
689
+ console.log(` 📝 Maintainability issues: ${summaryStats.maintainability}`);
690
+ console.log();
691
+
692
+ // Display detailed results by file and scope
693
+ for (const [scopeKey, scopeResults] of Object.entries(groupedResults)) {
694
+ const [file, scopeType] = scopeKey.split(':');
695
+ console.log(chalk.yellow(`📁 ${file} (${scopeType})`));
696
+
697
+ for (const result of scopeResults) {
698
+ const severityColor = getSeverityColor(result.severity);
699
+ const typeIcon = getTypeIcon(result.type);
700
+ console.log(` ${severityColor}${typeIcon} ${result.severity}: ${result.description}`);
701
+ if (result.line && result.line !== 'N/A') {
702
+ console.log(chalk.gray(` Line: ${result.lineRange}`));
703
+ }
704
+ }
705
+ console.log();
706
+ }
707
+ }
708
+
709
+ function getSeverityColor(severity) {
710
+ switch (severity?.toUpperCase()) {
711
+ case 'CRITICAL':
712
+ return chalk.red('🔴');
713
+ case 'HIGH':
714
+ return chalk.red('🟠');
715
+ case 'MEDIUM':
716
+ return chalk.yellow('🟡');
717
+ case 'LOW':
718
+ return chalk.green('🟢');
719
+ default:
720
+ return chalk.gray('⚪');
721
+ }
722
+ }
723
+
724
+ function getTypeIcon(type) {
725
+ switch (type?.toUpperCase()) {
726
+ case 'SECURITY':
727
+ return '🔒';
728
+ case 'ARCHITECTURE':
729
+ return '🏗️ ';
730
+ case 'MAINTAINABILITY':
731
+ return '📝';
732
+ default:
733
+ return '❓';
734
+ }
735
+ }
736
+
737
+ // Make sure the final line uses parseAsync
738
+ program.parseAsync(process.argv);