@eldrforge/kodrdriv 1.2.0 → 1.2.2

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.
@@ -2,20 +2,122 @@
2
2
  import { Formatter } from '@riotprompt/riotprompt';
3
3
  import { ValidationError, FileOperationError, CommandError } from '../error/CommandErrors.js';
4
4
  import { getLogger } from '../logging.js';
5
- import { getModelForCommand, createCompletion } from '../util/openai.js';
5
+ import { getModelForCommand, createCompletion, getOpenAIMaxOutputTokensForCommand, getOpenAIReasoningForCommand } from '../util/openai.js';
6
6
  import { createPrompt } from '../prompt/review.js';
7
- import { create } from '../content/log.js';
7
+ import { create as create$1 } from '../content/log.js';
8
8
  import { getReviewExcludedPatterns, getRecentDiffsForReview } from '../content/diff.js';
9
9
  import { get } from '../content/releaseNotes.js';
10
- import { get as get$1, handleIssueCreation } from '../content/issues.js';
10
+ import { handleIssueCreation, get as get$1 } from '../content/issues.js';
11
11
  import { DEFAULT_EXCLUDED_PATTERNS, DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
12
- import { getTimestampedReviewNotesFilename, getOutputPath, getTimestampedResponseFilename, getTimestampedRequestFilename, getTimestampedReviewFilename } from '../util/general.js';
13
- import { create as create$1 } from '../util/storage.js';
12
+ import { getTimestampedReviewNotesFilename, getOutputPath, getTimestampedReviewFilename, getTimestampedResponseFilename, getTimestampedRequestFilename } from '../util/general.js';
13
+ import { create } from '../util/storage.js';
14
+ import { getUserChoice } from '../util/interactive.js';
14
15
  import path__default from 'path';
15
16
  import os__default from 'os';
16
17
  import { spawn } from 'child_process';
17
18
  import fs__default from 'fs/promises';
18
19
 
20
+ // Utility function to read a review note from a file
21
+ const readReviewNoteFromFile = async (filePath)=>{
22
+ const logger = getLogger();
23
+ try {
24
+ logger.debug(`Reading review note from file: ${filePath}`);
25
+ const content = await fs__default.readFile(filePath, 'utf8');
26
+ if (!content.trim()) {
27
+ throw new ValidationError(`Review file is empty: ${filePath}`);
28
+ }
29
+ logger.debug(`Successfully read review note from file: ${filePath} (${content.length} characters)`);
30
+ return content.trim();
31
+ } catch (error) {
32
+ if (error.code === 'ENOENT') {
33
+ throw new FileOperationError(`Review file not found: ${filePath}`, filePath, error);
34
+ }
35
+ if (error instanceof ValidationError) {
36
+ throw error;
37
+ }
38
+ throw new FileOperationError(`Failed to read review file: ${error.message}`, filePath, error);
39
+ }
40
+ };
41
+ // Utility function to get all review files in a directory
42
+ const getReviewFilesInDirectory = async (directoryPath)=>{
43
+ const logger = getLogger();
44
+ try {
45
+ logger.debug(`Scanning directory for review files: ${directoryPath}`);
46
+ const entries = await fs__default.readdir(directoryPath, {
47
+ withFileTypes: true
48
+ });
49
+ // Filter for regular files (not directories) and get full paths
50
+ const files = entries.filter((entry)=>entry.isFile()).map((entry)=>path__default.join(directoryPath, entry.name)).sort(); // Sort alphabetically
51
+ logger.debug(`Found ${files.length} files in directory: ${directoryPath}`);
52
+ return files;
53
+ } catch (error) {
54
+ if (error.code === 'ENOENT') {
55
+ throw new FileOperationError(`Directory not found: ${directoryPath}`, directoryPath, error);
56
+ }
57
+ throw new FileOperationError(`Failed to read directory: ${directoryPath}`, directoryPath, error);
58
+ }
59
+ };
60
+ // New function for file selection phase
61
+ const selectFilesForProcessing = async (reviewFiles, senditMode)=>{
62
+ const logger = getLogger();
63
+ if (senditMode) {
64
+ logger.info(`Auto-selecting all ${reviewFiles.length} files for processing (--sendit mode)`);
65
+ return reviewFiles;
66
+ }
67
+ // Check if we're in an interactive environment
68
+ if (!isTTYSafe()) {
69
+ logger.warn(`Non-interactive environment detected, selecting all files for processing`);
70
+ return reviewFiles;
71
+ }
72
+ logger.info(`\nšŸ“ File Selection Phase`);
73
+ logger.info(`Found ${reviewFiles.length} files to review. Select which ones to process:`);
74
+ logger.info(`[c] Confirm this file for processing`);
75
+ logger.info(`[s] Skip this file`);
76
+ logger.info(`[a] Abort the entire review process`);
77
+ logger.info(``);
78
+ const selectedFiles = [];
79
+ let shouldAbort = false;
80
+ for(let i = 0; i < reviewFiles.length; i++){
81
+ const filePath = reviewFiles[i];
82
+ logger.info(`File ${i + 1}/${reviewFiles.length}: ${filePath}`);
83
+ const choice = await getUserChoice(`Select action for this file:`, [
84
+ {
85
+ key: 'c',
86
+ label: 'Confirm and process'
87
+ },
88
+ {
89
+ key: 's',
90
+ label: 'Skip this file'
91
+ },
92
+ {
93
+ key: 'a',
94
+ label: 'Abort entire review'
95
+ }
96
+ ]);
97
+ if (choice === 'a') {
98
+ logger.info(`šŸ›‘ Aborting review process as requested`);
99
+ shouldAbort = true;
100
+ break;
101
+ } else if (choice === 'c') {
102
+ selectedFiles.push(filePath);
103
+ logger.info(`āœ… File selected for processing: ${filePath}`);
104
+ } else if (choice === 's') {
105
+ logger.info(`ā­ļø File skipped: ${filePath}`);
106
+ }
107
+ }
108
+ if (shouldAbort) {
109
+ throw new Error('Review process aborted by user');
110
+ }
111
+ if (selectedFiles.length === 0) {
112
+ throw new Error('No files were selected for processing');
113
+ }
114
+ logger.info(`\nšŸ“‹ File selection complete. ${selectedFiles.length} files selected for processing:`);
115
+ selectedFiles.forEach((file, index)=>{
116
+ logger.info(` ${index + 1}. ${file}`);
117
+ });
118
+ logger.info(``);
119
+ return selectedFiles;
120
+ };
19
121
  // Safe temp file handling with proper permissions and validation
20
122
  const createSecureTempFile = async ()=>{
21
123
  const logger = getLogger();
@@ -192,99 +294,10 @@ const safeWriteFile = async (filePath, content, encoding = 'utf-8')=>{
192
294
  throw new Error(`Failed to write file ${filePath}: ${error.message}`);
193
295
  }
194
296
  };
195
- const executeInternal = async (runConfig)=>{
196
- var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review4, _runConfig_review5, _runConfig_review6, _runConfig_review7, _runConfig_review8, _runConfig_review9, _runConfig_review10, _runConfig_review11, _runConfig_review12, _runConfig_review13, _runConfig_review14, _runConfig_review_context, _runConfig_review15, _runConfig_review16, _analysisResult_issues, _runConfig_review17;
297
+ // Helper function to process a single review note
298
+ const processSingleReview = async (reviewNote, runConfig, outputDirectory)=>{
299
+ var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review_context, _runConfig_review4, _runConfig_review5, _analysisResult_issues;
197
300
  const logger = getLogger();
198
- const isDryRun = runConfig.dryRun || false;
199
- // Show configuration even in dry-run mode
200
- logger.debug('Review context configuration:');
201
- logger.debug(' Include commit history: %s', (_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.includeCommitHistory);
202
- logger.debug(' Include recent diffs: %s', (_runConfig_review1 = runConfig.review) === null || _runConfig_review1 === void 0 ? void 0 : _runConfig_review1.includeRecentDiffs);
203
- logger.debug(' Include release notes: %s', (_runConfig_review2 = runConfig.review) === null || _runConfig_review2 === void 0 ? void 0 : _runConfig_review2.includeReleaseNotes);
204
- logger.debug(' Include GitHub issues: %s', (_runConfig_review3 = runConfig.review) === null || _runConfig_review3 === void 0 ? void 0 : _runConfig_review3.includeGithubIssues);
205
- logger.debug(' Commit history limit: %d', (_runConfig_review4 = runConfig.review) === null || _runConfig_review4 === void 0 ? void 0 : _runConfig_review4.commitHistoryLimit);
206
- logger.debug(' Diff history limit: %d', (_runConfig_review5 = runConfig.review) === null || _runConfig_review5 === void 0 ? void 0 : _runConfig_review5.diffHistoryLimit);
207
- logger.debug(' Release notes limit: %d', (_runConfig_review6 = runConfig.review) === null || _runConfig_review6 === void 0 ? void 0 : _runConfig_review6.releaseNotesLimit);
208
- logger.debug(' GitHub issues limit: %d', (_runConfig_review7 = runConfig.review) === null || _runConfig_review7 === void 0 ? void 0 : _runConfig_review7.githubIssuesLimit);
209
- logger.debug(' Sendit mode (auto-create issues): %s', (_runConfig_review8 = runConfig.review) === null || _runConfig_review8 === void 0 ? void 0 : _runConfig_review8.sendit);
210
- if (isDryRun) {
211
- var _runConfig_review18, _runConfig_review19;
212
- logger.info('DRY RUN: Would analyze provided note for review');
213
- logger.info('DRY RUN: Would gather additional context based on configuration above');
214
- logger.info('DRY RUN: Would analyze note and identify issues');
215
- if ((_runConfig_review18 = runConfig.review) === null || _runConfig_review18 === void 0 ? void 0 : _runConfig_review18.sendit) {
216
- logger.info('DRY RUN: Would automatically create GitHub issues (sendit mode enabled)');
217
- } else {
218
- logger.info('DRY RUN: Would prompt for confirmation before creating GitHub issues');
219
- }
220
- // Show what exclusion patterns would be used in dry-run mode
221
- if ((_runConfig_review19 = runConfig.review) === null || _runConfig_review19 === void 0 ? void 0 : _runConfig_review19.includeRecentDiffs) {
222
- var _runConfig_excludedPatterns;
223
- const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
224
- const reviewExcluded = getReviewExcludedPatterns(basePatterns);
225
- logger.info('DRY RUN: Would use %d exclusion patterns for diff context', reviewExcluded.length);
226
- logger.debug('DRY RUN: Sample exclusions: %s', reviewExcluded.slice(0, 15).join(', ') + (reviewExcluded.length > 15 ? '...' : ''));
227
- }
228
- return 'DRY RUN: Review command would analyze note, gather context, and create GitHub issues';
229
- }
230
- // Enhanced TTY check with proper error handling
231
- const isInteractive = isTTYSafe();
232
- if (!isInteractive && !((_runConfig_review9 = runConfig.review) === null || _runConfig_review9 === void 0 ? void 0 : _runConfig_review9.sendit)) {
233
- logger.error('āŒ STDIN is piped but --sendit flag is not enabled');
234
- logger.error(' Interactive prompts cannot be used when input is piped');
235
- logger.error(' Solutions:');
236
- logger.error(' • Add --sendit flag to auto-create all issues');
237
- logger.error(' • Use terminal input instead of piping');
238
- logger.error(' • Example: echo "note" | kodrdriv review --sendit');
239
- throw new ValidationError('Piped input requires --sendit flag for non-interactive operation');
240
- }
241
- // Get the review note from configuration
242
- let reviewNote = (_runConfig_review10 = runConfig.review) === null || _runConfig_review10 === void 0 ? void 0 : _runConfig_review10.note;
243
- // If no review note was provided via CLI arg or STDIN, open the user's editor to capture it.
244
- if (!reviewNote || !reviewNote.trim()) {
245
- const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
246
- let tmpFilePath = null;
247
- try {
248
- var _runConfig_review20;
249
- // Create secure temporary file
250
- tmpFilePath = await createSecureTempFile();
251
- // Pre-populate the file with a helpful header so users know what to do.
252
- const templateContent = [
253
- '# Kodrdriv Review Note',
254
- '',
255
- '# Please enter your review note below. Lines starting with "#" will be ignored.',
256
- '# Save and close the editor when you are done.',
257
- '',
258
- ''
259
- ].join('\n');
260
- await safeWriteFile(tmpFilePath, templateContent);
261
- logger.info(`No review note provided – opening ${editor} to capture input...`);
262
- // Open the editor with optional timeout protection
263
- const editorTimeout = (_runConfig_review20 = runConfig.review) === null || _runConfig_review20 === void 0 ? void 0 : _runConfig_review20.editorTimeout; // No default timeout - let user take their time
264
- await openEditorWithTimeout(editor, tmpFilePath, editorTimeout);
265
- // Read the file back in, stripping comment lines and whitespace.
266
- const fileContent = (await fs__default.readFile(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
267
- if (!fileContent) {
268
- throw new ValidationError('Review note is empty – aborting. Provide a note as an argument, via STDIN, or through the editor.');
269
- }
270
- reviewNote = fileContent;
271
- // If the original runConfig.review object exists, update it so downstream code has the note.
272
- if (runConfig.review) {
273
- runConfig.review.note = reviewNote;
274
- }
275
- } catch (error) {
276
- logger.error(`Failed to capture review note via editor: ${error.message}`);
277
- throw error;
278
- } finally{
279
- // Always clean up the temp file
280
- if (tmpFilePath) {
281
- await cleanupTempFile(tmpFilePath);
282
- }
283
- }
284
- }
285
- logger.info('šŸ“ Starting review analysis...');
286
- logger.debug('Review note: %s', reviewNote);
287
- logger.debug('Review note length: %d characters', reviewNote.length);
288
301
  // Gather additional context based on configuration with improved error handling
289
302
  let logContext = '';
290
303
  let diffContext = '';
@@ -292,10 +305,10 @@ const executeInternal = async (runConfig)=>{
292
305
  let issuesContext = '';
293
306
  const contextErrors = [];
294
307
  // Fetch commit history if enabled
295
- if ((_runConfig_review11 = runConfig.review) === null || _runConfig_review11 === void 0 ? void 0 : _runConfig_review11.includeCommitHistory) {
308
+ if ((_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.includeCommitHistory) {
296
309
  try {
297
310
  logger.debug('Fetching recent commit history...');
298
- const log = await create({
311
+ const log = await create$1({
299
312
  limit: runConfig.review.commitHistoryLimit
300
313
  });
301
314
  const logContent = await log.get();
@@ -310,11 +323,11 @@ const executeInternal = async (runConfig)=>{
310
323
  }
311
324
  }
312
325
  // Fetch recent diffs if enabled
313
- if ((_runConfig_review12 = runConfig.review) === null || _runConfig_review12 === void 0 ? void 0 : _runConfig_review12.includeRecentDiffs) {
326
+ if ((_runConfig_review1 = runConfig.review) === null || _runConfig_review1 === void 0 ? void 0 : _runConfig_review1.includeRecentDiffs) {
314
327
  try {
315
328
  logger.debug('Fetching recent commit diffs...');
316
- var _runConfig_excludedPatterns1;
317
- const basePatterns = (_runConfig_excludedPatterns1 = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns1 !== void 0 ? _runConfig_excludedPatterns1 : DEFAULT_EXCLUDED_PATTERNS;
329
+ var _runConfig_excludedPatterns;
330
+ const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
318
331
  const recentDiffs = await getRecentDiffsForReview({
319
332
  limit: runConfig.review.diffHistoryLimit,
320
333
  baseExcludedPatterns: basePatterns
@@ -330,7 +343,7 @@ const executeInternal = async (runConfig)=>{
330
343
  }
331
344
  }
332
345
  // Fetch release notes if enabled
333
- if ((_runConfig_review13 = runConfig.review) === null || _runConfig_review13 === void 0 ? void 0 : _runConfig_review13.includeReleaseNotes) {
346
+ if ((_runConfig_review2 = runConfig.review) === null || _runConfig_review2 === void 0 ? void 0 : _runConfig_review2.includeReleaseNotes) {
334
347
  try {
335
348
  logger.debug('Fetching recent release notes from GitHub...');
336
349
  const releaseNotesContent = await get({
@@ -347,7 +360,7 @@ const executeInternal = async (runConfig)=>{
347
360
  }
348
361
  }
349
362
  // Fetch GitHub issues if enabled
350
- if ((_runConfig_review14 = runConfig.review) === null || _runConfig_review14 === void 0 ? void 0 : _runConfig_review14.includeGithubIssues) {
363
+ if ((_runConfig_review3 = runConfig.review) === null || _runConfig_review3 === void 0 ? void 0 : _runConfig_review3.includeGithubIssues) {
351
364
  try {
352
365
  logger.debug('Fetching open GitHub issues...');
353
366
  issuesContext = await get$1({
@@ -364,11 +377,11 @@ const executeInternal = async (runConfig)=>{
364
377
  }
365
378
  // Report context gathering results
366
379
  if (contextErrors.length > 0) {
367
- var _runConfig_review21;
380
+ var _runConfig_review6;
368
381
  logger.warn(`Context gathering completed with ${contextErrors.length} error(s):`);
369
382
  contextErrors.forEach((error)=>logger.warn(` - ${error}`));
370
383
  // For critical operations, consider failing if too many context sources fail
371
- const maxContextErrors = ((_runConfig_review21 = runConfig.review) === null || _runConfig_review21 === void 0 ? void 0 : _runConfig_review21.maxContextErrors) || contextErrors.length; // Default: allow all errors
384
+ const maxContextErrors = ((_runConfig_review6 = runConfig.review) === null || _runConfig_review6 === void 0 ? void 0 : _runConfig_review6.maxContextErrors) || contextErrors.length; // Default: allow all errors
372
385
  if (contextErrors.length > maxContextErrors) {
373
386
  throw new Error(`Too many context gathering errors (${contextErrors.length}), aborting review. Consider checking your configuration and network connectivity.`);
374
387
  }
@@ -381,7 +394,7 @@ const executeInternal = async (runConfig)=>{
381
394
  logger.debug(' - Diff context: %d chars', diffContext.length);
382
395
  logger.debug(' - Release notes context: %d chars', releaseNotesContext.length);
383
396
  logger.debug(' - Issues context: %d chars', issuesContext.length);
384
- logger.debug(' - User context: %d chars', ((_runConfig_review15 = runConfig.review) === null || _runConfig_review15 === void 0 ? void 0 : (_runConfig_review_context = _runConfig_review15.context) === null || _runConfig_review_context === void 0 ? void 0 : _runConfig_review_context.length) || 0);
397
+ logger.debug(' - User context: %d chars', ((_runConfig_review4 = runConfig.review) === null || _runConfig_review4 === void 0 ? void 0 : (_runConfig_review_context = _runConfig_review4.context) === null || _runConfig_review_context === void 0 ? void 0 : _runConfig_review_context.length) || 0);
385
398
  const promptConfig = {
386
399
  overridePaths: runConfig.discoveredConfigDirs || [],
387
400
  overrides: runConfig.overrides || false
@@ -390,47 +403,13 @@ const executeInternal = async (runConfig)=>{
390
403
  notes: reviewNote
391
404
  };
392
405
  const promptContext = {
393
- context: (_runConfig_review16 = runConfig.review) === null || _runConfig_review16 === void 0 ? void 0 : _runConfig_review16.context,
406
+ context: (_runConfig_review5 = runConfig.review) === null || _runConfig_review5 === void 0 ? void 0 : _runConfig_review5.context,
394
407
  logContext,
395
408
  diffContext,
396
409
  releaseNotesContext,
397
410
  issuesContext
398
411
  };
399
412
  const prompt = await createPrompt(promptConfig, promptContent, promptContext);
400
- const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
401
- const storage = create$1({
402
- log: logger.info
403
- });
404
- await storage.ensureDirectory(outputDirectory);
405
- // Save timestamped copy of review notes and context to output directory
406
- try {
407
- var _runConfig_review_context1, _runConfig_review22;
408
- // Save the original review note
409
- const reviewNotesFilename = getTimestampedReviewNotesFilename();
410
- const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
411
- let reviewNotesContent = `# Review Notes\n\n${reviewNote}\n\n`;
412
- // Add all context sections if they exist
413
- if (logContext.trim()) {
414
- reviewNotesContent += `# Commit History Context\n\n${logContext}\n\n`;
415
- }
416
- if (diffContext.trim()) {
417
- reviewNotesContent += `# Recent Diffs Context\n\n${diffContext}\n\n`;
418
- }
419
- if (releaseNotesContext.trim()) {
420
- reviewNotesContent += `# Release Notes Context\n\n${releaseNotesContext}\n\n`;
421
- }
422
- if (issuesContext.trim()) {
423
- reviewNotesContent += `# GitHub Issues Context\n\n${issuesContext}\n\n`;
424
- }
425
- if ((_runConfig_review22 = runConfig.review) === null || _runConfig_review22 === void 0 ? void 0 : (_runConfig_review_context1 = _runConfig_review22.context) === null || _runConfig_review_context1 === void 0 ? void 0 : _runConfig_review_context1.trim()) {
426
- reviewNotesContent += `# User Context\n\n${runConfig.review.context}\n\n`;
427
- }
428
- await safeWriteFile(reviewNotesPath, reviewNotesContent);
429
- logger.debug('Saved timestamped review notes and context: %s', reviewNotesPath);
430
- } catch (error) {
431
- logger.warn('Failed to save timestamped review notes: %s', error.message);
432
- // Don't fail the entire operation for this
433
- }
434
413
  const modelToUse = getModelForCommand(runConfig, 'review');
435
414
  const request = Formatter.create({
436
415
  logger
@@ -439,6 +418,8 @@ const executeInternal = async (runConfig)=>{
439
418
  try {
440
419
  const rawResult = await createCompletion(request.messages, {
441
420
  model: modelToUse,
421
+ openaiReasoning: getOpenAIReasoningForCommand(runConfig, 'review'),
422
+ openaiMaxOutputTokens: getOpenAIMaxOutputTokensForCommand(runConfig, 'review'),
442
423
  responseFormat: {
443
424
  type: 'json_object'
444
425
  },
@@ -473,8 +454,238 @@ const executeInternal = async (runConfig)=>{
473
454
  logger.warn('Failed to save timestamped review analysis: %s', error.message);
474
455
  // Don't fail the entire operation for this
475
456
  }
457
+ return analysisResult;
458
+ };
459
+ const executeInternal = async (runConfig)=>{
460
+ var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review4, _runConfig_review5, _runConfig_review6, _runConfig_review7, _runConfig_review8, _runConfig_review9, _runConfig_review10, _runConfig_review11, _runConfig_review12, _runConfig_review13, _runConfig_review14, _runConfig_review15, _runConfig_review16, _runConfig_review17, _runConfig_review18;
461
+ const logger = getLogger();
462
+ const isDryRun = runConfig.dryRun || false;
463
+ // Show configuration even in dry-run mode
464
+ logger.debug('Review context configuration:');
465
+ logger.debug(' Include commit history: %s', (_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.includeCommitHistory);
466
+ logger.debug(' Include recent diffs: %s', (_runConfig_review1 = runConfig.review) === null || _runConfig_review1 === void 0 ? void 0 : _runConfig_review1.includeRecentDiffs);
467
+ logger.debug(' Include release notes: %s', (_runConfig_review2 = runConfig.review) === null || _runConfig_review2 === void 0 ? void 0 : _runConfig_review2.includeReleaseNotes);
468
+ logger.debug(' Include GitHub issues: %s', (_runConfig_review3 = runConfig.review) === null || _runConfig_review3 === void 0 ? void 0 : _runConfig_review3.includeGithubIssues);
469
+ logger.debug(' Commit history limit: %d', (_runConfig_review4 = runConfig.review) === null || _runConfig_review4 === void 0 ? void 0 : _runConfig_review4.commitHistoryLimit);
470
+ logger.debug(' Diff history limit: %d', (_runConfig_review5 = runConfig.review) === null || _runConfig_review5 === void 0 ? void 0 : _runConfig_review5.diffHistoryLimit);
471
+ logger.debug(' Release notes limit: %d', (_runConfig_review6 = runConfig.review) === null || _runConfig_review6 === void 0 ? void 0 : _runConfig_review6.releaseNotesLimit);
472
+ logger.debug(' GitHub issues limit: %d', (_runConfig_review7 = runConfig.review) === null || _runConfig_review7 === void 0 ? void 0 : _runConfig_review7.githubIssuesLimit);
473
+ logger.debug(' Sendit mode (auto-create issues): %s', (_runConfig_review8 = runConfig.review) === null || _runConfig_review8 === void 0 ? void 0 : _runConfig_review8.sendit);
474
+ logger.debug(' File: %s', ((_runConfig_review9 = runConfig.review) === null || _runConfig_review9 === void 0 ? void 0 : _runConfig_review9.file) || 'not specified');
475
+ logger.debug(' Directory: %s', ((_runConfig_review10 = runConfig.review) === null || _runConfig_review10 === void 0 ? void 0 : _runConfig_review10.directory) || 'not specified');
476
+ if (isDryRun) {
477
+ var _runConfig_review19, _runConfig_review20, _runConfig_review21, _runConfig_review22, _runConfig_review23;
478
+ if ((_runConfig_review19 = runConfig.review) === null || _runConfig_review19 === void 0 ? void 0 : _runConfig_review19.file) {
479
+ logger.info('DRY RUN: Would read review note from file: %s', runConfig.review.file);
480
+ } else if ((_runConfig_review20 = runConfig.review) === null || _runConfig_review20 === void 0 ? void 0 : _runConfig_review20.directory) {
481
+ logger.info('DRY RUN: Would process review files in directory: %s', runConfig.review.directory);
482
+ logger.info('DRY RUN: Would first select which files to process, then analyze selected files');
483
+ } else if ((_runConfig_review21 = runConfig.review) === null || _runConfig_review21 === void 0 ? void 0 : _runConfig_review21.note) {
484
+ logger.info('DRY RUN: Would analyze provided note for review');
485
+ } else {
486
+ logger.info('DRY RUN: Would open editor to capture review note');
487
+ }
488
+ logger.info('DRY RUN: Would gather additional context based on configuration above');
489
+ logger.info('DRY RUN: Would analyze note and identify issues');
490
+ if ((_runConfig_review22 = runConfig.review) === null || _runConfig_review22 === void 0 ? void 0 : _runConfig_review22.sendit) {
491
+ logger.info('DRY RUN: Would automatically create GitHub issues (sendit mode enabled)');
492
+ } else {
493
+ logger.info('DRY RUN: Would prompt for confirmation before creating GitHub issues');
494
+ }
495
+ // Show what exclusion patterns would be used in dry-run mode
496
+ if ((_runConfig_review23 = runConfig.review) === null || _runConfig_review23 === void 0 ? void 0 : _runConfig_review23.includeRecentDiffs) {
497
+ var _runConfig_excludedPatterns;
498
+ const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
499
+ const reviewExcluded = getReviewExcludedPatterns(basePatterns);
500
+ logger.info('DRY RUN: Would use %d exclusion patterns for diff context', reviewExcluded.length);
501
+ logger.debug('DRY RUN: Sample exclusions: %s', reviewExcluded.slice(0, 15).join(', ') + (reviewExcluded.length > 15 ? '...' : ''));
502
+ }
503
+ return 'DRY RUN: Review command would analyze note, gather context, and create GitHub issues';
504
+ }
505
+ // Enhanced TTY check with proper error handling
506
+ const isInteractive = isTTYSafe();
507
+ if (!isInteractive && !((_runConfig_review11 = runConfig.review) === null || _runConfig_review11 === void 0 ? void 0 : _runConfig_review11.sendit)) {
508
+ logger.error('āŒ STDIN is piped but --sendit flag is not enabled');
509
+ logger.error(' Interactive prompts cannot be used when input is piped');
510
+ logger.error(' Solutions:');
511
+ logger.error(' • Add --sendit flag to auto-create all issues');
512
+ logger.error(' • Use terminal input instead of piping');
513
+ logger.error(' • Example: echo "note" | kodrdriv review --sendit');
514
+ throw new ValidationError('Piped input requires --sendit flag for non-interactive operation');
515
+ }
516
+ // Get the review note from configuration
517
+ let reviewNote = (_runConfig_review12 = runConfig.review) === null || _runConfig_review12 === void 0 ? void 0 : _runConfig_review12.note;
518
+ let reviewFiles = [];
519
+ // Check if we should process a single file
520
+ if ((_runConfig_review13 = runConfig.review) === null || _runConfig_review13 === void 0 ? void 0 : _runConfig_review13.file) {
521
+ logger.info(`šŸ“ Reading review note from file: ${runConfig.review.file}`);
522
+ reviewNote = await readReviewNoteFromFile(runConfig.review.file);
523
+ reviewFiles = [
524
+ runConfig.review.file
525
+ ];
526
+ } else if ((_runConfig_review14 = runConfig.review) === null || _runConfig_review14 === void 0 ? void 0 : _runConfig_review14.directory) {
527
+ var _runConfig_review24;
528
+ logger.info(`šŸ“ Processing review files in directory: ${runConfig.review.directory}`);
529
+ reviewFiles = await getReviewFilesInDirectory(runConfig.review.directory);
530
+ if (reviewFiles.length === 0) {
531
+ throw new ValidationError(`No review files found in directory: ${runConfig.review.directory}`);
532
+ }
533
+ logger.info(`šŸ“ Found ${reviewFiles.length} files to process`);
534
+ // Set a dummy reviewNote for directory mode to satisfy validation
535
+ // The actual review notes will be read from each file during processing
536
+ reviewNote = `Processing ${reviewFiles.length} files from directory`;
537
+ // If not in sendit mode, explain the two-phase process
538
+ if (!((_runConfig_review24 = runConfig.review) === null || _runConfig_review24 === void 0 ? void 0 : _runConfig_review24.sendit)) {
539
+ logger.info(`šŸ“ Interactive mode: You will first select which files to process, then they will be analyzed in order.`);
540
+ logger.info(`šŸ“ Use --sendit to process all files automatically without confirmation.`);
541
+ }
542
+ } else if ((_runConfig_review15 = runConfig.review) === null || _runConfig_review15 === void 0 ? void 0 : _runConfig_review15.note) {
543
+ reviewNote = runConfig.review.note;
544
+ reviewFiles = [
545
+ 'provided note'
546
+ ];
547
+ } else {
548
+ // Open editor to capture review note
549
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
550
+ let tmpFilePath = null;
551
+ try {
552
+ var _runConfig_review25;
553
+ // Create secure temporary file
554
+ tmpFilePath = await createSecureTempFile();
555
+ // Pre-populate the file with a helpful header so users know what to do.
556
+ const templateContent = [
557
+ '# Kodrdriv Review Note',
558
+ '',
559
+ '# Please enter your review note below. Lines starting with "#" will be ignored.',
560
+ '# Save and close the editor when you are done.',
561
+ '',
562
+ ''
563
+ ].join('\n');
564
+ await safeWriteFile(tmpFilePath, templateContent);
565
+ logger.info(`No review note provided – opening ${editor} to capture input...`);
566
+ // Open the editor with optional timeout protection
567
+ const editorTimeout = (_runConfig_review25 = runConfig.review) === null || _runConfig_review25 === void 0 ? void 0 : _runConfig_review25.editorTimeout; // No default timeout - let user take their time
568
+ await openEditorWithTimeout(editor, tmpFilePath, editorTimeout);
569
+ // Read the file back in, stripping comment lines and whitespace.
570
+ const fileContent = (await fs__default.readFile(tmpFilePath, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
571
+ if (!fileContent) {
572
+ throw new ValidationError('Review note is empty – aborting. Provide a note as an argument, via STDIN, or through the editor.');
573
+ }
574
+ reviewNote = fileContent;
575
+ // If the original runConfig.review object exists, update it so downstream code has the note.
576
+ if (runConfig.review) {
577
+ runConfig.review.note = reviewNote;
578
+ }
579
+ } catch (error) {
580
+ logger.error(`Failed to capture review note via editor: ${error.message}`);
581
+ throw error;
582
+ } finally{
583
+ // Always clean up the temp file
584
+ if (tmpFilePath) {
585
+ await cleanupTempFile(tmpFilePath);
586
+ }
587
+ }
588
+ reviewFiles = [
589
+ 'editor input'
590
+ ];
591
+ }
592
+ if (!reviewNote || !reviewNote.trim()) {
593
+ throw new ValidationError('No review note provided or captured');
594
+ }
595
+ logger.info('šŸ“ Starting review analysis...');
596
+ logger.debug('Review note: %s', reviewNote);
597
+ logger.debug('Review note length: %d characters', reviewNote.length);
598
+ const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
599
+ const storage = create({
600
+ log: logger.info
601
+ });
602
+ await storage.ensureDirectory(outputDirectory);
603
+ // Save timestamped copy of review notes to output directory
604
+ try {
605
+ const reviewNotesFilename = getTimestampedReviewNotesFilename();
606
+ const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
607
+ const reviewNotesContent = `# Review Notes\n\n${reviewNote}\n\n`;
608
+ await safeWriteFile(reviewNotesPath, reviewNotesContent);
609
+ logger.debug('Saved timestamped review notes: %s', reviewNotesPath);
610
+ } catch (error) {
611
+ logger.warn('Failed to save review notes: %s', error.message);
612
+ }
613
+ // Phase 1: File selection (only for directory mode)
614
+ let selectedFiles;
615
+ if ((_runConfig_review16 = runConfig.review) === null || _runConfig_review16 === void 0 ? void 0 : _runConfig_review16.directory) {
616
+ var _runConfig_review26;
617
+ selectedFiles = await selectFilesForProcessing(reviewFiles, ((_runConfig_review26 = runConfig.review) === null || _runConfig_review26 === void 0 ? void 0 : _runConfig_review26.sendit) || false);
618
+ } else {
619
+ // For single note mode, just use the note directly
620
+ selectedFiles = [
621
+ 'single note'
622
+ ];
623
+ }
624
+ // Phase 2: Process selected files in order
625
+ logger.info(`\nšŸ“ Starting analysis phase...`);
626
+ const results = [];
627
+ const processedFiles = [];
628
+ if ((_runConfig_review17 = runConfig.review) === null || _runConfig_review17 === void 0 ? void 0 : _runConfig_review17.directory) {
629
+ // Directory mode: process each selected file
630
+ for(let i = 0; i < selectedFiles.length; i++){
631
+ const filePath = selectedFiles[i];
632
+ try {
633
+ logger.info(`šŸ“ Processing file ${i + 1}/${selectedFiles.length}: ${filePath}`);
634
+ const fileNote = await readReviewNoteFromFile(filePath);
635
+ const fileResult = await processSingleReview(fileNote, runConfig, outputDirectory);
636
+ results.push(fileResult);
637
+ processedFiles.push(filePath);
638
+ } catch (error) {
639
+ // Check if this is a critical error that should be propagated
640
+ if (error.message.includes('Too many context gathering errors')) {
641
+ throw error; // Propagate critical context errors
642
+ }
643
+ logger.warn(`Failed to process file ${filePath}: ${error.message}`);
644
+ // Continue with other files for non-critical errors
645
+ }
646
+ }
647
+ } else {
648
+ // Single note mode: process the note directly
649
+ try {
650
+ logger.info(`šŸ“ Processing single review note`);
651
+ const fileResult = await processSingleReview(reviewNote, runConfig, outputDirectory);
652
+ results.push(fileResult);
653
+ processedFiles.push('single note');
654
+ } catch (error) {
655
+ logger.warn(`Failed to process review note: ${error.message}`);
656
+ throw error; // Re-throw for single note mode since there's only one item
657
+ }
658
+ }
659
+ if (results.length === 0) {
660
+ throw new ValidationError('No files were processed successfully');
661
+ }
662
+ // Combine results if we processed multiple files
663
+ let analysisResult;
664
+ if (results.length === 1) {
665
+ analysisResult = results[0];
666
+ } else {
667
+ logger.info(`āœ… Successfully processed ${results.length} review files`);
668
+ // Create a combined summary
669
+ const totalIssues = results.reduce((sum, result)=>sum + result.totalIssues, 0);
670
+ const allIssues = results.flatMap((result)=>result.issues || []);
671
+ analysisResult = {
672
+ summary: `Combined analysis of ${results.length} review files. Total issues found: ${totalIssues}`,
673
+ totalIssues,
674
+ issues: allIssues
675
+ };
676
+ // Save combined results
677
+ try {
678
+ const combinedFilename = getTimestampedReviewFilename();
679
+ const combinedPath = getOutputPath(outputDirectory, combinedFilename);
680
+ const combinedContent = `# Combined Review Analysis Result\n\n` + `## Summary\n${analysisResult.summary}\n\n` + `## Total Issues Found\n${totalIssues}\n\n` + `## Files Processed\n${processedFiles.join('\n')}\n\n` + `## Issues\n\n${JSON.stringify(allIssues, null, 2)}\n\n` + `---\n\n*Combined analysis completed at ${new Date().toISOString()}*`;
681
+ await safeWriteFile(combinedPath, combinedContent);
682
+ logger.debug('Saved combined review analysis: %s', combinedPath);
683
+ } catch (error) {
684
+ logger.warn('Failed to save combined review analysis: %s', error.message);
685
+ }
686
+ }
476
687
  // Handle GitHub issue creation using the issues module
477
- const senditMode = ((_runConfig_review17 = runConfig.review) === null || _runConfig_review17 === void 0 ? void 0 : _runConfig_review17.sendit) || false;
688
+ const senditMode = ((_runConfig_review18 = runConfig.review) === null || _runConfig_review18 === void 0 ? void 0 : _runConfig_review18.sendit) || false;
478
689
  return await handleIssueCreation(analysisResult, senditMode);
479
690
  };
480
691
  const execute = async (runConfig)=>{