@eldrforge/kodrdriv 0.0.14 → 0.0.17

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 (84) hide show
  1. package/README.md +1 -9
  2. package/dist/arguments.js +306 -55
  3. package/dist/arguments.js.map +1 -1
  4. package/dist/audio/devices.js +284 -0
  5. package/dist/audio/devices.js.map +1 -0
  6. package/dist/audio/index.js +31 -0
  7. package/dist/audio/index.js.map +1 -0
  8. package/dist/audio/processor.js +766 -0
  9. package/dist/audio/processor.js.map +1 -0
  10. package/dist/audio/types.js +16 -0
  11. package/dist/audio/types.js.map +1 -0
  12. package/dist/audio/validation.js +35 -0
  13. package/dist/audio/validation.js.map +1 -0
  14. package/dist/commands/audio-commit.js +64 -248
  15. package/dist/commands/audio-commit.js.map +1 -1
  16. package/dist/commands/audio-review.js +90 -701
  17. package/dist/commands/audio-review.js.map +1 -1
  18. package/dist/commands/commit.js +18 -18
  19. package/dist/commands/commit.js.map +1 -1
  20. package/dist/commands/release.js +14 -15
  21. package/dist/commands/release.js.map +1 -1
  22. package/dist/commands/review.js +152 -0
  23. package/dist/commands/review.js.map +1 -0
  24. package/dist/commands/select-audio.js +265 -0
  25. package/dist/commands/select-audio.js.map +1 -0
  26. package/dist/constants.js +86 -68
  27. package/dist/constants.js.map +1 -1
  28. package/dist/content/diff.js +155 -1
  29. package/dist/content/diff.js.map +1 -1
  30. package/dist/content/issues.js +256 -0
  31. package/dist/content/issues.js.map +1 -0
  32. package/dist/content/releaseNotes.js +90 -0
  33. package/dist/content/releaseNotes.js.map +1 -0
  34. package/dist/main.js +9 -2
  35. package/dist/main.js.map +1 -1
  36. package/dist/prompt/instructions/commit.md +18 -15
  37. package/dist/prompt/instructions/release.md +6 -5
  38. package/dist/prompt/instructions/{audio-review.md → review.md} +24 -18
  39. package/dist/prompt/prompts.js +87 -19
  40. package/dist/prompt/prompts.js.map +1 -1
  41. package/dist/types.js +27 -3
  42. package/dist/types.js.map +1 -1
  43. package/dist/util/general.js +7 -1
  44. package/dist/util/general.js.map +1 -1
  45. package/dist/util/openai.js +34 -3
  46. package/dist/util/openai.js.map +1 -1
  47. package/dist/util/stdin.js +61 -0
  48. package/dist/util/stdin.js.map +1 -0
  49. package/package.json +6 -6
  50. package/.kodrdriv/config.yaml +0 -20
  51. package/.kodrdriv/context/content.md +0 -7
  52. package/RELEASE_NOTES.md +0 -14
  53. package/docs/index.html +0 -17
  54. package/docs/package.json +0 -36
  55. package/docs/pnpm-lock.yaml +0 -3441
  56. package/docs/public/README.md +0 -132
  57. package/docs/public/advanced-usage.md +0 -188
  58. package/docs/public/code-icon.svg +0 -4
  59. package/docs/public/commands.md +0 -116
  60. package/docs/public/configuration.md +0 -274
  61. package/docs/public/examples.md +0 -352
  62. package/docs/public/kodrdriv-logo.svg +0 -62
  63. package/docs/src/App.css +0 -387
  64. package/docs/src/App.tsx +0 -60
  65. package/docs/src/components/DocumentPage.tsx +0 -56
  66. package/docs/src/components/ErrorMessage.tsx +0 -15
  67. package/docs/src/components/LoadingSpinner.tsx +0 -14
  68. package/docs/src/components/MarkdownRenderer.tsx +0 -56
  69. package/docs/src/components/Navigation.css +0 -73
  70. package/docs/src/components/Navigation.tsx +0 -36
  71. package/docs/src/index.css +0 -61
  72. package/docs/src/main.tsx +0 -10
  73. package/docs/src/test/setup.ts +0 -1
  74. package/docs/src/vite-env.d.ts +0 -10
  75. package/docs/tsconfig.node.json +0 -13
  76. package/docs/vite.config.ts +0 -15
  77. package/docs/vitest.config.ts +0 -15
  78. package/eslint.config.mjs +0 -83
  79. package/nodemon.json +0 -14
  80. package/output/kodrdriv/250701-1442-release-notes.md +0 -3
  81. package/pnpm-workspace.yaml +0 -5
  82. package/tsconfig.tsbuildinfo +0 -1
  83. package/vite.config.ts +0 -90
  84. package/vitest.config.ts +0 -24
@@ -0,0 +1,766 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
4
+ import { getLogger } from '../logging.js';
5
+ import { run } from '../util/child.js';
6
+ import { getTimestampedTranscriptFilename, getOutputPath, getTimestampedAudioFilename } from '../util/general.js';
7
+ import { transcribeAudio } from '../util/openai.js';
8
+ import { create } from '../util/storage.js';
9
+ import { audioDeviceConfigExists, loadAudioDeviceFromHomeConfig } from '../commands/select-audio.js';
10
+ import { listAudioDevices, detectBestAudioDevice } from './devices.js';
11
+ import { validateAudioFile } from './validation.js';
12
+
13
+ /* eslint-disable @typescript-eslint/no-unused-vars */ function _define_property(obj, key, value) {
14
+ if (key in obj) {
15
+ Object.defineProperty(obj, key, {
16
+ value: value,
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true
20
+ });
21
+ } else {
22
+ obj[key] = value;
23
+ }
24
+ return obj;
25
+ }
26
+ /**
27
+ * Main audio processor class that handles recording, processing, and transcription
28
+ */ class AudioProcessor {
29
+ /**
30
+ * Process audio from either a file or by recording new audio
31
+ * @param options Audio processing options
32
+ * @returns AudioProcessingResult with transcript and file paths
33
+ */ async processAudio(options) {
34
+ // Check if audio device is configured (only for recording, not for file processing)
35
+ if (!options.file && options.preferencesDirectory && !await audioDeviceConfigExists(options.preferencesDirectory)) {
36
+ throw new Error('No audio device configured. Please run "kodrdriv select-audio" first to configure your audio device.');
37
+ }
38
+ if (options.dryRun) {
39
+ if (options.file) {
40
+ this.logger.info('DRY RUN: Would process audio file: %s', options.file);
41
+ } else {
42
+ this.logger.info('DRY RUN: Would start audio recording');
43
+ }
44
+ this.logger.info('DRY RUN: Would transcribe audio and return transcript');
45
+ return {
46
+ transcript: '',
47
+ cancelled: false
48
+ };
49
+ }
50
+ if (options.file) {
51
+ // Process existing audio file
52
+ return await this.processAudioFile(options.file, options);
53
+ } else {
54
+ // Record new audio
55
+ return await this.recordAndTranscribeAudio(options);
56
+ }
57
+ }
58
+ /**
59
+ * Process an existing audio file
60
+ */ async processAudioFile(filePath, options) {
61
+ this.logger.info('🎯 Processing audio file: %s', filePath);
62
+ // Validate the audio file
63
+ await validateAudioFile(filePath);
64
+ // Transcribe the audio
65
+ this.logger.info('🎯 Transcribing audio...');
66
+ this.logger.info('⏳ This may take a few seconds depending on audio length...');
67
+ const transcription = await transcribeAudio(filePath);
68
+ const audioContext = transcription.text;
69
+ this.logger.info('✅ Audio transcribed successfully');
70
+ this.logger.debug('Transcription: %s', audioContext);
71
+ // Save transcript to output directory
72
+ let transcriptFilePath;
73
+ if (options.outputDirectory) {
74
+ transcriptFilePath = await this.saveTranscript(audioContext, filePath, options.outputDirectory);
75
+ }
76
+ if (!audioContext.trim()) {
77
+ this.logger.warn('No audio content was transcribed.');
78
+ return {
79
+ transcript: '',
80
+ audioFilePath: filePath,
81
+ transcriptFilePath,
82
+ cancelled: false
83
+ };
84
+ }
85
+ this.logger.info('📝 Audio transcribed successfully');
86
+ return {
87
+ transcript: audioContext,
88
+ audioFilePath: filePath,
89
+ transcriptFilePath,
90
+ cancelled: false
91
+ };
92
+ }
93
+ /**
94
+ * Record and transcribe new audio
95
+ */ async recordAndTranscribeAudio(options) {
96
+ const outputDirectory = options.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
97
+ const storage = create({
98
+ log: this.logger.info
99
+ });
100
+ await storage.ensureDirectory(outputDirectory);
101
+ // Create a deterministic temp directory inside the configured output directory
102
+ const tempDir = path.join(outputDirectory, 'tmp');
103
+ await fs.mkdir(tempDir, {
104
+ recursive: true
105
+ });
106
+ // Use a timestamped filename to avoid collisions if multiple recordings run quickly
107
+ const audioFilePath = path.join(tempDir, `recording-${Date.now()}.wav`);
108
+ // Recording state variables
109
+ let recordingProcess = null;
110
+ let recordingFinished = false;
111
+ let recordingCancelled = false;
112
+ let recordingFailed = false;
113
+ let countdownInterval = null;
114
+ let remainingSeconds = 30;
115
+ let intendedRecordingTime = 30;
116
+ const maxRecordingTime = options.maxRecordingTime || 300; // 5 minutes default
117
+ const extensionTime = 30; // 30 seconds per extension
118
+ // Cleanup functions that need to be accessible in finally block
119
+ let keyHandler = null;
120
+ const originalRawMode = false;
121
+ const sigintHandler = null;
122
+ const cleanupKeyboardHandling = ()=>{
123
+ try {
124
+ if (keyHandler) {
125
+ process.stdin.removeListener('data', keyHandler);
126
+ keyHandler = null;
127
+ }
128
+ if (process.stdin.setRawMode) {
129
+ process.stdin.setRawMode(originalRawMode);
130
+ }
131
+ process.stdin.pause();
132
+ } catch (e) {
133
+ // Ignore cleanup errors
134
+ }
135
+ };
136
+ try {
137
+ this.logger.info('🎤 Starting recording... Speak now!');
138
+ this.logger.info('📋 Controls: ENTER=done, E=extend+30s, C/Ctrl+C=cancel');
139
+ // List available audio devices in debug mode
140
+ if (options.debug) {
141
+ await listAudioDevices();
142
+ }
143
+ // Recording control functions
144
+ const updateCountdownDisplay = ()=>{
145
+ const maxMinutes = Math.floor(maxRecordingTime / 60);
146
+ const intendedMinutes = Math.floor(intendedRecordingTime / 60);
147
+ const intendedSeconds = intendedRecordingTime % 60;
148
+ process.stdout.write(`\r⏱️ Recording: ${remainingSeconds}s left (${intendedMinutes}:${intendedSeconds.toString().padStart(2, '0')}/${maxMinutes}:00 max) [ENTER=done, E=+30s, C=cancel]`);
149
+ };
150
+ const extendRecording = ()=>{
151
+ const newTotal = intendedRecordingTime + extensionTime;
152
+ if (newTotal <= maxRecordingTime) {
153
+ intendedRecordingTime = newTotal;
154
+ remainingSeconds += extensionTime;
155
+ this.logger.info(`🔄 Extended recording by ${extensionTime}s (total: ${Math.floor(intendedRecordingTime / 60)}:${(intendedRecordingTime % 60).toString().padStart(2, '0')})`);
156
+ updateCountdownDisplay();
157
+ } else {
158
+ const canExtend = maxRecordingTime - intendedRecordingTime;
159
+ if (canExtend > 0) {
160
+ intendedRecordingTime = maxRecordingTime;
161
+ remainingSeconds += canExtend;
162
+ this.logger.info(`🔄 Extended recording by ${canExtend}s (maximum reached: ${Math.floor(maxRecordingTime / 60)}:${(maxRecordingTime % 60).toString().padStart(2, '0')})`);
163
+ updateCountdownDisplay();
164
+ } else {
165
+ this.logger.warn(`⚠️ Cannot extend: maximum recording time (${Math.floor(maxRecordingTime / 60)}:${(maxRecordingTime % 60).toString().padStart(2, '0')}) reached`);
166
+ }
167
+ }
168
+ };
169
+ const stopRecording = async ()=>{
170
+ if (!recordingFinished && !recordingCancelled) {
171
+ recordingFinished = true;
172
+ if (countdownInterval) {
173
+ clearInterval(countdownInterval);
174
+ countdownInterval = null;
175
+ }
176
+ process.stdout.write('\r⏱️ Recording finished! \n');
177
+ if (recordingProcess && recordingProcess.kill) {
178
+ recordingProcess.kill('SIGTERM');
179
+ }
180
+ this.logger.info('🛑 Recording stopped');
181
+ }
182
+ };
183
+ const cancelRecording = async ()=>{
184
+ if (!recordingFinished && !recordingCancelled) {
185
+ recordingCancelled = true;
186
+ if (countdownInterval) {
187
+ clearInterval(countdownInterval);
188
+ countdownInterval = null;
189
+ }
190
+ process.stdout.write('\r❌ Recording cancelled! \n');
191
+ if (recordingProcess && recordingProcess.kill) {
192
+ recordingProcess.kill('SIGTERM');
193
+ }
194
+ this.logger.info('❌ Audio recording cancelled by user');
195
+ }
196
+ };
197
+ // Set up keyboard input handling
198
+ let keyHandler = null;
199
+ let originalRawMode = false;
200
+ const setupKeyboardHandling = ()=>{
201
+ // Ensure stdin is properly configured
202
+ if (!process.stdin.readable) {
203
+ this.logger.warn('stdin is not readable, keyboard controls may not work');
204
+ return;
205
+ }
206
+ // Save original stdin state
207
+ originalRawMode = process.stdin.isRaw || false;
208
+ try {
209
+ process.stdin.setRawMode(true);
210
+ process.stdin.resume();
211
+ process.stdin.setEncoding('utf8');
212
+ keyHandler = (data)=>{
213
+ const key = data.toString();
214
+ const keyCode = key.charCodeAt(0);
215
+ if (options.debug) {
216
+ this.logger.debug('Key pressed: code=%d, char=%s', keyCode, JSON.stringify(key));
217
+ }
218
+ if (keyCode === 13 || keyCode === 10) {
219
+ process.stdout.write('\r✅ ENTER pressed - stopping recording... \n');
220
+ stopRecording();
221
+ } else if (key.toLowerCase() === 'e') {
222
+ extendRecording();
223
+ } else if (key.toLowerCase() === 'c' || keyCode === 3) {
224
+ process.stdout.write('\r❌ Cancelling recording... \n');
225
+ cancelRecording();
226
+ }
227
+ };
228
+ process.stdin.on('data', keyHandler);
229
+ } catch (error) {
230
+ this.logger.warn('Failed to setup keyboard handling: %s', error.message);
231
+ this.logger.info('You may need to use Ctrl+C to stop recording');
232
+ }
233
+ };
234
+ // Start countdown display
235
+ const startCountdown = ()=>{
236
+ updateCountdownDisplay();
237
+ countdownInterval = setInterval(()=>{
238
+ remainingSeconds--;
239
+ if (remainingSeconds > 0) {
240
+ updateCountdownDisplay();
241
+ } else {
242
+ process.stdout.write('\r⏱️ Recording: Time\'s up! \n');
243
+ if (countdownInterval) {
244
+ clearInterval(countdownInterval);
245
+ countdownInterval = null;
246
+ }
247
+ stopRecording();
248
+ }
249
+ }, 1000);
250
+ };
251
+ // Set up recording command
252
+ recordingProcess = await this.setupRecording(audioFilePath, maxRecordingTime, options);
253
+ if (options.debug) {
254
+ this.logger.debug('setupRecording returned: %s', recordingProcess ? 'process object' : 'null');
255
+ }
256
+ // Handle SIGINT for cleanup
257
+ process.on('SIGINT', cancelRecording);
258
+ // Start keyboard handling and countdown if we have a recording process
259
+ if (recordingProcess) {
260
+ setupKeyboardHandling();
261
+ startCountdown();
262
+ // Create a promise that resolves when user manually stops recording
263
+ const manualStopPromise = new Promise((resolve)=>{
264
+ const checkInterval = setInterval(()=>{
265
+ if (recordingFinished || recordingCancelled) {
266
+ clearInterval(checkInterval);
267
+ resolve();
268
+ }
269
+ }, 100);
270
+ });
271
+ // Create a promise that waits for the recording process to finish
272
+ const recordingProcessPromise = new Promise((resolve, reject)=>{
273
+ if (!recordingProcess) {
274
+ resolve();
275
+ return;
276
+ }
277
+ const processStartTime = Date.now();
278
+ recordingProcess.on('exit', (code, signal)=>{
279
+ const processRunTime = Date.now() - processStartTime;
280
+ if (options.debug) {
281
+ this.logger.debug('Recording process exited with code %s, signal %s after %dms', code, signal, processRunTime);
282
+ }
283
+ // If the process exits very quickly with an error code, it likely failed to start recording
284
+ if (code !== 0 && processRunTime < 2000) {
285
+ this.logger.debug('Recording process failed early - runtime: %dms, exit code: %s', processRunTime, code);
286
+ recordingFailed = true;
287
+ }
288
+ resolve();
289
+ });
290
+ recordingProcess.on('error', (error)=>{
291
+ this.logger.error('Recording process error: %s', error.message);
292
+ recordingFailed = true;
293
+ reject(error);
294
+ });
295
+ });
296
+ // Wait for either the recording to finish naturally or manual stop
297
+ try {
298
+ await Promise.race([
299
+ recordingProcessPromise,
300
+ manualStopPromise
301
+ ]);
302
+ if (recordingFinished && recordingProcess && !recordingProcess.killed) {
303
+ recordingProcess.kill('SIGTERM');
304
+ await new Promise((resolve)=>setTimeout(resolve, 200));
305
+ if (!recordingProcess.killed) {
306
+ recordingProcess.kill('SIGKILL');
307
+ }
308
+ }
309
+ if (!recordingCancelled && !recordingFinished) {
310
+ if (countdownInterval) {
311
+ clearInterval(countdownInterval);
312
+ countdownInterval = null;
313
+ }
314
+ process.stdout.write('\r⏱️ Recording completed! \n');
315
+ this.logger.info('✅ Recording completed automatically');
316
+ }
317
+ } catch (error) {
318
+ if (!recordingCancelled && !recordingFinished) {
319
+ if (countdownInterval) {
320
+ clearInterval(countdownInterval);
321
+ countdownInterval = null;
322
+ }
323
+ if (error.signal === 'SIGTERM' || error.signal === 'SIGKILL') {
324
+ this.logger.debug('Recording process terminated as expected');
325
+ } else {
326
+ this.logger.warn('Recording process ended unexpectedly: %s', error.message);
327
+ }
328
+ }
329
+ }
330
+ // Clean up keyboard input and process listeners
331
+ cleanupKeyboardHandling();
332
+ if (sigintHandler) ;
333
+ }
334
+ // If recording was cancelled, return early
335
+ if (recordingCancelled) {
336
+ return {
337
+ transcript: '',
338
+ cancelled: true
339
+ };
340
+ }
341
+ // If recording failed (process exited with error too quickly), fail the command
342
+ if (recordingFailed) {
343
+ this.logger.error('❌ Audio recording failed to start or exited with an error');
344
+ this.logger.info('This usually means the audio device is busy, not accessible, or ffmpeg configuration is incorrect');
345
+ this.logger.info('💡 Try running "kodrdriv select-audio" to choose a different audio device');
346
+ throw new Error('Audio recording failed - cannot proceed with audio-commit command');
347
+ }
348
+ // Wait for the recording file to be fully written
349
+ if (recordingFinished) {
350
+ await new Promise((resolve)=>setTimeout(resolve, 500));
351
+ }
352
+ // Check if recording process failed early (before we try to verify the file)
353
+ try {
354
+ // Verify audio file was created
355
+ await this.verifyAudioFile(audioFilePath);
356
+ } catch (verifyError) {
357
+ // If the file doesn't exist, the recording process likely failed
358
+ this.logger.error('❌ Recording process failed - no audio file was created: %s', verifyError.message);
359
+ this.logger.info('This can happen if the audio device is busy, not accessible, or if ffmpeg is not properly configured');
360
+ this.logger.info('💡 Try running "kodrdriv select-audio" to choose a different audio device');
361
+ throw new Error(`Audio recording failed - ${verifyError.message}`);
362
+ }
363
+ // Transcribe the audio
364
+ const audioContext = await this.transcribeRecordedAudio(audioFilePath);
365
+ // Save files to output directory
366
+ const { audioOutputPath, transcriptOutputPath } = await this.saveRecordedFiles(audioFilePath, audioContext, outputDirectory);
367
+ if (!audioContext.trim()) {
368
+ this.logger.warn('No audio content was transcribed.');
369
+ return {
370
+ transcript: '',
371
+ audioFilePath: audioOutputPath,
372
+ transcriptFilePath: transcriptOutputPath,
373
+ cancelled: false
374
+ };
375
+ }
376
+ this.logger.info('📝 Audio recorded and transcribed successfully');
377
+ return {
378
+ transcript: audioContext,
379
+ audioFilePath: audioOutputPath,
380
+ transcriptFilePath: transcriptOutputPath,
381
+ cancelled: false
382
+ };
383
+ } catch (error) {
384
+ this.logger.error('Audio recording/transcription failed: %s', error.message);
385
+ // Re-throw the error so the command fails properly
386
+ throw error;
387
+ } finally{
388
+ // Cleanup is handled comprehensively in the cleanup function
389
+ await this.cleanup(countdownInterval, recordingProcess, tempDir, recordingFailed || options.keepTemp);
390
+ }
391
+ }
392
+ /**
393
+ * Set up recording command based on platform
394
+ */ async setupRecording(audioFilePath, maxRecordingTime, options) {
395
+ let recordCommand;
396
+ let argsForSpawn;
397
+ if (process.platform === 'darwin') {
398
+ // macOS - try ffmpeg first
399
+ try {
400
+ await run('which ffmpeg');
401
+ const homeDeviceConfig = options.preferencesDirectory ? await loadAudioDeviceFromHomeConfig(options.preferencesDirectory) : null;
402
+ const audioDevice = options.audioDevice || (homeDeviceConfig === null || homeDeviceConfig === void 0 ? void 0 : homeDeviceConfig.audioDevice) || await detectBestAudioDevice();
403
+ // Get the correct input format for this device
404
+ const { getDeviceInputFormat } = await import('./devices.js');
405
+ const inputFormat = await getDeviceInputFormat(audioDevice);
406
+ if (!inputFormat) {
407
+ throw new Error(`Unable to find working format for audio device ${audioDevice}`);
408
+ }
409
+ // Build argument list explicitly. If the detected inputFormat is wrapped in
410
+ // quotes (e.g. ":0"), remove those quotes for the spawn call. The shell would
411
+ // normally strip them, but child_process.spawn passes the string verbatim to
412
+ // the executable, which causes ffmpeg to treat the quotes as part of the
413
+ // device name leading to failures such as "Video device not found".
414
+ const strippedInputFormat = inputFormat.startsWith('"') && inputFormat.endsWith('"') ? inputFormat.slice(1, -1) : inputFormat;
415
+ // Determine audio parameters based on stored capabilities (if available)
416
+ const channels = options.audioDevice ? undefined : homeDeviceConfig === null || homeDeviceConfig === void 0 ? void 0 : homeDeviceConfig.channels; // user may override manually via options
417
+ const sampleRate = options.audioDevice ? undefined : homeDeviceConfig === null || homeDeviceConfig === void 0 ? void 0 : homeDeviceConfig.sampleRate;
418
+ const ffmpegAudioArgs = [
419
+ '-c:a',
420
+ 'pcm_s16le',
421
+ '-vn'
422
+ ];
423
+ if (channels) {
424
+ ffmpegAudioArgs.push('-ac', String(channels));
425
+ } else {
426
+ // Default to mono when channel count is unknown to reduce size
427
+ ffmpegAudioArgs.push('-ac', '1');
428
+ }
429
+ if (sampleRate) {
430
+ ffmpegAudioArgs.push('-ar', String(sampleRate));
431
+ }
432
+ const ffmpegArgs = [
433
+ '-f',
434
+ 'avfoundation',
435
+ '-i',
436
+ strippedInputFormat,
437
+ ...ffmpegAudioArgs,
438
+ '-t',
439
+ String(maxRecordingTime),
440
+ '-y',
441
+ audioFilePath
442
+ ];
443
+ // Store for later spawn
444
+ recordCommand = `ffmpeg ${ffmpegArgs.join(' ')}`;
445
+ argsForSpawn = ffmpegArgs;
446
+ // Log the exact command being executed
447
+ this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
448
+ if (options.audioDevice) {
449
+ this.logger.info(`🎙️ Using audio device ${audioDevice} (from configuration) with format ${inputFormat}`);
450
+ } else if (homeDeviceConfig) {
451
+ this.logger.info(`🎙️ Using audio device ${audioDevice} (${homeDeviceConfig.audioDeviceName}) with format ${inputFormat}`);
452
+ } else {
453
+ this.logger.info(`🎙️ Using audio device ${audioDevice} (auto-detected) with format ${inputFormat}`);
454
+ }
455
+ } catch {
456
+ // Try sox/rec as fallback
457
+ try {
458
+ await run('which rec');
459
+ recordCommand = `rec -c 1 -t wav "${audioFilePath}" trim 0 ${maxRecordingTime}`;
460
+ this.logger.info(`🔧 Executing recording command (sox fallback): ${recordCommand}`);
461
+ } catch {
462
+ throw new Error('MANUAL_RECORDING_NEEDED');
463
+ }
464
+ }
465
+ } else if (process.platform === 'win32') {
466
+ // Windows - use ffmpeg
467
+ try {
468
+ await run('where ffmpeg');
469
+ recordCommand = `ffmpeg -f dshow -i audio="Microphone" -ac 1 -c:a pcm_s16le -vn -t ${maxRecordingTime} -y "${audioFilePath}"`;
470
+ this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
471
+ } catch {
472
+ throw new Error('MANUAL_RECORDING_NEEDED');
473
+ }
474
+ } else {
475
+ // Linux - use arecord or ffmpeg
476
+ try {
477
+ await run('which arecord');
478
+ recordCommand = `arecord -f cd -t wav -d ${maxRecordingTime} "${audioFilePath}"`;
479
+ this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
480
+ } catch {
481
+ try {
482
+ await run('which ffmpeg');
483
+ recordCommand = `ffmpeg -f alsa -i default -ac 1 -c:a pcm_s16le -vn -t ${maxRecordingTime} -y "${audioFilePath}"`;
484
+ this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
485
+ } catch {
486
+ throw new Error('MANUAL_RECORDING_NEEDED');
487
+ }
488
+ }
489
+ }
490
+ try {
491
+ var _recordingProcess_stderr;
492
+ // Ensure the output directory exists (equivalent to `mkdir -p`)
493
+ await fs.mkdir(path.dirname(audioFilePath), {
494
+ recursive: true
495
+ });
496
+ // Use spawn instead of exec for better process control
497
+ const { spawn } = await import('child_process');
498
+ // If argsForSpawn was prepared (preferred path), use it; otherwise fall back to splitting
499
+ const command = 'ffmpeg';
500
+ const args = argsForSpawn !== null && argsForSpawn !== void 0 ? argsForSpawn : recordCommand.split(' ').slice(1);
501
+ this.logger.debug(`🔧 Spawning process: ${command} with args: ${JSON.stringify(args)}`);
502
+ const recordingProcess = spawn(command, args, {
503
+ stdio: [
504
+ 'ignore',
505
+ 'ignore',
506
+ 'pipe'
507
+ ],
508
+ detached: false
509
+ });
510
+ // Enhanced error handling and stderr capture
511
+ let stderrBuffer = '';
512
+ recordingProcess.on('error', (error)=>{
513
+ this.logger.error('Recording process error: %s', error.message);
514
+ this.logger.error('Full error details: %s', JSON.stringify(error));
515
+ });
516
+ (_recordingProcess_stderr = recordingProcess.stderr) === null || _recordingProcess_stderr === void 0 ? void 0 : _recordingProcess_stderr.on('data', (data)=>{
517
+ const output = data.toString().trim();
518
+ stderrBuffer += output + '\n';
519
+ if (options.debug) {
520
+ this.logger.debug('Recording process stderr: %s', output);
521
+ } else {
522
+ // In non-debug mode, still capture critical errors
523
+ if (output.toLowerCase().includes('error') || output.toLowerCase().includes('failed') || output.toLowerCase().includes('permission') || output.toLowerCase().includes('busy') || output.toLowerCase().includes('device')) {
524
+ this.logger.warn('Recording process stderr: %s', output);
525
+ }
526
+ }
527
+ });
528
+ // Enhanced exit handling with stderr output
529
+ recordingProcess.on('exit', (code, signal)=>{
530
+ if (code !== 0) {
531
+ this.logger.error('🚨 Recording process exited with error code %s, signal %s', code, signal);
532
+ if (stderrBuffer.trim()) {
533
+ this.logger.error('🚨 Recording process stderr output:\n%s', stderrBuffer);
534
+ }
535
+ } else if (options.debug) {
536
+ this.logger.debug('Recording process exited successfully with code %s', code);
537
+ }
538
+ });
539
+ return recordingProcess;
540
+ } catch (error) {
541
+ if (error.message === 'MANUAL_RECORDING_NEEDED') {
542
+ this.showManualRecordingInstructions(audioFilePath);
543
+ await this.waitForManualRecording();
544
+ return null;
545
+ } else {
546
+ throw error;
547
+ }
548
+ }
549
+ }
550
+ /**
551
+ * Show instructions for manual recording
552
+ */ showManualRecordingInstructions(audioFilePath) {
553
+ this.logger.warn('⚠️ Automatic recording not available on this system.');
554
+ this.logger.warn('📱 Please record audio manually using your system\'s built-in tools:');
555
+ this.logger.warn('');
556
+ if (process.platform === 'darwin') {
557
+ this.logger.warn('🍎 macOS options:');
558
+ this.logger.warn(' 1. Use QuickTime Player: File → New Audio Recording');
559
+ this.logger.warn(' 2. Use Voice Memos app');
560
+ this.logger.warn(' 3. Install ffmpeg: brew install ffmpeg');
561
+ this.logger.warn(' 4. Install sox: brew install sox');
562
+ } else if (process.platform === 'win32') {
563
+ this.logger.warn('🪟 Windows options:');
564
+ this.logger.warn(' 1. Use Voice Recorder app');
565
+ this.logger.warn(' 2. Install ffmpeg: https://ffmpeg.org/download.html');
566
+ } else {
567
+ this.logger.warn('🐧 Linux options:');
568
+ this.logger.warn(' 1. Install alsa-utils: sudo apt install alsa-utils');
569
+ this.logger.warn(' 2. Install ffmpeg: sudo apt install ffmpeg');
570
+ }
571
+ this.logger.warn('');
572
+ this.logger.warn(`💾 Save your recording as: ${audioFilePath}`);
573
+ this.logger.warn('🎵 Recommended format: WAV, 44.1kHz, mono or stereo');
574
+ this.logger.warn('');
575
+ this.logger.warn('⌨️ Press ENTER when you have saved the audio file...');
576
+ }
577
+ /**
578
+ * Wait for user to complete manual recording
579
+ */ async waitForManualRecording() {
580
+ return new Promise((resolve)=>{
581
+ process.stdin.setRawMode(true);
582
+ process.stdin.resume();
583
+ const enterHandler = (key)=>{
584
+ if (key[0] === 13) {
585
+ process.stdin.setRawMode(false);
586
+ process.stdin.pause();
587
+ process.stdin.removeListener('data', enterHandler);
588
+ resolve();
589
+ }
590
+ };
591
+ process.stdin.on('data', enterHandler);
592
+ });
593
+ }
594
+ /**
595
+ * Verify that the audio file was created successfully
596
+ */ async verifyAudioFile(audioFilePath) {
597
+ try {
598
+ await fs.access(audioFilePath);
599
+ const stats = await fs.stat(audioFilePath);
600
+ if (stats.size === 0) {
601
+ throw new Error('Audio file is empty');
602
+ }
603
+ this.logger.info('✅ Audio file created successfully (%d bytes)', stats.size);
604
+ } catch (error) {
605
+ throw new Error(`Failed to create audio file: ${error.message}`);
606
+ }
607
+ }
608
+ /**
609
+ * Transcribe recorded audio
610
+ */ async transcribeRecordedAudio(audioFilePath) {
611
+ this.logger.info('🎯 Transcribing audio...');
612
+ this.logger.info('⏳ This may take a few seconds depending on audio length...');
613
+ const transcription = await transcribeAudio(audioFilePath);
614
+ const audioContext = transcription.text;
615
+ this.logger.info('✅ Audio transcribed successfully');
616
+ this.logger.debug('Transcription: %s', audioContext);
617
+ return audioContext;
618
+ }
619
+ /**
620
+ * Save transcript file
621
+ */ async saveTranscript(audioContext, sourceFilePath, outputDirectory) {
622
+ if (!audioContext.trim()) return undefined;
623
+ try {
624
+ this.logger.info('💾 Saving transcript...');
625
+ const storage = create({
626
+ log: this.logger.info
627
+ });
628
+ await storage.ensureDirectory(outputDirectory);
629
+ const transcriptOutputFilename = getTimestampedTranscriptFilename();
630
+ const transcriptOutputPath = getOutputPath(outputDirectory, transcriptOutputFilename);
631
+ const transcriptContent = `# Audio Transcript\n\n**Source:** ${sourceFilePath}\n**Processed:** ${new Date().toISOString()}\n\n**Transcript:**\n\n${audioContext}`;
632
+ await storage.writeFile(transcriptOutputPath, transcriptContent, 'utf-8');
633
+ this.logger.debug('Saved transcript: %s', transcriptOutputPath);
634
+ return transcriptOutputPath;
635
+ } catch (error) {
636
+ this.logger.warn('Failed to save transcript file: %s', error.message);
637
+ return undefined;
638
+ }
639
+ }
640
+ /**
641
+ * Save recorded audio file and transcript
642
+ */ async saveRecordedFiles(audioFilePath, audioContext, outputDirectory) {
643
+ try {
644
+ this.logger.info('💾 Saving audio file and transcript...');
645
+ const storage = create({
646
+ log: this.logger.info
647
+ });
648
+ await storage.ensureDirectory(outputDirectory);
649
+ // Save audio file copy
650
+ const audioOutputFilename = getTimestampedAudioFilename();
651
+ const audioOutputPath = getOutputPath(outputDirectory, audioOutputFilename);
652
+ await fs.copyFile(audioFilePath, audioOutputPath);
653
+ this.logger.debug('Saved audio file: %s', audioOutputPath);
654
+ // Save transcript
655
+ let transcriptOutputPath;
656
+ if (audioContext.trim()) {
657
+ const transcriptOutputFilename = getTimestampedTranscriptFilename();
658
+ transcriptOutputPath = getOutputPath(outputDirectory, transcriptOutputFilename);
659
+ const transcriptContent = `# Audio Transcript\n\n**Recorded:** ${new Date().toISOString()}\n\n**Transcript:**\n\n${audioContext}`;
660
+ await storage.writeFile(transcriptOutputPath, transcriptContent, 'utf-8');
661
+ this.logger.debug('Saved transcript: %s', transcriptOutputPath);
662
+ }
663
+ return {
664
+ audioOutputPath,
665
+ transcriptOutputPath
666
+ };
667
+ } catch (error) {
668
+ this.logger.warn('Failed to save audio/transcript files: %s', error.message);
669
+ return {};
670
+ }
671
+ }
672
+ /**
673
+ * Clean up resources
674
+ */ async cleanup(countdownInterval, recordingProcess, tempDir, keepTemp = false) {
675
+ try {
676
+ // Clear countdown interval
677
+ if (countdownInterval) {
678
+ clearInterval(countdownInterval);
679
+ }
680
+ // Reset stdin thoroughly - this is critical for preventing hanging
681
+ try {
682
+ if (process.stdin.setRawMode) {
683
+ process.stdin.setRawMode(false);
684
+ }
685
+ process.stdin.pause();
686
+ process.stdin.removeAllListeners('data');
687
+ process.stdin.removeAllListeners('keypress');
688
+ process.stdin.removeAllListeners('readable');
689
+ process.stdin.removeAllListeners('end');
690
+ process.stdin.removeAllListeners('close');
691
+ // Force stdin to unpipe if it was piped
692
+ if (process.stdin.unpipe) {
693
+ process.stdin.unpipe();
694
+ }
695
+ // Completely detach stdin from the event-loop
696
+ if (typeof process.stdin.unref === 'function') {
697
+ process.stdin.unref();
698
+ }
699
+ } catch (stdinError) {
700
+ // Ignore stdin cleanup errors
701
+ }
702
+ // Remove ALL process event listeners to prevent hanging
703
+ process.removeAllListeners('SIGINT');
704
+ process.removeAllListeners('SIGTERM');
705
+ process.removeAllListeners('SIGQUIT');
706
+ process.removeAllListeners('SIGHUP');
707
+ process.removeAllListeners('exit');
708
+ process.removeAllListeners('beforeExit');
709
+ // Kill recording process aggressively
710
+ if (recordingProcess && !recordingProcess.killed) {
711
+ try {
712
+ recordingProcess.kill('SIGTERM');
713
+ // Give it a very short time to terminate gracefully
714
+ await new Promise((resolve)=>setTimeout(resolve, 50));
715
+ if (!recordingProcess.killed) {
716
+ recordingProcess.kill('SIGKILL');
717
+ }
718
+ // Destroy and detach any stdio streams
719
+ if (recordingProcess.stderr) {
720
+ try {
721
+ recordingProcess.stderr.removeAllListeners();
722
+ recordingProcess.stderr.destroy();
723
+ if (typeof recordingProcess.stderr.unref === 'function') {
724
+ recordingProcess.stderr.unref();
725
+ }
726
+ } catch {
727
+ /* ignore */ }
728
+ }
729
+ // Remove all listeners from the recording process itself and detach from event-loop
730
+ recordingProcess.removeAllListeners();
731
+ if (typeof recordingProcess.unref === 'function') {
732
+ recordingProcess.unref();
733
+ }
734
+ } catch (killError) {
735
+ // Ignore kill errors
736
+ }
737
+ }
738
+ // Clean up temporary directory unless instructed to keep it (e.g., on failure)
739
+ if (!keepTemp) {
740
+ try {
741
+ await fs.rm(tempDir, {
742
+ recursive: true,
743
+ force: true
744
+ });
745
+ } catch (fsError) {
746
+ // Ignore filesystem cleanup errors
747
+ }
748
+ } else {
749
+ this.logger.debug('Keeping temporary directory for inspection: %s', tempDir);
750
+ }
751
+ } catch (cleanupError) {
752
+ this.logger.debug('Cleanup warning: %s', cleanupError.message);
753
+ }
754
+ }
755
+ constructor(){
756
+ _define_property(this, "logger", getLogger());
757
+ }
758
+ }
759
+ /**
760
+ * Create a new audio processor instance
761
+ */ const createAudioProcessor = ()=>{
762
+ return new AudioProcessor();
763
+ };
764
+
765
+ export { AudioProcessor, createAudioProcessor };
766
+ //# sourceMappingURL=processor.js.map