codeflow-hook 1.1.0 â 1.2.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.
- package/bin/codeflow-hook.js +284 -99
- package/package.json +1 -1
package/bin/codeflow-hook.js
CHANGED
|
@@ -6,7 +6,7 @@ import ora from 'ora';
|
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
-
import
|
|
9
|
+
import os from 'os'; // Make sure os is imported
|
|
10
10
|
import readline from 'readline';
|
|
11
11
|
|
|
12
12
|
const program = new Command();
|
|
@@ -25,94 +25,168 @@ program
|
|
|
25
25
|
.option('-u, --url <url>', 'Custom API URL (optional)')
|
|
26
26
|
.option('-m, --model <model>', 'AI model name (optional - uses provider default)')
|
|
27
27
|
.action(async (options) => {
|
|
28
|
-
|
|
28
|
+
// Use USERPROFILE on Windows instead of HOME which might be undefined
|
|
29
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
30
|
+
const configDir = path.join(homeDir, '.codeflow-hook');
|
|
29
31
|
if (!fs.existsSync(configDir)) {
|
|
30
32
|
fs.mkdirSync(configDir, { recursive: true });
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const configPath = path.join(configDir, 'config.json');
|
|
36
|
+
|
|
37
|
+
// Configuration cascade: Check for project-level config first, then fall back to global
|
|
38
|
+
const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
|
|
39
|
+
const projectConfig = fs.existsSync(projectConfigPath) ? JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')) : {};
|
|
40
|
+
|
|
34
41
|
const existingConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
|
|
35
42
|
|
|
36
|
-
//
|
|
43
|
+
// Show config cascade message
|
|
44
|
+
if (Object.keys(projectConfig).length > 0) {
|
|
45
|
+
console.log(chalk.blue('đ Using configuration cascade (project â global):'));
|
|
46
|
+
console.log(chalk.gray(` Project config: ${projectConfigPath}`));
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
console.log(chalk.gray(` Global config: ${configPath}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Determine what the new provider and API key should be
|
|
53
|
+
const requestedProvider = (options.provider || existingConfig.provider || 'gemini').toLowerCase();
|
|
54
|
+
const requestedApiKey = options.key || existingConfig.apiKey;
|
|
55
|
+
|
|
56
|
+
// FIX: Explicit boolean coercion to prevent || operator from returning non-boolean values
|
|
57
|
+
// Step by step to ensure proper boolean evaluation
|
|
58
|
+
const hasNewKey = !!options.key;
|
|
59
|
+
const isFirstTimeSetup = !existingConfig.provider && !existingConfig.apiKey;
|
|
60
|
+
const shouldValidate = hasNewKey || isFirstTimeSetup;
|
|
61
|
+
|
|
62
|
+
console.log('VALIDATION DEBUG - provider:', requestedProvider, 'key_exists:', !!options.key, 'shouldValidate:', shouldValidate);
|
|
63
|
+
|
|
64
|
+
if (shouldValidate && requestedApiKey) {
|
|
65
|
+
console.log(chalk.blue('đ Validating API key for provider:', requestedProvider));
|
|
66
|
+
const validationSpinner = ora('Checking key permissions...').start();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await validateApiKey(requestedProvider, requestedApiKey);
|
|
70
|
+
validationSpinner.succeed('API key validated');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
validationSpinner.fail('Validation failed');
|
|
73
|
+
console.error(chalk.red(`â ${error.message}`));
|
|
74
|
+
console.error(chalk.red(`đĄ Make sure you're using a valid ${requestedProvider.toUpperCase()} API key for the ${requestedProvider} provider.`));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.yellow('â ī¸ Validation skipped - no API key validation needed'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize config with existing values, then override with verified CLI options
|
|
37
82
|
const config = {
|
|
38
|
-
provider:
|
|
39
|
-
apiKey:
|
|
83
|
+
provider: requestedProvider,
|
|
84
|
+
apiKey: requestedApiKey,
|
|
40
85
|
apiUrl: options.url || existingConfig.apiUrl,
|
|
41
86
|
model: options.model || existingConfig.model
|
|
42
87
|
};
|
|
43
88
|
|
|
44
|
-
//
|
|
89
|
+
// Interactive model selection if model is not explicitly provided
|
|
90
|
+
if (!options.model) {
|
|
91
|
+
// FIX: More robust logic for when to fetch new models
|
|
92
|
+
const isNewProvider = existingConfig.provider && (existingConfig.provider.toLowerCase() !== config.provider.toLowerCase());
|
|
93
|
+
const hasNewKey = !!options.key;
|
|
94
|
+
|
|
95
|
+
const shouldFetchModels = !existingConfig.model || isNewProvider || hasNewKey;
|
|
96
|
+
|
|
97
|
+
if (shouldFetchModels) {
|
|
98
|
+
console.log(chalk.blue('đ Fetching available models...'));
|
|
99
|
+
|
|
100
|
+
const modelSpinner = ora('Contacting API...').start();
|
|
101
|
+
try {
|
|
102
|
+
const models = await fetchModels(config.provider, config.apiKey);
|
|
103
|
+
modelSpinner.succeed('Models fetched successfully');
|
|
104
|
+
|
|
105
|
+
console.log(chalk.blue('Available models:'));
|
|
106
|
+
models.forEach((model, index) => {
|
|
107
|
+
console.log(chalk.gray(` ${index + 1}. ${model}`));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const rl = readline.createInterface({
|
|
111
|
+
input: process.stdin,
|
|
112
|
+
output: process.stdout
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const selectedModel = await new Promise(resolve => {
|
|
116
|
+
rl.question(chalk.blue(`Enter the model name (or number): `), (input) => {
|
|
117
|
+
rl.close();
|
|
118
|
+
resolve(input.trim());
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Check if user entered a number
|
|
123
|
+
const index = parseInt(selectedModel) - 1;
|
|
124
|
+
if (!isNaN(index) && index >= 0 && index < models.length) {
|
|
125
|
+
config.model = models[index];
|
|
126
|
+
} else {
|
|
127
|
+
config.model = selectedModel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(chalk.green(`â Selected model: ${config.model}`));
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
modelSpinner.fail('Failed to fetch models');
|
|
134
|
+
console.error(chalk.red(`â API Error: ${error.message}`));
|
|
135
|
+
// Fallback to hardcoded list if API call fails
|
|
136
|
+
const fallbackModels = getFallbackModels(config.provider);
|
|
137
|
+
|
|
138
|
+
console.log(chalk.yellow('â ī¸ Using fallback model list:'));
|
|
139
|
+
fallbackModels.forEach((model, index) => {
|
|
140
|
+
console.log(chalk.gray(` ${index + 1}. ${model}`));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const rl = readline.createInterface({
|
|
144
|
+
input: process.stdin,
|
|
145
|
+
output: process.stdout
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const selectedModel = await new Promise(resolve => {
|
|
149
|
+
rl.question(chalk.blue(`Select a model (name or number): `), (input) => {
|
|
150
|
+
rl.close();
|
|
151
|
+
resolve(input.trim());
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const index = parseInt(selectedModel) - 1;
|
|
156
|
+
if (!isNaN(index) && index >= 0 && index < fallbackModels.length) {
|
|
157
|
+
config.model = fallbackModels[index];
|
|
158
|
+
} else {
|
|
159
|
+
config.model = selectedModel;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(chalk.green(`â Selected fallback model: ${config.model}`));
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// Reuse existing model and config
|
|
166
|
+
console.log(chalk.blue(`đ Using existing configuration (provider: ${existingConfig.provider}, model: ${existingConfig.model})`));
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// User explicitly provided model
|
|
170
|
+
console.log(chalk.green(`â Using specified model: ${config.model}`));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Set default API URL if not provided by options or existing config
|
|
45
174
|
if (!config.apiUrl) {
|
|
46
175
|
switch (config.provider) {
|
|
47
176
|
case 'openai':
|
|
48
177
|
config.apiUrl = 'https://api.openai.com/v1/chat/completions';
|
|
49
|
-
config.model = config.model || 'gpt-4'; // Default model for OpenAI
|
|
50
178
|
break;
|
|
51
179
|
case 'claude':
|
|
52
180
|
config.apiUrl = 'https://api.anthropic.com/v1/messages';
|
|
53
|
-
config.model = config.model || 'claude-3-sonnet-20240229'; // Default model for Claude
|
|
54
181
|
break;
|
|
55
182
|
case 'gemini':
|
|
56
183
|
default:
|
|
57
|
-
// Updated Gemini API URL to v1
|
|
58
|
-
config.apiUrl = 'https://generativelanguage.googleapis.com/v1/models
|
|
59
|
-
// Default model for Gemini
|
|
60
|
-
config.model = config.model || 'gemini-pro'; // Using 'gemini-pro' as a common default
|
|
184
|
+
// Updated Gemini API URL to v1 - using a base URL
|
|
185
|
+
config.apiUrl = 'https://generativelanguage.googleapis.com/v1/models';
|
|
61
186
|
break;
|
|
62
187
|
}
|
|
63
188
|
}
|
|
64
189
|
|
|
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
190
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
117
191
|
console.log(chalk.green(`â
Configuration saved for ${config.provider} provider`));
|
|
118
192
|
if (config.model) {
|
|
@@ -127,63 +201,45 @@ program
|
|
|
127
201
|
.option('--hooks-dir <dir>', 'Custom hooks directory', '.git/hooks')
|
|
128
202
|
.action(async (options) => {
|
|
129
203
|
const spinner = ora('Installing git hooks...').start();
|
|
130
|
-
|
|
131
204
|
try {
|
|
132
205
|
const hooksDir = path.resolve(options.hooksDir);
|
|
133
206
|
if (!fs.existsSync(hooksDir)) {
|
|
134
207
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
135
208
|
}
|
|
136
209
|
|
|
137
|
-
//
|
|
210
|
+
// CHANGE 1: The git hooks are modified to PIPE the diff content via stdin
|
|
138
211
|
const preCommitHook = `#!/usr/bin/env bash
|
|
139
212
|
# Codeflow pre-commit hook
|
|
140
|
-
# Auto-generated by codeflow-hook CLI
|
|
141
|
-
|
|
142
213
|
set -e
|
|
143
|
-
|
|
144
214
|
echo "đŦ Running Codeflow AI Code Analysis..."
|
|
145
|
-
|
|
146
|
-
# Get staged changes
|
|
147
215
|
STAGED_DIFF=$(git diff --cached --no-color)
|
|
148
|
-
|
|
149
216
|
if [ -z "$STAGED_DIFF" ]; then
|
|
150
217
|
echo "âšī¸ No staged changes to analyze"
|
|
151
218
|
exit 0
|
|
152
219
|
fi
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
npx codeflow-hook analyze-diff "$STAGED_DIFF"
|
|
220
|
+
# Use stdin to avoid "command line too long" error
|
|
221
|
+
echo "$STAGED_DIFF" | npx codeflow-hook analyze-diff
|
|
156
222
|
`;
|
|
157
223
|
|
|
158
224
|
fs.writeFileSync(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
|
|
159
225
|
|
|
160
|
-
// Create pre-push hook (enhanced version)
|
|
161
226
|
const prePushHook = `#!/usr/bin/env bash
|
|
162
227
|
# Codeflow pre-push hook
|
|
163
|
-
# Auto-generated by codeflow-hook CLI
|
|
164
|
-
|
|
165
228
|
set -e
|
|
166
|
-
|
|
167
229
|
echo "đ Running Codeflow CI/CD simulation..."
|
|
168
|
-
|
|
169
|
-
# Run tests if available
|
|
170
230
|
if [ -f "package.json" ]; then
|
|
171
231
|
echo "đ§Ē Running tests..."
|
|
172
232
|
npm test || (echo "â Tests failed" && exit 1)
|
|
173
233
|
fi
|
|
174
|
-
|
|
175
|
-
# Get staged changes for AI analysis
|
|
176
234
|
STAGED_DIFF=$(git diff --cached --no-color)
|
|
177
|
-
|
|
178
235
|
if [ -n "$STAGED_DIFF" ]; then
|
|
179
236
|
echo "đŦ Running AI Code Review..."
|
|
180
|
-
|
|
237
|
+
# Use stdin to avoid "command line too long" error
|
|
238
|
+
echo "$STAGED_DIFF" | npx codeflow-hook analyze-diff || exit 1
|
|
181
239
|
fi
|
|
182
|
-
|
|
183
240
|
echo "â
All checks passed!"
|
|
184
241
|
exit 0
|
|
185
242
|
`;
|
|
186
|
-
|
|
187
243
|
fs.writeFileSync(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
|
|
188
244
|
|
|
189
245
|
spinner.succeed('Git hooks installed successfully');
|
|
@@ -202,10 +258,21 @@ exit 0
|
|
|
202
258
|
program
|
|
203
259
|
.command('analyze-diff')
|
|
204
260
|
.description('Analyze git diff with configured AI provider')
|
|
205
|
-
|
|
261
|
+
// CHANGE 2: The argument is now OPTIONAL (square brackets)
|
|
262
|
+
.argument('[diff]', 'Git diff content')
|
|
206
263
|
.action(async (diff) => {
|
|
207
264
|
try {
|
|
208
|
-
|
|
265
|
+
// CHANGE 3: New logic block to read from stdin if no argument is given
|
|
266
|
+
let diffContent = diff;
|
|
267
|
+
if (!diffContent) {
|
|
268
|
+
const chunks = [];
|
|
269
|
+
for await (const chunk of process.stdin) {
|
|
270
|
+
chunks.push(chunk);
|
|
271
|
+
}
|
|
272
|
+
diffContent = Buffer.concat(chunks).toString('utf8');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const configPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
|
|
209
276
|
|
|
210
277
|
if (!fs.existsSync(configPath)) {
|
|
211
278
|
console.log(chalk.red('No configuration found. Run: codeflow-hook config -k <api-key>'));
|
|
@@ -214,13 +281,13 @@ program
|
|
|
214
281
|
|
|
215
282
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
216
283
|
|
|
217
|
-
if (
|
|
284
|
+
if (diffContent.trim() === '') {
|
|
218
285
|
console.log(chalk.gray('âšī¸ No changes to analyze'));
|
|
219
286
|
return;
|
|
220
287
|
}
|
|
221
288
|
|
|
222
289
|
const spinner = ora(`Analyzing code with ${config.provider}...`).start();
|
|
223
|
-
const prompt = generateCodeReviewPrompt(
|
|
290
|
+
const prompt = generateCodeReviewPrompt(diffContent);
|
|
224
291
|
|
|
225
292
|
let result;
|
|
226
293
|
try {
|
|
@@ -232,8 +299,6 @@ program
|
|
|
232
299
|
}
|
|
233
300
|
|
|
234
301
|
spinner.succeed('Analysis complete');
|
|
235
|
-
|
|
236
|
-
// Parse and display results
|
|
237
302
|
displayAnalysisResults(result);
|
|
238
303
|
|
|
239
304
|
} catch (error) {
|
|
@@ -250,12 +315,27 @@ program
|
|
|
250
315
|
console.log(chalk.blue('đ Codeflow Hook Status'));
|
|
251
316
|
console.log();
|
|
252
317
|
|
|
253
|
-
// Check configuration
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
318
|
+
// Check configuration cascade
|
|
319
|
+
const globalConfigPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
|
|
320
|
+
const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
|
|
321
|
+
|
|
322
|
+
const hasGlobalConfig = fs.existsSync(globalConfigPath);
|
|
323
|
+
const hasProjectConfig = fs.existsSync(projectConfigPath);
|
|
324
|
+
|
|
325
|
+
if (hasProjectConfig) {
|
|
326
|
+
console.log(chalk.green('â
Project Configuration: Found (.codeflowrc.json)'));
|
|
327
|
+
} else {
|
|
328
|
+
console.log(chalk.gray('âšī¸ Project Configuration: Not found'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (hasGlobalConfig) {
|
|
332
|
+
console.log(chalk.green('â
Global Configuration: Found'));
|
|
257
333
|
} else {
|
|
258
|
-
console.log(chalk.red('â Configuration: Not found (run: codeflow-hook config)'));
|
|
334
|
+
console.log(chalk.red('â Global Configuration: Not found (run: codeflow-hook config)'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!hasGlobalConfig && !hasProjectConfig) {
|
|
338
|
+
console.log(chalk.red('â No configuration found. Run: codeflow-hook config -k <api-key>'));
|
|
259
339
|
}
|
|
260
340
|
|
|
261
341
|
// Check git hooks
|
|
@@ -274,8 +354,111 @@ program
|
|
|
274
354
|
} else {
|
|
275
355
|
console.log(chalk.red('â Git Hook (pre-push): Not installed'));
|
|
276
356
|
}
|
|
357
|
+
|
|
358
|
+
console.log();
|
|
359
|
+
console.log(chalk.blue('đĄ Tips:'));
|
|
360
|
+
console.log(chalk.gray(' âĸ Create .codeflowrc.json in project root for project-specific settings'));
|
|
361
|
+
console.log(chalk.gray(' âĸ Large diffs (>20KB) will prompt for confirmation to avoid high costs'));
|
|
362
|
+
console.log(chalk.gray(' âĸ Run "codeflow-hook config -h" for configuration options'));
|
|
277
363
|
});
|
|
278
364
|
|
|
365
|
+
|
|
366
|
+
async function validateApiKey(provider, apiKey) {
|
|
367
|
+
console.log(`DEBUG: validateApiKey called for provider: ${provider}`);
|
|
368
|
+
if (!apiKey) {
|
|
369
|
+
throw new Error('No API key provided');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
console.log(`DEBUG: Making API call for ${provider} validation...`);
|
|
374
|
+
switch (provider) {
|
|
375
|
+
case 'gemini':
|
|
376
|
+
// Test Gemini API key by making a simple models list request
|
|
377
|
+
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
|
|
378
|
+
console.log(`DEBUG: Gemini URL: ${geminiUrl.substring(0, 80)}...`);
|
|
379
|
+
await axios.get(geminiUrl);
|
|
380
|
+
console.log(`DEBUG: Gemini API call succeeded`);
|
|
381
|
+
break;
|
|
382
|
+
case 'openai':
|
|
383
|
+
const openaiUrl = 'https://api.openai.com/v1/models';
|
|
384
|
+
console.log(`DEBUG: OpenAI URL: ${openaiUrl} with Bearer token`);
|
|
385
|
+
await axios.get(openaiUrl, {
|
|
386
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
387
|
+
});
|
|
388
|
+
console.log(`DEBUG: OpenAI API call succeeded`);
|
|
389
|
+
break;
|
|
390
|
+
case 'claude':
|
|
391
|
+
// Test Claude API key with a minimal request (Anthropic doesn't have models endpoint, so we use a very basic check)
|
|
392
|
+
console.log(`DEBUG: Claude validation call`);
|
|
393
|
+
try {
|
|
394
|
+
await axios.post('https://api.anthropic.com/v1/messages', {
|
|
395
|
+
model: 'claude-3-haiku-20240307',
|
|
396
|
+
max_tokens: 1,
|
|
397
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
398
|
+
}, {
|
|
399
|
+
headers: {
|
|
400
|
+
'x-api-key': apiKey,
|
|
401
|
+
'anthropic-version': '2023-06-01',
|
|
402
|
+
'Content-Type': 'application/json'
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
} catch (claudeError) {
|
|
406
|
+
console.log(`DEBUG: Claude error status: ${claudeError.response?.status}`);
|
|
407
|
+
if (claudeError.response?.status === 401) {
|
|
408
|
+
throw new Error('Invalid Claude API key');
|
|
409
|
+
}
|
|
410
|
+
// If it's a different error (like rate limit), we'll allow it through
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
default:
|
|
414
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.log(`DEBUG: Validation failed with error: ${error.message}`);
|
|
418
|
+
console.log(`DEBUG: Error response status: ${error.response?.status}`);
|
|
419
|
+
if (error.response?.status === 401) {
|
|
420
|
+
throw new Error(`Invalid ${provider} API key`);
|
|
421
|
+
} else if (error.response?.status === 403) {
|
|
422
|
+
throw new Error(`API key lacks permissions for ${provider}`);
|
|
423
|
+
} else if (error.response?.status === 429) {
|
|
424
|
+
throw new Error(`API rate limit exceeded. Please try again later.`);
|
|
425
|
+
} else {
|
|
426
|
+
throw new Error(`${provider} API is currently unavailable: ${error.message}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function fetchModels(provider, apiKey) {
|
|
432
|
+
switch (provider) {
|
|
433
|
+
case 'gemini':
|
|
434
|
+
const geminiResponse = await axios.get(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
435
|
+
return geminiResponse.data.models.map(m => m.name.replace('models/', ''));
|
|
436
|
+
case 'openai':
|
|
437
|
+
const openaiResponse = await axios.get('https://api.openai.com/v1/models', {
|
|
438
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
439
|
+
});
|
|
440
|
+
return openaiResponse.data.data.map(m => m.id);
|
|
441
|
+
case 'claude':
|
|
442
|
+
console.log(chalk.yellow("Claude does not support dynamic model fetching. Using a standard list."));
|
|
443
|
+
return getFallbackModels('claude');
|
|
444
|
+
default:
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getFallbackModels(provider) {
|
|
450
|
+
switch (provider) {
|
|
451
|
+
case 'gemini':
|
|
452
|
+
return ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest', 'gemini-pro'];
|
|
453
|
+
case 'openai':
|
|
454
|
+
return ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'];
|
|
455
|
+
case 'claude':
|
|
456
|
+
return ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'];
|
|
457
|
+
default:
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
279
462
|
function generateCodeReviewPrompt(diff) {
|
|
280
463
|
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
464
|
|
|
@@ -330,7 +513,8 @@ async function callGemini(config, prompt) {
|
|
|
330
513
|
}
|
|
331
514
|
};
|
|
332
515
|
|
|
333
|
-
const
|
|
516
|
+
const url = `${config.apiUrl}/${config.model}:generateContent?key=${config.apiKey}`;
|
|
517
|
+
const response = await axios.post(url, payload, {
|
|
334
518
|
headers: {
|
|
335
519
|
'Content-Type': 'application/json'
|
|
336
520
|
}
|
|
@@ -406,4 +590,5 @@ function displayAnalysisResults(result) {
|
|
|
406
590
|
console.log();
|
|
407
591
|
}
|
|
408
592
|
|
|
409
|
-
|
|
593
|
+
// Make sure the final line uses parseAsync
|
|
594
|
+
program.parseAsync(process.argv);
|