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.
- package/README.md +286 -19
- package/bin/agents.js +324 -0
- package/bin/codeflow-hook.js +437 -108
- package/bin/rag.js +312 -0
- package/package.json +4 -1
package/bin/codeflow-hook.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
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:
|
|
39
|
-
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
301
|
+
// Analyze diff with specialized AI agents
|
|
202
302
|
program
|
|
203
303
|
.command('analyze-diff')
|
|
204
|
-
.description('Analyze git diff
|
|
205
|
-
.argument('
|
|
206
|
-
.
|
|
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
|
-
|
|
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 (
|
|
329
|
+
if (diffContent.trim() === '') {
|
|
218
330
|
console.log(chalk.gray('ℹ️ No changes to analyze'));
|
|
219
331
|
return;
|
|
220
332
|
}
|
|
221
333
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
356
|
+
let results;
|
|
226
357
|
try {
|
|
227
|
-
|
|
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(`
|
|
368
|
+
console.error(chalk.red(`Agent analysis failed: ${error.message}`));
|
|
231
369
|
process.exit(1);
|
|
232
370
|
}
|
|
233
371
|
|
|
234
|
-
spinner.succeed('
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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);
|