agentvibes 5.6.0 → 5.6.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.
Files changed (101) hide show
  1. package/.agentvibes/config.json +3 -38
  2. package/.claude/config/audio-effects.cfg +1 -1
  3. package/.claude/config/background-music-enabled.txt +1 -1
  4. package/.claude/config/background-music-position.txt +6 -6
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/play-tts-ssh-remote.sh +119 -42
  7. package/.claude/hooks/play-tts-windows-receiver.sh +31 -0
  8. package/.claude/hooks/stop.sh +2 -27
  9. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  10. package/.claude/hooks-windows/play-tts.ps1 +58 -8
  11. package/.claude/piper-voices-dir.txt +1 -1
  12. package/.clawdbot/skill/README.md +326 -0
  13. package/.mcp.json +17 -27
  14. package/README.md +15 -2
  15. package/RELEASE_NOTES.md +64 -0
  16. package/bin/agent-vibes +39 -39
  17. package/package.json +1 -1
  18. package/src/bmad-detector.js +71 -71
  19. package/src/cli/list-personalities.js +110 -110
  20. package/src/cli/list-voices.js +114 -114
  21. package/src/commands/bmad-voices.js +394 -394
  22. package/src/commands/install-mcp.js +476 -476
  23. package/src/console/brand-colors.js +13 -13
  24. package/src/console/constants/personalities.js +44 -44
  25. package/src/console/modals/modal-overlay.js +247 -247
  26. package/src/console/navigation.js +5 -1
  27. package/src/console/tabs/agents-tab.js +5 -5
  28. package/src/console/tabs/help-tab.js +314 -314
  29. package/src/console/tabs/readme-tab.js +272 -272
  30. package/src/console/tabs/setup-tab.js +32 -17
  31. package/src/console/tabs/voices-tab.js +2 -2
  32. package/src/console/widgets/destroy-list.js +25 -25
  33. package/src/console/widgets/notice.js +55 -55
  34. package/src/console/widgets/personality-picker.js +213 -213
  35. package/src/console/widgets/reverb-picker.js +97 -97
  36. package/src/console/widgets/track-picker.js +1 -1
  37. package/src/i18n/de.js +202 -202
  38. package/src/i18n/es.js +202 -202
  39. package/src/i18n/fr.js +202 -202
  40. package/src/i18n/hi.js +202 -202
  41. package/src/i18n/ja.js +202 -202
  42. package/src/i18n/ko.js +202 -202
  43. package/src/i18n/pt.js +202 -202
  44. package/src/i18n/strings.js +54 -54
  45. package/src/i18n/zh-CN.js +202 -202
  46. package/src/installer/language-screen.js +31 -31
  47. package/src/installer/music-file-input.js +304 -304
  48. package/src/services/agent-voice-store.js +420 -423
  49. package/src/services/config-service.js +264 -264
  50. package/src/services/language-service.js +47 -47
  51. package/src/services/llm-provider-service.js +11 -4
  52. package/src/services/navigation-service.js +34 -10
  53. package/src/services/provider-service.js +143 -143
  54. package/src/utils/audio-duration-validator.js +298 -298
  55. package/src/utils/audio-format-validator.js +277 -277
  56. package/src/utils/dependency-checker.js +469 -469
  57. package/src/utils/file-ownership-verifier.js +358 -358
  58. package/src/utils/list-formatter.js +194 -194
  59. package/src/utils/music-file-validator.js +285 -285
  60. package/src/utils/preview-list-prompt.js +136 -136
  61. package/src/utils/secure-music-storage.js +412 -412
  62. package/.agentvibes/LITE-MODE.md +0 -236
  63. package/.agentvibes/README.md +0 -136
  64. package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +0 -141
  65. package/.agentvibes/backups/agents/analyst_20260204_144958.md +0 -78
  66. package/.agentvibes/backups/agents/architect_20260204_144958.md +0 -72
  67. package/.agentvibes/backups/agents/dev_20260204_144958.md +0 -74
  68. package/.agentvibes/backups/agents/pm_20260204_144958.md +0 -72
  69. package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +0 -64
  70. package/.agentvibes/backups/agents/sm_20260204_144958.md +0 -87
  71. package/.agentvibes/backups/agents/tea_20260204_144958.md +0 -79
  72. package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +0 -82
  73. package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +0 -80
  74. package/.agentvibes/config/README-personality-defaults.md +0 -162
  75. package/.agentvibes/config/agentvibes.json +0 -1
  76. package/.agentvibes/config/mode.txt +0 -1
  77. package/.agentvibes/config/personality-voice-defaults.default.json +0 -21
  78. package/.agentvibes/config/save-audio.txt +0 -1
  79. package/.agentvibes/config/voice-metadata.json +0 -160
  80. package/.agentvibes/hooks/help.sh +0 -191
  81. package/.agentvibes/hooks/post-tool-use-lite.sh +0 -111
  82. package/.agentvibes/hooks/save-audio-manager.sh +0 -162
  83. package/.agentvibes/hooks/session-start-full-optimized.sh +0 -102
  84. package/.agentvibes/hooks/session-start-full.sh +0 -142
  85. package/.agentvibes/hooks/session-start-lite-v2.sh +0 -34
  86. package/.agentvibes/hooks/session-start-lite.sh +0 -29
  87. package/.agentvibes/hooks/stop-lite.sh +0 -115
  88. package/.agentvibes/hooks/switch-mode.sh +0 -215
  89. package/.agentvibes/output-styles/audio-summary.md +0 -30
  90. package/.claude/audio/voice-samples/piper/alan.wav +0 -0
  91. package/.claude/audio/voice-samples/piper/amy.wav +0 -0
  92. package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
  93. package/.claude/audio/voice-samples/piper/joe.wav +0 -0
  94. package/.claude/audio/voice-samples/piper/john.wav +0 -0
  95. package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
  96. package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
  97. package/.claude/audio/voice-samples/piper/linda.wav +0 -0
  98. package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
  99. package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
  100. package/.claude/hooks/post-response.sh +0 -41
  101. package/bin/ensure-soprano-running.sh +0 -43
@@ -1,412 +1,412 @@
1
- /**
2
- * Secure Music Storage - File Copy and Secure Storage
3
- * Story 4.4: File Copy and Secure Storage
4
- *
5
- * Copies validated music files to secure storage with:
6
- * - Secure directory and file permissions (700/600)
7
- * - Metadata storage (original filename, copy date, size, checksum)
8
- * - Checksum verification (source vs. copy)
9
- * - Backup of previous music before overwriting
10
- * - Atomic operations (complete or fail, no partial state)
11
- *
12
- * @module secure-music-storage
13
- * @requires fs
14
- * @requires path
15
- * @requires crypto
16
- */
17
-
18
- import fs from 'node:fs';
19
- import path from 'node:path';
20
- import crypto from 'node:crypto';
21
-
22
- /**
23
- * Default storage location relative to .claude directory
24
- */
25
- const MUSIC_STORAGE_DIR = 'audio/custom-music';
26
- const MUSIC_STORAGE_SUBDIR = 'tracks';
27
- const METADATA_FILE = 'music-metadata.json';
28
- const BACKUP_SUBDIR = 'backups';
29
-
30
- /**
31
- * Story 4.4: Copy validated audio file to secure storage
32
- *
33
- * Process:
34
- * 1. Create secure storage directory if not exists (700 permissions)
35
- * 2. Calculate checksum of source file
36
- * 3. Backup previous music if exists
37
- * 4. Copy file to storage with secure permissions (600)
38
- * 5. Verify checksums match (source vs. copy)
39
- * 6. Store metadata (original filename, date, size, checksum)
40
- * 7. Return success with file info
41
- *
42
- * @param {string} sourceFilePath - Path to validated source audio file
43
- * @param {string} claudeDir - Path to .claude directory
44
- * @param {Object} options - Storage options
45
- * @param {string} options.filename - Custom filename (default: source basename)
46
- * @param {Function} options.logger - Optional logger function
47
- * @returns {Object} {
48
- * success: boolean,
49
- * error: string|null,
50
- * storagePath: string|null,
51
- * metadataPath: string|null,
52
- * backupPath: string|null,
53
- * metadata: Object|null
54
- * }
55
- */
56
- export async function copyToSecureStorage(sourceFilePath, claudeDir, options = {}) {
57
- const { filename = null, logger = null } = options;
58
-
59
- try {
60
- logger?.(`storage: starting copy - source=${path.basename(sourceFilePath)}`);
61
-
62
- // Parameter validation
63
- if (!sourceFilePath || typeof sourceFilePath !== 'string') {
64
- throw new Error('Source file path must be a non-empty string');
65
- }
66
-
67
- if (!claudeDir || typeof claudeDir !== 'string') {
68
- throw new Error('Claude directory path must be a non-empty string');
69
- }
70
-
71
- // Verify source file exists and is readable
72
- if (!fs.existsSync(sourceFilePath)) {
73
- throw new Error('Source file does not exist');
74
- }
75
-
76
- const sourceStats = fs.statSync(sourceFilePath);
77
- if (!sourceStats.isFile()) {
78
- throw new Error('Source path must be a regular file');
79
- }
80
-
81
- // Get file extension
82
- const fileExt = path.extname(sourceFilePath).toLowerCase();
83
- const targetFilename = filename || path.basename(sourceFilePath);
84
-
85
- // Create storage directory structure
86
- const musicDir = path.join(claudeDir, MUSIC_STORAGE_DIR);
87
- const tracksDir = path.join(musicDir, MUSIC_STORAGE_SUBDIR);
88
- const backupDir = path.join(musicDir, BACKUP_SUBDIR);
89
-
90
- logger?.(`storage: creating directories - ${musicDir}`);
91
-
92
- // Create directories with secure permissions
93
- const dirCreated = ensureSecureDirectories(musicDir, tracksDir, backupDir, logger);
94
- if (!dirCreated.success) {
95
- throw new Error(dirCreated.error);
96
- }
97
-
98
- // Calculate source checksum
99
- logger?.(`storage: calculating source checksum`);
100
- const sourceChecksum = calculateFileChecksum(sourceFilePath);
101
-
102
- // Path where file will be stored
103
- const targetStoragePath = path.join(tracksDir, targetFilename);
104
-
105
- // Backup previous music if exists
106
- let backupPath = null;
107
- if (fs.existsSync(targetStoragePath)) {
108
- logger?.(`storage: backing up previous music`);
109
- const backupResult = backupPreviousFile(targetStoragePath, backupDir, logger);
110
- if (!backupResult.success) {
111
- throw new Error(`Failed to backup previous music: ${backupResult.error}`);
112
- }
113
- backupPath = backupResult.backupPath;
114
- }
115
-
116
- // Copy file to storage
117
- logger?.(`storage: copying file to storage`);
118
- try {
119
- fs.copyFileSync(sourceFilePath, targetStoragePath);
120
- } catch (err) {
121
- throw new Error(`Failed to copy file: ${err.message}`);
122
- }
123
-
124
- // Set file permissions (600 = user read/write only)
125
- try {
126
- fs.chmodSync(targetStoragePath, 0o600);
127
- } catch (err) {
128
- // Log warning but continue - copy is done
129
- logger?.(`storage: warning - could not set file permissions: ${err.message}`);
130
- }
131
-
132
- // Verify checksums match
133
- logger?.(`storage: verifying checksums`);
134
- const targetChecksum = calculateFileChecksum(targetStoragePath);
135
-
136
- if (sourceChecksum !== targetChecksum) {
137
- // Checksums don't match - file corruption
138
- try {
139
- fs.unlinkSync(targetStoragePath);
140
- } catch (err) {
141
- logger?.(`storage: warning - could not delete corrupted file: ${err.message}`);
142
- }
143
- throw new Error('File corruption detected: checksums do not match');
144
- }
145
-
146
- // Store metadata
147
- logger?.(`storage: storing metadata`);
148
- const metadata = {
149
- originalFilename: path.basename(sourceFilePath),
150
- storagePath: targetStoragePath,
151
- copiedDate: new Date().toISOString(),
152
- fileSize: sourceStats.size,
153
- checksum: sourceChecksum,
154
- extension: fileExt
155
- };
156
-
157
- const metadataPath = path.join(musicDir, METADATA_FILE);
158
- try {
159
- fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), { mode: 0o600 });
160
- } catch (err) {
161
- logger?.(`storage: warning - could not store metadata: ${err.message}`);
162
- }
163
-
164
- logger?.(`storage: success - file stored at ${path.relative(claudeDir, targetStoragePath)}`);
165
-
166
- return {
167
- success: true,
168
- error: null,
169
- storagePath: targetStoragePath,
170
- metadataPath,
171
- backupPath,
172
- metadata
173
- };
174
-
175
- } catch (err) {
176
- const error = err.message || String(err);
177
- logger?.(`storage: failed - ${error}`);
178
- return {
179
- success: false,
180
- error,
181
- storagePath: null,
182
- metadataPath: null,
183
- backupPath: null,
184
- metadata: null
185
- };
186
- }
187
- }
188
-
189
- /**
190
- * Ensure secure storage directories exist with proper permissions
191
- *
192
- * @private
193
- */
194
- function ensureSecureDirectories(musicDir, tracksDir, backupDir, logger) {
195
- try {
196
- // Create main music directory
197
- if (!fs.existsSync(musicDir)) {
198
- fs.mkdirSync(musicDir, { recursive: true, mode: 0o700 });
199
- logger?.(`storage: created music directory with 700 permissions`);
200
- } else {
201
- // Verify permissions on existing directory
202
- const stats = fs.statSync(musicDir);
203
- if ((stats.mode & 0o077) !== 0) {
204
- // Has group/other permissions - try to fix
205
- try {
206
- fs.chmodSync(musicDir, 0o700);
207
- logger?.(`storage: fixed insecure permissions on music directory`);
208
- } catch (err) {
209
- logger?.(`storage: warning - could not fix permissions: ${err.message}`);
210
- }
211
- }
212
- }
213
-
214
- // Create tracks subdirectory
215
- if (!fs.existsSync(tracksDir)) {
216
- fs.mkdirSync(tracksDir, { recursive: true, mode: 0o700 });
217
- logger?.(`storage: created tracks directory with 700 permissions`);
218
- }
219
-
220
- // Create backup subdirectory
221
- if (!fs.existsSync(backupDir)) {
222
- fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
223
- logger?.(`storage: created backup directory with 700 permissions`);
224
- }
225
-
226
- return {
227
- success: true,
228
- error: null
229
- };
230
- } catch (err) {
231
- return {
232
- success: false,
233
- error: `Failed to create directories: ${err.message}`
234
- };
235
- }
236
- }
237
-
238
- /**
239
- * Calculate SHA256 checksum of file
240
- *
241
- * @private
242
- */
243
- function calculateFileChecksum(filePath) {
244
- const hash = crypto.createHash('sha256');
245
- const fileContent = fs.readFileSync(filePath);
246
- hash.update(fileContent);
247
- return hash.digest('hex');
248
- }
249
-
250
- /**
251
- * Backup previous music file to backup directory
252
- *
253
- * @private
254
- */
255
- function backupPreviousFile(previousPath, backupDir, logger) {
256
- try {
257
- const filename = path.basename(previousPath);
258
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
259
- const backupFilename = `${timestamp}-${filename}`;
260
- const backupPath = path.join(backupDir, backupFilename);
261
-
262
- // Copy to backup
263
- fs.copyFileSync(previousPath, backupPath);
264
- fs.chmodSync(backupPath, 0o600);
265
-
266
- logger?.(`storage: backed up previous file to ${path.basename(backupPath)}`);
267
-
268
- return {
269
- success: true,
270
- error: null,
271
- backupPath
272
- };
273
- } catch (err) {
274
- return {
275
- success: false,
276
- error: `Failed to backup previous file: ${err.message}`,
277
- backupPath: null
278
- };
279
- }
280
- }
281
-
282
- /**
283
- * Story 4.4: Read stored music metadata
284
- *
285
- * @param {string} claudeDir - Path to .claude directory
286
- * @returns {Object} {
287
- * success: boolean,
288
- * error: string|null,
289
- * metadata: Object|null
290
- * }
291
- */
292
- export function readMusicMetadata(claudeDir) {
293
- try {
294
- const metadataPath = path.join(claudeDir, MUSIC_STORAGE_DIR, METADATA_FILE);
295
-
296
- if (!fs.existsSync(metadataPath)) {
297
- return {
298
- success: true,
299
- error: null,
300
- metadata: null
301
- };
302
- }
303
-
304
- const content = fs.readFileSync(metadataPath, 'utf-8');
305
- const metadata = JSON.parse(content);
306
-
307
- return {
308
- success: true,
309
- error: null,
310
- metadata
311
- };
312
- } catch (err) {
313
- return {
314
- success: false,
315
- error: `Failed to read metadata: ${err.message}`,
316
- metadata: null
317
- };
318
- }
319
- }
320
-
321
- /**
322
- * Story 4.4: Get path to stored custom music file
323
- *
324
- * @param {string} claudeDir - Path to .claude directory
325
- * @returns {Object} {
326
- * exists: boolean,
327
- * path: string|null,
328
- * filename: string|null
329
- * }
330
- */
331
- export function getStoredMusicPath(claudeDir) {
332
- try {
333
- const metadataPath = path.join(claudeDir, MUSIC_STORAGE_DIR, METADATA_FILE);
334
-
335
- if (!fs.existsSync(metadataPath)) {
336
- return {
337
- exists: false,
338
- path: null,
339
- filename: null
340
- };
341
- }
342
-
343
- const content = fs.readFileSync(metadataPath, 'utf-8');
344
- const metadata = JSON.parse(content);
345
-
346
- // Verify the stored file actually exists
347
- if (!fs.existsSync(metadata.storagePath)) {
348
- return {
349
- exists: false,
350
- path: null,
351
- filename: null
352
- };
353
- }
354
-
355
- return {
356
- exists: true,
357
- path: metadata.storagePath,
358
- filename: metadata.originalFilename
359
- };
360
- } catch (err) {
361
- return {
362
- exists: false,
363
- path: null,
364
- filename: null
365
- };
366
- }
367
- }
368
-
369
- /**
370
- * Story 4.4: Delete stored custom music
371
- *
372
- * @param {string} claudeDir - Path to .claude directory
373
- * @returns {Object} { success: boolean, error: string|null }
374
- */
375
- export function deleteStoredMusic(claudeDir) {
376
- try {
377
- const musicInfo = getStoredMusicPath(claudeDir);
378
-
379
- if (!musicInfo.exists) {
380
- return {
381
- success: true,
382
- error: null
383
- };
384
- }
385
-
386
- // Delete the stored file
387
- fs.unlinkSync(musicInfo.path);
388
-
389
- // Delete metadata
390
- const metadataPath = path.join(claudeDir, MUSIC_STORAGE_DIR, METADATA_FILE);
391
- if (fs.existsSync(metadataPath)) {
392
- fs.unlinkSync(metadataPath);
393
- }
394
-
395
- return {
396
- success: true,
397
- error: null
398
- };
399
- } catch (err) {
400
- return {
401
- success: false,
402
- error: `Failed to delete stored music: ${err.message}`
403
- };
404
- }
405
- }
406
-
407
- export default {
408
- copyToSecureStorage,
409
- readMusicMetadata,
410
- getStoredMusicPath,
411
- deleteStoredMusic
412
- };
1
+ /**
2
+ * Secure Music Storage - File Copy and Secure Storage
3
+ * Story 4.4: File Copy and Secure Storage
4
+ *
5
+ * Copies validated music files to secure storage with:
6
+ * - Secure directory and file permissions (700/600)
7
+ * - Metadata storage (original filename, copy date, size, checksum)
8
+ * - Checksum verification (source vs. copy)
9
+ * - Backup of previous music before overwriting
10
+ * - Atomic operations (complete or fail, no partial state)
11
+ *
12
+ * @module secure-music-storage
13
+ * @requires fs
14
+ * @requires path
15
+ * @requires crypto
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import crypto from 'node:crypto';
21
+
22
+ /**
23
+ * Default storage location relative to .claude directory
24
+ */
25
+ const MUSIC_STORAGE_DIR = 'audio/custom-music';
26
+ const MUSIC_STORAGE_SUBDIR = 'tracks';
27
+ const METADATA_FILE = 'music-metadata.json';
28
+ const BACKUP_SUBDIR = 'backups';
29
+
30
+ /**
31
+ * Story 4.4: Copy validated audio file to secure storage
32
+ *
33
+ * Process:
34
+ * 1. Create secure storage directory if not exists (700 permissions)
35
+ * 2. Calculate checksum of source file
36
+ * 3. Backup previous music if exists
37
+ * 4. Copy file to storage with secure permissions (600)
38
+ * 5. Verify checksums match (source vs. copy)
39
+ * 6. Store metadata (original filename, date, size, checksum)
40
+ * 7. Return success with file info
41
+ *
42
+ * @param {string} sourceFilePath - Path to validated source audio file
43
+ * @param {string} claudeDir - Path to .claude directory
44
+ * @param {Object} options - Storage options
45
+ * @param {string} options.filename - Custom filename (default: source basename)
46
+ * @param {Function} options.logger - Optional logger function
47
+ * @returns {Object} {
48
+ * success: boolean,
49
+ * error: string|null,
50
+ * storagePath: string|null,
51
+ * metadataPath: string|null,
52
+ * backupPath: string|null,
53
+ * metadata: Object|null
54
+ * }
55
+ */
56
+ export async function copyToSecureStorage(sourceFilePath, claudeDir, options = {}) {
57
+ const { filename = null, logger = null } = options;
58
+
59
+ try {
60
+ logger?.(`storage: starting copy - source=${path.basename(sourceFilePath)}`);
61
+
62
+ // Parameter validation
63
+ if (!sourceFilePath || typeof sourceFilePath !== 'string') {
64
+ throw new Error('Source file path must be a non-empty string');
65
+ }
66
+
67
+ if (!claudeDir || typeof claudeDir !== 'string') {
68
+ throw new Error('Claude directory path must be a non-empty string');
69
+ }
70
+
71
+ // Verify source file exists and is readable
72
+ if (!fs.existsSync(sourceFilePath)) {
73
+ throw new Error('Source file does not exist');
74
+ }
75
+
76
+ const sourceStats = fs.statSync(sourceFilePath);
77
+ if (!sourceStats.isFile()) {
78
+ throw new Error('Source path must be a regular file');
79
+ }
80
+
81
+ // Get file extension
82
+ const fileExt = path.extname(sourceFilePath).toLowerCase();
83
+ const targetFilename = filename || path.basename(sourceFilePath);
84
+
85
+ // Create storage directory structure
86
+ const musicDir = path.join(claudeDir, MUSIC_STORAGE_DIR);
87
+ const tracksDir = path.join(musicDir, MUSIC_STORAGE_SUBDIR);
88
+ const backupDir = path.join(musicDir, BACKUP_SUBDIR);
89
+
90
+ logger?.(`storage: creating directories - ${musicDir}`);
91
+
92
+ // Create directories with secure permissions
93
+ const dirCreated = ensureSecureDirectories(musicDir, tracksDir, backupDir, logger);
94
+ if (!dirCreated.success) {
95
+ throw new Error(dirCreated.error);
96
+ }
97
+
98
+ // Calculate source checksum
99
+ logger?.(`storage: calculating source checksum`);
100
+ const sourceChecksum = calculateFileChecksum(sourceFilePath);
101
+
102
+ // Path where file will be stored
103
+ const targetStoragePath = path.join(tracksDir, targetFilename);
104
+
105
+ // Backup previous music if exists
106
+ let backupPath = null;
107
+ if (fs.existsSync(targetStoragePath)) {
108
+ logger?.(`storage: backing up previous music`);
109
+ const backupResult = backupPreviousFile(targetStoragePath, backupDir, logger);
110
+ if (!backupResult.success) {
111
+ throw new Error(`Failed to backup previous music: ${backupResult.error}`);
112
+ }
113
+ backupPath = backupResult.backupPath;
114
+ }
115
+
116
+ // Copy file to storage
117
+ logger?.(`storage: copying file to storage`);
118
+ try {
119
+ fs.copyFileSync(sourceFilePath, targetStoragePath);
120
+ } catch (err) {
121
+ throw new Error(`Failed to copy file: ${err.message}`);
122
+ }
123
+
124
+ // Set file permissions (600 = user read/write only)
125
+ try {
126
+ fs.chmodSync(targetStoragePath, 0o600);
127
+ } catch (err) {
128
+ // Log warning but continue - copy is done
129
+ logger?.(`storage: warning - could not set file permissions: ${err.message}`);
130
+ }
131
+
132
+ // Verify checksums match
133
+ logger?.(`storage: verifying checksums`);
134
+ const targetChecksum = calculateFileChecksum(targetStoragePath);
135
+
136
+ if (sourceChecksum !== targetChecksum) {
137
+ // Checksums don't match - file corruption
138
+ try {
139
+ fs.unlinkSync(targetStoragePath);
140
+ } catch (err) {
141
+ logger?.(`storage: warning - could not delete corrupted file: ${err.message}`);
142
+ }
143
+ throw new Error('File corruption detected: checksums do not match');
144
+ }
145
+
146
+ // Store metadata
147
+ logger?.(`storage: storing metadata`);
148
+ const metadata = {
149
+ originalFilename: path.basename(sourceFilePath),
150
+ storagePath: targetStoragePath,
151
+ copiedDate: new Date().toISOString(),
152
+ fileSize: sourceStats.size,
153
+ checksum: sourceChecksum,
154
+ extension: fileExt
155
+ };
156
+
157
+ const metadataPath = path.join(musicDir, METADATA_FILE);
158
+ try {
159
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), { mode: 0o600 });
160
+ } catch (err) {
161
+ logger?.(`storage: warning - could not store metadata: ${err.message}`);
162
+ }
163
+
164
+ logger?.(`storage: success - file stored at ${path.relative(claudeDir, targetStoragePath)}`);
165
+
166
+ return {
167
+ success: true,
168
+ error: null,
169
+ storagePath: targetStoragePath,
170
+ metadataPath,
171
+ backupPath,
172
+ metadata
173
+ };
174
+
175
+ } catch (err) {
176
+ const error = err.message || String(err);
177
+ logger?.(`storage: failed - ${error}`);
178
+ return {
179
+ success: false,
180
+ error,
181
+ storagePath: null,
182
+ metadataPath: null,
183
+ backupPath: null,
184
+ metadata: null
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Ensure secure storage directories exist with proper permissions
191
+ *
192
+ * @private
193
+ */
194
+ function ensureSecureDirectories(musicDir, tracksDir, backupDir, logger) {
195
+ try {
196
+ // Create main music directory
197
+ if (!fs.existsSync(musicDir)) {
198
+ fs.mkdirSync(musicDir, { recursive: true, mode: 0o700 });
199
+ logger?.(`storage: created music directory with 700 permissions`);
200
+ } else {
201
+ // Verify permissions on existing directory
202
+ const stats = fs.statSync(musicDir);
203
+ if ((stats.mode & 0o077) !== 0) {
204
+ // Has group/other permissions - try to fix
205
+ try {
206
+ fs.chmodSync(musicDir, 0o700);
207
+ logger?.(`storage: fixed insecure permissions on music directory`);
208
+ } catch (err) {
209
+ logger?.(`storage: warning - could not fix permissions: ${err.message}`);
210
+ }
211
+ }
212
+ }
213
+
214
+ // Create tracks subdirectory
215
+ if (!fs.existsSync(tracksDir)) {
216
+ fs.mkdirSync(tracksDir, { recursive: true, mode: 0o700 });
217
+ logger?.(`storage: created tracks directory with 700 permissions`);
218
+ }
219
+
220
+ // Create backup subdirectory
221
+ if (!fs.existsSync(backupDir)) {
222
+ fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
223
+ logger?.(`storage: created backup directory with 700 permissions`);
224
+ }
225
+
226
+ return {
227
+ success: true,
228
+ error: null
229
+ };
230
+ } catch (err) {
231
+ return {
232
+ success: false,
233
+ error: `Failed to create directories: ${err.message}`
234
+ };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Calculate SHA256 checksum of file
240
+ *
241
+ * @private
242
+ */
243
+ function calculateFileChecksum(filePath) {
244
+ const hash = crypto.createHash('sha256');
245
+ const fileContent = fs.readFileSync(filePath);
246
+ hash.update(fileContent);
247
+ return hash.digest('hex');
248
+ }
249
+
250
+ /**
251
+ * Backup previous music file to backup directory
252
+ *
253
+ * @private
254
+ */
255
+ function backupPreviousFile(previousPath, backupDir, logger) {
256
+ try {
257
+ const filename = path.basename(previousPath);
258
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
259
+ const backupFilename = `${timestamp}-${filename}`;
260
+ const backupPath = path.join(backupDir, backupFilename);
261
+
262
+ // Copy to backup
263
+ fs.copyFileSync(previousPath, backupPath);
264
+ fs.chmodSync(backupPath, 0o600);
265
+
266
+ logger?.(`storage: backed up previous file to ${path.basename(backupPath)}`);
267
+
268
+ return {
269
+ success: true,
270
+ error: null,
271
+ backupPath
272
+ };
273
+ } catch (err) {
274
+ return {
275
+ success: false,
276
+ error: `Failed to backup previous file: ${err.message}`,
277
+ backupPath: null
278
+ };
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Story 4.4: Read stored music metadata
284
+ *
285
+ * @param {string} claudeDir - Path to .claude directory
286
+ * @returns {Object} {
287
+ * success: boolean,
288
+ * error: string|null,
289
+ * metadata: Object|null
290
+ * }
291
+ */
292
+ export function readMusicMetadata(claudeDir) {
293
+ try {
294
+ const metadataPath = path.join(claudeDir, MUSIC_STORAGE_DIR, METADATA_FILE);
295
+
296
+ if (!fs.existsSync(metadataPath)) {
297
+ return {
298
+ success: true,
299
+ error: null,
300
+ metadata: null
301
+ };
302
+ }
303
+
304
+ const content = fs.readFileSync(metadataPath, 'utf-8');
305
+ const metadata = JSON.parse(content);
306
+
307
+ return {
308
+ success: true,
309
+ error: null,
310
+ metadata
311
+ };
312
+ } catch (err) {
313
+ return {
314
+ success: false,
315
+ error: `Failed to read metadata: ${err.message}`,
316
+ metadata: null
317
+ };
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Story 4.4: Get path to stored custom music file
323
+ *
324
+ * @param {string} claudeDir - Path to .claude directory
325
+ * @returns {Object} {
326
+ * exists: boolean,
327
+ * path: string|null,
328
+ * filename: string|null
329
+ * }
330
+ */
331
+ export function getStoredMusicPath(claudeDir) {
332
+ try {
333
+ const metadataPath = path.join(claudeDir, MUSIC_STORAGE_DIR, METADATA_FILE);
334
+
335
+ if (!fs.existsSync(metadataPath)) {
336
+ return {
337
+ exists: false,
338
+ path: null,
339
+ filename: null
340
+ };
341
+ }
342
+
343
+ const content = fs.readFileSync(metadataPath, 'utf-8');
344
+ const metadata = JSON.parse(content);
345
+
346
+ // Verify the stored file actually exists
347
+ if (!fs.existsSync(metadata.storagePath)) {
348
+ return {
349
+ exists: false,
350
+ path: null,
351
+ filename: null
352
+ };
353
+ }
354
+
355
+ return {
356
+ exists: true,
357
+ path: metadata.storagePath,
358
+ filename: metadata.originalFilename
359
+ };
360
+ } catch (err) {
361
+ return {
362
+ exists: false,
363
+ path: null,
364
+ filename: null
365
+ };
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Story 4.4: Delete stored custom music
371
+ *
372
+ * @param {string} claudeDir - Path to .claude directory
373
+ * @returns {Object} { success: boolean, error: string|null }
374
+ */
375
+ export function deleteStoredMusic(claudeDir) {
376
+ try {
377
+ const musicInfo = getStoredMusicPath(claudeDir);
378
+
379
+ if (!musicInfo.exists) {
380
+ return {
381
+ success: true,
382
+ error: null
383
+ };
384
+ }
385
+
386
+ // Delete the stored file
387
+ fs.unlinkSync(musicInfo.path);
388
+
389
+ // Delete metadata
390
+ const metadataPath = path.join(claudeDir, MUSIC_STORAGE_DIR, METADATA_FILE);
391
+ if (fs.existsSync(metadataPath)) {
392
+ fs.unlinkSync(metadataPath);
393
+ }
394
+
395
+ return {
396
+ success: true,
397
+ error: null
398
+ };
399
+ } catch (err) {
400
+ return {
401
+ success: false,
402
+ error: `Failed to delete stored music: ${err.message}`
403
+ };
404
+ }
405
+ }
406
+
407
+ export default {
408
+ copyToSecureStorage,
409
+ readMusicMetadata,
410
+ getStoredMusicPath,
411
+ deleteStoredMusic
412
+ };