agentvibes 4.2.0 → 4.4.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 (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +2 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -433
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -269
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -81
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -84
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1453
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -110
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -824
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -285
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -482
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -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
+ };