agentvibes 5.7.6 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.agentvibes/config.json +12 -5
  2. package/.agentvibes/install-manifest.json +188 -300
  3. package/.claude/audio/tracks/celestial_velvet.mp3 +0 -0
  4. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  5. package/.claude/commands/agent-vibes-rdp.md +24 -24
  6. package/.claude/config/audio-effects.cfg +3 -2
  7. package/.claude/config/audio-effects.cfg.sample +52 -52
  8. package/.claude/config/background-music-enabled.txt +1 -0
  9. package/.claude/config/background-music-position.txt +1 -1
  10. package/.claude/config/language.txt +1 -0
  11. package/.claude/docs/TERMUX_SETUP.md +408 -408
  12. package/.claude/hooks/audio-cache-utils.sh +0 -0
  13. package/.claude/hooks/audio-processor.sh +0 -0
  14. package/.claude/hooks/background-music-manager.sh +0 -0
  15. package/.claude/hooks/bmad-party-speak.sh +27 -6
  16. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  17. package/.claude/hooks/bmad-speak.sh +0 -0
  18. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  19. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  20. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  21. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  22. package/.claude/hooks/clean-audio-cache.sh +0 -0
  23. package/.claude/hooks/cleanup-cache.sh +0 -0
  24. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  25. package/.claude/hooks/download-extra-voices.sh +0 -0
  26. package/.claude/hooks/effects-manager.sh +0 -0
  27. package/.claude/hooks/github-star-reminder.sh +0 -0
  28. package/.claude/hooks/language-manager.sh +0 -0
  29. package/.claude/hooks/learn-manager.sh +0 -0
  30. package/.claude/hooks/macos-voice-manager.sh +0 -0
  31. package/.claude/hooks/migrate-background-music.sh +0 -0
  32. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  33. package/.claude/hooks/optimize-background-music.sh +0 -0
  34. package/.claude/hooks/path-resolver.sh +0 -0
  35. package/.claude/hooks/personality-manager.sh +0 -0
  36. package/.claude/hooks/piper-download-voices.sh +0 -0
  37. package/.claude/hooks/piper-installer.sh +0 -0
  38. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  39. package/.claude/hooks/piper-voice-manager.sh +0 -0
  40. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  41. package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
  42. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  43. package/.claude/hooks/play-tts-macos.sh +0 -0
  44. package/.claude/hooks/play-tts-piper.sh +0 -0
  45. package/.claude/hooks/play-tts-soprano.sh +0 -0
  46. package/.claude/hooks/play-tts-ssh-remote.sh +11 -8
  47. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  48. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  49. package/.claude/hooks/play-tts.sh +0 -0
  50. package/.claude/hooks/prepare-release.sh +0 -0
  51. package/.claude/hooks/provider-commands.sh +0 -0
  52. package/.claude/hooks/provider-manager.sh +0 -0
  53. package/.claude/hooks/replay-target-audio.sh +0 -0
  54. package/.claude/hooks/requirements.txt +6 -6
  55. package/.claude/hooks/sentiment-manager.sh +0 -0
  56. package/.claude/hooks/session-start-tts.sh +0 -0
  57. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  58. package/.claude/hooks/speed-manager.sh +0 -0
  59. package/.claude/hooks/stop-tts.sh +0 -0
  60. package/.claude/hooks/termux-installer.sh +0 -0
  61. package/.claude/hooks/translate-manager.sh +0 -0
  62. package/.claude/hooks/translator.py +237 -237
  63. package/.claude/hooks/tts-queue-worker.sh +0 -0
  64. package/.claude/hooks/tts-queue.sh +0 -0
  65. package/.claude/hooks/verbosity-manager.sh +0 -0
  66. package/.claude/hooks/voice-manager.sh +0 -0
  67. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  68. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  69. package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
  70. package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
  71. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  72. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  73. package/.claude/piper-voices-dir.txt +1 -1
  74. package/.claude/verbosity.txt +1 -1
  75. package/.clawdbot/README.md +105 -105
  76. package/.mcp.json +5 -14
  77. package/README.md +43 -2
  78. package/RELEASE_NOTES.md +110 -0
  79. package/WINDOWS-SETUP.md +208 -208
  80. package/bin/agent-vibes +39 -39
  81. package/bin/agentvibes-voice-browser.js +0 -0
  82. package/bin/agentvibes.js +0 -0
  83. package/bin/mcp-server.js +121 -121
  84. package/bin/mcp-server.sh +0 -0
  85. package/bin/test-bmad-pr +78 -78
  86. package/mcp-server/QUICK_START.md +203 -203
  87. package/mcp-server/README.md +345 -345
  88. package/mcp-server/WINDOWS_SETUP.md +0 -0
  89. package/mcp-server/examples/claude_desktop_config.json +11 -11
  90. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  91. package/mcp-server/examples/custom_instructions.md +169 -169
  92. package/mcp-server/install-deps.js +0 -0
  93. package/mcp-server/server.py +1797 -1797
  94. package/mcp-server/test_server.py +0 -0
  95. package/package.json +1 -1
  96. package/src/cli/list-personalities.js +110 -110
  97. package/src/cli/list-voices.js +114 -114
  98. package/src/commands/bmad-voices.js +394 -394
  99. package/src/commands/install-mcp.js +476 -476
  100. package/src/console/audio-env.js +4 -1
  101. package/src/console/brand-colors.js +13 -13
  102. package/src/console/constants/personalities.js +44 -44
  103. package/src/console/tabs/agents-tab.js +85 -62
  104. package/src/console/tabs/help-tab.js +314 -314
  105. package/src/console/tabs/music-tab.js +3 -0
  106. package/src/console/tabs/readme-tab.js +272 -272
  107. package/src/console/tabs/setup-tab.js +285 -41
  108. package/src/console/tabs/voices-tab.js +14 -2
  109. package/src/console/widgets/destroy-list.js +25 -25
  110. package/src/console/widgets/notice.js +55 -55
  111. package/src/i18n/de.js +202 -202
  112. package/src/i18n/es.js +202 -202
  113. package/src/i18n/fr.js +202 -202
  114. package/src/i18n/hi.js +202 -202
  115. package/src/i18n/ja.js +202 -202
  116. package/src/i18n/ko.js +202 -202
  117. package/src/i18n/pt.js +202 -202
  118. package/src/i18n/strings.js +54 -54
  119. package/src/i18n/zh-CN.js +202 -202
  120. package/src/installer/language-screen.js +31 -31
  121. package/src/installer/music-file-input.js +304 -304
  122. package/src/installer.js +0 -0
  123. package/src/services/config-service.js +264 -264
  124. package/src/services/language-service.js +47 -47
  125. package/src/services/provider-service.js +143 -143
  126. package/src/utils/audio-duration-validator.js +298 -298
  127. package/src/utils/audio-format-validator.js +277 -277
  128. package/src/utils/dependency-checker.js +469 -469
  129. package/src/utils/file-ownership-verifier.js +358 -358
  130. package/src/utils/list-formatter.js +194 -194
  131. package/src/utils/music-file-validator.js +285 -285
  132. package/src/utils/preview-list-prompt.js +136 -136
  133. package/src/utils/secure-music-storage.js +412 -412
  134. package/templates/agentvibes-receiver.sh +231 -231
  135. package/templates/audio/welcome-music.mp3 +0 -0
  136. package/.claude/hooks/bmad-party-manager.sh +0 -225
  137. package/.claude/hooks/stop.sh +0 -38
@@ -1,277 +1,277 @@
1
- /**
2
- * Audio Format Validator - Magic Number Detection
3
- * Story 4.2: Audio Format Detection (Magic Number Validation)
4
- *
5
- * Validates audio file format by checking file headers (magic numbers).
6
- * Supported formats: MP3, WAV, OGG, M4A
7
- *
8
- * @module audio-format-validator
9
- * @requires fs
10
- */
11
-
12
- import fs from 'node:fs';
13
- import path from 'node:path';
14
-
15
- /**
16
- * Supported audio formats with their magic number signatures
17
- */
18
- const AUDIO_FORMATS = {
19
- mp3: {
20
- extension: '.mp3',
21
- magicNumbers: [
22
- // MPEG-1/2/2.5 Audio Frame Header
23
- { bytes: Buffer.from([0xFF, 0xFB]), offset: 0, name: 'MP3 MPEG-2 Layer III' },
24
- { bytes: Buffer.from([0xFF, 0xFA]), offset: 0, name: 'MP3 MPEG-1 Layer III' },
25
- // ID3 Tag (v2.x and v2.4)
26
- { bytes: Buffer.from([0x49, 0x44, 0x33]), offset: 0, name: 'ID3v2 Tag' } // "ID3"
27
- ]
28
- },
29
- wav: {
30
- extension: '.wav',
31
- magicNumbers: [
32
- { bytes: Buffer.from('RIFF'), offset: 0, name: 'RIFF Header' },
33
- { bytes: Buffer.from('WAVE'), offset: 8, name: 'WAV Format' }
34
- ]
35
- },
36
- ogg: {
37
- extension: '.ogg',
38
- magicNumbers: [
39
- { bytes: Buffer.from('OggS'), offset: 0, name: 'OggS Header' }
40
- ]
41
- },
42
- m4a: {
43
- extension: '.m4a',
44
- magicNumbers: [
45
- { bytes: Buffer.from('ftypisom'), offset: 4, name: 'M4A ISO Base Media' },
46
- { bytes: Buffer.from('ftypM4A'), offset: 4, name: 'M4A Apple iTunes' }
47
- ]
48
- }
49
- };
50
-
51
- const SUPPORTED_EXTENSIONS = Object.values(AUDIO_FORMATS).map(f => f.extension);
52
-
53
- /**
54
- * Story 4.2: Detect audio format by checking magic numbers in file header
55
- *
56
- * Reads the first 256 bytes of the file to check magic number signatures.
57
- * Does NOT require the file to have the correct extension matching format.
58
- *
59
- * @param {string} filePath - Path to audio file (must already be validated with isPathSafe)
60
- * @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null }
61
- *
62
- * Object properties:
63
- * - isValid: true if file is valid audio format
64
- * - error: null on success, error message on failure
65
- * - format: detected format ('mp3', 'wav', 'ogg', 'm4a') or null
66
- * - detectedFormat: human-readable format name (e.g., "MP3 MPEG-1 Layer III")
67
- */
68
- function detectAudioFormat(filePath) {
69
- try {
70
- if (!filePath || typeof filePath !== 'string') {
71
- return {
72
- isValid: false,
73
- error: 'File path must be a non-empty string',
74
- format: null,
75
- detectedFormat: null
76
- };
77
- }
78
-
79
- // Check file exists and is readable
80
- if (!fs.existsSync(filePath)) {
81
- return {
82
- isValid: false,
83
- error: 'File does not exist',
84
- format: null,
85
- detectedFormat: null
86
- };
87
- }
88
-
89
- const stats = fs.statSync(filePath);
90
- if (!stats.isFile()) {
91
- return {
92
- isValid: false,
93
- error: 'Path must be a regular file',
94
- format: null,
95
- detectedFormat: null
96
- };
97
- }
98
-
99
- // Check minimum file size (at least 12 bytes for WAV format check)
100
- if (stats.size < 12) {
101
- return {
102
- isValid: false,
103
- error: 'File is too small to be a valid audio file (minimum 12 bytes)',
104
- format: null,
105
- detectedFormat: null
106
- };
107
- }
108
-
109
- // Read first 256 bytes for magic number detection
110
- const buffer = Buffer.alloc(256);
111
- const fd = fs.openSync(filePath, 'r');
112
- try {
113
- fs.readSync(fd, buffer, 0, 256, 0);
114
- } finally {
115
- fs.closeSync(fd);
116
- }
117
-
118
- // Check each format's magic numbers
119
- for (const [formatKey, formatInfo] of Object.entries(AUDIO_FORMATS)) {
120
- for (const magicInfo of formatInfo.magicNumbers) {
121
- // Check if magic bytes exist at the specified offset
122
- const isMatch = buffer.subarray(magicInfo.offset, magicInfo.offset + magicInfo.bytes.length)
123
- .equals(magicInfo.bytes);
124
-
125
- if (isMatch) {
126
- return {
127
- isValid: true,
128
- error: null,
129
- format: formatKey,
130
- detectedFormat: magicInfo.name
131
- };
132
- }
133
- }
134
- }
135
-
136
- // No magic numbers matched
137
- return {
138
- isValid: false,
139
- error: `Unsupported audio format. Supported formats: ${SUPPORTED_EXTENSIONS.join(', ')}`,
140
- format: null,
141
- detectedFormat: null
142
- };
143
-
144
- } catch (err) {
145
- return {
146
- isValid: false,
147
- error: `Error detecting audio format: ${err.message}`,
148
- format: null,
149
- detectedFormat: null
150
- };
151
- }
152
- }
153
-
154
- /**
155
- * Story 4.2: Validate that file extension matches detected audio format
156
- *
157
- * Ensures the file extension is correct for the detected format.
158
- * This prevents accidental misnamed files (e.g., .txt file with MP3 content).
159
- *
160
- * @param {string} filePath - Path to audio file
161
- * @param {string} detectedFormat - Format from detectAudioFormat() result
162
- * @returns {Object} { isValid: boolean, error: string|null }
163
- */
164
- function validateFileExtension(filePath, detectedFormat) {
165
- try {
166
- if (!filePath || !detectedFormat) {
167
- return {
168
- isValid: false,
169
- error: 'File path and detected format are required'
170
- };
171
- }
172
-
173
- const ext = path.extname(filePath).toLowerCase();
174
- const expectedExt = AUDIO_FORMATS[detectedFormat]?.extension;
175
-
176
- if (!expectedExt) {
177
- return {
178
- isValid: false,
179
- error: `Unknown detected format: ${detectedFormat}`
180
- };
181
- }
182
-
183
- if (ext === expectedExt) {
184
- return {
185
- isValid: true,
186
- error: null
187
- };
188
- }
189
-
190
- // Extension doesn't match - return specific error
191
- return {
192
- isValid: false,
193
- error: `File extension (${ext || 'none'}) doesn't match actual format (.${detectedFormat})`
194
- };
195
-
196
- } catch (err) {
197
- return {
198
- isValid: false,
199
- error: `Error validating file extension: ${err.message}`
200
- };
201
- }
202
- }
203
-
204
- /**
205
- * Story 4.2: Comprehensive audio file validation
206
- *
207
- * Combines format detection and extension validation in one call.
208
- * Returns false only if BOTH format is unsupported AND extension is wrong.
209
- * Returns true if format is valid (even if extension differs).
210
- *
211
- * @param {string} filePath - Path to audio file
212
- * @param {Object} options - Validation options
213
- * @param {boolean} options.strictExtension - If true, reject files with wrong extension (default: false)
214
- * @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null, extensionMatch: boolean }
215
- */
216
- function validateAudioFile(filePath, options = {}) {
217
- const { strictExtension = false } = options;
218
-
219
- try {
220
- // Detect format first
221
- const formatResult = detectAudioFormat(filePath);
222
-
223
- if (!formatResult.isValid) {
224
- return {
225
- isValid: false,
226
- error: formatResult.error,
227
- format: null,
228
- detectedFormat: null,
229
- extensionMatch: false
230
- };
231
- }
232
-
233
- // Check extension if strict mode enabled
234
- if (strictExtension) {
235
- const extResult = validateFileExtension(filePath, formatResult.format);
236
- if (!extResult.isValid) {
237
- return {
238
- isValid: false,
239
- error: extResult.error,
240
- format: formatResult.format,
241
- detectedFormat: formatResult.detectedFormat,
242
- extensionMatch: false
243
- };
244
- }
245
- }
246
-
247
- // Format is valid - check if extension matches (informational)
248
- const ext = path.extname(filePath).toLowerCase();
249
- const expectedExt = AUDIO_FORMATS[formatResult.format]?.extension;
250
- const extensionMatch = ext === expectedExt;
251
-
252
- return {
253
- isValid: true,
254
- error: null,
255
- format: formatResult.format,
256
- detectedFormat: formatResult.detectedFormat,
257
- extensionMatch
258
- };
259
-
260
- } catch (err) {
261
- return {
262
- isValid: false,
263
- error: `Unexpected validation error: ${err.message}`,
264
- format: null,
265
- detectedFormat: null,
266
- extensionMatch: false
267
- };
268
- }
269
- }
270
-
271
- export {
272
- detectAudioFormat,
273
- validateFileExtension,
274
- validateAudioFile,
275
- AUDIO_FORMATS,
276
- SUPPORTED_EXTENSIONS
277
- };
1
+ /**
2
+ * Audio Format Validator - Magic Number Detection
3
+ * Story 4.2: Audio Format Detection (Magic Number Validation)
4
+ *
5
+ * Validates audio file format by checking file headers (magic numbers).
6
+ * Supported formats: MP3, WAV, OGG, M4A
7
+ *
8
+ * @module audio-format-validator
9
+ * @requires fs
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ /**
16
+ * Supported audio formats with their magic number signatures
17
+ */
18
+ const AUDIO_FORMATS = {
19
+ mp3: {
20
+ extension: '.mp3',
21
+ magicNumbers: [
22
+ // MPEG-1/2/2.5 Audio Frame Header
23
+ { bytes: Buffer.from([0xFF, 0xFB]), offset: 0, name: 'MP3 MPEG-2 Layer III' },
24
+ { bytes: Buffer.from([0xFF, 0xFA]), offset: 0, name: 'MP3 MPEG-1 Layer III' },
25
+ // ID3 Tag (v2.x and v2.4)
26
+ { bytes: Buffer.from([0x49, 0x44, 0x33]), offset: 0, name: 'ID3v2 Tag' } // "ID3"
27
+ ]
28
+ },
29
+ wav: {
30
+ extension: '.wav',
31
+ magicNumbers: [
32
+ { bytes: Buffer.from('RIFF'), offset: 0, name: 'RIFF Header' },
33
+ { bytes: Buffer.from('WAVE'), offset: 8, name: 'WAV Format' }
34
+ ]
35
+ },
36
+ ogg: {
37
+ extension: '.ogg',
38
+ magicNumbers: [
39
+ { bytes: Buffer.from('OggS'), offset: 0, name: 'OggS Header' }
40
+ ]
41
+ },
42
+ m4a: {
43
+ extension: '.m4a',
44
+ magicNumbers: [
45
+ { bytes: Buffer.from('ftypisom'), offset: 4, name: 'M4A ISO Base Media' },
46
+ { bytes: Buffer.from('ftypM4A'), offset: 4, name: 'M4A Apple iTunes' }
47
+ ]
48
+ }
49
+ };
50
+
51
+ const SUPPORTED_EXTENSIONS = Object.values(AUDIO_FORMATS).map(f => f.extension);
52
+
53
+ /**
54
+ * Story 4.2: Detect audio format by checking magic numbers in file header
55
+ *
56
+ * Reads the first 256 bytes of the file to check magic number signatures.
57
+ * Does NOT require the file to have the correct extension matching format.
58
+ *
59
+ * @param {string} filePath - Path to audio file (must already be validated with isPathSafe)
60
+ * @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null }
61
+ *
62
+ * Object properties:
63
+ * - isValid: true if file is valid audio format
64
+ * - error: null on success, error message on failure
65
+ * - format: detected format ('mp3', 'wav', 'ogg', 'm4a') or null
66
+ * - detectedFormat: human-readable format name (e.g., "MP3 MPEG-1 Layer III")
67
+ */
68
+ function detectAudioFormat(filePath) {
69
+ try {
70
+ if (!filePath || typeof filePath !== 'string') {
71
+ return {
72
+ isValid: false,
73
+ error: 'File path must be a non-empty string',
74
+ format: null,
75
+ detectedFormat: null
76
+ };
77
+ }
78
+
79
+ // Check file exists and is readable
80
+ if (!fs.existsSync(filePath)) {
81
+ return {
82
+ isValid: false,
83
+ error: 'File does not exist',
84
+ format: null,
85
+ detectedFormat: null
86
+ };
87
+ }
88
+
89
+ const stats = fs.statSync(filePath);
90
+ if (!stats.isFile()) {
91
+ return {
92
+ isValid: false,
93
+ error: 'Path must be a regular file',
94
+ format: null,
95
+ detectedFormat: null
96
+ };
97
+ }
98
+
99
+ // Check minimum file size (at least 12 bytes for WAV format check)
100
+ if (stats.size < 12) {
101
+ return {
102
+ isValid: false,
103
+ error: 'File is too small to be a valid audio file (minimum 12 bytes)',
104
+ format: null,
105
+ detectedFormat: null
106
+ };
107
+ }
108
+
109
+ // Read first 256 bytes for magic number detection
110
+ const buffer = Buffer.alloc(256);
111
+ const fd = fs.openSync(filePath, 'r');
112
+ try {
113
+ fs.readSync(fd, buffer, 0, 256, 0);
114
+ } finally {
115
+ fs.closeSync(fd);
116
+ }
117
+
118
+ // Check each format's magic numbers
119
+ for (const [formatKey, formatInfo] of Object.entries(AUDIO_FORMATS)) {
120
+ for (const magicInfo of formatInfo.magicNumbers) {
121
+ // Check if magic bytes exist at the specified offset
122
+ const isMatch = buffer.subarray(magicInfo.offset, magicInfo.offset + magicInfo.bytes.length)
123
+ .equals(magicInfo.bytes);
124
+
125
+ if (isMatch) {
126
+ return {
127
+ isValid: true,
128
+ error: null,
129
+ format: formatKey,
130
+ detectedFormat: magicInfo.name
131
+ };
132
+ }
133
+ }
134
+ }
135
+
136
+ // No magic numbers matched
137
+ return {
138
+ isValid: false,
139
+ error: `Unsupported audio format. Supported formats: ${SUPPORTED_EXTENSIONS.join(', ')}`,
140
+ format: null,
141
+ detectedFormat: null
142
+ };
143
+
144
+ } catch (err) {
145
+ return {
146
+ isValid: false,
147
+ error: `Error detecting audio format: ${err.message}`,
148
+ format: null,
149
+ detectedFormat: null
150
+ };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Story 4.2: Validate that file extension matches detected audio format
156
+ *
157
+ * Ensures the file extension is correct for the detected format.
158
+ * This prevents accidental misnamed files (e.g., .txt file with MP3 content).
159
+ *
160
+ * @param {string} filePath - Path to audio file
161
+ * @param {string} detectedFormat - Format from detectAudioFormat() result
162
+ * @returns {Object} { isValid: boolean, error: string|null }
163
+ */
164
+ function validateFileExtension(filePath, detectedFormat) {
165
+ try {
166
+ if (!filePath || !detectedFormat) {
167
+ return {
168
+ isValid: false,
169
+ error: 'File path and detected format are required'
170
+ };
171
+ }
172
+
173
+ const ext = path.extname(filePath).toLowerCase();
174
+ const expectedExt = AUDIO_FORMATS[detectedFormat]?.extension;
175
+
176
+ if (!expectedExt) {
177
+ return {
178
+ isValid: false,
179
+ error: `Unknown detected format: ${detectedFormat}`
180
+ };
181
+ }
182
+
183
+ if (ext === expectedExt) {
184
+ return {
185
+ isValid: true,
186
+ error: null
187
+ };
188
+ }
189
+
190
+ // Extension doesn't match - return specific error
191
+ return {
192
+ isValid: false,
193
+ error: `File extension (${ext || 'none'}) doesn't match actual format (.${detectedFormat})`
194
+ };
195
+
196
+ } catch (err) {
197
+ return {
198
+ isValid: false,
199
+ error: `Error validating file extension: ${err.message}`
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Story 4.2: Comprehensive audio file validation
206
+ *
207
+ * Combines format detection and extension validation in one call.
208
+ * Returns false only if BOTH format is unsupported AND extension is wrong.
209
+ * Returns true if format is valid (even if extension differs).
210
+ *
211
+ * @param {string} filePath - Path to audio file
212
+ * @param {Object} options - Validation options
213
+ * @param {boolean} options.strictExtension - If true, reject files with wrong extension (default: false)
214
+ * @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null, extensionMatch: boolean }
215
+ */
216
+ function validateAudioFile(filePath, options = {}) {
217
+ const { strictExtension = false } = options;
218
+
219
+ try {
220
+ // Detect format first
221
+ const formatResult = detectAudioFormat(filePath);
222
+
223
+ if (!formatResult.isValid) {
224
+ return {
225
+ isValid: false,
226
+ error: formatResult.error,
227
+ format: null,
228
+ detectedFormat: null,
229
+ extensionMatch: false
230
+ };
231
+ }
232
+
233
+ // Check extension if strict mode enabled
234
+ if (strictExtension) {
235
+ const extResult = validateFileExtension(filePath, formatResult.format);
236
+ if (!extResult.isValid) {
237
+ return {
238
+ isValid: false,
239
+ error: extResult.error,
240
+ format: formatResult.format,
241
+ detectedFormat: formatResult.detectedFormat,
242
+ extensionMatch: false
243
+ };
244
+ }
245
+ }
246
+
247
+ // Format is valid - check if extension matches (informational)
248
+ const ext = path.extname(filePath).toLowerCase();
249
+ const expectedExt = AUDIO_FORMATS[formatResult.format]?.extension;
250
+ const extensionMatch = ext === expectedExt;
251
+
252
+ return {
253
+ isValid: true,
254
+ error: null,
255
+ format: formatResult.format,
256
+ detectedFormat: formatResult.detectedFormat,
257
+ extensionMatch
258
+ };
259
+
260
+ } catch (err) {
261
+ return {
262
+ isValid: false,
263
+ error: `Unexpected validation error: ${err.message}`,
264
+ format: null,
265
+ detectedFormat: null,
266
+ extensionMatch: false
267
+ };
268
+ }
269
+ }
270
+
271
+ export {
272
+ detectAudioFormat,
273
+ validateFileExtension,
274
+ validateAudioFile,
275
+ AUDIO_FORMATS,
276
+ SUPPORTED_EXTENSIONS
277
+ };