korekt-cli 0.11.2 → 0.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korekt-cli",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "AI-powered code review CLI - Keep your kode korekt",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/formatter.js CHANGED
@@ -76,7 +76,7 @@ function toAbsolutePath(filePath) {
76
76
  * @param {Object} data - The API response data
77
77
  */
78
78
  export function formatReviewOutput(data) {
79
- const { review, summary, change_classification: changeClassification } = data.data;
79
+ const { review, summary, change_classification: changeClassification, model } = data.data;
80
80
 
81
81
  console.log(chalk.bold.blue('šŸ¤– Automated Code Review Results\n'));
82
82
 
@@ -162,4 +162,9 @@ export function formatReviewOutput(data) {
162
162
  console.log(); // Add a blank line for spacing
163
163
  });
164
164
  }
165
+
166
+ // Footer with model info
167
+ if (model) {
168
+ console.log(chalk.gray(`Analyzed with ${model}`));
169
+ }
165
170
  }
package/src/index.js CHANGED
@@ -72,6 +72,57 @@ async function confirmAction(message) {
72
72
  });
73
73
  }
74
74
 
75
+ /**
76
+ * Available Gemini models for code review
77
+ */
78
+ const GEMINI_MODELS = [
79
+ { value: 'gemini-2.5-pro', label: 'gemini-2.5-pro (high quality)' },
80
+ { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash (avoid, the worst quality)' },
81
+ { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview (experimental - the best model)' },
82
+ {
83
+ value: 'gemini-3-flash-preview',
84
+ label: 'gemini-3-flash-preview (experimental - the most efficient model)',
85
+ },
86
+ ];
87
+
88
+ /**
89
+ * Prompt user to select a Gemini model
90
+ * @returns {Promise<string>} - Selected model name
91
+ * @throws {Error} - If not running in interactive terminal
92
+ */
93
+ async function selectModel() {
94
+ // Check if running in interactive terminal
95
+ if (!process.stdin.isTTY) {
96
+ log(chalk.red('Error: --model flag requires a value in non-interactive environments.'));
97
+ log(chalk.gray('Example: kk review --model=gemini-2.5-pro'));
98
+ process.exit(1);
99
+ }
100
+
101
+ const rl = readline.createInterface({
102
+ input: process.stdin,
103
+ output: process.stderr,
104
+ });
105
+
106
+ log(chalk.yellow('\nSelect Gemini model:\n'));
107
+ GEMINI_MODELS.forEach((model, index) => {
108
+ log(` ${index + 1}. ${model.label}`);
109
+ });
110
+ log('');
111
+
112
+ return new Promise((resolvePromise) => {
113
+ rl.question(chalk.bold('Enter number (1-4): '), (answer) => {
114
+ rl.close();
115
+ const num = parseInt(answer, 10);
116
+ if (num >= 1 && num <= GEMINI_MODELS.length) {
117
+ resolvePromise(GEMINI_MODELS[num - 1].value);
118
+ } else {
119
+ log(chalk.red('Invalid selection. Please run the command again and enter 1-4.'));
120
+ process.exit(1);
121
+ }
122
+ });
123
+ });
124
+ }
125
+
75
126
  /**
76
127
  * Run the CI integration script to post comments
77
128
  * @param {string} provider - CI provider (github, azure, bitbucket)
@@ -128,6 +179,8 @@ program
128
179
  Examples:
129
180
  $ kk review Review committed changes (auto-detect base)
130
181
  $ kk review main Review changes against main branch
182
+ $ kk review --model Select Gemini model interactively
183
+ $ kk review -m gemini-2.5-pro Use specific model
131
184
  $ kk stg --dry-run Preview staged changes review
132
185
  $ kk diff Review unstaged changes
133
186
  $ kk review main --json Output raw JSON (for CI/CD integration)
@@ -137,6 +190,7 @@ Common Options:
137
190
  --dry-run Show payload without sending to API
138
191
  --json Output raw API response as JSON
139
192
  --comment Post review results as PR comments
193
+ -m, --model [name] Gemini model (picker if no value)
140
194
 
141
195
  Configuration:
142
196
  $ kk config --key YOUR_KEY
@@ -159,6 +213,7 @@ program
159
213
  .option('--json', 'Output raw API response as JSON')
160
214
  .option('--comment', 'Post review results as PR comments (auto-detects CI provider)')
161
215
  .option('--post-ticket', 'Post review results to linked ticket (e.g., JIRA)')
216
+ .option('-m, --model [name]', 'Gemini model to use (interactive picker if no value provided)')
162
217
  .action(async (targetBranch, options) => {
163
218
  const reviewTarget = targetBranch ? `against '${targetBranch}'` : '(auto-detecting fork point)';
164
219
 
@@ -179,6 +234,15 @@ program
179
234
  process.exit(1);
180
235
  }
181
236
 
237
+ // Handle model selection: if --model without value, show picker
238
+ let selectedModel = null;
239
+ if (options.model === true) {
240
+ selectedModel = await selectModel();
241
+ log(chalk.green(`Using model: ${selectedModel}\n`));
242
+ } else if (typeof options.model === 'string') {
243
+ selectedModel = options.model;
244
+ }
245
+
182
246
  // Fetch file rules config from API
183
247
  const fileRulesConfig = await fetchFileRulesConfig(apiEndpoint, apiKey);
184
248
 
@@ -206,6 +270,11 @@ program
206
270
  changed_files: payload.changed_files.map((file) => truncateFileData(file)),
207
271
  };
208
272
 
273
+ // Add model if specified
274
+ if (selectedModel) {
275
+ displayPayload.model = selectedModel;
276
+ }
277
+
209
278
  log(JSON.stringify(displayPayload, null, 2));
210
279
  log(chalk.gray('\nšŸ’” Run without --dry-run to send to API'));
211
280
  log(chalk.gray('šŸ’” Diffs and content are truncated in dry-run for readability'));
@@ -255,6 +324,11 @@ program
255
324
  payload.post_to_ticket = true;
256
325
  }
257
326
 
327
+ // Add model if specified
328
+ if (selectedModel) {
329
+ payload.model = selectedModel;
330
+ }
331
+
258
332
  try {
259
333
  const response = await axios.post(apiEndpoint, payload, {
260
334
  headers: {
@@ -328,6 +402,7 @@ program
328
402
  .description('Review staged changes (git diff --cached)')
329
403
  .option('--dry-run', 'Show payload without sending to API')
330
404
  .option('--json', 'Output raw API response as JSON')
405
+ .option('-m, --model [name]', 'Gemini model to use (interactive picker if no value provided)')
331
406
  .action(async (options) => {
332
407
  log(chalk.blue.bold('šŸš€ Reviewing staged changes...'));
333
408
  await reviewUncommitted('staged', options);
@@ -339,6 +414,7 @@ program
339
414
  .description('Review unstaged changes (git diff)')
340
415
  .option('--dry-run', 'Show payload without sending to API')
341
416
  .option('--json', 'Output raw API response as JSON')
417
+ .option('-m, --model [name]', 'Gemini model to use (interactive picker if no value provided)')
342
418
  .action(async (options) => {
343
419
  log(chalk.blue.bold('šŸš€ Reviewing unstaged changes...'));
344
420
  await reviewUncommitted('unstaged', options);
@@ -359,6 +435,15 @@ async function reviewUncommitted(mode, options) {
359
435
  process.exit(1);
360
436
  }
361
437
 
438
+ // Handle model selection: if --model without value, show picker
439
+ let selectedModel = null;
440
+ if (options.model === true) {
441
+ selectedModel = await selectModel();
442
+ log(chalk.green(`Using model: ${selectedModel}\n`));
443
+ } else if (typeof options.model === 'string') {
444
+ selectedModel = options.model;
445
+ }
446
+
362
447
  // Fetch file rules config from API
363
448
  const fileRulesConfig = await fetchFileRulesConfig(apiEndpoint, apiKey);
364
449
 
@@ -378,6 +463,11 @@ async function reviewUncommitted(mode, options) {
378
463
  changed_files: payload.changed_files.map((file) => truncateFileData(file)),
379
464
  };
380
465
 
466
+ // Add model if specified
467
+ if (selectedModel) {
468
+ displayPayload.model = selectedModel;
469
+ }
470
+
381
471
  log(JSON.stringify(displayPayload, null, 2));
382
472
  log(chalk.gray('\nšŸ’” Run without --dry-run to send to API'));
383
473
  log(chalk.gray('šŸ’” Diffs and content are truncated in dry-run for readability'));
@@ -410,6 +500,11 @@ async function reviewUncommitted(mode, options) {
410
500
  }
411
501
  }
412
502
 
503
+ // Add model if specified
504
+ if (selectedModel) {
505
+ payload.model = selectedModel;
506
+ }
507
+
413
508
  const spinner = ora('Submitting review to the AI...').start();
414
509
  const startTime = Date.now();
415
510
 
package/src/index.test.js CHANGED
@@ -469,6 +469,86 @@ describe('--comment flag behavior', () => {
469
469
  });
470
470
  });
471
471
 
472
+ describe('--model flag behavior', () => {
473
+ describe('model selection logic', () => {
474
+ it('should treat --model without value as true (picker mode)', () => {
475
+ const options = { model: true };
476
+
477
+ // When --model is used without a value, Commander sets it to true
478
+ const shouldShowPicker = options.model === true;
479
+ const hasDirectValue = typeof options.model === 'string';
480
+
481
+ expect(shouldShowPicker).toBe(true);
482
+ expect(hasDirectValue).toBe(false);
483
+ });
484
+
485
+ it('should treat --model=value as string (direct mode)', () => {
486
+ const options = { model: 'gemini-2.5-flash' };
487
+
488
+ const shouldShowPicker = options.model === true;
489
+ const hasDirectValue = typeof options.model === 'string';
490
+
491
+ expect(shouldShowPicker).toBe(false);
492
+ expect(hasDirectValue).toBe(true);
493
+ expect(options.model).toBe('gemini-2.5-flash');
494
+ });
495
+
496
+ it('should not include model when flag is not used', () => {
497
+ const options = {};
498
+
499
+ const shouldIncludeModel = options.model !== undefined;
500
+
501
+ expect(shouldIncludeModel).toBe(false);
502
+ });
503
+ });
504
+
505
+ describe('payload model field', () => {
506
+ it('should add model to payload when specified', () => {
507
+ const payload = {
508
+ repo_url: 'https://github.com/user/repo',
509
+ changed_files: [],
510
+ };
511
+
512
+ const selectedModel = 'gemini-2.5-flash';
513
+ if (selectedModel) {
514
+ payload.model = selectedModel;
515
+ }
516
+
517
+ expect(payload.model).toBe('gemini-2.5-flash');
518
+ });
519
+
520
+ it('should not add model to payload when not specified', () => {
521
+ const payload = {
522
+ repo_url: 'https://github.com/user/repo',
523
+ changed_files: [],
524
+ };
525
+
526
+ const selectedModel = null;
527
+ if (selectedModel) {
528
+ payload.model = selectedModel;
529
+ }
530
+
531
+ expect(payload.model).toBeUndefined();
532
+ });
533
+ });
534
+
535
+ describe('available models', () => {
536
+ it('should have valid model values', () => {
537
+ const validModels = [
538
+ 'gemini-2.5-pro',
539
+ 'gemini-2.5-flash',
540
+ 'gemini-3-pro-preview',
541
+ 'gemini-3-flash-preview',
542
+ ];
543
+
544
+ // Test that all expected models are valid Gemini model names
545
+ validModels.forEach((model) => {
546
+ expect(model).toMatch(/^gemini-/);
547
+ });
548
+ });
549
+ });
550
+ });
551
+
472
552
  describe('getPrUrl', () => {
473
553
  const originalEnv = process.env;
474
554