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,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
+ };