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.
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +2 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -433
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -269
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-macos.sh +368 -368
- package/.claude/hooks/play-tts-piper.sh +679 -679
- package/.claude/hooks/play-tts-soprano.sh +356 -356
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -301
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -548
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -170
- package/README.md +2029 -2007
- package/RELEASE_NOTES.md +1310 -1203
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1840
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1453
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -110
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -824
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +50 -50
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -62
- package/src/console/tabs/agents-tab.js +1684 -1516
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -53
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -1212
- package/src/console/tabs/settings-tab.js +152 -79
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +89 -89
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +185 -185
- package/src/console/widgets/reverb-picker.js +94 -94
- package/src/console/widgets/track-picker.js +285 -285
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5882 -5829
- package/src/services/agent-voice-store.js +423 -423
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +132 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -482
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- 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
|
+
};
|