@eldrforge/kodrdriv 0.0.13 → 0.0.15

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.
Files changed (51) hide show
  1. package/.kodrdriv/context/content.md +7 -1
  2. package/RELEASE_NOTES.md +14 -0
  3. package/dist/arguments.js +332 -9
  4. package/dist/arguments.js.map +1 -1
  5. package/dist/commands/audio-commit.js +666 -0
  6. package/dist/commands/audio-commit.js.map +1 -0
  7. package/dist/commands/audio-review.js +677 -0
  8. package/dist/commands/audio-review.js.map +1 -0
  9. package/dist/commands/clean.js +36 -0
  10. package/dist/commands/clean.js.map +1 -0
  11. package/dist/commands/commit.js +32 -7
  12. package/dist/commands/commit.js.map +1 -1
  13. package/dist/commands/publish.js +15 -7
  14. package/dist/commands/publish.js.map +1 -1
  15. package/dist/commands/release.js +31 -3
  16. package/dist/commands/release.js.map +1 -1
  17. package/dist/commands/review.js +152 -0
  18. package/dist/commands/review.js.map +1 -0
  19. package/dist/constants.js +90 -59
  20. package/dist/constants.js.map +1 -1
  21. package/dist/content/diff.js +155 -1
  22. package/dist/content/diff.js.map +1 -1
  23. package/dist/content/issues.js +240 -0
  24. package/dist/content/issues.js.map +1 -0
  25. package/dist/content/releaseNotes.js +90 -0
  26. package/dist/content/releaseNotes.js.map +1 -0
  27. package/dist/main.js +23 -10
  28. package/dist/main.js.map +1 -1
  29. package/dist/prompt/instructions/commit.md +18 -15
  30. package/dist/prompt/instructions/release.md +6 -5
  31. package/dist/prompt/instructions/review.md +108 -0
  32. package/dist/prompt/personas/reviewer.md +29 -0
  33. package/dist/prompt/prompts.js +110 -13
  34. package/dist/prompt/prompts.js.map +1 -1
  35. package/dist/types.js +36 -1
  36. package/dist/types.js.map +1 -1
  37. package/dist/util/general.js +35 -2
  38. package/dist/util/general.js.map +1 -1
  39. package/dist/util/github.js +54 -1
  40. package/dist/util/github.js.map +1 -1
  41. package/dist/util/openai.js +68 -4
  42. package/dist/util/openai.js.map +1 -1
  43. package/dist/util/stdin.js +61 -0
  44. package/dist/util/stdin.js.map +1 -0
  45. package/dist/util/storage.js +20 -1
  46. package/dist/util/storage.js.map +1 -1
  47. package/docs/public/commands.md +20 -0
  48. package/output/kodrdriv/250702-0552-release-notes.md +3 -0
  49. package/package.json +7 -6
  50. package/pnpm-workspace.yaml +2 -0
  51. package/vitest.config.ts +4 -4
@@ -0,0 +1,666 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
5
+ import { getLogger } from '../logging.js';
6
+ import { run } from '../util/child.js';
7
+ import { getTimestampedAudioFilename, getOutputPath, getTimestampedTranscriptFilename } from '../util/general.js';
8
+ import { transcribeAudio } from '../util/openai.js';
9
+ import { create } from '../util/storage.js';
10
+ import { execute as execute$1 } from './commit.js';
11
+
12
+ const detectBestAudioDevice = async ()=>{
13
+ try {
14
+ // Get list of audio devices - this command always "fails" but gives us the device list
15
+ try {
16
+ await run('ffmpeg -f avfoundation -list_devices true -i ""');
17
+ } catch (result) {
18
+ // ffmpeg returns error code but we get the device list in stderr
19
+ const output = result.stderr || result.stdout || '';
20
+ // Parse audio devices from output
21
+ const audioDevicesSection = output.split('AVFoundation audio devices:')[1];
22
+ if (!audioDevicesSection) return '1'; // Default fallback
23
+ const deviceLines = audioDevicesSection.split('\n').filter((line)=>line.includes('[') && line.includes(']')).map((line)=>line.trim());
24
+ // Prefer AirPods, then built-in microphone over virtual/external devices
25
+ const preferredDevices = [
26
+ 'AirPods',
27
+ 'MacBook Pro Microphone',
28
+ 'MacBook Air Microphone',
29
+ 'Built-in Microphone',
30
+ 'Internal Microphone'
31
+ ];
32
+ for (const deviceLine of deviceLines){
33
+ for (const preferred of preferredDevices){
34
+ if (deviceLine.toLowerCase().includes(preferred.toLowerCase())) {
35
+ // Extract device index
36
+ const match = deviceLine.match(/\[(\d+)\]/);
37
+ if (match) {
38
+ return match[1];
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ // If no preferred device found, use device 1 as default (usually better than 0)
45
+ return '1';
46
+ } catch (error) {
47
+ // Fallback to device 1
48
+ return '1';
49
+ }
50
+ };
51
+ const parseAudioDevices = async ()=>{
52
+ try {
53
+ try {
54
+ await run('ffmpeg -f avfoundation -list_devices true -i ""');
55
+ } catch (result) {
56
+ const output = result.stderr || result.stdout || '';
57
+ const audioDevicesSection = output.split('AVFoundation audio devices:')[1];
58
+ if (audioDevicesSection) {
59
+ const deviceLines = audioDevicesSection.split('\n').filter((line)=>line.includes('[') && line.includes(']')).map((line)=>line.trim());
60
+ return deviceLines.map((line)=>{
61
+ const match = line.match(/\[(\d+)\]\s+(.+)/);
62
+ if (match) {
63
+ return {
64
+ index: match[1],
65
+ name: match[2]
66
+ };
67
+ }
68
+ return null;
69
+ }).filter(Boolean);
70
+ }
71
+ }
72
+ return [];
73
+ } catch (error) {
74
+ return [];
75
+ }
76
+ };
77
+ const selectAudioDeviceInteractively = async (runConfig)=>{
78
+ const logger = getLogger();
79
+ logger.info('🎙️ Available audio devices:');
80
+ const devices = await parseAudioDevices();
81
+ if (devices.length === 0) {
82
+ logger.error('❌ No audio devices found. Make sure ffmpeg is installed and audio devices are available.');
83
+ return null;
84
+ }
85
+ // Display devices
86
+ devices.forEach((device, i)=>{
87
+ logger.info(` ${i + 1}. [${device.index}] ${device.name}`);
88
+ });
89
+ logger.info('');
90
+ logger.info('📋 Select an audio device by entering its number (1-' + devices.length + '):');
91
+ return new Promise((resolve)=>{
92
+ // Set up keyboard input
93
+ process.stdin.setRawMode(true);
94
+ process.stdin.resume();
95
+ process.stdin.setEncoding('utf8');
96
+ let inputBuffer = '';
97
+ const keyHandler = (key)=>{
98
+ const keyCode = key.charCodeAt(0);
99
+ if (keyCode === 13) {
100
+ const selectedIndex = parseInt(inputBuffer) - 1;
101
+ if (selectedIndex >= 0 && selectedIndex < devices.length) {
102
+ const selectedDevice = devices[selectedIndex];
103
+ logger.info(`✅ Selected: [${selectedDevice.index}] ${selectedDevice.name}`);
104
+ // Save to configuration
105
+ saveAudioDeviceToConfig(runConfig, selectedDevice.index, selectedDevice.name).then(()=>{
106
+ logger.info('💾 Audio device saved to configuration');
107
+ }).catch((error)=>{
108
+ logger.warn('⚠️ Failed to save audio device to configuration: %s', error.message);
109
+ });
110
+ // Cleanup and resolve
111
+ process.stdin.setRawMode(false);
112
+ process.stdin.pause();
113
+ process.stdin.removeListener('data', keyHandler);
114
+ resolve(selectedDevice.index);
115
+ } else {
116
+ logger.error('❌ Invalid selection. Please enter a number between 1 and ' + devices.length);
117
+ inputBuffer = '';
118
+ process.stdout.write('📋 Select an audio device: ');
119
+ }
120
+ } else if (keyCode === 3) {
121
+ logger.info('\n❌ Selection cancelled');
122
+ process.stdin.setRawMode(false);
123
+ process.stdin.pause();
124
+ process.stdin.removeListener('data', keyHandler);
125
+ resolve(null);
126
+ } else if (keyCode >= 48 && keyCode <= 57) {
127
+ inputBuffer += key;
128
+ process.stdout.write(key);
129
+ } else if (keyCode === 127) {
130
+ if (inputBuffer.length > 0) {
131
+ inputBuffer = inputBuffer.slice(0, -1);
132
+ process.stdout.write('\b \b');
133
+ }
134
+ }
135
+ };
136
+ process.stdin.on('data', keyHandler);
137
+ process.stdout.write('📋 Select an audio device: ');
138
+ });
139
+ };
140
+ const saveAudioDeviceToConfig = async (runConfig, deviceIndex, deviceName)=>{
141
+ const logger = getLogger();
142
+ const storage = create({
143
+ log: logger.info
144
+ });
145
+ try {
146
+ const configDir = runConfig.configDirectory || DEFAULT_OUTPUT_DIRECTORY;
147
+ await storage.ensureDirectory(configDir);
148
+ const configPath = getOutputPath(configDir, 'audio-config.json');
149
+ // Read existing config or create new one
150
+ let audioConfig = {};
151
+ try {
152
+ const existingConfig = await storage.readFile(configPath, 'utf-8');
153
+ audioConfig = JSON.parse(existingConfig);
154
+ } catch (error) {
155
+ // File doesn't exist or is invalid, start with empty config
156
+ audioConfig = {};
157
+ }
158
+ // Update audio device
159
+ audioConfig.audioDevice = deviceIndex;
160
+ audioConfig.audioDeviceName = deviceName;
161
+ audioConfig.lastUpdated = new Date().toISOString();
162
+ // Save updated config
163
+ await storage.writeFile(configPath, JSON.stringify(audioConfig, null, 2), 'utf-8');
164
+ logger.debug('Saved audio configuration to: %s', configPath);
165
+ } catch (error) {
166
+ logger.error('Failed to save audio configuration: %s', error.message);
167
+ throw error;
168
+ }
169
+ };
170
+ const loadAudioDeviceFromConfig = async (runConfig)=>{
171
+ const logger = getLogger();
172
+ const storage = create({
173
+ log: logger.info
174
+ });
175
+ try {
176
+ const configDir = runConfig.configDirectory || DEFAULT_OUTPUT_DIRECTORY;
177
+ const configPath = getOutputPath(configDir, 'audio-config.json');
178
+ const configContent = await storage.readFile(configPath, 'utf-8');
179
+ const audioConfig = JSON.parse(configContent);
180
+ if (audioConfig.audioDevice) {
181
+ logger.debug('Loaded audio device from config: [%s] %s', audioConfig.audioDevice, audioConfig.audioDeviceName || 'Unknown');
182
+ return audioConfig.audioDevice;
183
+ }
184
+ return null;
185
+ } catch (error) {
186
+ // Config file doesn't exist or is invalid
187
+ logger.debug('No saved audio device configuration found');
188
+ return null;
189
+ }
190
+ };
191
+ const listAudioDevices = async ()=>{
192
+ const logger = getLogger();
193
+ try {
194
+ try {
195
+ await run('ffmpeg -f avfoundation -list_devices true -i ""');
196
+ } catch (result) {
197
+ const output = result.stderr || result.stdout || '';
198
+ const audioDevicesSection = output.split('AVFoundation audio devices:')[1];
199
+ if (audioDevicesSection) {
200
+ logger.info('🎙️ Available audio devices:');
201
+ const deviceLines = audioDevicesSection.split('\n').filter((line)=>line.includes('[') && line.includes(']')).map((line)=>line.trim());
202
+ deviceLines.forEach((line)=>{
203
+ const match = line.match(/\[(\d+)\]\s+(.+)/);
204
+ if (match) {
205
+ logger.info(` [${match[1]}] ${match[2]}`);
206
+ }
207
+ });
208
+ }
209
+ }
210
+ } catch (error) {
211
+ logger.debug('Could not list audio devices');
212
+ }
213
+ };
214
+ const execute = async (runConfig)=>{
215
+ var _runConfig_audioCommit, _runConfig_commit;
216
+ const logger = getLogger();
217
+ const isDryRun = runConfig.dryRun || false;
218
+ // Handle audio device selection if requested
219
+ if ((_runConfig_audioCommit = runConfig.audioCommit) === null || _runConfig_audioCommit === void 0 ? void 0 : _runConfig_audioCommit.selectAudioDevice) {
220
+ logger.info('🎛️ Starting audio device selection...');
221
+ const selectedDevice = await selectAudioDeviceInteractively(runConfig);
222
+ if (selectedDevice === null) {
223
+ logger.error('❌ Audio device selection cancelled or failed');
224
+ process.exit(1);
225
+ }
226
+ logger.info('✅ Audio device selection complete');
227
+ logger.info('');
228
+ logger.info('You can now run the audio-commit command without --select-audio-device to use your saved device');
229
+ return 'Audio device configured successfully';
230
+ }
231
+ if (isDryRun) {
232
+ var _runConfig_commit1;
233
+ logger.info('DRY RUN: Would start audio recording for commit context');
234
+ logger.info('DRY RUN: Would transcribe audio and use as context for commit message generation');
235
+ logger.info('DRY RUN: Would then delegate to regular commit command');
236
+ // In dry run, just call the regular commit command with empty audio context
237
+ return execute$1({
238
+ ...runConfig,
239
+ commit: {
240
+ ...runConfig.commit,
241
+ direction: ((_runConfig_commit1 = runConfig.commit) === null || _runConfig_commit1 === void 0 ? void 0 : _runConfig_commit1.direction) || ''
242
+ }
243
+ });
244
+ }
245
+ // Start audio recording and transcription
246
+ logger.info('Starting audio recording for commit context...');
247
+ logger.info('This command will use your system\'s default audio recording tool');
248
+ logger.info('💡 Tip: Use --select-audio-device to choose a specific microphone');
249
+ logger.info('Press Ctrl+C after you finish speaking to generate your commit message');
250
+ const audioContext = await recordAndTranscribeAudio(runConfig);
251
+ // Now delegate to the regular commit command with the audio context
252
+ logger.info('🤖 Generating commit message using audio context...');
253
+ const result = await execute$1({
254
+ ...runConfig,
255
+ commit: {
256
+ ...runConfig.commit,
257
+ direction: audioContext.trim() || ((_runConfig_commit = runConfig.commit) === null || _runConfig_commit === void 0 ? void 0 : _runConfig_commit.direction) || ''
258
+ }
259
+ });
260
+ // Final cleanup to ensure process can exit
261
+ try {
262
+ if (process.stdin.setRawMode) {
263
+ process.stdin.setRawMode(false);
264
+ process.stdin.pause();
265
+ process.stdin.removeAllListeners();
266
+ }
267
+ process.removeAllListeners('SIGINT');
268
+ process.removeAllListeners('SIGTERM');
269
+ } catch (error) {
270
+ // Ignore cleanup errors
271
+ }
272
+ return result;
273
+ };
274
+ const recordAndTranscribeAudio = async (runConfig)=>{
275
+ var _runConfig_audioCommit;
276
+ const logger = getLogger();
277
+ const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
278
+ const storage = create({
279
+ log: logger.info
280
+ });
281
+ await storage.ensureDirectory(outputDirectory);
282
+ const tempDir = await fs.mkdtemp(path.join(outputDirectory, '.temp-audio-'));
283
+ const audioFilePath = path.join(tempDir, 'recording.wav');
284
+ // Declare variables at function scope for cleanup access
285
+ let recordingProcess = null;
286
+ let recordingFinished = false;
287
+ let recordingCancelled = false;
288
+ let countdownInterval = null;
289
+ let remainingSeconds = 30;
290
+ let intendedRecordingTime = 30;
291
+ const maxRecordingTime = ((_runConfig_audioCommit = runConfig.audioCommit) === null || _runConfig_audioCommit === void 0 ? void 0 : _runConfig_audioCommit.maxRecordingTime) || 300; // 5 minutes default
292
+ const extensionTime = 30; // 30 seconds per extension
293
+ try {
294
+ // Use system recording tool - cross-platform approach
295
+ logger.info('🎤 Starting recording... Speak now!');
296
+ logger.info('📋 Controls: ENTER=done, E=extend+30s, C/Ctrl+C=cancel');
297
+ // List available audio devices in debug mode
298
+ if (runConfig.debug) {
299
+ await listAudioDevices();
300
+ }
301
+ // Start countdown display
302
+ const startCountdown = ()=>{
303
+ // Show initial countdown
304
+ updateCountdownDisplay();
305
+ countdownInterval = setInterval(()=>{
306
+ remainingSeconds--;
307
+ if (remainingSeconds > 0) {
308
+ updateCountdownDisplay();
309
+ } else {
310
+ process.stdout.write('\r⏱️ Recording: Time\'s up! \n');
311
+ if (countdownInterval) {
312
+ clearInterval(countdownInterval);
313
+ countdownInterval = null;
314
+ }
315
+ // Auto-stop when intended time is reached
316
+ stopRecording();
317
+ }
318
+ }, 1000);
319
+ };
320
+ const updateCountdownDisplay = ()=>{
321
+ const maxMinutes = Math.floor(maxRecordingTime / 60);
322
+ const intendedMinutes = Math.floor(intendedRecordingTime / 60);
323
+ const intendedSeconds = intendedRecordingTime % 60;
324
+ process.stdout.write(`\r⏱️ Recording: ${remainingSeconds}s left (${intendedMinutes}:${intendedSeconds.toString().padStart(2, '0')}/${maxMinutes}:00 max) [ENTER=done, E=+30s, C=cancel]`);
325
+ };
326
+ const extendRecording = ()=>{
327
+ const newTotal = intendedRecordingTime + extensionTime;
328
+ if (newTotal <= maxRecordingTime) {
329
+ intendedRecordingTime = newTotal;
330
+ remainingSeconds += extensionTime;
331
+ logger.info(`🔄 Extended recording by ${extensionTime}s (total: ${Math.floor(intendedRecordingTime / 60)}:${(intendedRecordingTime % 60).toString().padStart(2, '0')})`);
332
+ updateCountdownDisplay();
333
+ } else {
334
+ const canExtend = maxRecordingTime - intendedRecordingTime;
335
+ if (canExtend > 0) {
336
+ intendedRecordingTime = maxRecordingTime;
337
+ remainingSeconds += canExtend;
338
+ logger.info(`🔄 Extended recording by ${canExtend}s (maximum reached: ${Math.floor(maxRecordingTime / 60)}:${(maxRecordingTime % 60).toString().padStart(2, '0')})`);
339
+ updateCountdownDisplay();
340
+ } else {
341
+ logger.warn(`⚠️ Cannot extend: maximum recording time (${Math.floor(maxRecordingTime / 60)}:${(maxRecordingTime % 60).toString().padStart(2, '0')}) reached`);
342
+ }
343
+ }
344
+ };
345
+ // Set up keyboard input handling
346
+ const setupKeyboardHandling = ()=>{
347
+ process.stdin.setRawMode(true);
348
+ process.stdin.resume();
349
+ process.stdin.setEncoding('utf8');
350
+ const keyHandler = (key)=>{
351
+ const keyCode = key.charCodeAt(0);
352
+ if (keyCode === 13) {
353
+ // Immediate feedback
354
+ process.stdout.write('\r✅ ENTER pressed - stopping recording... \n');
355
+ process.stdin.setRawMode(false);
356
+ process.stdin.pause();
357
+ process.stdin.removeListener('data', keyHandler);
358
+ stopRecording();
359
+ } else if (key.toLowerCase() === 'e') {
360
+ extendRecording();
361
+ } else if (key.toLowerCase() === 'c' || keyCode === 3) {
362
+ // Immediate feedback
363
+ process.stdout.write('\r❌ Cancelling recording... \n');
364
+ process.stdin.setRawMode(false);
365
+ process.stdin.pause();
366
+ process.stdin.removeListener('data', keyHandler);
367
+ cancelRecording();
368
+ }
369
+ };
370
+ process.stdin.on('data', keyHandler);
371
+ };
372
+ // Determine which recording command to use based on platform (using max time)
373
+ let recordCommand;
374
+ if (process.platform === 'darwin') {
375
+ // macOS - try ffmpeg first, then fall back to manual recording
376
+ try {
377
+ var _runConfig_audioCommit1, _runConfig_audioCommit2;
378
+ // Check if ffmpeg is available
379
+ await run('which ffmpeg');
380
+ // Get the best audio device (from saved config, CLI config, or auto-detected)
381
+ const savedDevice = await loadAudioDeviceFromConfig(runConfig);
382
+ const audioDevice = ((_runConfig_audioCommit1 = runConfig.audioCommit) === null || _runConfig_audioCommit1 === void 0 ? void 0 : _runConfig_audioCommit1.audioDevice) || savedDevice || await detectBestAudioDevice();
383
+ recordCommand = `ffmpeg -f avfoundation -i ":${audioDevice}" -t ${maxRecordingTime} -y "${audioFilePath}"`;
384
+ if ((_runConfig_audioCommit2 = runConfig.audioCommit) === null || _runConfig_audioCommit2 === void 0 ? void 0 : _runConfig_audioCommit2.audioDevice) {
385
+ logger.info(`🎙️ Using audio device ${audioDevice} (from CLI configuration)`);
386
+ } else if (savedDevice) {
387
+ logger.info(`🎙️ Using audio device ${audioDevice} (from saved configuration)`);
388
+ } else {
389
+ logger.info(`🎙️ Using audio device ${audioDevice} (auto-detected)`);
390
+ }
391
+ } catch {
392
+ // ffmpeg not available, try sox/rec
393
+ try {
394
+ await run('which rec');
395
+ recordCommand = `rec -r 44100 -c 1 -t wav "${audioFilePath}" trim 0 ${maxRecordingTime}`;
396
+ } catch {
397
+ // Neither available, use manual fallback
398
+ throw new Error('MANUAL_RECORDING_NEEDED');
399
+ }
400
+ }
401
+ } else if (process.platform === 'win32') {
402
+ // Windows - use ffmpeg if available, otherwise fallback
403
+ try {
404
+ await run('where ffmpeg');
405
+ recordCommand = `ffmpeg -f dshow -i audio="Microphone" -t ${maxRecordingTime} -y "${audioFilePath}"`;
406
+ } catch {
407
+ throw new Error('MANUAL_RECORDING_NEEDED');
408
+ }
409
+ } else {
410
+ // Linux - use arecord (ALSA) or ffmpeg
411
+ try {
412
+ await run('which arecord');
413
+ recordCommand = `arecord -f cd -t wav -d ${maxRecordingTime} "${audioFilePath}"`;
414
+ } catch {
415
+ try {
416
+ await run('which ffmpeg');
417
+ recordCommand = `ffmpeg -f alsa -i default -t ${maxRecordingTime} -y "${audioFilePath}"`;
418
+ } catch {
419
+ throw new Error('MANUAL_RECORDING_NEEDED');
420
+ }
421
+ }
422
+ }
423
+ // Start recording as a background process (with max time, we'll stop it early if needed)
424
+ try {
425
+ recordingProcess = run(recordCommand);
426
+ } catch (error) {
427
+ if (error.message === 'MANUAL_RECORDING_NEEDED') {
428
+ // Provide helpful instructions for manual recording
429
+ logger.warn('⚠️ Automatic recording not available on this system.');
430
+ logger.warn('📱 Please record audio manually using your system\'s built-in tools:');
431
+ logger.warn('');
432
+ if (process.platform === 'darwin') {
433
+ logger.warn('🍎 macOS options:');
434
+ logger.warn(' 1. Use QuickTime Player: File → New Audio Recording');
435
+ logger.warn(' 2. Use Voice Memos app');
436
+ logger.warn(' 3. Install ffmpeg: brew install ffmpeg');
437
+ logger.warn(' 4. Install sox: brew install sox');
438
+ } else if (process.platform === 'win32') {
439
+ logger.warn('🪟 Windows options:');
440
+ logger.warn(' 1. Use Voice Recorder app');
441
+ logger.warn(' 2. Install ffmpeg: https://ffmpeg.org/download.html');
442
+ } else {
443
+ logger.warn('🐧 Linux options:');
444
+ logger.warn(' 1. Install alsa-utils: sudo apt install alsa-utils');
445
+ logger.warn(' 2. Install ffmpeg: sudo apt install ffmpeg');
446
+ }
447
+ logger.warn('');
448
+ logger.warn(`💾 Save your recording as: ${audioFilePath}`);
449
+ logger.warn('🎵 Recommended format: WAV, 44.1kHz, mono or stereo');
450
+ logger.warn('');
451
+ logger.warn('⌨️ Press ENTER when you have saved the audio file...');
452
+ // Wait for user input (disable our keyboard handling for this)
453
+ await new Promise((resolve)=>{
454
+ const originalRawMode = process.stdin.setRawMode;
455
+ process.stdin.setRawMode(true);
456
+ process.stdin.resume();
457
+ const enterHandler = (key)=>{
458
+ if (key[0] === 13) {
459
+ process.stdin.setRawMode(false);
460
+ process.stdin.pause();
461
+ process.stdin.removeListener('data', enterHandler);
462
+ resolve(void 0);
463
+ }
464
+ };
465
+ process.stdin.on('data', enterHandler);
466
+ });
467
+ // Skip the automatic recording and keyboard handling for manual recording
468
+ recordingProcess = null;
469
+ } else {
470
+ throw error;
471
+ }
472
+ }
473
+ // Set up graceful shutdown
474
+ const stopRecording = async ()=>{
475
+ if (!recordingFinished && !recordingCancelled) {
476
+ recordingFinished = true;
477
+ // Clear countdown
478
+ if (countdownInterval) {
479
+ clearInterval(countdownInterval);
480
+ countdownInterval = null;
481
+ }
482
+ // Clear the countdown line and show recording stopped
483
+ process.stdout.write('\r⏱️ Recording finished! \n');
484
+ if (recordingProcess && recordingProcess.kill) {
485
+ recordingProcess.kill('SIGTERM');
486
+ }
487
+ logger.info('🛑 Recording stopped - proceeding with commit');
488
+ }
489
+ };
490
+ // Set up cancellation
491
+ const cancelRecording = async ()=>{
492
+ if (!recordingFinished && !recordingCancelled) {
493
+ recordingCancelled = true;
494
+ // Clear countdown
495
+ if (countdownInterval) {
496
+ clearInterval(countdownInterval);
497
+ countdownInterval = null;
498
+ }
499
+ // Clear the countdown line and show cancellation
500
+ process.stdout.write('\r❌ Recording cancelled! \n');
501
+ if (recordingProcess && recordingProcess.kill) {
502
+ recordingProcess.kill('SIGTERM');
503
+ }
504
+ logger.info('❌ Audio commit cancelled by user');
505
+ process.exit(0);
506
+ }
507
+ };
508
+ // Remove the old SIGINT handler and use our new keyboard handling
509
+ // Note: We'll still handle SIGINT for cleanup, but route it through cancelRecording
510
+ process.on('SIGINT', cancelRecording);
511
+ // Start keyboard handling and countdown if we have a recording process
512
+ if (recordingProcess) {
513
+ setupKeyboardHandling();
514
+ startCountdown();
515
+ // Create a promise that resolves when user manually stops recording
516
+ const manualStopPromise = new Promise((resolve)=>{
517
+ const checkInterval = setInterval(()=>{
518
+ if (recordingFinished || recordingCancelled) {
519
+ clearInterval(checkInterval);
520
+ resolve();
521
+ }
522
+ }, 100);
523
+ });
524
+ // Wait for either the recording to finish naturally or manual stop
525
+ try {
526
+ await Promise.race([
527
+ recordingProcess,
528
+ manualStopPromise
529
+ ]);
530
+ // If manually stopped, force kill the process if it's still running
531
+ if (recordingFinished && recordingProcess && !recordingProcess.killed) {
532
+ recordingProcess.kill('SIGKILL');
533
+ // Give it a moment to die
534
+ await new Promise((resolve)=>setTimeout(resolve, 200));
535
+ }
536
+ // Only show completion message if not manually finished
537
+ if (!recordingCancelled && !recordingFinished) {
538
+ // Clear countdown on successful completion
539
+ if (countdownInterval) {
540
+ clearInterval(countdownInterval);
541
+ countdownInterval = null;
542
+ }
543
+ process.stdout.write('\r⏱️ Recording completed! \n');
544
+ logger.info('✅ Recording completed automatically');
545
+ }
546
+ } catch (error) {
547
+ // Only handle errors if not cancelled and not manually finished
548
+ if (!recordingCancelled && !recordingFinished) {
549
+ // Clear countdown on error
550
+ if (countdownInterval) {
551
+ clearInterval(countdownInterval);
552
+ countdownInterval = null;
553
+ }
554
+ if (error.signal === 'SIGTERM' || error.signal === 'SIGKILL') {
555
+ // This is expected when we kill the process
556
+ logger.debug('Recording process terminated as expected');
557
+ } else {
558
+ logger.warn('Recording process ended unexpectedly: %s', error.message);
559
+ }
560
+ }
561
+ }
562
+ // Always clean up keyboard input
563
+ if (process.stdin.setRawMode) {
564
+ process.stdin.setRawMode(false);
565
+ process.stdin.pause();
566
+ }
567
+ }
568
+ // If recording was cancelled, exit early
569
+ if (recordingCancelled) {
570
+ return '';
571
+ }
572
+ // Wait a moment for the recording file to be fully written
573
+ if (recordingFinished) {
574
+ await new Promise((resolve)=>setTimeout(resolve, 500));
575
+ }
576
+ // Check if audio file exists
577
+ try {
578
+ await fs.access(audioFilePath);
579
+ const stats = await fs.stat(audioFilePath);
580
+ if (stats.size === 0) {
581
+ throw new Error('Audio file is empty');
582
+ }
583
+ logger.info('✅ Audio file created successfully (%d bytes)', stats.size);
584
+ } catch (error) {
585
+ throw new Error(`Failed to create audio file: ${error.message}`);
586
+ }
587
+ // Transcribe the audio
588
+ logger.info('🎯 Transcribing audio...');
589
+ logger.info('⏳ This may take a few seconds depending on audio length...');
590
+ const transcription = await transcribeAudio(audioFilePath);
591
+ const audioContext = transcription.text;
592
+ logger.info('✅ Audio transcribed successfully');
593
+ logger.debug('Transcription: %s', audioContext);
594
+ // Save audio file and transcript to output directory
595
+ logger.info('💾 Saving audio file and transcript...');
596
+ try {
597
+ const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
598
+ const storage = create({
599
+ log: logger.info
600
+ });
601
+ await storage.ensureDirectory(outputDirectory);
602
+ // Save audio file copy
603
+ const audioOutputFilename = getTimestampedAudioFilename();
604
+ const audioOutputPath = getOutputPath(outputDirectory, audioOutputFilename);
605
+ await fs.copyFile(audioFilePath, audioOutputPath);
606
+ logger.debug('Saved audio file: %s', audioOutputPath);
607
+ // Save transcript
608
+ if (audioContext.trim()) {
609
+ const transcriptOutputFilename = getTimestampedTranscriptFilename();
610
+ const transcriptOutputPath = getOutputPath(outputDirectory, transcriptOutputFilename);
611
+ const transcriptContent = `# Audio Transcript\n\n**Recorded:** ${new Date().toISOString()}\n\n**Transcript:**\n\n${audioContext}`;
612
+ await storage.writeFile(transcriptOutputPath, transcriptContent, 'utf-8');
613
+ logger.debug('Saved transcript: %s', transcriptOutputPath);
614
+ }
615
+ } catch (error) {
616
+ logger.warn('Failed to save audio/transcript files: %s', error.message);
617
+ }
618
+ if (!audioContext.trim()) {
619
+ logger.warn('No audio content was transcribed. Proceeding without audio context.');
620
+ return '';
621
+ } else {
622
+ logger.info('📝 Using transcribed audio as commit context');
623
+ return audioContext;
624
+ }
625
+ } catch (error) {
626
+ logger.error('Audio recording/transcription failed: %s', error.message);
627
+ logger.info('Proceeding with commit generation without audio context...');
628
+ return '';
629
+ } finally{
630
+ // Comprehensive cleanup to ensure program can exit
631
+ try {
632
+ // Clear any remaining countdown interval
633
+ if (countdownInterval) {
634
+ clearInterval(countdownInterval);
635
+ countdownInterval = null;
636
+ }
637
+ // Ensure stdin is properly reset
638
+ if (process.stdin.setRawMode) {
639
+ process.stdin.setRawMode(false);
640
+ process.stdin.pause();
641
+ process.stdin.removeAllListeners('data');
642
+ }
643
+ // Remove process event listeners that we added
644
+ process.removeAllListeners('SIGINT');
645
+ process.removeAllListeners('SIGTERM');
646
+ // Force kill any remaining recording process
647
+ if (recordingProcess && !recordingProcess.killed) {
648
+ try {
649
+ recordingProcess.kill('SIGKILL');
650
+ } catch (killError) {
651
+ // Ignore kill errors
652
+ }
653
+ }
654
+ // Clean up temporary directory
655
+ await fs.rm(tempDir, {
656
+ recursive: true,
657
+ force: true
658
+ });
659
+ } catch (cleanupError) {
660
+ logger.debug('Cleanup warning: %s', cleanupError.message);
661
+ }
662
+ }
663
+ };
664
+
665
+ export { execute };
666
+ //# sourceMappingURL=audio-commit.js.map