codeflow-hook 2.0.3 → 2.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.
@@ -11,7 +11,7 @@ import readline from 'readline';
11
11
  import { orchestrateReview } from './agents.js';
12
12
 
13
13
  // Import CLI integration service
14
- import { indexProject, analyzeDiff } from '../lib/cli-integration/dist/index.js';
14
+ import { indexProject } from '../lib/cli-integration/dist/index.js';
15
15
 
16
16
  // Export for use in agents module
17
17
  export { callAIProvider };
@@ -26,13 +26,16 @@ program
26
26
  // Configure AI provider settings
27
27
  program
28
28
  .command('config')
29
- .description('Configure AI provider settings')
30
- .option('-p, --provider <provider>', 'AI provider (gemini, openai, claude)', 'gemini')
31
- .option('-k, --key <key>', 'API key for the chosen provider')
32
- .option('-u, --url <url>', 'Custom API URL (optional)')
33
- .option('-m, --model <model>', 'AI model name (optional - uses provider default)')
29
+ .description('Configure AI provider settings (gemini, openai, claude, ollama)')
30
+ .option('-p, --provider <provider>', 'AI provider (gemini, openai, claude, ollama)', 'gemini')
31
+ .option('-k, --key <key>', 'API key for cloud providers (not needed for ollama)')
32
+ .option('-u, --url <url>', 'Custom API URL (for ollama: http://localhost:11434)')
33
+ .option('-m, --model <model>', 'AI model name (for ollama: auto-discovered if not specified)')
34
+ .option('--ollama-enable', 'Enable Ollama as the primary provider')
35
+ .option('--ollama-disable', 'Disable Ollama and use cloud provider instead')
36
+ .option('--ollama-url <url>', 'Ollama server URL (default: http://localhost:11434)')
37
+ .option('--list-models', 'List available Ollama models and exit')
34
38
  .action(async (options) => {
35
- // Use USERPROFILE on Windows instead of HOME which might be undefined
36
39
  const homeDir = process.env.HOME || process.env.USERPROFILE;
37
40
  const configDir = path.join(homeDir, '.codeflow-hook');
38
41
  if (!fs.existsSync(configDir)) {
@@ -40,164 +43,186 @@ program
40
43
  }
41
44
 
42
45
  const configPath = path.join(configDir, 'config.json');
43
-
44
- // Configuration cascade: Check for project-level config first, then fall back to global
45
- const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
46
- const projectConfig = fs.existsSync(projectConfigPath) ? JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')) : {};
47
-
48
46
  const existingConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
49
47
 
50
- // Show config cascade message
51
- if (Object.keys(projectConfig).length > 0) {
52
- console.log(chalk.blue('📁 Using configuration cascade (project global):'));
53
- console.log(chalk.gray(` Project config: ${projectConfigPath}`));
54
- if (fs.existsSync(configPath)) {
55
- console.log(chalk.gray(` Global config: ${configPath}`));
56
- }
57
- }
58
-
59
- // Determine what the new provider and API key should be
60
- const requestedProvider = (options.provider || existingConfig.provider || 'gemini').toLowerCase();
61
- const requestedApiKey = options.key || existingConfig.apiKey;
62
-
63
- // FIX: Explicit boolean coercion to prevent || operator from returning non-boolean values
64
- // Step by step to ensure proper boolean evaluation
65
- const hasNewKey = !!options.key;
66
- const isFirstTimeSetup = !existingConfig.provider && !existingConfig.apiKey;
67
- const shouldValidate = hasNewKey || isFirstTimeSetup;
68
-
69
- console.log('VALIDATION DEBUG - provider:', requestedProvider, 'key_exists:', !!options.key, 'shouldValidate:', shouldValidate);
70
-
71
- if (shouldValidate && requestedApiKey) {
72
- console.log(chalk.blue('🔐 Validating API key for provider:', requestedProvider));
73
- const validationSpinner = ora('Checking key permissions...').start();
74
-
48
+ // List Ollama models mode
49
+ if (options.listModels) {
50
+ const ollamaUrl = options.ollamaUrl || existingConfig.ollama?.url || 'http://localhost:11434';
51
+ console.log(chalk.blue(`🔍 Fetching models from Ollama at ${ollamaUrl}...`));
75
52
  try {
76
- await validateApiKey(requestedProvider, requestedApiKey);
77
- validationSpinner.succeed('API key validated');
53
+ const { listOllamaModels, isOllamaRunning } = await import('../lib/ai-reviewer.cjs');
54
+ const running = await isOllamaRunning(ollamaUrl);
55
+ if (!running) {
56
+ console.log(chalk.red('❌ Ollama is not running or not reachable'));
57
+ console.log(chalk.yellow(`💡 Start Ollama: ollama serve`));
58
+ process.exit(1);
59
+ }
60
+ const models = await listOllamaModels(ollamaUrl);
61
+ if (models.length === 0) {
62
+ console.log(chalk.yellow('⚠️ No models found. Pull a model first:'));
63
+ console.log(chalk.gray(' ollama pull qwen2.5-coder'));
64
+ console.log(chalk.gray(' ollama pull deepseek-coder'));
65
+ console.log(chalk.gray(' ollama pull codellama'));
66
+ } else {
67
+ console.log(chalk.green(`✅ Found ${models.length} model(s):`));
68
+ models.forEach((m, i) => console.log(chalk.gray(` ${i + 1}. ${m}`)));
69
+ }
70
+ process.exit(0);
78
71
  } catch (error) {
79
- validationSpinner.fail('Validation failed');
80
- console.error(chalk.red(`❌ ${error.message}`));
81
- console.error(chalk.red(`💡 Make sure you're using a valid ${requestedProvider.toUpperCase()} API key for the ${requestedProvider} provider.`));
72
+ console.log(chalk.red(`❌ Failed to connect to Ollama: ${error.message}`));
82
73
  process.exit(1);
83
74
  }
84
- } else {
85
- console.log(chalk.yellow('⚠️ Validation skipped - no API key validation needed'));
86
75
  }
87
76
 
88
- // Initialize config with existing values, then override with verified CLI options
77
+ const requestedProvider = (options.provider || existingConfig.provider || 'gemini').toLowerCase();
78
+ const ollamaEnabled = options.ollamaEnable === true ||
79
+ (options.ollamaEnable === undefined && options.ollamaDisable === undefined && existingConfig.ollama?.enabled === true) ||
80
+ requestedProvider === 'ollama';
81
+
82
+ // Initialize config structure
89
83
  const config = {
90
- provider: requestedProvider,
91
- apiKey: requestedApiKey,
84
+ provider: requestedProvider === 'ollama' ? 'ollama' : (existingConfig.provider || 'gemini'),
85
+ apiKey: options.key || existingConfig.apiKey,
92
86
  apiUrl: options.url || existingConfig.apiUrl,
93
- model: options.model || existingConfig.model
87
+ model: options.model || existingConfig.model,
88
+ ollama: {
89
+ enabled: ollamaEnabled,
90
+ url: options.ollamaUrl || existingConfig.ollama?.url || 'http://localhost:11434'
91
+ }
94
92
  };
95
93
 
96
- // Interactive model selection if model is not explicitly provided
97
- if (!options.model) {
98
- // FIX: More robust logic for when to fetch new models
99
- const isNewProvider = existingConfig.provider && (existingConfig.provider.toLowerCase() !== config.provider.toLowerCase());
100
- const hasNewKey = !!options.key;
94
+ // If Ollama is the primary provider, discover models
95
+ if (config.ollama.enabled) {
96
+ console.log(chalk.blue('🦙 Ollama provider detected'));
97
+ console.log(chalk.gray(` URL: ${config.ollama.url}`));
101
98
 
102
- const shouldFetchModels = !existingConfig.model || isNewProvider || hasNewKey;
103
-
104
- if (shouldFetchModels) {
105
- console.log(chalk.blue('🔍 Fetching available models...'));
106
-
107
- const modelSpinner = ora('Contacting API...').start();
108
- try {
109
- const models = await fetchModels(config.provider, config.apiKey);
110
- modelSpinner.succeed('Models fetched successfully');
111
-
112
- console.log(chalk.blue('Available models:'));
113
- models.forEach((model, index) => {
114
- console.log(chalk.gray(` ${index + 1}. ${model}`));
115
- });
116
-
117
- const rl = readline.createInterface({
118
- input: process.stdin,
119
- output: process.stdout
120
- });
121
-
122
- const selectedModel = await new Promise(resolve => {
123
- rl.question(chalk.blue(`Enter the model name (or number): `), (input) => {
124
- rl.close();
125
- resolve(input.trim());
126
- });
127
- });
128
-
129
- // Check if user entered a number
130
- const index = parseInt(selectedModel) - 1;
131
- if (!isNaN(index) && index >= 0 && index < models.length) {
132
- config.model = models[index];
99
+ try {
100
+ const { listOllamaModels, isOllamaRunning } = await import('../lib/ai-reviewer.cjs');
101
+ const running = await isOllamaRunning(config.ollama.url);
102
+
103
+ if (!running) {
104
+ console.log(chalk.yellow('⚠️ Ollama is not running or not reachable'));
105
+ console.log(chalk.yellow(' Models will not be auto-discovered'));
106
+ if (!config.model) {
107
+ console.log(chalk.gray(' Using default model: qwen2.5-coder'));
108
+ config.model = 'qwen2.5-coder';
109
+ }
110
+ } else {
111
+ const models = await listOllamaModels(config.ollama.url);
112
+ if (models.length === 0) {
113
+ console.log(chalk.yellow('⚠️ No Ollama models found'));
114
+ console.log(chalk.gray(' Pull a model: ollama pull qwen2.5-coder'));
115
+ if (!config.model) {
116
+ config.model = 'qwen2.5-coder';
117
+ }
133
118
  } else {
134
- config.model = selectedModel;
119
+ console.log(chalk.green(`✅ Found ${models.length} model(s):`));
120
+ models.forEach((m, i) => console.log(chalk.gray(` ${i + 1}. ${m}`)));
121
+
122
+ // Auto-select or prompt
123
+ if (!options.model) {
124
+ const defaultModel = models.find(m => m.includes('coder') || m.includes('code')) || models[0];
125
+ console.log(chalk.blue(`\n💡 Recommended: ${defaultModel}`));
126
+
127
+ const rl = readline.createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout
130
+ });
131
+
132
+ const input = await new Promise(resolve => {
133
+ rl.question(chalk.blue(`Select model (name or number, Enter for ${defaultModel}): `), (answer) => {
134
+ rl.close();
135
+ resolve(answer.trim());
136
+ });
137
+ });
138
+
139
+ if (input === '') {
140
+ config.model = defaultModel;
141
+ } else {
142
+ const idx = parseInt(input) - 1;
143
+ config.model = (!isNaN(idx) && idx >= 0 && idx < models.length) ? models[idx] : input;
144
+ }
145
+ console.log(chalk.green(`✓ Selected: ${config.model}`));
146
+ }
135
147
  }
148
+ }
149
+ } catch (error) {
150
+ console.log(chalk.yellow(`⚠️ Could not connect to Ollama: ${error.message}`));
151
+ if (!config.model) {
152
+ config.model = 'qwen2.5-coder';
153
+ }
154
+ }
155
+ } else if (options.key || !existingConfig.apiKey) {
156
+ // Cloud provider setup with API key validation
157
+ const hasNewKey = !!options.key;
158
+ const isFirstTime = !existingConfig.provider && !existingConfig.apiKey;
159
+ const shouldValidate = hasNewKey || isFirstTime;
136
160
 
137
- console.log(chalk.green(`✓ Selected model: ${config.model}`));
138
-
161
+ if (shouldValidate && config.apiKey) {
162
+ console.log(chalk.blue(`🔐 Validating API key for ${config.provider}...`));
163
+ try {
164
+ await validateApiKey(config.provider, config.apiKey);
165
+ console.log(chalk.green('✅ API key validated'));
139
166
  } catch (error) {
140
- modelSpinner.fail('Failed to fetch models');
141
- console.error(chalk.red(`❌ API Error: ${error.message}`));
142
- // Fallback to hardcoded list if API call fails
143
- const fallbackModels = getFallbackModels(config.provider);
144
-
145
- console.log(chalk.yellow('⚠️ Using fallback model list:'));
146
- fallbackModels.forEach((model, index) => {
147
- console.log(chalk.gray(` ${index + 1}. ${model}`));
148
- });
167
+ console.log(chalk.red(`❌ ${error.message}`));
168
+ process.exit(1);
169
+ }
170
+ }
149
171
 
150
- const rl = readline.createInterface({
151
- input: process.stdin,
152
- output: process.stdout
153
- });
172
+ // Set default API URL for cloud providers
173
+ if (!config.apiUrl) {
174
+ switch (config.provider) {
175
+ case 'openai':
176
+ config.apiUrl = 'https://api.openai.com/v1/chat/completions';
177
+ break;
178
+ case 'claude':
179
+ config.apiUrl = 'https://api.anthropic.com/v1/messages';
180
+ break;
181
+ case 'gemini':
182
+ default:
183
+ config.apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
184
+ break;
185
+ }
186
+ }
154
187
 
155
- const selectedModel = await new Promise(resolve => {
156
- rl.question(chalk.blue(`Select a model (name or number): `), (input) => {
157
- rl.close();
158
- resolve(input.trim());
188
+ // Interactive model selection for cloud providers
189
+ if (!options.model) {
190
+ const shouldFetch = !existingConfig.model || existingConfig.provider !== config.provider || hasNewKey;
191
+ if (shouldFetch) {
192
+ console.log(chalk.blue('🔍 Fetching available models...'));
193
+ try {
194
+ const models = await fetchModels(config.provider, config.apiKey);
195
+ console.log(chalk.blue('Available models:'));
196
+ models.forEach((m, i) => console.log(chalk.gray(` ${i + 1}. ${m}`)));
197
+
198
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
199
+ const input = await new Promise(resolve => {
200
+ rl.question(chalk.blue('Enter model name or number: '), (a) => { rl.close(); resolve(a.trim()); });
159
201
  });
160
- });
161
-
162
- const index = parseInt(selectedModel) - 1;
163
- if (!isNaN(index) && index >= 0 && index < fallbackModels.length) {
164
- config.model = fallbackModels[index];
165
- } else {
166
- config.model = selectedModel;
202
+ const idx = parseInt(input) - 1;
203
+ config.model = (!isNaN(idx) && idx >= 0 && idx < models.length) ? models[idx] : input;
204
+ } catch {
205
+ const fallbacks = getFallbackModels(config.provider);
206
+ fallbacks.forEach((m, i) => console.log(chalk.gray(` ${i + 1}. ${m}`)));
207
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
208
+ const input = await new Promise(resolve => {
209
+ rl.question(chalk.blue('Select model: '), (a) => { rl.close(); resolve(a.trim()); });
210
+ });
211
+ const idx = parseInt(input) - 1;
212
+ config.model = (!isNaN(idx) && idx >= 0 && idx < fallbacks.length) ? fallbacks[idx] : input;
167
213
  }
168
-
169
- console.log(chalk.green(`✓ Selected fallback model: ${config.model}`));
170
214
  }
171
- } else {
172
- // Reuse existing model and config
173
- console.log(chalk.blue(`📚 Using existing configuration (provider: ${existingConfig.provider}, model: ${existingConfig.model})`));
174
- }
175
- } else {
176
- // User explicitly provided model
177
- console.log(chalk.green(`✓ Using specified model: ${config.model}`));
178
- }
179
-
180
- // Set default API URL if not provided by options or existing config
181
- if (!config.apiUrl) {
182
- switch (config.provider) {
183
- case 'openai':
184
- config.apiUrl = 'https://api.openai.com/v1/chat/completions';
185
- break;
186
- case 'claude':
187
- config.apiUrl = 'https://api.anthropic.com/v1/messages';
188
- break;
189
- case 'gemini':
190
- default:
191
- // Updated Gemini API URL to v1 - using a base URL
192
- config.apiUrl = 'https://generativelanguage.googleapis.com/v1/models';
193
- break;
194
215
  }
195
216
  }
196
217
 
218
+ // Save config
197
219
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
198
- console.log(chalk.green(`✅ Configuration saved for ${config.provider} provider`));
199
- if (config.model) {
200
- console.log(chalk.green(` Model: ${config.model}`));
220
+ console.log(chalk.green('\n✅ Configuration saved'));
221
+ console.log(chalk.gray(` Provider: ${config.provider}`));
222
+ console.log(chalk.gray(` Model: ${config.model}`));
223
+ console.log(chalk.gray(` Ollama: ${config.ollama.enabled ? 'enabled (' + config.ollama.url + ')' : 'disabled'}`));
224
+ if (config.apiKey) {
225
+ console.log(chalk.gray(` API Key: ${config.apiKey.substring(0, 8)}...`));
201
226
  }
202
227
  });
203
228
 
@@ -293,12 +318,13 @@ exit 0
293
318
  }
294
319
  });
295
320
 
296
- // Analyze diff with EKG Query Service context enhancement (Phase 4)
321
+ // Analyze diff with AI review + heuristic fallback
297
322
  program
298
323
  .command('analyze-diff')
299
- .description('Analyze git diff using EKG context enhancement')
324
+ .description('Analyze git diff with AI code review (Gemini API) and heuristic fallback')
300
325
  .argument('[diff]', 'Git diff content')
301
- .option('--legacy', 'Use legacy analysis instead of EKG-enhanced analysis')
326
+ .option('--min-score <score>', 'Minimum score to pass (1-10, default: 3)', '3')
327
+ .option('--json', 'Output results as JSON only')
302
328
  .action(async (diff, options) => {
303
329
  try {
304
330
  // Read diff content from stdin or argument
@@ -316,24 +342,48 @@ program
316
342
  return;
317
343
  }
318
344
 
319
- console.log(chalk.blue('🔬 Analyzing diff with EKG context enhancement...'));
345
+ // Guard against huge diffs
346
+ if (diffContent.length > 20000) {
347
+ console.log(chalk.yellow('⚠️ Diff too large for AI review (>20KB), using heuristic'));
348
+ }
320
349
 
321
- const result = await analyzeDiff(diffContent, {
322
- legacy: options.legacy || false,
323
- outputFormat: 'console'
324
- });
350
+ const minScore = parseInt(options.minScore, 10);
351
+ const { reviewDiff } = await import('../lib/ai-reviewer.cjs');
325
352
 
326
- if (result.success) {
327
- console.log(chalk.green(`✅ ${result.message}`));
328
- displayEKGAnalysisResults(result.analysis);
353
+ const result = await reviewDiff(diffContent, { minScore });
329
354
 
330
- if (result.stats) {
331
- console.log(chalk.gray(`📊 EKG Queries: ${result.stats.ekg_queries}`));
332
- console.log(chalk.gray(`👥 Similar Repos Found: ${result.stats.similar_repos_found}`));
333
- console.log(chalk.gray(`⏱️ Analysis Time: ${result.stats.analysis_time}ms`));
334
- }
355
+ if (options.json) {
356
+ console.log(JSON.stringify(result, null, 2));
335
357
  } else {
336
- console.log(chalk.red(`❌ Analysis failed: ${result.message}`));
358
+ const icon = result.success ? '✅' : '❌';
359
+ const color = result.success ? chalk.green : chalk.red;
360
+ const providerLabel = result.provider === 'ollama' ? '🦙 Ollama' :
361
+ result.provider === 'heuristic' ? '🔍 Heuristic' :
362
+ result.provider === 'gemini' ? '💎 Gemini' :
363
+ result.provider === 'openai' ? '🔵 OpenAI' :
364
+ result.provider === 'claude' ? '🟣 Claude' : result.provider;
365
+ console.log(color(`${icon} ${result.message}`));
366
+ console.log(chalk.gray(` Provider: ${providerLabel}${result.usedFallback ? ' (fallback)' : ''}`));
367
+
368
+ if (result.result.score) {
369
+ console.log(chalk.blue(`📊 Score: ${result.result.score}/10 (threshold: ${minScore}/10)`));
370
+ }
371
+ if (result.result.summary) {
372
+ console.log(chalk.gray(`📝 ${result.result.summary}`));
373
+ }
374
+ if (result.result.files && result.result.files.length > 0) {
375
+ for (const file of result.result.files) {
376
+ if (file.issues && file.issues.length > 0) {
377
+ console.log(chalk.yellow(`\n📁 ${file.fileName}:`));
378
+ for (const issue of file.issues) {
379
+ console.log(chalk.red(` - [${issue.type}] ${issue.description}`));
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ if (!result.success) {
337
387
  process.exit(1);
338
388
  }
339
389
 
@@ -433,11 +483,10 @@ program
433
483
  program
434
484
  .command('status')
435
485
  .description('Show installation and configuration status')
436
- .action(() => {
486
+ .action(async () => {
437
487
  console.log(chalk.blue('🔍 Codeflow Hook Status'));
438
488
  console.log();
439
489
 
440
- // Check configuration cascade
441
490
  const globalConfigPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
442
491
  const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
443
492
 
@@ -451,16 +500,35 @@ program
451
500
  }
452
501
 
453
502
  if (hasGlobalConfig) {
503
+ const config = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
454
504
  console.log(chalk.green('✅ Global Configuration: Found'));
505
+ console.log(chalk.gray(` Provider: ${config.provider || 'gemini'}`));
506
+ console.log(chalk.gray(` Model: ${config.model || '(not set)'}`));
507
+ if (config.ollama?.enabled) {
508
+ console.log(chalk.green(` Ollama: enabled (${config.ollama.url || 'http://localhost:11434'})`));
509
+ try {
510
+ const { isOllamaRunning } = await import('../lib/ai-reviewer.cjs');
511
+ const running = await isOllamaRunning(config.ollama.url);
512
+ console.log(running ? chalk.green(' Ollama Status: running') : chalk.yellow(' Ollama Status: not running'));
513
+ } catch {
514
+ console.log(chalk.yellow(' Ollama Status: unknown'));
515
+ }
516
+ } else {
517
+ console.log(chalk.gray(' Ollama: disabled'));
518
+ }
519
+ if (config.apiKey) {
520
+ console.log(chalk.gray(` API Key: ${config.apiKey.substring(0, 8)}...`));
521
+ }
455
522
  } else {
456
523
  console.log(chalk.red('❌ Global Configuration: Not found (run: codeflow-hook config)'));
457
524
  }
458
525
 
459
526
  if (!hasGlobalConfig && !hasProjectConfig) {
460
- console.log(chalk.red('❌ No configuration found. Run: codeflow-hook config -k <api-key>'));
527
+ console.log(chalk.red('❌ No configuration found. Run: codeflow-hook config'));
528
+ console.log(chalk.gray(' Cloud: codeflow-hook config -k <api-key> -p gemini'));
529
+ console.log(chalk.gray(' Local: codeflow-hook config --ollama-enable'));
461
530
  }
462
531
 
463
- // Check git hooks
464
532
  const hooksDir = '.git/hooks';
465
533
  const preCommitHook = path.join(hooksDir, 'pre-commit');
466
534
  const prePushHook = path.join(hooksDir, 'pre-push');
@@ -479,9 +547,9 @@ program
479
547
 
480
548
  console.log();
481
549
  console.log(chalk.blue('💡 Tips:'));
482
- console.log(chalk.gray(' • Create .codeflowrc.json in project root for project-specific settings'));
483
- console.log(chalk.gray(' • Large diffs (>20KB) will prompt for confirmation to avoid high costs'));
484
- console.log(chalk.gray(' • Run "codeflow-hook config -h" for configuration options'));
550
+ console.log(chalk.gray(' • Use --ollama-enable for local AI (no API key needed)'));
551
+ console.log(chalk.gray(' • List Ollama models: codeflow-hook config --list-models'));
552
+ console.log(chalk.gray(' • Large diffs (>20KB) use heuristic fallback'));
485
553
  });
486
554
 
487
555
 
@@ -0,0 +1,422 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const http = require('http');
5
+ const https = require('https');
6
+
7
+ /**
8
+ * AI Code Review Module
9
+ *
10
+ * Supports multiple AI providers:
11
+ * - gemini: Google Gemini API (cloud, requires API key)
12
+ * - openai: OpenAI API (cloud, requires API key)
13
+ * - claude: Anthropic Claude API (cloud, requires API key)
14
+ * - ollama: Ollama local models (self-hosted, no API key needed)
15
+ *
16
+ * Falls back to the deterministic heuristic analyzer if:
17
+ * - No provider is configured or enabled
18
+ * - API call fails (network, timeout, invalid key)
19
+ * - Response cannot be parsed
20
+ *
21
+ * This ensures the pre-commit hook never blocks development due to
22
+ * missing or broken AI configuration.
23
+ */
24
+
25
+ const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.codeflow-hook', 'config.json');
26
+
27
+ const AI_REVIEW_PROMPT = `You are "Codeflow", a Principal Engineer performing a rigorous code review on the provided git diff.
28
+
29
+ Your response MUST be a single, valid JSON object with no markdown, no code blocks, no extra text.
30
+
31
+ The JSON must have this exact structure:
32
+ {
33
+ "overallStatus": "PASS" or "FAIL",
34
+ "score": integer 1-10,
35
+ "summary": "one sentence executive summary",
36
+ "files": [
37
+ {
38
+ "fileName": "path/to/file",
39
+ "status": "PASS" or "FAIL",
40
+ "issues": [
41
+ { "line": number, "type": "Security"|"Bug"|"Performance"|"Quality"|"Best Practice", "description": "clear description" }
42
+ ],
43
+ "suggestions": ["actionable suggestion"]
44
+ }
45
+ ]
46
+ }
47
+
48
+ Rules:
49
+ - overallStatus must be "FAIL" if ANY Security, Bug, or critical Performance issues exist
50
+ - score: 1-10 (10 = production-ready, 1 = fundamentally broken)
51
+ - Be constructive and precise
52
+ - Focus on: security vulnerabilities, bugs, performance issues, code quality
53
+
54
+ Git diff to review:
55
+ `;
56
+
57
+ /**
58
+ * Load AI provider configuration from ~/.codeflow-hook/config.json
59
+ * @returns {{ provider: string, apiKey: string, apiUrl: string, model: string, ollama: { enabled: boolean, url: string } } | null}
60
+ */
61
+ function loadConfig() {
62
+ try {
63
+ if (fs.existsSync(DEFAULT_CONFIG_PATH)) {
64
+ const raw = fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf8');
65
+ const config = JSON.parse(raw);
66
+ // Ensure ollama section exists with defaults
67
+ if (!config.ollama) {
68
+ config.ollama = { enabled: false, url: 'http://localhost:11434' };
69
+ }
70
+ return config;
71
+ }
72
+ } catch (e) {
73
+ // Config file exists but is invalid — fall back to heuristic
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Call Ollama local API with the diff and return structured review.
80
+ * Uses /api/generate endpoint with format: "json" for structured output.
81
+ * @param {string} diff - Git diff content
82
+ * @param {{ model: string, url: string }} config
83
+ * @returns {Promise<string>} - Raw text response from Ollama
84
+ */
85
+ async function callOllamaAPI(diff, config) {
86
+ const prompt = AI_REVIEW_PROMPT + diff;
87
+ const baseUrl = config.url || 'http://localhost:11434';
88
+ const urlObj = new URL(`${baseUrl}/api/generate`);
89
+
90
+ const payload = JSON.stringify({
91
+ model: config.model || 'qwen2.5-coder',
92
+ prompt,
93
+ stream: false,
94
+ format: 'json',
95
+ options: {
96
+ temperature: 0.2,
97
+ num_ctx: 8192,
98
+ }
99
+ });
100
+
101
+ const isHttps = urlObj.protocol === 'https:';
102
+ const client = isHttps ? https : http;
103
+
104
+ return new Promise((resolve, reject) => {
105
+ const req = client.request(urlObj, {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ 'Content-Length': Buffer.byteLength(payload)
110
+ },
111
+ timeout: 120000 // 2 minute timeout for local models
112
+ }, (res) => {
113
+ let data = '';
114
+ res.on('data', (chunk) => { data += chunk; });
115
+ res.on('end', () => {
116
+ if (res.statusCode !== 200) {
117
+ return reject(new Error(`Ollama returned ${res.statusCode}: ${data.substring(0, 200)}`));
118
+ }
119
+ try {
120
+ const response = JSON.parse(data);
121
+ const text = response.response;
122
+ if (!text) {
123
+ return reject(new Error('Ollama returned no text response'));
124
+ }
125
+ resolve(text);
126
+ } catch (e) {
127
+ reject(new Error(`Failed to parse Ollama response: ${e.message}`));
128
+ }
129
+ });
130
+ });
131
+
132
+ req.on('error', (e) => reject(e));
133
+ req.on('timeout', () => {
134
+ req.destroy();
135
+ reject(new Error('Ollama request timed out after 120s — is the model loaded?'));
136
+ });
137
+
138
+ req.write(payload);
139
+ req.end();
140
+ });
141
+ }
142
+
143
+ /**
144
+ * List available Ollama models by calling /api/tags
145
+ * @param {string} baseUrl - Ollama URL (default: http://localhost:11434)
146
+ * @returns {Promise<string[]>} - Array of model names
147
+ */
148
+ async function listOllamaModels(baseUrl) {
149
+ const url = `${baseUrl || 'http://localhost:11434'}/api/tags`;
150
+ const urlObj = new URL(url);
151
+ const isHttps = urlObj.protocol === 'https:';
152
+ const client = isHttps ? https : http;
153
+
154
+ return new Promise((resolve, reject) => {
155
+ const req = client.request(urlObj, {
156
+ method: 'GET',
157
+ timeout: 10000
158
+ }, (res) => {
159
+ let data = '';
160
+ res.on('data', (chunk) => { data += chunk; });
161
+ res.on('end', () => {
162
+ if (res.statusCode !== 200) {
163
+ return reject(new Error(`Ollama returned ${res.statusCode}`));
164
+ }
165
+ try {
166
+ const response = JSON.parse(data);
167
+ const models = (response.models || []).map(m => m.name);
168
+ resolve(models);
169
+ } catch (e) {
170
+ reject(new Error(`Failed to parse Ollama model list: ${e.message}`));
171
+ }
172
+ });
173
+ });
174
+
175
+ req.on('error', (e) => reject(e));
176
+ req.on('timeout', () => {
177
+ req.destroy();
178
+ reject(new Error('Ollama connection timed out — is Ollama running?'));
179
+ });
180
+
181
+ req.end();
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Check if Ollama is running and reachable
187
+ * @param {string} baseUrl - Ollama URL
188
+ * @returns {Promise<boolean>}
189
+ */
190
+ async function isOllamaRunning(baseUrl) {
191
+ try {
192
+ const models = await listOllamaModels(baseUrl);
193
+ return models.length > 0;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Call Gemini API with the diff and return structured review.
201
+ * @param {string} diff - Git diff content
202
+ * @param {{ apiKey: string, apiUrl: string, model: string }} config
203
+ * @returns {Promise<string>} - Raw text response
204
+ */
205
+ async function callGeminiAPI(diff, config) {
206
+ const prompt = AI_REVIEW_PROMPT + diff;
207
+
208
+ let url;
209
+ if (config.apiUrl && config.apiUrl.includes('generativelanguage')) {
210
+ url = `${config.apiUrl}/${config.model || 'gemini-2.0-flash'}:generateContent?key=${config.apiKey}`;
211
+ } else if (config.apiUrl) {
212
+ url = config.apiUrl;
213
+ } else {
214
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model || 'gemini-2.0-flash'}:generateContent?key=${config.apiKey}`;
215
+ }
216
+
217
+ const payload = JSON.stringify({
218
+ contents: [{
219
+ parts: [{ text: prompt }]
220
+ }],
221
+ generationConfig: {
222
+ temperature: 0.2,
223
+ maxOutputTokens: 4096,
224
+ }
225
+ });
226
+
227
+ return new Promise((resolve, reject) => {
228
+ const req = https.request(url, {
229
+ method: 'POST',
230
+ headers: {
231
+ 'Content-Type': 'application/json',
232
+ 'Content-Length': Buffer.byteLength(payload)
233
+ },
234
+ timeout: 30000
235
+ }, (res) => {
236
+ let data = '';
237
+ res.on('data', (chunk) => { data += chunk; });
238
+ res.on('end', () => {
239
+ if (res.statusCode !== 200) {
240
+ return reject(new Error(`Gemini API returned ${res.statusCode}: ${data.substring(0, 200)}`));
241
+ }
242
+ try {
243
+ const response = JSON.parse(data);
244
+ const text = response.candidates?.[0]?.content?.parts?.[0]?.text;
245
+ if (!text) {
246
+ return reject(new Error('Gemini returned no text response'));
247
+ }
248
+ resolve(text);
249
+ } catch (e) {
250
+ reject(new Error(`Failed to parse Gemini response: ${e.message}`));
251
+ }
252
+ });
253
+ });
254
+
255
+ req.on('error', (e) => reject(e));
256
+ req.on('timeout', () => {
257
+ req.destroy();
258
+ reject(new Error('Gemini API request timed out after 30s'));
259
+ });
260
+
261
+ req.write(payload);
262
+ req.end();
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Parse AI response text into structured review result.
268
+ * Handles cases where the response is wrapped in markdown code blocks.
269
+ * @param {string} text - Raw AI response text
270
+ * @returns {{ overallStatus: string, score: number, summary: string, files: Array }}
271
+ */
272
+ function parseAIResponse(text) {
273
+ let cleaned = text.trim();
274
+ const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
275
+ if (codeBlockMatch) {
276
+ cleaned = codeBlockMatch[1].trim();
277
+ }
278
+
279
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
280
+ if (!jsonMatch) {
281
+ throw new Error('No JSON object found in AI response');
282
+ }
283
+
284
+ const parsed = JSON.parse(jsonMatch[0]);
285
+
286
+ if (!parsed.overallStatus || !parsed.score || !parsed.files) {
287
+ throw new Error('AI response missing required fields (overallStatus, score, files)');
288
+ }
289
+
290
+ const score = Math.max(1, Math.min(10, Math.round(parsed.score)));
291
+
292
+ return {
293
+ overallStatus: parsed.overallStatus.toUpperCase() === 'FAIL' ? 'FAIL' : 'PASS',
294
+ score,
295
+ summary: parsed.summary || 'No summary provided',
296
+ files: Array.isArray(parsed.files) ? parsed.files : []
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Main entry point: review a git diff.
302
+ * Provider priority: Ollama (if enabled) → Cloud provider (Gemini/OpenAI/Claude) → Heuristic fallback
303
+ *
304
+ * @param {string} diff - Git diff content
305
+ * @param {{ minScore?: number }} options
306
+ * @returns {Promise<{ success: boolean, result: object, message: string, usedFallback: boolean, provider: string }>}
307
+ */
308
+ async function reviewDiff(diff, options = {}) {
309
+ const minScore = options.minScore || 3;
310
+ const config = loadConfig();
311
+
312
+ // Determine which provider to use
313
+ const useOllama = config?.ollama?.enabled === true;
314
+ const provider = useOllama ? 'ollama' : (config?.provider || 'none');
315
+
316
+ // Try Ollama if enabled
317
+ if (useOllama) {
318
+ try {
319
+ const ollamaConfig = {
320
+ model: config.model || 'qwen2.5-coder',
321
+ url: config.ollama.url || 'http://localhost:11434'
322
+ };
323
+ const responseText = await callOllamaAPI(diff, ollamaConfig);
324
+ const result = parseAIResponse(responseText);
325
+
326
+ if (result.score < minScore) {
327
+ return {
328
+ success: false,
329
+ result,
330
+ message: `Ollama review score ${result.score}/10 is below threshold ${minScore}/10`,
331
+ usedFallback: false,
332
+ provider: 'ollama'
333
+ };
334
+ }
335
+
336
+ if (result.overallStatus === 'FAIL') {
337
+ return {
338
+ success: false,
339
+ result,
340
+ message: 'Ollama review found critical issues — review required',
341
+ usedFallback: false,
342
+ provider: 'ollama'
343
+ };
344
+ }
345
+
346
+ return {
347
+ success: true,
348
+ result,
349
+ message: `Ollama review passed — score ${result.score}/10 (${ollamaConfig.model})`,
350
+ usedFallback: false,
351
+ provider: 'ollama'
352
+ };
353
+ } catch (error) {
354
+ console.error(`Ollama review failed (${error.message}), trying cloud provider...`);
355
+ }
356
+ }
357
+
358
+ // Try cloud provider (Gemini, OpenAI, Claude)
359
+ if (config?.apiKey) {
360
+ try {
361
+ const responseText = await callGeminiAPI(diff, config);
362
+ const result = parseAIResponse(responseText);
363
+
364
+ if (result.score < minScore) {
365
+ return {
366
+ success: false,
367
+ result,
368
+ message: `${provider} review score ${result.score}/10 is below threshold ${minScore}/10`,
369
+ usedFallback: false,
370
+ provider
371
+ };
372
+ }
373
+
374
+ if (result.overallStatus === 'FAIL') {
375
+ return {
376
+ success: false,
377
+ result,
378
+ message: `${provider} review found critical issues — review required`,
379
+ usedFallback: false,
380
+ provider
381
+ };
382
+ }
383
+
384
+ return {
385
+ success: true,
386
+ result,
387
+ message: `${provider} review passed — score ${result.score}/10`,
388
+ usedFallback: false,
389
+ provider
390
+ };
391
+ } catch (error) {
392
+ console.error(`${provider} review failed (${error.message}), using heuristic fallback`);
393
+ }
394
+ }
395
+
396
+ // Final fallback: heuristic analyzer
397
+ const { analyzeCode } = require('../../../server/analyzer.js');
398
+ const heuristicResult = analyzeCode(diff);
399
+ const score = heuristicResult.files.reduce((min, f) => Math.min(min, f.score), 10);
400
+ const passed = score >= minScore && heuristicResult.overallStatus === 'PASS';
401
+
402
+ return {
403
+ success: passed,
404
+ result: { ...heuristicResult, score },
405
+ message: passed
406
+ ? `Heuristic review passed — score ${score}/10 (AI unavailable)`
407
+ : `Heuristic review failed — score ${score}/10 (AI unavailable)`,
408
+ usedFallback: true,
409
+ provider: 'heuristic'
410
+ };
411
+ }
412
+
413
+ module.exports = {
414
+ reviewDiff,
415
+ loadConfig,
416
+ callGeminiAPI,
417
+ callOllamaAPI,
418
+ listOllamaModels,
419
+ isOllamaRunning,
420
+ parseAIResponse,
421
+ AI_REVIEW_PROMPT
422
+ };
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CLI Integration Service - Phase 4
4
+ * CLI Integration Service
5
5
  *
6
- * Bridges the local CLI commands with EKG backend services.
7
- * Transforms CLI operations from local processing to backend-driven workflows.
6
+ * Bridges CLI commands with local git operations.
7
+ * EKG backend integration (Phase 4) removed not deployed.
8
8
  *
9
- * Key transformations:
10
- * - `codeflow index` → EKG Ingestion Service webhook simulation
11
- * - `codeflow analyze-diff` → EKG Query Service context-enhanced analysis
9
+ * Key operations:
10
+ * - `codeflow index` → Submit repository for indexing
11
+ * - `codeflow analyze-diff` → Local diff parsing only
12
12
  */
13
13
 
14
14
  import axios from 'axios';
@@ -235,9 +235,8 @@ export class CLIIntegrationService {
235
235
  }
236
236
 
237
237
  /**
238
- * Analyze code diff with EKG context enhancement
239
- *
240
- * Sends diff to Query Service for EKG-enhanced analysis instead of local RAG
238
+ * Analyze code diff simplified to local diff parsing only.
239
+ * EKG backend integration removed (Phase 4 not deployed).
241
240
  */
242
241
  async analyzeDiff(diffContent: string, options: {
243
242
  legacy?: boolean;
@@ -263,22 +262,12 @@ export class CLIIntegrationService {
263
262
  };
264
263
  }
265
264
 
266
- if (options.legacy) {
267
- // Fallback to local analysis (would integrate with existing agents)
268
- logger.warn('Legacy mode requested - falling back to local analysis');
269
- return {
270
- success: false,
271
- analysis: null,
272
- message: 'Legacy mode not yet implemented with EKG integration'
273
- };
274
- }
275
-
276
- logger.info('Analyzing diff with EKG context enhancement', {
265
+ logger.info('Analyzing diff (local only — EKG backend not deployed)', {
277
266
  diffSize: diffContent.length,
278
267
  lines: diffContent.split('\n').length
279
268
  });
280
269
 
281
- // Analyze diff and extract context
270
+ // Parse diff content locally
282
271
  const diffAnalysis = this.analyzeDiffContent(diffContent);
283
272
 
284
273
  if (diffAnalysis.files.length === 0) {
@@ -289,28 +278,30 @@ export class CLIIntegrationService {
289
278
  };
290
279
  }
291
280
 
292
- // Query EKG for context on affected files
293
- const ekgContext = await this.getEKGContext(diffAnalysis);
294
-
295
- // Generate enhanced analysis with EKG data
296
- const enhancedAnalysis = await this.generateEKGEnhancedAnalysis(diffAnalysis, ekgContext);
297
-
298
281
  const analysisTime = Date.now() - startTime;
299
282
 
300
- logger.info('Diff analysis completed with EKG enhancement', {
283
+ logger.info('Diff analysis completed', {
301
284
  affectedFiles: diffAnalysis.files.length,
302
- ekgQueries: ekgContext.queriesMade,
303
- similarReposFound: ekgContext.similarRepositories?.length || 0,
304
285
  analysisTime
305
286
  });
306
287
 
307
288
  return {
308
289
  success: true,
309
- analysis: enhancedAnalysis,
310
- message: 'Diff analyzed with EKG context enhancement',
290
+ analysis: {
291
+ summary: {
292
+ totalFiles: diffAnalysis.files.length,
293
+ totalAdditions: diffAnalysis.totalAdditions,
294
+ totalDeletions: diffAnalysis.totalDeletions,
295
+ ekgEnhanced: false
296
+ },
297
+ files: diffAnalysis.files,
298
+ issues: [],
299
+ recommendations: []
300
+ },
301
+ message: 'Diff analyzed (local analysis only)',
311
302
  stats: {
312
- ekg_queries: ekgContext.queriesMade,
313
- similar_repos_found: ekgContext.similarRepositories?.length || 0,
303
+ ekg_queries: 0,
304
+ similar_repos_found: 0,
314
305
  analysis_time: analysisTime
315
306
  }
316
307
  };
@@ -410,178 +401,6 @@ export class CLIIntegrationService {
410
401
  };
411
402
  }
412
403
 
413
- /**
414
- * Query EKG for context on affected files
415
- */
416
- private async getEKGContext(diffAnalysis: any): Promise<{
417
- queriesMade: number;
418
- repositoryIntelligence?: any;
419
- similarRepositories: any[];
420
- patterns: any[];
421
- }> {
422
- let queriesMade = 0;
423
-
424
- try {
425
- // Get current repository information
426
- const repoInfo = await this.getRepositoryInfo();
427
- const repositoryId = this.generateRepositoryId(repoInfo.fullName);
428
-
429
- // Query repository intelligence if repository exists in EKG
430
- let repositoryIntelligence = null;
431
- try {
432
- const response = await this.makeGraphQLRequest(
433
- `
434
- query GetRepositoryIntelligence($repoId: ID!) {
435
- repositoryIntelligence(repositoryId: $repoId) {
436
- repository {
437
- id name fullName language
438
- }
439
- patterns {
440
- name type confidence category
441
- }
442
- dependencies {
443
- dependencyType currentVersion confidence
444
- }
445
- }
446
- }
447
- `,
448
- { repoId: repositoryId }
449
- );
450
- repositoryIntelligence = response.data?.repositoryIntelligence;
451
- queriesMade++;
452
- } catch (error) {
453
- logger.warn('Repository not found in EKG, continuing analysis', { repositoryId });
454
- }
455
-
456
- // Find similar repositories and patterns for context
457
- let similarRepositories: any[] = [];
458
- try {
459
- const response = await this.makeGraphQLRequest(
460
- `
461
- query FindSimilarRepositories($repoId: ID!, $limit: Int) {
462
- similarRepositories(repositoryId: $repoId, limit: $limit) {
463
- repository { name fullName language }
464
- similarityScore reasons
465
- sharedPatterns sizeComparison
466
- }
467
- }
468
- `,
469
- { repoId: repositoryId, limit: 5 }
470
- );
471
- similarRepositories = response.data?.similarRepositories || [];
472
- queriesMade++;
473
- } catch (error) {
474
- logger.warn('Could not fetch similar repositories', { error: this.formatError(error) });
475
- }
476
-
477
- // Get enterprise-wide patterns that might be relevant
478
- let patterns: any[] = [];
479
- try {
480
- const languages = [...new Set(diffAnalysis.files.map((f: any) => f.language))];
481
-
482
- const response = await this.makeGraphQLRequest(
483
- `
484
- query GetRelevantPatterns($language: String, $limit: Int) {
485
- patterns(language: $language, minConfidence: 0.7, limit: $limit) {
486
- name type category confidence observationCount
487
- }
488
- }
489
- `,
490
- { language: languages[0], limit: 10 }
491
- );
492
- patterns = response.data?.patterns || [];
493
- queriesMade++;
494
- } catch (error) {
495
- logger.warn('Could not fetch patterns', { error: this.formatError(error) });
496
- }
497
-
498
- return {
499
- queriesMade,
500
- repositoryIntelligence,
501
- similarRepositories,
502
- patterns
503
- };
504
-
505
- } catch (error) {
506
- logger.error('EKG context retrieval failed', { error: this.formatError(error) });
507
- return {
508
- queriesMade,
509
- similarRepositories: [],
510
- patterns: []
511
- };
512
- }
513
- }
514
-
515
- /**
516
- * Generate enhanced analysis using EKG context
517
- */
518
- private async generateEKGEnhancedAnalysis(diffAnalysis: any, ekgContext: any): Promise<any> {
519
- // Analyze changes with EKG context
520
- const issues: any[] = [];
521
- const recommendations: any[] = [];
522
-
523
- // Check against existing patterns
524
- if (ekgContext.patterns && ekgContext.patterns.length > 0) {
525
- for (const file of diffAnalysis.files) {
526
- const relevantPatterns = ekgContext.patterns.filter((p: any) =>
527
- p.type === 'security' || p.type === 'architecture'
528
- );
529
-
530
- if (relevantPatterns.length > 0) {
531
- recommendations.push({
532
- type: 'ekg_pattern_alignment',
533
- description: `File ${file.path} modified - consider these established patterns: ${relevantPatterns.map((p: any) => p.name).join(', ')}`,
534
- severity: 'info',
535
- file: file.path
536
- });
537
- }
538
- }
539
- }
540
-
541
- // Compare against similar repositories
542
- if (ekgContext.similarRepositories && ekgContext.similarRepositories.length > 0) {
543
- const similarRepoNames = ekgContext.similarRepositories.map((sr: any) => sr.repository.fullName);
544
- recommendations.push({
545
- type: 'similar_repositories',
546
- description: `Changes similar to patterns seen in: ${similarRepoNames.slice(0, 3).join(', ')}`,
547
- severity: 'info'
548
- });
549
- }
550
-
551
- // Add repository-specific context if available
552
- if (ekgContext.repositoryIntelligence) {
553
- const repo = ekgContext.repositoryIntelligence.repository;
554
- issues.push({
555
- type: 'repository_context',
556
- description: `Analyzing changes in repository ${repo.fullName} (${repo.language})`,
557
- severity: 'info'
558
- });
559
- }
560
-
561
- return {
562
- summary: {
563
- totalFiles: diffAnalysis.files.length,
564
- totalAdditions: diffAnalysis.totalAdditions,
565
- totalDeletions: diffAnalysis.totalDeletions,
566
- ekgEnhanced: true
567
- },
568
- files: diffAnalysis.files.map((file: any) => ({
569
- path: file.path,
570
- language: file.language,
571
- additions: file.additions,
572
- deletions: file.deletions,
573
- isNew: file.isNew
574
- })),
575
- issues,
576
- recommendations,
577
- ekg_context: {
578
- patterns_analyzed: ekgContext.patterns?.length || 0,
579
- similar_repositories_found: ekgContext.similarRepositories?.length || 0,
580
- repository_known: !!ekgContext.repositoryIntelligence
581
- }
582
- };
583
- }
584
-
585
404
  /**
586
405
  * Get current repository information
587
406
  */
@@ -780,18 +599,7 @@ export class CLIIntegrationService {
780
599
  }
781
600
 
782
601
  /**
783
- * Make GraphQL request to Query Service
784
- */
785
- private async makeGraphQLRequest(query: string, variables: any = {}): Promise<any> {
786
- return this.makeBackendRequest(
787
- `${this.config.queryServiceUrl}/graphql`,
788
- { query, variables },
789
- { 'Content-Type': 'application/json' }
790
- );
791
- }
792
-
793
- /**
794
- * Generate repository ID (similar to ingestion service)
602
+ * Generate repository ID
795
603
  */
796
604
  private generateRepositoryId(fullName: string): string {
797
605
  return `${fullName.replace('/', '-')}-${Date.now().toString(36)}`;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  {
3
3
  "name": "codeflow-hook",
4
- "version": "2.0.3",
5
- "description": "An interactive CI/CD simulator and lightweight pre-push code reviewer using Gemini AI",
4
+ "version": "2.2.0",
5
+ "description": "Local AI-powered code analysis and pre-push review for development workflows",
6
6
  "type": "module",
7
7
  "main": "index.js",
8
8
  "bin": {