codeflow-hook 2.1.0 → 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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import chalk from 'chalk';
@@ -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