agentvibes 3.5.9 → 4.0.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 (71) hide show
  1. package/.agentvibes/bmad/bmad-voices-enabled.flag +0 -0
  2. package/.agentvibes/bmad/bmad-voices.md +69 -0
  3. package/.claude/config/audio-effects.cfg +1 -1
  4. package/.claude/config/background-music-position.txt +1 -27
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/audio-processor.sh +32 -17
  7. package/.claude/hooks/bmad-speak-enhanced.sh +5 -5
  8. package/.claude/hooks/bmad-speak.sh +4 -4
  9. package/.claude/hooks/bmad-voice-manager.sh +8 -8
  10. package/.claude/hooks/clawdbot-receiver-SECURE.sh +23 -25
  11. package/.claude/hooks/clawdbot-receiver.sh +28 -4
  12. package/.claude/hooks/language-manager.sh +1 -1
  13. package/.claude/hooks/path-resolver.sh +60 -0
  14. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -0
  15. package/.claude/hooks/play-tts-piper.sh +82 -24
  16. package/.claude/hooks/play-tts-ssh-remote.sh +13 -15
  17. package/.claude/hooks/play-tts.sh +16 -5
  18. package/.claude/hooks/session-start-tts.sh +26 -56
  19. package/.claude/hooks/soprano-gradio-synth.py +1 -1
  20. package/.claude/hooks/verbosity-manager.sh +10 -4
  21. package/.claude/settings.json +1 -1
  22. package/CLAUDE.md +129 -104
  23. package/README.md +418 -10
  24. package/RELEASE_NOTES.md +60 -1036
  25. package/bin/agentvibes-voice-browser.js +1827 -0
  26. package/bin/agentvibes.js +100 -0
  27. package/mcp-server/server.py +67 -3
  28. package/package.json +11 -2
  29. package/src/console/app.js +806 -0
  30. package/src/console/audio-env.js +123 -0
  31. package/src/console/brand-colors.js +13 -0
  32. package/src/console/footer-config.js +42 -0
  33. package/src/console/modals/.gitkeep +0 -0
  34. package/src/console/modals/modal-overlay.js +247 -0
  35. package/src/console/navigation.js +60 -0
  36. package/src/console/tabs/.gitkeep +0 -0
  37. package/src/console/tabs/agents-tab.js +369 -0
  38. package/src/console/tabs/help-tab.js +261 -0
  39. package/src/console/tabs/install-tab.js +990 -0
  40. package/src/console/tabs/music-tab.js +997 -0
  41. package/src/console/tabs/placeholder-tab.js +45 -0
  42. package/src/console/tabs/readme-tab.js +267 -0
  43. package/src/console/tabs/settings-tab.js +3949 -0
  44. package/src/console/tabs/voices-tab.js +1574 -0
  45. package/src/installer/music-file-input.js +304 -0
  46. package/src/installer.js +1353 -676
  47. package/src/services/.gitkeep +0 -0
  48. package/src/services/agent-voice-store.js +163 -0
  49. package/src/services/config-service.js +240 -0
  50. package/src/services/navigation-service.js +123 -0
  51. package/src/services/provider-service.js +132 -0
  52. package/src/services/verbosity-service.js +157 -0
  53. package/src/utils/audio-duration-validator.js +298 -0
  54. package/src/utils/audio-format-validator.js +277 -0
  55. package/src/utils/dependency-checker.js +3 -3
  56. package/src/utils/file-ownership-verifier.js +358 -0
  57. package/src/utils/music-file-validator.js +275 -0
  58. package/src/utils/preview-list-prompt.js +136 -0
  59. package/src/utils/provider-validator.js +144 -132
  60. package/src/utils/secure-music-storage.js +412 -0
  61. package/templates/agentvibes-receiver.sh +11 -7
  62. package/voice-assignments.json +8245 -0
  63. package/.claude/config/background-music-volume.txt +0 -1
  64. package/.claude/config/background-music.cfg +0 -1
  65. package/.claude/config/background-music.txt +0 -1
  66. package/.claude/config/tts-speech-rate.txt +0 -1
  67. package/.claude/config/tts-verbosity.txt +0 -1
  68. package/.claude/hooks/bmad-party-manager.sh +0 -225
  69. package/.claude/hooks/stop.sh +0 -38
  70. package/.claude/piper-voices-dir.txt +0 -1
  71. package/.mcp.json +0 -34
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Audio Format Validator - Magic Number Detection
3
+ * Story 4.2: Audio Format Detection (Magic Number Validation)
4
+ *
5
+ * Validates audio file format by checking file headers (magic numbers).
6
+ * Supported formats: MP3, WAV, OGG, M4A
7
+ *
8
+ * @module audio-format-validator
9
+ * @requires fs
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ /**
16
+ * Supported audio formats with their magic number signatures
17
+ */
18
+ const AUDIO_FORMATS = {
19
+ mp3: {
20
+ extension: '.mp3',
21
+ magicNumbers: [
22
+ // MPEG-1/2/2.5 Audio Frame Header
23
+ { bytes: Buffer.from([0xFF, 0xFB]), offset: 0, name: 'MP3 MPEG-2 Layer III' },
24
+ { bytes: Buffer.from([0xFF, 0xFA]), offset: 0, name: 'MP3 MPEG-1 Layer III' },
25
+ // ID3 Tag (v2.x and v2.4)
26
+ { bytes: Buffer.from([0x49, 0x44, 0x33]), offset: 0, name: 'ID3v2 Tag' } // "ID3"
27
+ ]
28
+ },
29
+ wav: {
30
+ extension: '.wav',
31
+ magicNumbers: [
32
+ { bytes: Buffer.from('RIFF'), offset: 0, name: 'RIFF Header' },
33
+ { bytes: Buffer.from('WAVE'), offset: 8, name: 'WAV Format' }
34
+ ]
35
+ },
36
+ ogg: {
37
+ extension: '.ogg',
38
+ magicNumbers: [
39
+ { bytes: Buffer.from('OggS'), offset: 0, name: 'OggS Header' }
40
+ ]
41
+ },
42
+ m4a: {
43
+ extension: '.m4a',
44
+ magicNumbers: [
45
+ { bytes: Buffer.from('ftypisom'), offset: 4, name: 'M4A ISO Base Media' },
46
+ { bytes: Buffer.from('ftypM4A'), offset: 4, name: 'M4A Apple iTunes' }
47
+ ]
48
+ }
49
+ };
50
+
51
+ const SUPPORTED_EXTENSIONS = Object.values(AUDIO_FORMATS).map(f => f.extension);
52
+
53
+ /**
54
+ * Story 4.2: Detect audio format by checking magic numbers in file header
55
+ *
56
+ * Reads the first 256 bytes of the file to check magic number signatures.
57
+ * Does NOT require the file to have the correct extension matching format.
58
+ *
59
+ * @param {string} filePath - Path to audio file (must already be validated with isPathSafe)
60
+ * @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null }
61
+ *
62
+ * Object properties:
63
+ * - isValid: true if file is valid audio format
64
+ * - error: null on success, error message on failure
65
+ * - format: detected format ('mp3', 'wav', 'ogg', 'm4a') or null
66
+ * - detectedFormat: human-readable format name (e.g., "MP3 MPEG-1 Layer III")
67
+ */
68
+ function detectAudioFormat(filePath) {
69
+ try {
70
+ if (!filePath || typeof filePath !== 'string') {
71
+ return {
72
+ isValid: false,
73
+ error: 'File path must be a non-empty string',
74
+ format: null,
75
+ detectedFormat: null
76
+ };
77
+ }
78
+
79
+ // Check file exists and is readable
80
+ if (!fs.existsSync(filePath)) {
81
+ return {
82
+ isValid: false,
83
+ error: 'File does not exist',
84
+ format: null,
85
+ detectedFormat: null
86
+ };
87
+ }
88
+
89
+ const stats = fs.statSync(filePath);
90
+ if (!stats.isFile()) {
91
+ return {
92
+ isValid: false,
93
+ error: 'Path must be a regular file',
94
+ format: null,
95
+ detectedFormat: null
96
+ };
97
+ }
98
+
99
+ // Check minimum file size (at least 12 bytes for WAV format check)
100
+ if (stats.size < 12) {
101
+ return {
102
+ isValid: false,
103
+ error: 'File is too small to be a valid audio file (minimum 12 bytes)',
104
+ format: null,
105
+ detectedFormat: null
106
+ };
107
+ }
108
+
109
+ // Read first 256 bytes for magic number detection
110
+ const buffer = Buffer.alloc(256);
111
+ const fd = fs.openSync(filePath, 'r');
112
+ try {
113
+ fs.readSync(fd, buffer, 0, 256, 0);
114
+ } finally {
115
+ fs.closeSync(fd);
116
+ }
117
+
118
+ // Check each format's magic numbers
119
+ for (const [formatKey, formatInfo] of Object.entries(AUDIO_FORMATS)) {
120
+ for (const magicInfo of formatInfo.magicNumbers) {
121
+ // Check if magic bytes exist at the specified offset
122
+ const isMatch = buffer.subarray(magicInfo.offset, magicInfo.offset + magicInfo.bytes.length)
123
+ .equals(magicInfo.bytes);
124
+
125
+ if (isMatch) {
126
+ return {
127
+ isValid: true,
128
+ error: null,
129
+ format: formatKey,
130
+ detectedFormat: magicInfo.name
131
+ };
132
+ }
133
+ }
134
+ }
135
+
136
+ // No magic numbers matched
137
+ return {
138
+ isValid: false,
139
+ error: `Unsupported audio format. Supported formats: ${SUPPORTED_EXTENSIONS.join(', ')}`,
140
+ format: null,
141
+ detectedFormat: null
142
+ };
143
+
144
+ } catch (err) {
145
+ return {
146
+ isValid: false,
147
+ error: `Error detecting audio format: ${err.message}`,
148
+ format: null,
149
+ detectedFormat: null
150
+ };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Story 4.2: Validate that file extension matches detected audio format
156
+ *
157
+ * Ensures the file extension is correct for the detected format.
158
+ * This prevents accidental misnamed files (e.g., .txt file with MP3 content).
159
+ *
160
+ * @param {string} filePath - Path to audio file
161
+ * @param {string} detectedFormat - Format from detectAudioFormat() result
162
+ * @returns {Object} { isValid: boolean, error: string|null }
163
+ */
164
+ function validateFileExtension(filePath, detectedFormat) {
165
+ try {
166
+ if (!filePath || !detectedFormat) {
167
+ return {
168
+ isValid: false,
169
+ error: 'File path and detected format are required'
170
+ };
171
+ }
172
+
173
+ const ext = path.extname(filePath).toLowerCase();
174
+ const expectedExt = AUDIO_FORMATS[detectedFormat]?.extension;
175
+
176
+ if (!expectedExt) {
177
+ return {
178
+ isValid: false,
179
+ error: `Unknown detected format: ${detectedFormat}`
180
+ };
181
+ }
182
+
183
+ if (ext === expectedExt) {
184
+ return {
185
+ isValid: true,
186
+ error: null
187
+ };
188
+ }
189
+
190
+ // Extension doesn't match - return specific error
191
+ return {
192
+ isValid: false,
193
+ error: `File extension (${ext || 'none'}) doesn't match actual format (.${detectedFormat})`
194
+ };
195
+
196
+ } catch (err) {
197
+ return {
198
+ isValid: false,
199
+ error: `Error validating file extension: ${err.message}`
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Story 4.2: Comprehensive audio file validation
206
+ *
207
+ * Combines format detection and extension validation in one call.
208
+ * Returns false only if BOTH format is unsupported AND extension is wrong.
209
+ * Returns true if format is valid (even if extension differs).
210
+ *
211
+ * @param {string} filePath - Path to audio file
212
+ * @param {Object} options - Validation options
213
+ * @param {boolean} options.strictExtension - If true, reject files with wrong extension (default: false)
214
+ * @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null, extensionMatch: boolean }
215
+ */
216
+ function validateAudioFile(filePath, options = {}) {
217
+ const { strictExtension = false } = options;
218
+
219
+ try {
220
+ // Detect format first
221
+ const formatResult = detectAudioFormat(filePath);
222
+
223
+ if (!formatResult.isValid) {
224
+ return {
225
+ isValid: false,
226
+ error: formatResult.error,
227
+ format: null,
228
+ detectedFormat: null,
229
+ extensionMatch: false
230
+ };
231
+ }
232
+
233
+ // Check extension if strict mode enabled
234
+ if (strictExtension) {
235
+ const extResult = validateFileExtension(filePath, formatResult.format);
236
+ if (!extResult.isValid) {
237
+ return {
238
+ isValid: false,
239
+ error: extResult.error,
240
+ format: formatResult.format,
241
+ detectedFormat: formatResult.detectedFormat,
242
+ extensionMatch: false
243
+ };
244
+ }
245
+ }
246
+
247
+ // Format is valid - check if extension matches (informational)
248
+ const ext = path.extname(filePath).toLowerCase();
249
+ const expectedExt = AUDIO_FORMATS[formatResult.format]?.extension;
250
+ const extensionMatch = ext === expectedExt;
251
+
252
+ return {
253
+ isValid: true,
254
+ error: null,
255
+ format: formatResult.format,
256
+ detectedFormat: formatResult.detectedFormat,
257
+ extensionMatch
258
+ };
259
+
260
+ } catch (err) {
261
+ return {
262
+ isValid: false,
263
+ error: `Unexpected validation error: ${err.message}`,
264
+ format: null,
265
+ detectedFormat: null,
266
+ extensionMatch: false
267
+ };
268
+ }
269
+ }
270
+
271
+ export {
272
+ detectAudioFormat,
273
+ validateFileExtension,
274
+ validateAudioFile,
275
+ AUDIO_FORMATS,
276
+ SUPPORTED_EXTENSIONS
277
+ };
@@ -147,7 +147,7 @@ function buildLinuxPackages(missing) {
147
147
  const pacman = [];
148
148
 
149
149
  const packageMap = {
150
- sox: { apt: 'sox', dnf: 'sox', pacman: 'sox' },
150
+ sox: { apt: 'sox libsox-fmt-mp3', dnf: 'sox', pacman: 'sox' },
151
151
  ffmpeg: { apt: 'ffmpeg', dnf: 'ffmpeg', pacman: 'ffmpeg' },
152
152
  python: { apt: 'python3-pip', dnf: 'python3-pip', pacman: 'python-pip' },
153
153
  pipx: { apt: 'pipx', dnf: 'pipx', pacman: 'python-pipx' },
@@ -368,8 +368,8 @@ function buildCoreMissingList(missing, results) {
368
368
  function buildOptionalMissingList(missing) {
369
369
  const optionalMap = {
370
370
  curl: '• curl (downloading Piper TTS and voices)',
371
- sox: '• sox (audio effects)',
372
- ffmpeg: '• ffmpeg (background music, RDP optimization)',
371
+ sox: '• sox (background music mixing, audio effects)',
372
+ ffmpeg: '• ffmpeg (audio processing, RDP optimization)',
373
373
  bc: '• bc (audio processing calculations)',
374
374
  pipx: '• pipx (Piper TTS installation)',
375
375
  flock: '• flock (TTS queue file locking)',
@@ -0,0 +1,358 @@
1
+ /**
2
+ * File Ownership Verifier - Cross-Platform Security Validation
3
+ * Story 4.3: File Ownership Verification
4
+ *
5
+ * Verifies that files are owned by the current user before processing.
6
+ * This prevents malicious files planted by other users from being used.
7
+ *
8
+ * Handles platform-specific differences:
9
+ * - Unix/Linux/macOS: UID-based ownership checking
10
+ * - Windows: Uses fs.statSync().uid if available, graceful fallback
11
+ * - Network mounts: Documented limitation, best-effort checking
12
+ *
13
+ * @module file-ownership-verifier
14
+ * @requires fs
15
+ * @requires os
16
+ * @requires path
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+
23
+ /**
24
+ * Story 4.3: Verify file is owned by current user
25
+ *
26
+ * On Unix-like systems (Linux, macOS):
27
+ * - Compares fs.statSync().uid with process.getuid()
28
+ *
29
+ * On Windows:
30
+ * - Attempts UID comparison if available
31
+ * - Gracefully falls back to path/permission checks if UID unavailable
32
+ * - Network mount files may not have reliable ownership info
33
+ *
34
+ * Performance: Typically < 5ms (stat operation only, no I/O)
35
+ *
36
+ * @param {string} filePath - Path to file to check
37
+ * @param {Object} options - Verification options
38
+ * @param {boolean} options.allowNetworkMounts - Allow verification to succeed on network mounts (default: true)
39
+ * @param {Function} options.logger - Optional logger function for sanitized logs
40
+ * @returns {Object} {
41
+ * isOwned: boolean,
42
+ * error: string|null,
43
+ * ownerUid: number|null,
44
+ * currentUid: number|null,
45
+ * isNetworkMount: boolean,
46
+ * platform: string
47
+ * }
48
+ */
49
+ export function verifyFileOwnership(filePath, options = {}) {
50
+ const { allowNetworkMounts = true, logger = null } = options;
51
+
52
+ try {
53
+ if (!filePath || typeof filePath !== 'string') {
54
+ const error = 'File path must be a non-empty string';
55
+ logger?.(`ownership-check: invalid-path - ${error}`);
56
+ return {
57
+ isOwned: false,
58
+ error,
59
+ ownerUid: null,
60
+ currentUid: null,
61
+ isNetworkMount: false,
62
+ platform: process.platform
63
+ };
64
+ }
65
+
66
+ // Check file exists
67
+ if (!fs.existsSync(filePath)) {
68
+ const error = 'File does not exist';
69
+ logger?.(`ownership-check: missing-file - ${filePath}`);
70
+ return {
71
+ isOwned: false,
72
+ error,
73
+ ownerUid: null,
74
+ currentUid: null,
75
+ isNetworkMount: false,
76
+ platform: process.platform
77
+ };
78
+ }
79
+
80
+ // Get file stats
81
+ const stats = fs.statSync(filePath);
82
+
83
+ // Check if it's a regular file
84
+ if (!stats.isFile()) {
85
+ const error = 'Path must be a regular file';
86
+ logger?.(`ownership-check: not-file - ${filePath}`);
87
+ return {
88
+ isOwned: false,
89
+ error,
90
+ ownerUid: null,
91
+ currentUid: null,
92
+ isNetworkMount: false,
93
+ platform: process.platform
94
+ };
95
+ }
96
+
97
+ const currentUid = process.getuid ? process.getuid() : null;
98
+ const fileUid = stats.uid;
99
+
100
+ // Determine platform type for logging
101
+ const platformType = getPlatformType();
102
+ const isNetworkMount = checkIsNetworkMount(filePath);
103
+
104
+ // Log verification attempt (sanitized - no sensitive paths)
105
+ logger?.(`ownership-check: started - platform=${platformType}, network=${isNetworkMount}`);
106
+
107
+ // Platform-specific ownership verification
108
+ if (process.platform === 'win32') {
109
+ return verifyWindowsOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger);
110
+ } else {
111
+ // Unix-like systems (Linux, macOS)
112
+ return verifyUnixOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger);
113
+ }
114
+
115
+ } catch (err) {
116
+ const error = `Error verifying file ownership: ${err.message}`;
117
+ logger?.(`ownership-check: error - ${error}`);
118
+ return {
119
+ isOwned: false,
120
+ error,
121
+ ownerUid: null,
122
+ currentUid: null,
123
+ isNetworkMount: false,
124
+ platform: process.platform
125
+ };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Unix/Linux/macOS ownership verification
131
+ * Uses UID comparison for reliable security
132
+ *
133
+ * @private
134
+ */
135
+ function verifyUnixOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger) {
136
+ if (currentUid === null) {
137
+ const error = 'Unable to determine current user UID (not available on this platform)';
138
+ logger?.(`ownership-check: no-uid-support`);
139
+ return {
140
+ isOwned: false,
141
+ error,
142
+ ownerUid: stats.uid,
143
+ currentUid: null,
144
+ isNetworkMount,
145
+ platform: process.platform
146
+ };
147
+ }
148
+
149
+ // Check if UIDs match
150
+ if (stats.uid === currentUid) {
151
+ logger?.(`ownership-check: success - uid=${currentUid}`);
152
+ return {
153
+ isOwned: true,
154
+ error: null,
155
+ ownerUid: stats.uid,
156
+ currentUid,
157
+ isNetworkMount,
158
+ platform: process.platform
159
+ };
160
+ }
161
+
162
+ // UIDs don't match - file owned by different user
163
+ const error = 'File not owned by current user (security check failed)';
164
+ logger?.(`ownership-check: failed - file-uid=${stats.uid}, user-uid=${currentUid}`);
165
+ return {
166
+ isOwned: false,
167
+ error,
168
+ ownerUid: stats.uid,
169
+ currentUid,
170
+ isNetworkMount,
171
+ platform: process.platform
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Windows ownership verification
177
+ * Windows doesn't have traditional UID system, use file permissions
178
+ *
179
+ * @private
180
+ */
181
+ function verifyWindowsOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger) {
182
+ // On Windows, process.getuid() is typically undefined
183
+ // Try to verify through accessible permissions instead
184
+
185
+ // If running in WSL (Windows Subsystem for Linux), we have UID available
186
+ if (currentUid !== null && stats.uid !== undefined) {
187
+ if (stats.uid === currentUid) {
188
+ logger?.(`ownership-check: windows-wsl-success - uid=${currentUid}`);
189
+ return {
190
+ isOwned: true,
191
+ error: null,
192
+ ownerUid: stats.uid,
193
+ currentUid,
194
+ isNetworkMount,
195
+ platform: process.platform
196
+ };
197
+ }
198
+
199
+ const error = 'File not owned by current user (security check failed)';
200
+ logger?.(`ownership-check: windows-wsl-failed - file-uid=${stats.uid}, user-uid=${currentUid}`);
201
+ return {
202
+ isOwned: false,
203
+ error,
204
+ ownerUid: stats.uid,
205
+ currentUid,
206
+ isNetworkMount,
207
+ platform: process.platform
208
+ };
209
+ }
210
+
211
+ // Native Windows without UID support
212
+ // Check if file is readable and writable by current user (best effort)
213
+ try {
214
+ // Try to read and write to verify we own it
215
+ fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK);
216
+
217
+ // If we can read and write, assume we own it
218
+ logger?.(`ownership-check: windows-native-success - readable-writable`);
219
+ return {
220
+ isOwned: true,
221
+ error: null,
222
+ ownerUid: null,
223
+ currentUid: null,
224
+ isNetworkMount,
225
+ platform: process.platform
226
+ };
227
+ } catch (err) {
228
+ // Can't read/write - likely not owned by us or permission issue
229
+ const error = `File not owned by current user or not accessible (Windows): ${err.message}`;
230
+ logger?.(`ownership-check: windows-native-failed - ${err.code}`);
231
+ return {
232
+ isOwned: false,
233
+ error,
234
+ ownerUid: null,
235
+ currentUid: null,
236
+ isNetworkMount,
237
+ platform: process.platform
238
+ };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Determine if path is likely a network mount
244
+ * Used to set expectations for ownership checking reliability
245
+ *
246
+ * @private
247
+ */
248
+ function checkIsNetworkMount(filePath) {
249
+ const resolvedPath = path.resolve(filePath);
250
+
251
+ if (process.platform === 'win32') {
252
+ // Windows: check for UNC paths (\\server\share) or mapped drives
253
+ // Also check for paths under %APPDATA% or similar which might be network-synced
254
+ return resolvedPath.startsWith('\\\\') ||
255
+ resolvedPath.match(/^[A-Z]:\\[^\\]*\\netshare/i);
256
+ } else {
257
+ // Unix: check for common network mount prefixes
258
+ // /mnt, /media, NFS mount points
259
+ return resolvedPath.startsWith('/mnt/') ||
260
+ resolvedPath.startsWith('/media/') ||
261
+ resolvedPath.includes(':/'); // NFS mount indicators
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Get human-readable platform type
267
+ *
268
+ * @private
269
+ */
270
+ function getPlatformType() {
271
+ switch (process.platform) {
272
+ case 'linux':
273
+ return 'linux';
274
+ case 'darwin':
275
+ return 'macos';
276
+ case 'win32':
277
+ return 'windows';
278
+ default:
279
+ return process.platform;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Story 4.3: Get current user information for logging/debugging
285
+ *
286
+ * Returns user info in a sanitized format safe for logging.
287
+ * Does NOT include sensitive paths or full usernames.
288
+ *
289
+ * @returns {Object} {
290
+ * uid: number|null,
291
+ * gid: number|null,
292
+ * username: string,
293
+ * platform: string
294
+ * }
295
+ */
296
+ export function getCurrentUserInfo() {
297
+ try {
298
+ const uid = process.getuid ? process.getuid() : null;
299
+ const gid = process.getgid ? process.getgid() : null;
300
+ const username = os.userInfo().username || 'unknown';
301
+
302
+ return {
303
+ uid,
304
+ gid,
305
+ username,
306
+ platform: getPlatformType()
307
+ };
308
+ } catch (err) {
309
+ return {
310
+ uid: null,
311
+ gid: null,
312
+ username: 'unknown',
313
+ platform: getPlatformType()
314
+ };
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Story 4.3: Batch verify ownership of multiple files
320
+ *
321
+ * Efficiently checks ownership of multiple files.
322
+ * Returns detailed results for each file.
323
+ *
324
+ * @param {string[]} filePaths - Array of file paths to check
325
+ * @param {Object} options - Verification options (passed to verifyFileOwnership)
326
+ * @returns {Object} {
327
+ * allOwned: boolean,
328
+ * results: Array<{path: string, isOwned: boolean, error: string|null}>
329
+ * }
330
+ */
331
+ export function verifyMultipleFiles(filePaths, options = {}) {
332
+ const results = [];
333
+ let allOwned = true;
334
+
335
+ for (const filePath of filePaths) {
336
+ const result = verifyFileOwnership(filePath, options);
337
+ results.push({
338
+ path: filePath,
339
+ isOwned: result.isOwned,
340
+ error: result.error
341
+ });
342
+
343
+ if (!result.isOwned) {
344
+ allOwned = false;
345
+ }
346
+ }
347
+
348
+ return {
349
+ allOwned,
350
+ results
351
+ };
352
+ }
353
+
354
+ export default {
355
+ verifyFileOwnership,
356
+ getCurrentUserInfo,
357
+ verifyMultipleFiles
358
+ };