├── .gitignore ├── README.md ├── SPEC.md ├── ideas └── history │ ├── history.js │ └── history.md ├── index.js ├── package-lock.json ├── package.json ├── prompt.md ├── swift-version ├── Package.swift ├── README.md ├── Sources │ └── EfficientRecorder │ │ ├── AppDelegate.swift │ │ ├── AudioRecorder.swift │ │ ├── ConfigManager.swift │ │ ├── Info.plist │ │ ├── PermissionManager.swift │ │ ├── ScreenRecorder.swift │ │ ├── StatusManager.swift │ │ ├── StorageConfig.swift │ │ ├── TempFileManager.swift │ │ ├── UploadManager.swift │ │ ├── efficient-recorder.entitlements │ │ └── main.swift ├── implementation.md ├── pseudocode.md └── write.js └── tweet.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Today I was inspired by this tweet after which I fell into this rabbit hole to create an efficient recorder for your Screen, System audio, and Mic. 2 | 3 | [![](tweet.png)](https://x.com/RichardMCNgo/status/1875093600612261909) 4 | 5 | After trying to implement this using Claude in Swift I completely failed (see folder [swift-version](swift-version)) so I decided to create a simpler version (also using Claude) in Node.js 6 | 7 | # Efficient Recorder (that's the goal, at least) 8 | 9 | Goal: Create the most **battery-life friendly** recorder to stream video/screen/mic/system-audio to any **S3-compatible** cloud-storage-service of choice, open source. 10 | 11 | ## Features 12 | 13 | - Captures Mic audio, screenshots, and webcam photos at configurable fps 14 | - For audio, uses low-energy volume detection to determine what to record 15 | - Streams all data to configurable S3 endpoint (Can also use [your own server using MinIO](https://github.com/minio/minio)) 16 | 17 | I really want this to exist, but won't pursue it for now. This repo serves as an example. Open to contributors! [DM me](https://x.com/janwilmake) 18 | 19 | After the [hackernews thread](https://news.ycombinator.com/item?id=42596607), I saw there are a couple other similar projects in various levels of completion (but none focuses on battery): 20 | 21 | - Screenpipe https://github.com/mediar-ai/screenpipe 22 | - Memento https://github.com/apirrone/Memento 23 | - Rem https://github.com/jasonjmcghee/rem 24 | 25 | Other related repos I made: 26 | 27 | - https://github.com/janwilmake/ip-camera-to-s3-macos 28 | - https://github.com/janwilmake/yaptabber 29 | - https://github.com/janwilmake/export-safari 30 | 31 | ## Prerequisites 32 | 33 | ### Required Software 34 | 35 | This package requires: 36 | 37 | - [SoX (Sound eXchange)](http://sox.sourceforge.net/) for audio recording 38 | 39 | - Linux: `sudo apt-get install sox libsox-fmt-all` 40 | - MacOS: `brew install sox` 41 | - Windows: Download from [SourceForge](http://sourceforge.net/projects/sox/files/latest/download) 42 | 43 | - Webcam capture tools 44 | - Ubuntu: `sudo apt-get install fswebcam` 45 | - Arch Linux: `sudo pamac build fswebcam` 46 | - MacOS: `brew install imagesnap` 47 | - Windows: Standalone exe included in node-webcam 48 | 49 | ### S3 Configuration 50 | 51 | You'll need: 52 | 53 | - S3-compatible storage endpoint 54 | - Access key 55 | - Secret key 56 | - A bucket named "recordings" (or modify the code to use a different bucket name) 57 | 58 | ## Usage 59 | 60 | Run the recorder using npx: 61 | 62 | ```bash 63 | npx efficient-recorder \ 64 | --endpoint YOUR_S3_ENDPOINT \ 65 | --key YOUR_ACCESS_KEY \ 66 | --secret YOUR_SECRET_KEY \ 67 | --enable-screenshot \ 68 | --screenshot-interval 5000 \ 69 | --enable-webcam \ 70 | --webcam-interval 3000 \ 71 | --image-quality 80 72 | ``` 73 | 74 | ### Command Line Options 75 | 76 | - `--endpoint`: Your S3-compatible storage endpoint URL 77 | - `--key`: Your AWS/S3 access key 78 | - `--secret`: Your AWS/S3 secret key 79 | - `--enable-screenshot`: Enable screenshot capture 80 | - `--screenshot-interval`: Interval between screenshots (ms) 81 | - `--enable-webcam`: Enable webcam capture 82 | - `--webcam-interval`: Interval between webcam captures (ms) 83 | - `--webcam-device`: Specify webcam device (optional) 84 | - `--image-quality`: Image quality for webcam/screenshots (1-100) 85 | 86 | ## How It Works 87 | 88 | 1. **Audio Monitoring** 89 | 90 | - Continuous low-quality audio monitoring 91 | - Switches to high-quality recording when speech is detected 92 | - Automatic recording start and stop based on sound levels 93 | 94 | 2. **Screenshot Capture** 95 | 96 | - Captures screenshots at specified intervals 97 | - Immediate upload to S3 98 | - Configurable capture frequency 99 | 100 | 3. **Webcam Capture** 101 | 102 | - Captures webcam images at specified intervals 103 | - Supports multiple webcam devices 104 | - Immediate upload to S3 105 | 106 | 4. **Efficient Upload** 107 | - Queued upload processing 108 | - Concurrent uploads with multi-part support 109 | - Minimal system resource overhead 110 | 111 | ### Multimedia Specifications 112 | 113 | **Audio** 114 | 115 | - Monitoring: 8kHz, Mono 116 | - Recording: 44.1kHz, Stereo 117 | - Format: WAV (16-bit PCM) 118 | 119 | **Screenshots** 120 | 121 | - Captured at system screen resolution 122 | - Uploaded as PNG 123 | 124 | **Webcam** 125 | 126 | - Resolution: 1280x720 127 | - Format: JPEG 128 | - Configurable quality 129 | 130 | ## Troubleshooting 131 | 132 | 1. **"Command not found: rec"** 133 | 134 | - Ensure SoX is installed correctly 135 | - Verify SoX is in your system PATH 136 | 137 | 2. **S3 Upload Issues** 138 | 139 | - Check S3 credentials 140 | - Verify bucket exists and write permissions are granted 141 | 142 | 3. **No Audio/Video Input** 143 | - Check system input devices 144 | - Verify microphone and webcam permissions 145 | 146 | ## License 147 | 148 | MIT License 149 | -------------------------------------------------------------------------------- /SPEC.md: -------------------------------------------------------------------------------- 1 | Create a node.js program that records: 2 | 3 | - audio in 8khz to detect if the mic is above 50db, and switches to 44.1khz otherwise. this way it can efficiently record mic audio but only when im talking. 4 | - screen recording at 5fps and 1440\*900 (configurable) and 16bit RGB 5 | - it streams the recording to s3 using the s3 uses the s3 api provided. 6 | - it should be able to be ran as a cli using npx efficient-recorder --endpoint ENDPOINT --key KEY --secret SECRET 7 | -------------------------------------------------------------------------------- /ideas/history/history.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require("sqlite3").verbose(); 2 | const fs = require("fs/promises"); 3 | const path = require("path"); 4 | const os = require("os"); 5 | 6 | const SAFARI_HISTORY_PATH = path.join( 7 | os.homedir(), 8 | "Library/Safari/History.db", 9 | ); 10 | const CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds 11 | 12 | async function getRecentHistory() { 13 | try { 14 | // Create a temporary copy of the database 15 | const tempDBPath = path.join( 16 | os.tmpdir(), 17 | `safari_history_temp_${Date.now()}.db`, 18 | ); 19 | await fs.copyFile(SAFARI_HISTORY_PATH, tempDBPath); 20 | 21 | const db = new sqlite3.Database(tempDBPath); 22 | 23 | // Safari uses a different timestamp format (macOS time) 24 | // Convert our current timestamp to Safari's format 25 | const tenMinutesAgo = Date.now() / 1000 - 10 * 60; 26 | 27 | return new Promise((resolve, reject) => { 28 | // Let's first debug what timestamps look like 29 | db.all( 30 | ` 31 | SELECT 32 | history_items.url, 33 | history_visits.visit_time, 34 | history_items.title 35 | FROM history_items 36 | JOIN history_visits 37 | ON history_items.id = history_visits.history_item 38 | ORDER BY history_visits.visit_time DESC 39 | LIMIT 5 40 | `, 41 | [], 42 | async (err, rows) => { 43 | if (err) { 44 | reject(err); 45 | return; 46 | } 47 | 48 | // Clean up temp file 49 | db.close(async () => { 50 | try { 51 | await fs.unlink(tempDBPath); 52 | } catch (e) { 53 | console.error("Error cleaning up temp file:", e); 54 | } 55 | 56 | const processedRows = rows.map((row) => ({ 57 | url: row.url, 58 | title: row.title, 59 | visit_time: new Date(row.visit_time * 1000).toISOString(), 60 | raw_timestamp: row.visit_time, // Adding this for debugging 61 | })); 62 | 63 | resolve(processedRows); 64 | }); 65 | }, 66 | ); 67 | }); 68 | } catch (error) { 69 | throw new Error(`Failed to read Safari history: ${error.message}`); 70 | } 71 | } 72 | 73 | async function checkHistory() { 74 | try { 75 | console.log(`\nChecking history at ${new Date().toISOString()}`); 76 | const history = await getRecentHistory(); 77 | 78 | if (history.length > 0) { 79 | console.log(`Found ${history.length} most recent entries:`); 80 | history.forEach((item) => { 81 | console.log(`${item.visit_time} - ${item.title || "No title"}`); 82 | console.log(`URL: ${item.url}`); 83 | console.log(`Raw timestamp: ${item.raw_timestamp}`); 84 | console.log("---"); 85 | }); 86 | 87 | // For debugging, let's also log the current timestamp 88 | console.log(`Current timestamp: ${Date.now() / 1000}`); 89 | } else { 90 | console.log("No history entries found"); 91 | } 92 | } catch (error) { 93 | console.error("Error:", error.message); 94 | } 95 | } 96 | 97 | async function main() { 98 | console.log("Starting Safari history monitor (DEBUG MODE)..."); 99 | 100 | // Run immediately on start 101 | await checkHistory(); 102 | 103 | // Then set up interval 104 | setInterval(checkHistory, CHECK_INTERVAL); 105 | } 106 | 107 | // Handle graceful shutdown 108 | process.on("SIGINT", async () => { 109 | console.log("\nShutting down..."); 110 | process.exit(0); 111 | }); 112 | 113 | // Run the script 114 | main().catch(console.error); 115 | -------------------------------------------------------------------------------- /ideas/history/history.md: -------------------------------------------------------------------------------- 1 | We could also intermittently safe browsing history besides the screenshots/video and audio, because this could be a great entry point for the agent to retrieve data as context if you are later talking to it about what you were doing on the pc. 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { S3Client } = require("@aws-sdk/client-s3"); 4 | const { Upload } = require("@aws-sdk/lib-storage"); 5 | const AudioRecorder = require("node-audiorecorder"); 6 | const screenshot = require("screenshot-desktop"); 7 | const NodeWebcam = require("node-webcam"); 8 | const { program } = require("commander"); 9 | const { PassThrough } = require("stream"); 10 | 11 | // CLI configuration 12 | program 13 | .requiredOption("--endpoint ", "S3 endpoint") 14 | .requiredOption("--region ", "S3 region") 15 | .requiredOption("--key ", "AWS access key") 16 | .requiredOption("--secret ", "AWS secret key") 17 | .option( 18 | "--screenshot-interval ", 19 | "Screenshot interval in ms", 20 | "1000", 21 | ) 22 | .option( 23 | "--webcam-interval ", 24 | "Webcam capture interval in ms", 25 | "1000", 26 | ) 27 | .option("--enable-screenshot", "Enable screenshot capture", false) 28 | .option("--enable-webcam", "Enable webcam capture", false) 29 | .option("--webcam-device ", "Webcam device name") 30 | .option("--image-quality ", "Image quality (1-100)", "80") 31 | .parse(process.argv); 32 | 33 | const opts = program.opts(); 34 | 35 | // Initialize S3 client 36 | const s3Client = new S3Client({ 37 | endpoint: opts.endpoint, 38 | region: opts.region, 39 | credentials: { 40 | accessKeyId: opts.key, 41 | secretAccessKey: opts.secret, 42 | }, 43 | forcePathStyle: true, 44 | }); 45 | 46 | // Initialize webcam 47 | const webcamOptions = { 48 | width: 1280, 49 | height: 720, 50 | quality: parseInt(opts.imageQuality), 51 | delay: 0, 52 | saveShots: false, 53 | output: "buffer", 54 | device: opts.webcamDevice, 55 | callbackReturn: "buffer", 56 | verbose: false, 57 | }; 58 | 59 | class EfficientRecorder { 60 | constructor() { 61 | this.isRecording = false; 62 | this.currentStream = null; 63 | this.monitorStream = null; 64 | this.lowQualityRecorder = null; 65 | this.highQualityRecorder = null; 66 | this.silenceTimer = null; 67 | this.recordingChunks = []; 68 | this.screenshotInterval = null; 69 | this.webcamInterval = null; 70 | this.webcam = null; 71 | this.uploadQueue = []; 72 | this.isUploading = false; 73 | this.setupRecorders(); 74 | } 75 | 76 | setupRecorders() { 77 | // Low quality recorder for detection (8kHz) 78 | this.lowQualityRecorder = new AudioRecorder( 79 | { 80 | program: "rec", 81 | rate: 8000, 82 | channels: 1, 83 | silence: 0, 84 | thresholdStart: 0.5, 85 | thresholdStop: 0.5, 86 | keepSilence: true, 87 | }, 88 | console, 89 | ); 90 | 91 | // High quality recorder for actual recording (44.1kHz) 92 | this.highQualityRecorder = new AudioRecorder( 93 | { 94 | program: "rec", 95 | rate: 44100, 96 | channels: 2, 97 | silence: 0, 98 | thresholdStart: 0, 99 | thresholdStop: 0, 100 | keepSilence: true, 101 | }, 102 | console, 103 | ); 104 | 105 | // Setup webcam if enabled 106 | if (opts.enableWebcam) { 107 | this.webcam = NodeWebcam.create(webcamOptions); 108 | // Promisify the capture method 109 | this.captureWebcam = () => { 110 | return new Promise((resolve, reject) => { 111 | this.webcam.capture("", (err, data) => { 112 | if (err) reject(err); 113 | else resolve(data); 114 | }); 115 | }); 116 | }; 117 | } 118 | } 119 | 120 | async start() { 121 | console.log("Starting efficient recorder..."); 122 | this.startMonitoring(); 123 | 124 | if (opts.enableScreenshot) { 125 | this.startScreenshotCapture(); 126 | } 127 | 128 | if (opts.enableWebcam) { 129 | this.startWebcamCapture(); 130 | } 131 | 132 | // Start the upload processor 133 | this.processUploadQueue(); 134 | } 135 | 136 | startWebcamCapture() { 137 | const interval = parseInt(opts.webcamInterval); 138 | console.log(`Starting webcam capture with interval: ${interval}ms`); 139 | 140 | this.webcamInterval = setInterval(async () => { 141 | try { 142 | const imageBuffer = await this.captureWebcam(); 143 | this.queueUpload(imageBuffer, "webcam"); 144 | } catch (error) { 145 | console.error("Error capturing webcam:", error); 146 | } 147 | }, interval); 148 | } 149 | 150 | startScreenshotCapture() { 151 | const interval = parseInt(opts.screenshotInterval); 152 | console.log(`Starting screenshot capture with interval: ${interval}ms`); 153 | 154 | this.screenshotInterval = setInterval(async () => { 155 | try { 156 | const screenshotBuffer = await screenshot({ format: "png" }); 157 | this.queueUpload(screenshotBuffer, "screenshot"); 158 | } catch (error) { 159 | console.error("Error capturing screenshot:", error); 160 | } 161 | }, interval); 162 | } 163 | 164 | queueUpload(buffer, type) { 165 | this.uploadQueue.push({ 166 | buffer, 167 | type, 168 | timestamp: new Date().toISOString(), 169 | }); 170 | } 171 | 172 | async processUploadQueue() { 173 | while (true) { 174 | if (this.uploadQueue.length > 0 && !this.isUploading) { 175 | this.isUploading = true; 176 | const item = this.uploadQueue.shift(); 177 | try { 178 | await this.uploadImage(item.buffer, item.type, item.timestamp); 179 | } catch (error) { 180 | console.error(`Error processing upload for ${item.type}:`, error); 181 | } 182 | this.isUploading = false; 183 | } 184 | await new Promise((resolve) => setTimeout(resolve, 100)); // Small delay to prevent CPU hogging 185 | } 186 | } 187 | 188 | async uploadImage(buffer, type, timestamp) { 189 | try { 190 | const key = `${type}-${timestamp}.png`; 191 | 192 | const upload = new Upload({ 193 | client: s3Client, 194 | params: { 195 | Bucket: "recordings", 196 | Key: key, 197 | Body: buffer, 198 | ContentType: type === "webcam" ? "image/jpeg" : "image/png", 199 | }, 200 | queueSize: 4, 201 | partSize: 1024 * 1024 * 5, // 5MB parts 202 | }); 203 | 204 | const result = await upload.done(); 205 | console.log(`${type} upload completed:`, result.Key); 206 | } catch (error) { 207 | console.error(`Error uploading ${type}:`, error); 208 | } 209 | } 210 | 211 | startMonitoring() { 212 | // Start the low quality recorder and get its stream 213 | this.lowQualityRecorder.start(); 214 | this.monitorStream = this.lowQualityRecorder.stream(); 215 | 216 | // Process the monitor stream 217 | this.monitorStream.on("data", (chunk) => { 218 | // Convert Buffer to Int16Array for proper audio sample reading 219 | const samples = new Int16Array(chunk.buffer); 220 | 221 | // Calculate RMS of the audio buffer to estimate dB level 222 | const rms = Math.sqrt( 223 | Array.from(samples).reduce((sum, value) => sum + value * value, 0) / 224 | samples.length, 225 | ); 226 | const db = 20 * Math.log10(rms); 227 | 228 | if (db > 50 && !this.isRecording) { 229 | this.startRecording(); 230 | } else if (db <= 50 && this.isRecording) { 231 | if (!this.silenceTimer) { 232 | this.silenceTimer = setTimeout(() => { 233 | this.stopRecording(); 234 | }, 2000); 235 | } 236 | } else if (db > 50 && this.silenceTimer) { 237 | clearTimeout(this.silenceTimer); 238 | this.silenceTimer = null; 239 | } 240 | }); 241 | 242 | this.monitorStream.on("error", (error) => { 243 | console.error("Error in monitoring stream:", error); 244 | }); 245 | } 246 | 247 | startRecording() { 248 | if (this.isRecording) return; 249 | 250 | console.log("Starting high-quality recording..."); 251 | this.startTime = Date.now(); 252 | this.isRecording = true; 253 | this.recordingChunks = []; 254 | 255 | // Start high quality recording and get its stream 256 | this.highQualityRecorder.start(); 257 | this.currentStream = this.highQualityRecorder.stream(); 258 | 259 | // Collect chunks of audio data 260 | this.currentStream.on("data", (chunk) => { 261 | this.recordingChunks.push(chunk); 262 | }); 263 | 264 | // Handle any errors in the recording stream 265 | this.currentStream.on("error", (err) => { 266 | console.error("Error in recording stream:", err); 267 | }); 268 | } 269 | 270 | async stopRecording() { 271 | if (!this.isRecording) return; 272 | 273 | const duration = (Date.now() - this.startTime) / 1000; 274 | console.log("Stopping recording... Duration:", duration, "seconds"); 275 | 276 | this.isRecording = false; 277 | this.silenceTimer = null; 278 | 279 | // Stop the recorder 280 | this.highQualityRecorder.stop(); 281 | 282 | // Wait a bit for any final chunks 283 | await new Promise((resolve) => setTimeout(resolve, 500)); 284 | 285 | try { 286 | // Combine all chunks into a single buffer 287 | const completeBuffer = Buffer.concat(this.recordingChunks); 288 | console.log(`Total recording size: ${completeBuffer.length} bytes`); 289 | 290 | // Create and start the upload 291 | const timestamp = new Date().toISOString(); 292 | const key = `recording-${timestamp}.wav`; 293 | 294 | const upload = new Upload({ 295 | client: s3Client, 296 | params: { 297 | Bucket: "recordings", 298 | Key: key, 299 | Body: completeBuffer, 300 | ContentType: "audio/wav", 301 | }, 302 | queueSize: 4, 303 | partSize: 1024 * 1024 * 5, // 5MB parts 304 | }); 305 | 306 | const result = await upload.done(); 307 | console.log("Audio upload completed successfully:", result.Key); 308 | 309 | // Clean up 310 | this.currentStream = null; 311 | this.recordingChunks = []; 312 | } catch (err) { 313 | console.error("Error completing audio upload:", err); 314 | throw err; 315 | } 316 | } 317 | 318 | async cleanup() { 319 | if (this.screenshotInterval) { 320 | clearInterval(this.screenshotInterval); 321 | } 322 | if (this.webcamInterval) { 323 | clearInterval(this.webcamInterval); 324 | if (this.webcam) { 325 | this.webcam.clear(); 326 | } 327 | } 328 | if (this.isRecording) { 329 | await this.stopRecording(); 330 | } 331 | if (this.lowQualityRecorder) { 332 | this.lowQualityRecorder.stop(); 333 | } 334 | 335 | // Wait for any pending uploads to complete 336 | while (this.uploadQueue.length > 0 || this.isUploading) { 337 | await new Promise((resolve) => setTimeout(resolve, 100)); 338 | } 339 | } 340 | } 341 | 342 | // Start the recorder 343 | const recorder = new EfficientRecorder(); 344 | recorder.start(); 345 | 346 | // Handle cleanup on exit 347 | process.on("SIGINT", async () => { 348 | console.log("Cleaning up..."); 349 | await recorder.cleanup(); 350 | process.exit(0); 351 | }); 352 | 353 | // Handle uncaught errors 354 | process.on("uncaughtException", async (error) => { 355 | console.error("Uncaught exception:", error); 356 | await recorder.cleanup(); 357 | process.exit(1); 358 | }); 359 | 360 | process.on("unhandledRejection", async (error) => { 361 | console.error("Unhandled rejection:", error); 362 | await recorder.cleanup(); 363 | process.exit(1); 364 | }); 365 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "efficient-recorder", 3 | "version": "1.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "efficient-recorder", 9 | "version": "1.0.1", 10 | "dependencies": { 11 | "@aws-sdk/client-s3": "^3.722.0", 12 | "@aws-sdk/lib-storage": "^3.722.0", 13 | "commander": "^13.0.0", 14 | "node-audiorecorder": "^3.0.0", 15 | "node-webcam": "^0.8.2", 16 | "screenshot-desktop": "^1.15.1" 17 | }, 18 | "bin": { 19 | "efficient-recorder": "index.js" 20 | }, 21 | "engines": { 22 | "node": ">=14.0.0" 23 | } 24 | }, 25 | "node_modules/@aws-crypto/crc32": { 26 | "version": "5.2.0", 27 | "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", 28 | "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", 29 | "license": "Apache-2.0", 30 | "dependencies": { 31 | "@aws-crypto/util": "^5.2.0", 32 | "@aws-sdk/types": "^3.222.0", 33 | "tslib": "^2.6.2" 34 | }, 35 | "engines": { 36 | "node": ">=16.0.0" 37 | } 38 | }, 39 | "node_modules/@aws-crypto/crc32c": { 40 | "version": "5.2.0", 41 | "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", 42 | "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", 43 | "license": "Apache-2.0", 44 | "dependencies": { 45 | "@aws-crypto/util": "^5.2.0", 46 | "@aws-sdk/types": "^3.222.0", 47 | "tslib": "^2.6.2" 48 | } 49 | }, 50 | "node_modules/@aws-crypto/sha1-browser": { 51 | "version": "5.2.0", 52 | "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", 53 | "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", 54 | "license": "Apache-2.0", 55 | "dependencies": { 56 | "@aws-crypto/supports-web-crypto": "^5.2.0", 57 | "@aws-crypto/util": "^5.2.0", 58 | "@aws-sdk/types": "^3.222.0", 59 | "@aws-sdk/util-locate-window": "^3.0.0", 60 | "@smithy/util-utf8": "^2.0.0", 61 | "tslib": "^2.6.2" 62 | } 63 | }, 64 | "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { 65 | "version": "2.2.0", 66 | "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", 67 | "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 68 | "license": "Apache-2.0", 69 | "dependencies": { 70 | "tslib": "^2.6.2" 71 | }, 72 | "engines": { 73 | "node": ">=14.0.0" 74 | } 75 | }, 76 | "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { 77 | "version": "2.2.0", 78 | "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", 79 | "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 80 | "license": "Apache-2.0", 81 | "dependencies": { 82 | "@smithy/is-array-buffer": "^2.2.0", 83 | "tslib": "^2.6.2" 84 | }, 85 | "engines": { 86 | "node": ">=14.0.0" 87 | } 88 | }, 89 | "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { 90 | "version": "2.3.0", 91 | "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", 92 | "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 93 | "license": "Apache-2.0", 94 | "dependencies": { 95 | "@smithy/util-buffer-from": "^2.2.0", 96 | "tslib": "^2.6.2" 97 | }, 98 | "engines": { 99 | "node": ">=14.0.0" 100 | } 101 | }, 102 | "node_modules/@aws-crypto/sha256-browser": { 103 | "version": "5.2.0", 104 | "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", 105 | "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", 106 | "license": "Apache-2.0", 107 | "dependencies": { 108 | "@aws-crypto/sha256-js": "^5.2.0", 109 | "@aws-crypto/supports-web-crypto": "^5.2.0", 110 | "@aws-crypto/util": "^5.2.0", 111 | "@aws-sdk/types": "^3.222.0", 112 | "@aws-sdk/util-locate-window": "^3.0.0", 113 | "@smithy/util-utf8": "^2.0.0", 114 | "tslib": "^2.6.2" 115 | } 116 | }, 117 | "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { 118 | "version": "2.2.0", 119 | "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", 120 | "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 121 | "license": "Apache-2.0", 122 | "dependencies": { 123 | "tslib": "^2.6.2" 124 | }, 125 | "engines": { 126 | "node": ">=14.0.0" 127 | } 128 | }, 129 | "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { 130 | "version": "2.2.0", 131 | "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", 132 | "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 133 | "license": "Apache-2.0", 134 | "dependencies": { 135 | "@smithy/is-array-buffer": "^2.2.0", 136 | "tslib": "^2.6.2" 137 | }, 138 | "engines": { 139 | "node": ">=14.0.0" 140 | } 141 | }, 142 | "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { 143 | "version": "2.3.0", 144 | "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", 145 | "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 146 | "license": "Apache-2.0", 147 | "dependencies": { 148 | "@smithy/util-buffer-from": "^2.2.0", 149 | "tslib": "^2.6.2" 150 | }, 151 | "engines": { 152 | "node": ">=14.0.0" 153 | } 154 | }, 155 | "node_modules/@aws-crypto/sha256-js": { 156 | "version": "5.2.0", 157 | "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", 158 | "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", 159 | "license": "Apache-2.0", 160 | "dependencies": { 161 | "@aws-crypto/util": "^5.2.0", 162 | "@aws-sdk/types": "^3.222.0", 163 | "tslib": "^2.6.2" 164 | }, 165 | "engines": { 166 | "node": ">=16.0.0" 167 | } 168 | }, 169 | "node_modules/@aws-crypto/supports-web-crypto": { 170 | "version": "5.2.0", 171 | "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", 172 | "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", 173 | "license": "Apache-2.0", 174 | "dependencies": { 175 | "tslib": "^2.6.2" 176 | } 177 | }, 178 | "node_modules/@aws-crypto/util": { 179 | "version": "5.2.0", 180 | "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", 181 | "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", 182 | "license": "Apache-2.0", 183 | "dependencies": { 184 | "@aws-sdk/types": "^3.222.0", 185 | "@smithy/util-utf8": "^2.0.0", 186 | "tslib": "^2.6.2" 187 | } 188 | }, 189 | "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { 190 | "version": "2.2.0", 191 | "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", 192 | "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 193 | "license": "Apache-2.0", 194 | "dependencies": { 195 | "tslib": "^2.6.2" 196 | }, 197 | "engines": { 198 | "node": ">=14.0.0" 199 | } 200 | }, 201 | "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { 202 | "version": "2.2.0", 203 | "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", 204 | "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 205 | "license": "Apache-2.0", 206 | "dependencies": { 207 | "@smithy/is-array-buffer": "^2.2.0", 208 | "tslib": "^2.6.2" 209 | }, 210 | "engines": { 211 | "node": ">=14.0.0" 212 | } 213 | }, 214 | "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { 215 | "version": "2.3.0", 216 | "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", 217 | "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 218 | "license": "Apache-2.0", 219 | "dependencies": { 220 | "@smithy/util-buffer-from": "^2.2.0", 221 | "tslib": "^2.6.2" 222 | }, 223 | "engines": { 224 | "node": ">=14.0.0" 225 | } 226 | }, 227 | "node_modules/@aws-sdk/client-s3": { 228 | "version": "3.722.0", 229 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.722.0.tgz", 230 | "integrity": "sha512-FttdkB39TKjqEITfZJcs6Ihh6alICsNEne0ouLvh8re+gAuTK96zWcfX22mP5ap1QEsATaOGRNsMnyfsDSM0zw==", 231 | "license": "Apache-2.0", 232 | "dependencies": { 233 | "@aws-crypto/sha1-browser": "5.2.0", 234 | "@aws-crypto/sha256-browser": "5.2.0", 235 | "@aws-crypto/sha256-js": "5.2.0", 236 | "@aws-sdk/client-sso-oidc": "3.721.0", 237 | "@aws-sdk/client-sts": "3.721.0", 238 | "@aws-sdk/core": "3.716.0", 239 | "@aws-sdk/credential-provider-node": "3.721.0", 240 | "@aws-sdk/middleware-bucket-endpoint": "3.721.0", 241 | "@aws-sdk/middleware-expect-continue": "3.714.0", 242 | "@aws-sdk/middleware-flexible-checksums": "3.717.0", 243 | "@aws-sdk/middleware-host-header": "3.714.0", 244 | "@aws-sdk/middleware-location-constraint": "3.714.0", 245 | "@aws-sdk/middleware-logger": "3.714.0", 246 | "@aws-sdk/middleware-recursion-detection": "3.714.0", 247 | "@aws-sdk/middleware-sdk-s3": "3.716.0", 248 | "@aws-sdk/middleware-ssec": "3.714.0", 249 | "@aws-sdk/middleware-user-agent": "3.721.0", 250 | "@aws-sdk/region-config-resolver": "3.714.0", 251 | "@aws-sdk/signature-v4-multi-region": "3.716.0", 252 | "@aws-sdk/types": "3.714.0", 253 | "@aws-sdk/util-endpoints": "3.714.0", 254 | "@aws-sdk/util-user-agent-browser": "3.714.0", 255 | "@aws-sdk/util-user-agent-node": "3.721.0", 256 | "@aws-sdk/xml-builder": "3.709.0", 257 | "@smithy/config-resolver": "^3.0.13", 258 | "@smithy/core": "^2.5.5", 259 | "@smithy/eventstream-serde-browser": "^3.0.14", 260 | "@smithy/eventstream-serde-config-resolver": "^3.0.11", 261 | "@smithy/eventstream-serde-node": "^3.0.13", 262 | "@smithy/fetch-http-handler": "^4.1.2", 263 | "@smithy/hash-blob-browser": "^3.1.10", 264 | "@smithy/hash-node": "^3.0.11", 265 | "@smithy/hash-stream-node": "^3.1.10", 266 | "@smithy/invalid-dependency": "^3.0.11", 267 | "@smithy/md5-js": "^3.0.11", 268 | "@smithy/middleware-content-length": "^3.0.13", 269 | "@smithy/middleware-endpoint": "^3.2.6", 270 | "@smithy/middleware-retry": "^3.0.31", 271 | "@smithy/middleware-serde": "^3.0.11", 272 | "@smithy/middleware-stack": "^3.0.11", 273 | "@smithy/node-config-provider": "^3.1.12", 274 | "@smithy/node-http-handler": "^3.3.2", 275 | "@smithy/protocol-http": "^4.1.8", 276 | "@smithy/smithy-client": "^3.5.1", 277 | "@smithy/types": "^3.7.2", 278 | "@smithy/url-parser": "^3.0.11", 279 | "@smithy/util-base64": "^3.0.0", 280 | "@smithy/util-body-length-browser": "^3.0.0", 281 | "@smithy/util-body-length-node": "^3.0.0", 282 | "@smithy/util-defaults-mode-browser": "^3.0.31", 283 | "@smithy/util-defaults-mode-node": "^3.0.31", 284 | "@smithy/util-endpoints": "^2.1.7", 285 | "@smithy/util-middleware": "^3.0.11", 286 | "@smithy/util-retry": "^3.0.11", 287 | "@smithy/util-stream": "^3.3.2", 288 | "@smithy/util-utf8": "^3.0.0", 289 | "@smithy/util-waiter": "^3.2.0", 290 | "tslib": "^2.6.2" 291 | }, 292 | "engines": { 293 | "node": ">=16.0.0" 294 | } 295 | }, 296 | "node_modules/@aws-sdk/client-sso": { 297 | "version": "3.721.0", 298 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.721.0.tgz", 299 | "integrity": "sha512-UrYAF4ilpO2cZBFddQmbETfo0xKP3CEcantcMQTc0xPY3quHLZhYuBiRae+McWi6yZpH4ErnFZIWeKSJ2OQgqQ==", 300 | "license": "Apache-2.0", 301 | "dependencies": { 302 | "@aws-crypto/sha256-browser": "5.2.0", 303 | "@aws-crypto/sha256-js": "5.2.0", 304 | "@aws-sdk/core": "3.716.0", 305 | "@aws-sdk/middleware-host-header": "3.714.0", 306 | "@aws-sdk/middleware-logger": "3.714.0", 307 | "@aws-sdk/middleware-recursion-detection": "3.714.0", 308 | "@aws-sdk/middleware-user-agent": "3.721.0", 309 | "@aws-sdk/region-config-resolver": "3.714.0", 310 | "@aws-sdk/types": "3.714.0", 311 | "@aws-sdk/util-endpoints": "3.714.0", 312 | "@aws-sdk/util-user-agent-browser": "3.714.0", 313 | "@aws-sdk/util-user-agent-node": "3.721.0", 314 | "@smithy/config-resolver": "^3.0.13", 315 | "@smithy/core": "^2.5.5", 316 | "@smithy/fetch-http-handler": "^4.1.2", 317 | "@smithy/hash-node": "^3.0.11", 318 | "@smithy/invalid-dependency": "^3.0.11", 319 | "@smithy/middleware-content-length": "^3.0.13", 320 | "@smithy/middleware-endpoint": "^3.2.6", 321 | "@smithy/middleware-retry": "^3.0.31", 322 | "@smithy/middleware-serde": "^3.0.11", 323 | "@smithy/middleware-stack": "^3.0.11", 324 | "@smithy/node-config-provider": "^3.1.12", 325 | "@smithy/node-http-handler": "^3.3.2", 326 | "@smithy/protocol-http": "^4.1.8", 327 | "@smithy/smithy-client": "^3.5.1", 328 | "@smithy/types": "^3.7.2", 329 | "@smithy/url-parser": "^3.0.11", 330 | "@smithy/util-base64": "^3.0.0", 331 | "@smithy/util-body-length-browser": "^3.0.0", 332 | "@smithy/util-body-length-node": "^3.0.0", 333 | "@smithy/util-defaults-mode-browser": "^3.0.31", 334 | "@smithy/util-defaults-mode-node": "^3.0.31", 335 | "@smithy/util-endpoints": "^2.1.7", 336 | "@smithy/util-middleware": "^3.0.11", 337 | "@smithy/util-retry": "^3.0.11", 338 | "@smithy/util-utf8": "^3.0.0", 339 | "tslib": "^2.6.2" 340 | }, 341 | "engines": { 342 | "node": ">=16.0.0" 343 | } 344 | }, 345 | "node_modules/@aws-sdk/client-sso-oidc": { 346 | "version": "3.721.0", 347 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.721.0.tgz", 348 | "integrity": "sha512-jwsgdUEbNJqs1O0AQtf9M6SI7hFIjxH+IKeKCMca0xVt+Tr1UqLr/qMK/6W8LoMtRFnE0lpBSHW6hvmLp2OCoQ==", 349 | "license": "Apache-2.0", 350 | "dependencies": { 351 | "@aws-crypto/sha256-browser": "5.2.0", 352 | "@aws-crypto/sha256-js": "5.2.0", 353 | "@aws-sdk/core": "3.716.0", 354 | "@aws-sdk/credential-provider-node": "3.721.0", 355 | "@aws-sdk/middleware-host-header": "3.714.0", 356 | "@aws-sdk/middleware-logger": "3.714.0", 357 | "@aws-sdk/middleware-recursion-detection": "3.714.0", 358 | "@aws-sdk/middleware-user-agent": "3.721.0", 359 | "@aws-sdk/region-config-resolver": "3.714.0", 360 | "@aws-sdk/types": "3.714.0", 361 | "@aws-sdk/util-endpoints": "3.714.0", 362 | "@aws-sdk/util-user-agent-browser": "3.714.0", 363 | "@aws-sdk/util-user-agent-node": "3.721.0", 364 | "@smithy/config-resolver": "^3.0.13", 365 | "@smithy/core": "^2.5.5", 366 | "@smithy/fetch-http-handler": "^4.1.2", 367 | "@smithy/hash-node": "^3.0.11", 368 | "@smithy/invalid-dependency": "^3.0.11", 369 | "@smithy/middleware-content-length": "^3.0.13", 370 | "@smithy/middleware-endpoint": "^3.2.6", 371 | "@smithy/middleware-retry": "^3.0.31", 372 | "@smithy/middleware-serde": "^3.0.11", 373 | "@smithy/middleware-stack": "^3.0.11", 374 | "@smithy/node-config-provider": "^3.1.12", 375 | "@smithy/node-http-handler": "^3.3.2", 376 | "@smithy/protocol-http": "^4.1.8", 377 | "@smithy/smithy-client": "^3.5.1", 378 | "@smithy/types": "^3.7.2", 379 | "@smithy/url-parser": "^3.0.11", 380 | "@smithy/util-base64": "^3.0.0", 381 | "@smithy/util-body-length-browser": "^3.0.0", 382 | "@smithy/util-body-length-node": "^3.0.0", 383 | "@smithy/util-defaults-mode-browser": "^3.0.31", 384 | "@smithy/util-defaults-mode-node": "^3.0.31", 385 | "@smithy/util-endpoints": "^2.1.7", 386 | "@smithy/util-middleware": "^3.0.11", 387 | "@smithy/util-retry": "^3.0.11", 388 | "@smithy/util-utf8": "^3.0.0", 389 | "tslib": "^2.6.2" 390 | }, 391 | "engines": { 392 | "node": ">=16.0.0" 393 | }, 394 | "peerDependencies": { 395 | "@aws-sdk/client-sts": "^3.721.0" 396 | } 397 | }, 398 | "node_modules/@aws-sdk/client-sts": { 399 | "version": "3.721.0", 400 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.721.0.tgz", 401 | "integrity": "sha512-1Pv8F02hQFmPZs7WtGfQNlnInbG1lLzyngJc/MlZ3Ld2fIoWjaWp7bJWgYAjnzHNEuDtCabWJvIfePdRqsbYoA==", 402 | "license": "Apache-2.0", 403 | "dependencies": { 404 | "@aws-crypto/sha256-browser": "5.2.0", 405 | "@aws-crypto/sha256-js": "5.2.0", 406 | "@aws-sdk/client-sso-oidc": "3.721.0", 407 | "@aws-sdk/core": "3.716.0", 408 | "@aws-sdk/credential-provider-node": "3.721.0", 409 | "@aws-sdk/middleware-host-header": "3.714.0", 410 | "@aws-sdk/middleware-logger": "3.714.0", 411 | "@aws-sdk/middleware-recursion-detection": "3.714.0", 412 | "@aws-sdk/middleware-user-agent": "3.721.0", 413 | "@aws-sdk/region-config-resolver": "3.714.0", 414 | "@aws-sdk/types": "3.714.0", 415 | "@aws-sdk/util-endpoints": "3.714.0", 416 | "@aws-sdk/util-user-agent-browser": "3.714.0", 417 | "@aws-sdk/util-user-agent-node": "3.721.0", 418 | "@smithy/config-resolver": "^3.0.13", 419 | "@smithy/core": "^2.5.5", 420 | "@smithy/fetch-http-handler": "^4.1.2", 421 | "@smithy/hash-node": "^3.0.11", 422 | "@smithy/invalid-dependency": "^3.0.11", 423 | "@smithy/middleware-content-length": "^3.0.13", 424 | "@smithy/middleware-endpoint": "^3.2.6", 425 | "@smithy/middleware-retry": "^3.0.31", 426 | "@smithy/middleware-serde": "^3.0.11", 427 | "@smithy/middleware-stack": "^3.0.11", 428 | "@smithy/node-config-provider": "^3.1.12", 429 | "@smithy/node-http-handler": "^3.3.2", 430 | "@smithy/protocol-http": "^4.1.8", 431 | "@smithy/smithy-client": "^3.5.1", 432 | "@smithy/types": "^3.7.2", 433 | "@smithy/url-parser": "^3.0.11", 434 | "@smithy/util-base64": "^3.0.0", 435 | "@smithy/util-body-length-browser": "^3.0.0", 436 | "@smithy/util-body-length-node": "^3.0.0", 437 | "@smithy/util-defaults-mode-browser": "^3.0.31", 438 | "@smithy/util-defaults-mode-node": "^3.0.31", 439 | "@smithy/util-endpoints": "^2.1.7", 440 | "@smithy/util-middleware": "^3.0.11", 441 | "@smithy/util-retry": "^3.0.11", 442 | "@smithy/util-utf8": "^3.0.0", 443 | "tslib": "^2.6.2" 444 | }, 445 | "engines": { 446 | "node": ">=16.0.0" 447 | } 448 | }, 449 | "node_modules/@aws-sdk/core": { 450 | "version": "3.716.0", 451 | "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.716.0.tgz", 452 | "integrity": "sha512-5DkUiTrbyzO8/W4g7UFEqRFpuhgizayHI/Zbh0wtFMcot8801nJV+MP/YMhdjimlvAr/OqYB08FbGsPyWppMTw==", 453 | "license": "Apache-2.0", 454 | "dependencies": { 455 | "@aws-sdk/types": "3.714.0", 456 | "@smithy/core": "^2.5.5", 457 | "@smithy/node-config-provider": "^3.1.12", 458 | "@smithy/property-provider": "^3.1.11", 459 | "@smithy/protocol-http": "^4.1.8", 460 | "@smithy/signature-v4": "^4.2.4", 461 | "@smithy/smithy-client": "^3.5.1", 462 | "@smithy/types": "^3.7.2", 463 | "@smithy/util-middleware": "^3.0.11", 464 | "fast-xml-parser": "4.4.1", 465 | "tslib": "^2.6.2" 466 | }, 467 | "engines": { 468 | "node": ">=16.0.0" 469 | } 470 | }, 471 | "node_modules/@aws-sdk/credential-provider-env": { 472 | "version": "3.716.0", 473 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.716.0.tgz", 474 | "integrity": "sha512-JI2KQUnn2arICwP9F3CnqP1W3nAbm4+meQg/yOhp9X0DMzQiHrHRd4HIrK2vyVgi2/6hGhONY5uLF26yRTA7nQ==", 475 | "license": "Apache-2.0", 476 | "dependencies": { 477 | "@aws-sdk/core": "3.716.0", 478 | "@aws-sdk/types": "3.714.0", 479 | "@smithy/property-provider": "^3.1.11", 480 | "@smithy/types": "^3.7.2", 481 | "tslib": "^2.6.2" 482 | }, 483 | "engines": { 484 | "node": ">=16.0.0" 485 | } 486 | }, 487 | "node_modules/@aws-sdk/credential-provider-http": { 488 | "version": "3.716.0", 489 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.716.0.tgz", 490 | "integrity": "sha512-CZ04pl2z7igQPysQyH2xKZHM3fLwkemxQbKOlje3TmiS1NwXvcKvERhp9PE/H23kOL7beTM19NMRog/Fka/rlw==", 491 | "license": "Apache-2.0", 492 | "dependencies": { 493 | "@aws-sdk/core": "3.716.0", 494 | "@aws-sdk/types": "3.714.0", 495 | "@smithy/fetch-http-handler": "^4.1.2", 496 | "@smithy/node-http-handler": "^3.3.2", 497 | "@smithy/property-provider": "^3.1.11", 498 | "@smithy/protocol-http": "^4.1.8", 499 | "@smithy/smithy-client": "^3.5.1", 500 | "@smithy/types": "^3.7.2", 501 | "@smithy/util-stream": "^3.3.2", 502 | "tslib": "^2.6.2" 503 | }, 504 | "engines": { 505 | "node": ">=16.0.0" 506 | } 507 | }, 508 | "node_modules/@aws-sdk/credential-provider-ini": { 509 | "version": "3.721.0", 510 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.721.0.tgz", 511 | "integrity": "sha512-8J/c2rI+4ZoduBCnPurfdblqs2DyRvL9ztqzzOWWEhLccoYZzYeAMwBapEAsiVsD1iNrIGY7LRDC4TsVmJBf6Q==", 512 | "license": "Apache-2.0", 513 | "dependencies": { 514 | "@aws-sdk/core": "3.716.0", 515 | "@aws-sdk/credential-provider-env": "3.716.0", 516 | "@aws-sdk/credential-provider-http": "3.716.0", 517 | "@aws-sdk/credential-provider-process": "3.716.0", 518 | "@aws-sdk/credential-provider-sso": "3.721.0", 519 | "@aws-sdk/credential-provider-web-identity": "3.716.0", 520 | "@aws-sdk/types": "3.714.0", 521 | "@smithy/credential-provider-imds": "^3.2.8", 522 | "@smithy/property-provider": "^3.1.11", 523 | "@smithy/shared-ini-file-loader": "^3.1.12", 524 | "@smithy/types": "^3.7.2", 525 | "tslib": "^2.6.2" 526 | }, 527 | "engines": { 528 | "node": ">=16.0.0" 529 | }, 530 | "peerDependencies": { 531 | "@aws-sdk/client-sts": "^3.721.0" 532 | } 533 | }, 534 | "node_modules/@aws-sdk/credential-provider-node": { 535 | "version": "3.721.0", 536 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.721.0.tgz", 537 | "integrity": "sha512-D6xodzdMjVhF9xRhy9gNf0gqP0Dek9fQ6BDZzqO/i54d7CjWHVZTADcVcxjLQq6nyUNf0QPf8UXLaqi+w25GGQ==", 538 | "license": "Apache-2.0", 539 | "dependencies": { 540 | "@aws-sdk/credential-provider-env": "3.716.0", 541 | "@aws-sdk/credential-provider-http": "3.716.0", 542 | "@aws-sdk/credential-provider-ini": "3.721.0", 543 | "@aws-sdk/credential-provider-process": "3.716.0", 544 | "@aws-sdk/credential-provider-sso": "3.721.0", 545 | "@aws-sdk/credential-provider-web-identity": "3.716.0", 546 | "@aws-sdk/types": "3.714.0", 547 | "@smithy/credential-provider-imds": "^3.2.8", 548 | "@smithy/property-provider": "^3.1.11", 549 | "@smithy/shared-ini-file-loader": "^3.1.12", 550 | "@smithy/types": "^3.7.2", 551 | "tslib": "^2.6.2" 552 | }, 553 | "engines": { 554 | "node": ">=16.0.0" 555 | } 556 | }, 557 | "node_modules/@aws-sdk/credential-provider-process": { 558 | "version": "3.716.0", 559 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.716.0.tgz", 560 | "integrity": "sha512-0spcu2MWVVHSTHH3WE2E//ttUJPwXRM3BCp+WyI41xLzpNu1Fd8zjOrDpEo0SnGUzsSiRTIJWgkuu/tqv9NJ2A==", 561 | "license": "Apache-2.0", 562 | "dependencies": { 563 | "@aws-sdk/core": "3.716.0", 564 | "@aws-sdk/types": "3.714.0", 565 | "@smithy/property-provider": "^3.1.11", 566 | "@smithy/shared-ini-file-loader": "^3.1.12", 567 | "@smithy/types": "^3.7.2", 568 | "tslib": "^2.6.2" 569 | }, 570 | "engines": { 571 | "node": ">=16.0.0" 572 | } 573 | }, 574 | "node_modules/@aws-sdk/credential-provider-sso": { 575 | "version": "3.721.0", 576 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.721.0.tgz", 577 | "integrity": "sha512-v7npnYqfuY1vdcb0/F4Mcz+mcFyZaYry9qXhSRCPIbLPe2PRV4E4HXIaPKmir8PhuRLEGs0QJWhvIWr7u6holQ==", 578 | "license": "Apache-2.0", 579 | "dependencies": { 580 | "@aws-sdk/client-sso": "3.721.0", 581 | "@aws-sdk/core": "3.716.0", 582 | "@aws-sdk/token-providers": "3.721.0", 583 | "@aws-sdk/types": "3.714.0", 584 | "@smithy/property-provider": "^3.1.11", 585 | "@smithy/shared-ini-file-loader": "^3.1.12", 586 | "@smithy/types": "^3.7.2", 587 | "tslib": "^2.6.2" 588 | }, 589 | "engines": { 590 | "node": ">=16.0.0" 591 | } 592 | }, 593 | "node_modules/@aws-sdk/credential-provider-web-identity": { 594 | "version": "3.716.0", 595 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.716.0.tgz", 596 | "integrity": "sha512-vzgpWKs2gGXZGdbMKRFrMW4PqEFWkGvwWH2T7ZwQv9m+8lQ7P4Dk2uimqu0f37HZAbpn8HFMqRh4CaySjU354A==", 597 | "license": "Apache-2.0", 598 | "dependencies": { 599 | "@aws-sdk/core": "3.716.0", 600 | "@aws-sdk/types": "3.714.0", 601 | "@smithy/property-provider": "^3.1.11", 602 | "@smithy/types": "^3.7.2", 603 | "tslib": "^2.6.2" 604 | }, 605 | "engines": { 606 | "node": ">=16.0.0" 607 | }, 608 | "peerDependencies": { 609 | "@aws-sdk/client-sts": "^3.716.0" 610 | } 611 | }, 612 | "node_modules/@aws-sdk/lib-storage": { 613 | "version": "3.722.0", 614 | "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.722.0.tgz", 615 | "integrity": "sha512-dl0aCBlQyYPVkEeGu7zSOA5mGbpDoBg0wppqTWH+By7tGEf401Pzawn5GMIk0XLKEomfu3UoPbZ8zA6BkZNRMg==", 616 | "license": "Apache-2.0", 617 | "dependencies": { 618 | "@smithy/abort-controller": "^3.1.9", 619 | "@smithy/middleware-endpoint": "^3.2.6", 620 | "@smithy/smithy-client": "^3.5.1", 621 | "buffer": "5.6.0", 622 | "events": "3.3.0", 623 | "stream-browserify": "3.0.0", 624 | "tslib": "^2.6.2" 625 | }, 626 | "engines": { 627 | "node": ">=16.0.0" 628 | }, 629 | "peerDependencies": { 630 | "@aws-sdk/client-s3": "^3.722.0" 631 | } 632 | }, 633 | "node_modules/@aws-sdk/middleware-bucket-endpoint": { 634 | "version": "3.721.0", 635 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.721.0.tgz", 636 | "integrity": "sha512-5UyoDoX3z3UhmetoqqqZulq2uF55Jyj9lUKAJWgTxVhDEG5TijTQS40LP9DqwRl0hJkoUUZKAwE0hwnUsiGXAg==", 637 | "license": "Apache-2.0", 638 | "dependencies": { 639 | "@aws-sdk/types": "3.714.0", 640 | "@aws-sdk/util-arn-parser": "3.693.0", 641 | "@smithy/node-config-provider": "^3.1.12", 642 | "@smithy/protocol-http": "^4.1.8", 643 | "@smithy/types": "^3.7.2", 644 | "@smithy/util-config-provider": "^3.0.0", 645 | "tslib": "^2.6.2" 646 | }, 647 | "engines": { 648 | "node": ">=16.0.0" 649 | } 650 | }, 651 | "node_modules/@aws-sdk/middleware-expect-continue": { 652 | "version": "3.714.0", 653 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.714.0.tgz", 654 | "integrity": "sha512-rlzsXdG8Lzo4Qpl35ZnpOBAWlzvDHpP9++0AXoUwAJA0QmMm7auIRmgxJuNj91VwT9h15ZU6xjU4S7fJl4W0+w==", 655 | "license": "Apache-2.0", 656 | "dependencies": { 657 | "@aws-sdk/types": "3.714.0", 658 | "@smithy/protocol-http": "^4.1.8", 659 | "@smithy/types": "^3.7.2", 660 | "tslib": "^2.6.2" 661 | }, 662 | "engines": { 663 | "node": ">=16.0.0" 664 | } 665 | }, 666 | "node_modules/@aws-sdk/middleware-flexible-checksums": { 667 | "version": "3.717.0", 668 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.717.0.tgz", 669 | "integrity": "sha512-a5kY5r7/7bDZZlOQQGWOR1ulQewdtNexdW1Ex5DD0FLKlFY7RD0va24hxQ6BP7mWHol+Dx4pj6UQ8ahk0ap1tw==", 670 | "license": "Apache-2.0", 671 | "dependencies": { 672 | "@aws-crypto/crc32": "5.2.0", 673 | "@aws-crypto/crc32c": "5.2.0", 674 | "@aws-crypto/util": "5.2.0", 675 | "@aws-sdk/core": "3.716.0", 676 | "@aws-sdk/types": "3.714.0", 677 | "@smithy/is-array-buffer": "^3.0.0", 678 | "@smithy/node-config-provider": "^3.1.12", 679 | "@smithy/protocol-http": "^4.1.8", 680 | "@smithy/types": "^3.7.2", 681 | "@smithy/util-middleware": "^3.0.11", 682 | "@smithy/util-stream": "^3.3.2", 683 | "@smithy/util-utf8": "^3.0.0", 684 | "tslib": "^2.6.2" 685 | }, 686 | "engines": { 687 | "node": ">=16.0.0" 688 | } 689 | }, 690 | "node_modules/@aws-sdk/middleware-host-header": { 691 | "version": "3.714.0", 692 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.714.0.tgz", 693 | "integrity": "sha512-6l68kjNrh5QC8FGX3I3geBDavWN5Tg1RLHJ2HLA8ByGBtJyCwnz3hEkKfaxn0bBx0hF9DzbfjEOUF6cDqy2Kjg==", 694 | "license": "Apache-2.0", 695 | "dependencies": { 696 | "@aws-sdk/types": "3.714.0", 697 | "@smithy/protocol-http": "^4.1.8", 698 | "@smithy/types": "^3.7.2", 699 | "tslib": "^2.6.2" 700 | }, 701 | "engines": { 702 | "node": ">=16.0.0" 703 | } 704 | }, 705 | "node_modules/@aws-sdk/middleware-location-constraint": { 706 | "version": "3.714.0", 707 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.714.0.tgz", 708 | "integrity": "sha512-MX7M+V+FblujKck3fyuzePVIAy9530gY719IiSxV6uN1qLHl7VDJxNblpF/KpXakD6rOg8OpvtmqsXj9aBMftw==", 709 | "license": "Apache-2.0", 710 | "dependencies": { 711 | "@aws-sdk/types": "3.714.0", 712 | "@smithy/types": "^3.7.2", 713 | "tslib": "^2.6.2" 714 | }, 715 | "engines": { 716 | "node": ">=16.0.0" 717 | } 718 | }, 719 | "node_modules/@aws-sdk/middleware-logger": { 720 | "version": "3.714.0", 721 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.714.0.tgz", 722 | "integrity": "sha512-RkqHlMvQWUaRklU1bMfUuBvdWwxgUtEqpADaHXlGVj3vtEY2UgBjy+57CveC4MByqKIunNvVHBBbjrGVtwY7Lg==", 723 | "license": "Apache-2.0", 724 | "dependencies": { 725 | "@aws-sdk/types": "3.714.0", 726 | "@smithy/types": "^3.7.2", 727 | "tslib": "^2.6.2" 728 | }, 729 | "engines": { 730 | "node": ">=16.0.0" 731 | } 732 | }, 733 | "node_modules/@aws-sdk/middleware-recursion-detection": { 734 | "version": "3.714.0", 735 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.714.0.tgz", 736 | "integrity": "sha512-AVU5ixnh93nqtsfgNc284oXsXaadyHGPHpql/jwgaaqQfEXjS/1/j3j9E/vpacfTTz2Vzo7hAOjnvrOXSEVDaA==", 737 | "license": "Apache-2.0", 738 | "dependencies": { 739 | "@aws-sdk/types": "3.714.0", 740 | "@smithy/protocol-http": "^4.1.8", 741 | "@smithy/types": "^3.7.2", 742 | "tslib": "^2.6.2" 743 | }, 744 | "engines": { 745 | "node": ">=16.0.0" 746 | } 747 | }, 748 | "node_modules/@aws-sdk/middleware-sdk-s3": { 749 | "version": "3.716.0", 750 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.716.0.tgz", 751 | "integrity": "sha512-Qzz5OfRA/5brqfvq+JHTInwS1EuJ1+tC6qMtwKWJN3czMnVJVdnnsPTf+G5IM/1yYaGEIjY8rC1ExQLcc8ApFQ==", 752 | "license": "Apache-2.0", 753 | "dependencies": { 754 | "@aws-sdk/core": "3.716.0", 755 | "@aws-sdk/types": "3.714.0", 756 | "@aws-sdk/util-arn-parser": "3.693.0", 757 | "@smithy/core": "^2.5.5", 758 | "@smithy/node-config-provider": "^3.1.12", 759 | "@smithy/protocol-http": "^4.1.8", 760 | "@smithy/signature-v4": "^4.2.4", 761 | "@smithy/smithy-client": "^3.5.1", 762 | "@smithy/types": "^3.7.2", 763 | "@smithy/util-config-provider": "^3.0.0", 764 | "@smithy/util-middleware": "^3.0.11", 765 | "@smithy/util-stream": "^3.3.2", 766 | "@smithy/util-utf8": "^3.0.0", 767 | "tslib": "^2.6.2" 768 | }, 769 | "engines": { 770 | "node": ">=16.0.0" 771 | } 772 | }, 773 | "node_modules/@aws-sdk/middleware-ssec": { 774 | "version": "3.714.0", 775 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.714.0.tgz", 776 | "integrity": "sha512-RkK8REAVwNUQmYbIDRw8eYbMJ8F1Rw4C9mlME4BBMhFlelGcD3ErU2ce24moQbDxBjNwHNESmIqgmdQk93CDCQ==", 777 | "license": "Apache-2.0", 778 | "dependencies": { 779 | "@aws-sdk/types": "3.714.0", 780 | "@smithy/types": "^3.7.2", 781 | "tslib": "^2.6.2" 782 | }, 783 | "engines": { 784 | "node": ">=16.0.0" 785 | } 786 | }, 787 | "node_modules/@aws-sdk/middleware-user-agent": { 788 | "version": "3.721.0", 789 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.721.0.tgz", 790 | "integrity": "sha512-Z3Vksb970ArsfLlARW4KVpqO+pQ1cvvGTrTQPxWDsmOzg1kU92t9oWXGW+1M/x6bHbMQlI/EulQ/D8ZE/Pu46Q==", 791 | "license": "Apache-2.0", 792 | "dependencies": { 793 | "@aws-sdk/core": "3.716.0", 794 | "@aws-sdk/types": "3.714.0", 795 | "@aws-sdk/util-endpoints": "3.714.0", 796 | "@smithy/core": "^2.5.5", 797 | "@smithy/protocol-http": "^4.1.8", 798 | "@smithy/types": "^3.7.2", 799 | "tslib": "^2.6.2" 800 | }, 801 | "engines": { 802 | "node": ">=16.0.0" 803 | } 804 | }, 805 | "node_modules/@aws-sdk/region-config-resolver": { 806 | "version": "3.714.0", 807 | "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.714.0.tgz", 808 | "integrity": "sha512-HJzsQxgMOAzZrbf/YIqEx30or4tZK1oNAk6Wm6xecUQx+23JXIaePRu1YFUOLBBERQ4QBPpISFurZWBMZ5ibAw==", 809 | "license": "Apache-2.0", 810 | "dependencies": { 811 | "@aws-sdk/types": "3.714.0", 812 | "@smithy/node-config-provider": "^3.1.12", 813 | "@smithy/types": "^3.7.2", 814 | "@smithy/util-config-provider": "^3.0.0", 815 | "@smithy/util-middleware": "^3.0.11", 816 | "tslib": "^2.6.2" 817 | }, 818 | "engines": { 819 | "node": ">=16.0.0" 820 | } 821 | }, 822 | "node_modules/@aws-sdk/signature-v4-multi-region": { 823 | "version": "3.716.0", 824 | "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.716.0.tgz", 825 | "integrity": "sha512-k0goWotZKKz+kV6Ln0qeAMSeSVi4NipuIIz5R8A0uCF2zBK4CXWdZR7KeaIoLBhJwQnHj1UU7E+2MK74KIUBzA==", 826 | "license": "Apache-2.0", 827 | "dependencies": { 828 | "@aws-sdk/middleware-sdk-s3": "3.716.0", 829 | "@aws-sdk/types": "3.714.0", 830 | "@smithy/protocol-http": "^4.1.8", 831 | "@smithy/signature-v4": "^4.2.4", 832 | "@smithy/types": "^3.7.2", 833 | "tslib": "^2.6.2" 834 | }, 835 | "engines": { 836 | "node": ">=16.0.0" 837 | } 838 | }, 839 | "node_modules/@aws-sdk/token-providers": { 840 | "version": "3.721.0", 841 | "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.721.0.tgz", 842 | "integrity": "sha512-cIZmKdLeEWUzPR+2lA+JcZHPvaFf/Ih+s3LXBa/uQwRFdK+o7WfGRf7Oqe6yLRekO2jJJl4LBJXxDOH++M9+ag==", 843 | "license": "Apache-2.0", 844 | "dependencies": { 845 | "@aws-sdk/types": "3.714.0", 846 | "@smithy/property-provider": "^3.1.11", 847 | "@smithy/shared-ini-file-loader": "^3.1.12", 848 | "@smithy/types": "^3.7.2", 849 | "tslib": "^2.6.2" 850 | }, 851 | "engines": { 852 | "node": ">=16.0.0" 853 | }, 854 | "peerDependencies": { 855 | "@aws-sdk/client-sso-oidc": "^3.721.0" 856 | } 857 | }, 858 | "node_modules/@aws-sdk/types": { 859 | "version": "3.714.0", 860 | "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.714.0.tgz", 861 | "integrity": "sha512-ZjpP2gYbSFlxxaUDa1Il5AVvfggvUPbjzzB/l3q0gIE5Thd6xKW+yzEpt2mLZ5s5UaYSABZbF94g8NUOF4CVGA==", 862 | "license": "Apache-2.0", 863 | "dependencies": { 864 | "@smithy/types": "^3.7.2", 865 | "tslib": "^2.6.2" 866 | }, 867 | "engines": { 868 | "node": ">=16.0.0" 869 | } 870 | }, 871 | "node_modules/@aws-sdk/util-arn-parser": { 872 | "version": "3.693.0", 873 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.693.0.tgz", 874 | "integrity": "sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==", 875 | "license": "Apache-2.0", 876 | "dependencies": { 877 | "tslib": "^2.6.2" 878 | }, 879 | "engines": { 880 | "node": ">=16.0.0" 881 | } 882 | }, 883 | "node_modules/@aws-sdk/util-endpoints": { 884 | "version": "3.714.0", 885 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.714.0.tgz", 886 | "integrity": "sha512-Xv+Z2lhe7w7ZZRsgBwBMZgGTVmS+dkkj2S13uNHAx9lhB5ovM8PhK5G/j28xYf6vIibeuHkRAbb7/ozdZIGR+A==", 887 | "license": "Apache-2.0", 888 | "dependencies": { 889 | "@aws-sdk/types": "3.714.0", 890 | "@smithy/types": "^3.7.2", 891 | "@smithy/util-endpoints": "^2.1.7", 892 | "tslib": "^2.6.2" 893 | }, 894 | "engines": { 895 | "node": ">=16.0.0" 896 | } 897 | }, 898 | "node_modules/@aws-sdk/util-locate-window": { 899 | "version": "3.693.0", 900 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.693.0.tgz", 901 | "integrity": "sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==", 902 | "license": "Apache-2.0", 903 | "dependencies": { 904 | "tslib": "^2.6.2" 905 | }, 906 | "engines": { 907 | "node": ">=16.0.0" 908 | } 909 | }, 910 | "node_modules/@aws-sdk/util-user-agent-browser": { 911 | "version": "3.714.0", 912 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.714.0.tgz", 913 | "integrity": "sha512-OdJJ03cP9/MgIVToPJPCPUImbpZzTcwdIgbXC0tUQPJhbD7b7cB4LdnkhNHko+MptpOrCq4CPY/33EpOjRdofw==", 914 | "license": "Apache-2.0", 915 | "dependencies": { 916 | "@aws-sdk/types": "3.714.0", 917 | "@smithy/types": "^3.7.2", 918 | "bowser": "^2.11.0", 919 | "tslib": "^2.6.2" 920 | } 921 | }, 922 | "node_modules/@aws-sdk/util-user-agent-node": { 923 | "version": "3.721.0", 924 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.721.0.tgz", 925 | "integrity": "sha512-5VsNdC3zQnjrt7KNEeFHWJl3FIamgIS0puG18BMvPsdzcKWEbWDih+yd1kMWrcpAu1Riez9co/gB9y99pBghDA==", 926 | "license": "Apache-2.0", 927 | "dependencies": { 928 | "@aws-sdk/middleware-user-agent": "3.721.0", 929 | "@aws-sdk/types": "3.714.0", 930 | "@smithy/node-config-provider": "^3.1.12", 931 | "@smithy/types": "^3.7.2", 932 | "tslib": "^2.6.2" 933 | }, 934 | "engines": { 935 | "node": ">=16.0.0" 936 | }, 937 | "peerDependencies": { 938 | "aws-crt": ">=1.0.0" 939 | }, 940 | "peerDependenciesMeta": { 941 | "aws-crt": { 942 | "optional": true 943 | } 944 | } 945 | }, 946 | "node_modules/@aws-sdk/xml-builder": { 947 | "version": "3.709.0", 948 | "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.709.0.tgz", 949 | "integrity": "sha512-2GPCwlNxeHspoK/Mc8nbk9cBOkSpp3j2SJUQmFnyQK6V/pR6II2oPRyZkMomug1Rc10hqlBHByMecq4zhV2uUw==", 950 | "license": "Apache-2.0", 951 | "dependencies": { 952 | "@smithy/types": "^3.7.2", 953 | "tslib": "^2.6.2" 954 | }, 955 | "engines": { 956 | "node": ">=16.0.0" 957 | } 958 | }, 959 | "node_modules/@smithy/abort-controller": { 960 | "version": "3.1.9", 961 | "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz", 962 | "integrity": "sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==", 963 | "license": "Apache-2.0", 964 | "dependencies": { 965 | "@smithy/types": "^3.7.2", 966 | "tslib": "^2.6.2" 967 | }, 968 | "engines": { 969 | "node": ">=16.0.0" 970 | } 971 | }, 972 | "node_modules/@smithy/chunked-blob-reader": { 973 | "version": "4.0.0", 974 | "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-4.0.0.tgz", 975 | "integrity": "sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==", 976 | "license": "Apache-2.0", 977 | "dependencies": { 978 | "tslib": "^2.6.2" 979 | } 980 | }, 981 | "node_modules/@smithy/chunked-blob-reader-native": { 982 | "version": "3.0.1", 983 | "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.1.tgz", 984 | "integrity": "sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==", 985 | "license": "Apache-2.0", 986 | "dependencies": { 987 | "@smithy/util-base64": "^3.0.0", 988 | "tslib": "^2.6.2" 989 | } 990 | }, 991 | "node_modules/@smithy/config-resolver": { 992 | "version": "3.0.13", 993 | "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.13.tgz", 994 | "integrity": "sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg==", 995 | "license": "Apache-2.0", 996 | "dependencies": { 997 | "@smithy/node-config-provider": "^3.1.12", 998 | "@smithy/types": "^3.7.2", 999 | "@smithy/util-config-provider": "^3.0.0", 1000 | "@smithy/util-middleware": "^3.0.11", 1001 | "tslib": "^2.6.2" 1002 | }, 1003 | "engines": { 1004 | "node": ">=16.0.0" 1005 | } 1006 | }, 1007 | "node_modules/@smithy/core": { 1008 | "version": "2.5.7", 1009 | "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.7.tgz", 1010 | "integrity": "sha512-8olpW6mKCa0v+ibCjoCzgZHQx1SQmZuW/WkrdZo73wiTprTH6qhmskT60QLFdT9DRa5mXxjz89kQPZ7ZSsoqqg==", 1011 | "license": "Apache-2.0", 1012 | "dependencies": { 1013 | "@smithy/middleware-serde": "^3.0.11", 1014 | "@smithy/protocol-http": "^4.1.8", 1015 | "@smithy/types": "^3.7.2", 1016 | "@smithy/util-body-length-browser": "^3.0.0", 1017 | "@smithy/util-middleware": "^3.0.11", 1018 | "@smithy/util-stream": "^3.3.4", 1019 | "@smithy/util-utf8": "^3.0.0", 1020 | "tslib": "^2.6.2" 1021 | }, 1022 | "engines": { 1023 | "node": ">=16.0.0" 1024 | } 1025 | }, 1026 | "node_modules/@smithy/credential-provider-imds": { 1027 | "version": "3.2.8", 1028 | "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.8.tgz", 1029 | "integrity": "sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw==", 1030 | "license": "Apache-2.0", 1031 | "dependencies": { 1032 | "@smithy/node-config-provider": "^3.1.12", 1033 | "@smithy/property-provider": "^3.1.11", 1034 | "@smithy/types": "^3.7.2", 1035 | "@smithy/url-parser": "^3.0.11", 1036 | "tslib": "^2.6.2" 1037 | }, 1038 | "engines": { 1039 | "node": ">=16.0.0" 1040 | } 1041 | }, 1042 | "node_modules/@smithy/eventstream-codec": { 1043 | "version": "3.1.10", 1044 | "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.10.tgz", 1045 | "integrity": "sha512-323B8YckSbUH0nMIpXn7HZsAVKHYHFUODa8gG9cHo0ySvA1fr5iWaNT+iIL0UCqUzG6QPHA3BSsBtRQou4mMqQ==", 1046 | "license": "Apache-2.0", 1047 | "dependencies": { 1048 | "@aws-crypto/crc32": "5.2.0", 1049 | "@smithy/types": "^3.7.2", 1050 | "@smithy/util-hex-encoding": "^3.0.0", 1051 | "tslib": "^2.6.2" 1052 | } 1053 | }, 1054 | "node_modules/@smithy/eventstream-serde-browser": { 1055 | "version": "3.0.14", 1056 | "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.14.tgz", 1057 | "integrity": "sha512-kbrt0vjOIihW3V7Cqj1SXQvAI5BR8SnyQYsandva0AOR307cXAc+IhPngxIPslxTLfxwDpNu0HzCAq6g42kCPg==", 1058 | "license": "Apache-2.0", 1059 | "dependencies": { 1060 | "@smithy/eventstream-serde-universal": "^3.0.13", 1061 | "@smithy/types": "^3.7.2", 1062 | "tslib": "^2.6.2" 1063 | }, 1064 | "engines": { 1065 | "node": ">=16.0.0" 1066 | } 1067 | }, 1068 | "node_modules/@smithy/eventstream-serde-config-resolver": { 1069 | "version": "3.0.11", 1070 | "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.11.tgz", 1071 | "integrity": "sha512-P2pnEp4n75O+QHjyO7cbw/vsw5l93K/8EWyjNCAAybYwUmj3M+hjSQZ9P5TVdUgEG08ueMAP5R4FkuSkElZ5tQ==", 1072 | "license": "Apache-2.0", 1073 | "dependencies": { 1074 | "@smithy/types": "^3.7.2", 1075 | "tslib": "^2.6.2" 1076 | }, 1077 | "engines": { 1078 | "node": ">=16.0.0" 1079 | } 1080 | }, 1081 | "node_modules/@smithy/eventstream-serde-node": { 1082 | "version": "3.0.13", 1083 | "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.13.tgz", 1084 | "integrity": "sha512-zqy/9iwbj8Wysmvi7Lq7XFLeDgjRpTbCfwBhJa8WbrylTAHiAu6oQTwdY7iu2lxigbc9YYr9vPv5SzYny5tCXQ==", 1085 | "license": "Apache-2.0", 1086 | "dependencies": { 1087 | "@smithy/eventstream-serde-universal": "^3.0.13", 1088 | "@smithy/types": "^3.7.2", 1089 | "tslib": "^2.6.2" 1090 | }, 1091 | "engines": { 1092 | "node": ">=16.0.0" 1093 | } 1094 | }, 1095 | "node_modules/@smithy/eventstream-serde-universal": { 1096 | "version": "3.0.13", 1097 | "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.13.tgz", 1098 | "integrity": "sha512-L1Ib66+gg9uTnqp/18Gz4MDpJPKRE44geOjOQ2SVc0eiaO5l255ADziATZgjQjqumC7yPtp1XnjHlF1srcwjKw==", 1099 | "license": "Apache-2.0", 1100 | "dependencies": { 1101 | "@smithy/eventstream-codec": "^3.1.10", 1102 | "@smithy/types": "^3.7.2", 1103 | "tslib": "^2.6.2" 1104 | }, 1105 | "engines": { 1106 | "node": ">=16.0.0" 1107 | } 1108 | }, 1109 | "node_modules/@smithy/fetch-http-handler": { 1110 | "version": "4.1.3", 1111 | "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz", 1112 | "integrity": "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==", 1113 | "license": "Apache-2.0", 1114 | "dependencies": { 1115 | "@smithy/protocol-http": "^4.1.8", 1116 | "@smithy/querystring-builder": "^3.0.11", 1117 | "@smithy/types": "^3.7.2", 1118 | "@smithy/util-base64": "^3.0.0", 1119 | "tslib": "^2.6.2" 1120 | } 1121 | }, 1122 | "node_modules/@smithy/hash-blob-browser": { 1123 | "version": "3.1.10", 1124 | "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.10.tgz", 1125 | "integrity": "sha512-elwslXOoNunmfS0fh55jHggyhccobFkexLYC1ZeZ1xP2BTSrcIBaHV2b4xUQOdctrSNOpMqOZH1r2XzWTEhyfA==", 1126 | "license": "Apache-2.0", 1127 | "dependencies": { 1128 | "@smithy/chunked-blob-reader": "^4.0.0", 1129 | "@smithy/chunked-blob-reader-native": "^3.0.1", 1130 | "@smithy/types": "^3.7.2", 1131 | "tslib": "^2.6.2" 1132 | } 1133 | }, 1134 | "node_modules/@smithy/hash-node": { 1135 | "version": "3.0.11", 1136 | "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.11.tgz", 1137 | "integrity": "sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA==", 1138 | "license": "Apache-2.0", 1139 | "dependencies": { 1140 | "@smithy/types": "^3.7.2", 1141 | "@smithy/util-buffer-from": "^3.0.0", 1142 | "@smithy/util-utf8": "^3.0.0", 1143 | "tslib": "^2.6.2" 1144 | }, 1145 | "engines": { 1146 | "node": ">=16.0.0" 1147 | } 1148 | }, 1149 | "node_modules/@smithy/hash-stream-node": { 1150 | "version": "3.1.10", 1151 | "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.10.tgz", 1152 | "integrity": "sha512-olomK/jZQ93OMayW1zfTHwcbwBdhcZOHsyWyiZ9h9IXvc1mCD/VuvzbLb3Gy/qNJwI4MANPLctTp2BucV2oU/Q==", 1153 | "license": "Apache-2.0", 1154 | "dependencies": { 1155 | "@smithy/types": "^3.7.2", 1156 | "@smithy/util-utf8": "^3.0.0", 1157 | "tslib": "^2.6.2" 1158 | }, 1159 | "engines": { 1160 | "node": ">=16.0.0" 1161 | } 1162 | }, 1163 | "node_modules/@smithy/invalid-dependency": { 1164 | "version": "3.0.11", 1165 | "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.11.tgz", 1166 | "integrity": "sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ==", 1167 | "license": "Apache-2.0", 1168 | "dependencies": { 1169 | "@smithy/types": "^3.7.2", 1170 | "tslib": "^2.6.2" 1171 | } 1172 | }, 1173 | "node_modules/@smithy/is-array-buffer": { 1174 | "version": "3.0.0", 1175 | "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", 1176 | "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", 1177 | "license": "Apache-2.0", 1178 | "dependencies": { 1179 | "tslib": "^2.6.2" 1180 | }, 1181 | "engines": { 1182 | "node": ">=16.0.0" 1183 | } 1184 | }, 1185 | "node_modules/@smithy/md5-js": { 1186 | "version": "3.0.11", 1187 | "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.11.tgz", 1188 | "integrity": "sha512-3NM0L3i2Zm4bbgG6Ymi9NBcxXhryi3uE8fIfHJZIOfZVxOkGdjdgjR9A06SFIZCfnEIWKXZdm6Yq5/aPXFFhsQ==", 1189 | "license": "Apache-2.0", 1190 | "dependencies": { 1191 | "@smithy/types": "^3.7.2", 1192 | "@smithy/util-utf8": "^3.0.0", 1193 | "tslib": "^2.6.2" 1194 | } 1195 | }, 1196 | "node_modules/@smithy/middleware-content-length": { 1197 | "version": "3.0.13", 1198 | "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.13.tgz", 1199 | "integrity": "sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw==", 1200 | "license": "Apache-2.0", 1201 | "dependencies": { 1202 | "@smithy/protocol-http": "^4.1.8", 1203 | "@smithy/types": "^3.7.2", 1204 | "tslib": "^2.6.2" 1205 | }, 1206 | "engines": { 1207 | "node": ">=16.0.0" 1208 | } 1209 | }, 1210 | "node_modules/@smithy/middleware-endpoint": { 1211 | "version": "3.2.8", 1212 | "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.8.tgz", 1213 | "integrity": "sha512-OEJZKVUEhMOqMs3ktrTWp7UvvluMJEvD5XgQwRePSbDg1VvBaL8pX8mwPltFn6wk1GySbcVwwyldL8S+iqnrEQ==", 1214 | "license": "Apache-2.0", 1215 | "dependencies": { 1216 | "@smithy/core": "^2.5.7", 1217 | "@smithy/middleware-serde": "^3.0.11", 1218 | "@smithy/node-config-provider": "^3.1.12", 1219 | "@smithy/shared-ini-file-loader": "^3.1.12", 1220 | "@smithy/types": "^3.7.2", 1221 | "@smithy/url-parser": "^3.0.11", 1222 | "@smithy/util-middleware": "^3.0.11", 1223 | "tslib": "^2.6.2" 1224 | }, 1225 | "engines": { 1226 | "node": ">=16.0.0" 1227 | } 1228 | }, 1229 | "node_modules/@smithy/middleware-retry": { 1230 | "version": "3.0.34", 1231 | "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.34.tgz", 1232 | "integrity": "sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==", 1233 | "license": "Apache-2.0", 1234 | "dependencies": { 1235 | "@smithy/node-config-provider": "^3.1.12", 1236 | "@smithy/protocol-http": "^4.1.8", 1237 | "@smithy/service-error-classification": "^3.0.11", 1238 | "@smithy/smithy-client": "^3.7.0", 1239 | "@smithy/types": "^3.7.2", 1240 | "@smithy/util-middleware": "^3.0.11", 1241 | "@smithy/util-retry": "^3.0.11", 1242 | "tslib": "^2.6.2", 1243 | "uuid": "^9.0.1" 1244 | }, 1245 | "engines": { 1246 | "node": ">=16.0.0" 1247 | } 1248 | }, 1249 | "node_modules/@smithy/middleware-serde": { 1250 | "version": "3.0.11", 1251 | "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.11.tgz", 1252 | "integrity": "sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw==", 1253 | "license": "Apache-2.0", 1254 | "dependencies": { 1255 | "@smithy/types": "^3.7.2", 1256 | "tslib": "^2.6.2" 1257 | }, 1258 | "engines": { 1259 | "node": ">=16.0.0" 1260 | } 1261 | }, 1262 | "node_modules/@smithy/middleware-stack": { 1263 | "version": "3.0.11", 1264 | "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.11.tgz", 1265 | "integrity": "sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA==", 1266 | "license": "Apache-2.0", 1267 | "dependencies": { 1268 | "@smithy/types": "^3.7.2", 1269 | "tslib": "^2.6.2" 1270 | }, 1271 | "engines": { 1272 | "node": ">=16.0.0" 1273 | } 1274 | }, 1275 | "node_modules/@smithy/node-config-provider": { 1276 | "version": "3.1.12", 1277 | "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.12.tgz", 1278 | "integrity": "sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ==", 1279 | "license": "Apache-2.0", 1280 | "dependencies": { 1281 | "@smithy/property-provider": "^3.1.11", 1282 | "@smithy/shared-ini-file-loader": "^3.1.12", 1283 | "@smithy/types": "^3.7.2", 1284 | "tslib": "^2.6.2" 1285 | }, 1286 | "engines": { 1287 | "node": ">=16.0.0" 1288 | } 1289 | }, 1290 | "node_modules/@smithy/node-http-handler": { 1291 | "version": "3.3.3", 1292 | "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.3.tgz", 1293 | "integrity": "sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==", 1294 | "license": "Apache-2.0", 1295 | "dependencies": { 1296 | "@smithy/abort-controller": "^3.1.9", 1297 | "@smithy/protocol-http": "^4.1.8", 1298 | "@smithy/querystring-builder": "^3.0.11", 1299 | "@smithy/types": "^3.7.2", 1300 | "tslib": "^2.6.2" 1301 | }, 1302 | "engines": { 1303 | "node": ">=16.0.0" 1304 | } 1305 | }, 1306 | "node_modules/@smithy/property-provider": { 1307 | "version": "3.1.11", 1308 | "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.11.tgz", 1309 | "integrity": "sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A==", 1310 | "license": "Apache-2.0", 1311 | "dependencies": { 1312 | "@smithy/types": "^3.7.2", 1313 | "tslib": "^2.6.2" 1314 | }, 1315 | "engines": { 1316 | "node": ">=16.0.0" 1317 | } 1318 | }, 1319 | "node_modules/@smithy/protocol-http": { 1320 | "version": "4.1.8", 1321 | "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", 1322 | "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", 1323 | "license": "Apache-2.0", 1324 | "dependencies": { 1325 | "@smithy/types": "^3.7.2", 1326 | "tslib": "^2.6.2" 1327 | }, 1328 | "engines": { 1329 | "node": ">=16.0.0" 1330 | } 1331 | }, 1332 | "node_modules/@smithy/querystring-builder": { 1333 | "version": "3.0.11", 1334 | "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.11.tgz", 1335 | "integrity": "sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg==", 1336 | "license": "Apache-2.0", 1337 | "dependencies": { 1338 | "@smithy/types": "^3.7.2", 1339 | "@smithy/util-uri-escape": "^3.0.0", 1340 | "tslib": "^2.6.2" 1341 | }, 1342 | "engines": { 1343 | "node": ">=16.0.0" 1344 | } 1345 | }, 1346 | "node_modules/@smithy/querystring-parser": { 1347 | "version": "3.0.11", 1348 | "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.11.tgz", 1349 | "integrity": "sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw==", 1350 | "license": "Apache-2.0", 1351 | "dependencies": { 1352 | "@smithy/types": "^3.7.2", 1353 | "tslib": "^2.6.2" 1354 | }, 1355 | "engines": { 1356 | "node": ">=16.0.0" 1357 | } 1358 | }, 1359 | "node_modules/@smithy/service-error-classification": { 1360 | "version": "3.0.11", 1361 | "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz", 1362 | "integrity": "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==", 1363 | "license": "Apache-2.0", 1364 | "dependencies": { 1365 | "@smithy/types": "^3.7.2" 1366 | }, 1367 | "engines": { 1368 | "node": ">=16.0.0" 1369 | } 1370 | }, 1371 | "node_modules/@smithy/shared-ini-file-loader": { 1372 | "version": "3.1.12", 1373 | "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.12.tgz", 1374 | "integrity": "sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q==", 1375 | "license": "Apache-2.0", 1376 | "dependencies": { 1377 | "@smithy/types": "^3.7.2", 1378 | "tslib": "^2.6.2" 1379 | }, 1380 | "engines": { 1381 | "node": ">=16.0.0" 1382 | } 1383 | }, 1384 | "node_modules/@smithy/signature-v4": { 1385 | "version": "4.2.4", 1386 | "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.4.tgz", 1387 | "integrity": "sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA==", 1388 | "license": "Apache-2.0", 1389 | "dependencies": { 1390 | "@smithy/is-array-buffer": "^3.0.0", 1391 | "@smithy/protocol-http": "^4.1.8", 1392 | "@smithy/types": "^3.7.2", 1393 | "@smithy/util-hex-encoding": "^3.0.0", 1394 | "@smithy/util-middleware": "^3.0.11", 1395 | "@smithy/util-uri-escape": "^3.0.0", 1396 | "@smithy/util-utf8": "^3.0.0", 1397 | "tslib": "^2.6.2" 1398 | }, 1399 | "engines": { 1400 | "node": ">=16.0.0" 1401 | } 1402 | }, 1403 | "node_modules/@smithy/smithy-client": { 1404 | "version": "3.7.0", 1405 | "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.7.0.tgz", 1406 | "integrity": "sha512-9wYrjAZFlqWhgVo3C4y/9kpc68jgiSsKUnsFPzr/MSiRL93+QRDafGTfhhKAb2wsr69Ru87WTiqSfQusSmWipA==", 1407 | "license": "Apache-2.0", 1408 | "dependencies": { 1409 | "@smithy/core": "^2.5.7", 1410 | "@smithy/middleware-endpoint": "^3.2.8", 1411 | "@smithy/middleware-stack": "^3.0.11", 1412 | "@smithy/protocol-http": "^4.1.8", 1413 | "@smithy/types": "^3.7.2", 1414 | "@smithy/util-stream": "^3.3.4", 1415 | "tslib": "^2.6.2" 1416 | }, 1417 | "engines": { 1418 | "node": ">=16.0.0" 1419 | } 1420 | }, 1421 | "node_modules/@smithy/types": { 1422 | "version": "3.7.2", 1423 | "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", 1424 | "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", 1425 | "license": "Apache-2.0", 1426 | "dependencies": { 1427 | "tslib": "^2.6.2" 1428 | }, 1429 | "engines": { 1430 | "node": ">=16.0.0" 1431 | } 1432 | }, 1433 | "node_modules/@smithy/url-parser": { 1434 | "version": "3.0.11", 1435 | "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.11.tgz", 1436 | "integrity": "sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw==", 1437 | "license": "Apache-2.0", 1438 | "dependencies": { 1439 | "@smithy/querystring-parser": "^3.0.11", 1440 | "@smithy/types": "^3.7.2", 1441 | "tslib": "^2.6.2" 1442 | } 1443 | }, 1444 | "node_modules/@smithy/util-base64": { 1445 | "version": "3.0.0", 1446 | "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", 1447 | "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", 1448 | "license": "Apache-2.0", 1449 | "dependencies": { 1450 | "@smithy/util-buffer-from": "^3.0.0", 1451 | "@smithy/util-utf8": "^3.0.0", 1452 | "tslib": "^2.6.2" 1453 | }, 1454 | "engines": { 1455 | "node": ">=16.0.0" 1456 | } 1457 | }, 1458 | "node_modules/@smithy/util-body-length-browser": { 1459 | "version": "3.0.0", 1460 | "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", 1461 | "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", 1462 | "license": "Apache-2.0", 1463 | "dependencies": { 1464 | "tslib": "^2.6.2" 1465 | } 1466 | }, 1467 | "node_modules/@smithy/util-body-length-node": { 1468 | "version": "3.0.0", 1469 | "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", 1470 | "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", 1471 | "license": "Apache-2.0", 1472 | "dependencies": { 1473 | "tslib": "^2.6.2" 1474 | }, 1475 | "engines": { 1476 | "node": ">=16.0.0" 1477 | } 1478 | }, 1479 | "node_modules/@smithy/util-buffer-from": { 1480 | "version": "3.0.0", 1481 | "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", 1482 | "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", 1483 | "license": "Apache-2.0", 1484 | "dependencies": { 1485 | "@smithy/is-array-buffer": "^3.0.0", 1486 | "tslib": "^2.6.2" 1487 | }, 1488 | "engines": { 1489 | "node": ">=16.0.0" 1490 | } 1491 | }, 1492 | "node_modules/@smithy/util-config-provider": { 1493 | "version": "3.0.0", 1494 | "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", 1495 | "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", 1496 | "license": "Apache-2.0", 1497 | "dependencies": { 1498 | "tslib": "^2.6.2" 1499 | }, 1500 | "engines": { 1501 | "node": ">=16.0.0" 1502 | } 1503 | }, 1504 | "node_modules/@smithy/util-defaults-mode-browser": { 1505 | "version": "3.0.34", 1506 | "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.34.tgz", 1507 | "integrity": "sha512-FumjjF631lR521cX+svMLBj3SwSDh9VdtyynTYDAiBDEf8YPP5xORNXKQ9j0105o5+ARAGnOOP/RqSl40uXddA==", 1508 | "license": "Apache-2.0", 1509 | "dependencies": { 1510 | "@smithy/property-provider": "^3.1.11", 1511 | "@smithy/smithy-client": "^3.7.0", 1512 | "@smithy/types": "^3.7.2", 1513 | "bowser": "^2.11.0", 1514 | "tslib": "^2.6.2" 1515 | }, 1516 | "engines": { 1517 | "node": ">= 10.0.0" 1518 | } 1519 | }, 1520 | "node_modules/@smithy/util-defaults-mode-node": { 1521 | "version": "3.0.34", 1522 | "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.34.tgz", 1523 | "integrity": "sha512-vN6aHfzW9dVVzkI0wcZoUXvfjkl4CSbM9nE//08lmUMyf00S75uuCpTrqF9uD4bD9eldIXlt53colrlwKAT8Gw==", 1524 | "license": "Apache-2.0", 1525 | "dependencies": { 1526 | "@smithy/config-resolver": "^3.0.13", 1527 | "@smithy/credential-provider-imds": "^3.2.8", 1528 | "@smithy/node-config-provider": "^3.1.12", 1529 | "@smithy/property-provider": "^3.1.11", 1530 | "@smithy/smithy-client": "^3.7.0", 1531 | "@smithy/types": "^3.7.2", 1532 | "tslib": "^2.6.2" 1533 | }, 1534 | "engines": { 1535 | "node": ">= 10.0.0" 1536 | } 1537 | }, 1538 | "node_modules/@smithy/util-endpoints": { 1539 | "version": "2.1.7", 1540 | "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.7.tgz", 1541 | "integrity": "sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw==", 1542 | "license": "Apache-2.0", 1543 | "dependencies": { 1544 | "@smithy/node-config-provider": "^3.1.12", 1545 | "@smithy/types": "^3.7.2", 1546 | "tslib": "^2.6.2" 1547 | }, 1548 | "engines": { 1549 | "node": ">=16.0.0" 1550 | } 1551 | }, 1552 | "node_modules/@smithy/util-hex-encoding": { 1553 | "version": "3.0.0", 1554 | "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", 1555 | "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", 1556 | "license": "Apache-2.0", 1557 | "dependencies": { 1558 | "tslib": "^2.6.2" 1559 | }, 1560 | "engines": { 1561 | "node": ">=16.0.0" 1562 | } 1563 | }, 1564 | "node_modules/@smithy/util-middleware": { 1565 | "version": "3.0.11", 1566 | "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.11.tgz", 1567 | "integrity": "sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==", 1568 | "license": "Apache-2.0", 1569 | "dependencies": { 1570 | "@smithy/types": "^3.7.2", 1571 | "tslib": "^2.6.2" 1572 | }, 1573 | "engines": { 1574 | "node": ">=16.0.0" 1575 | } 1576 | }, 1577 | "node_modules/@smithy/util-retry": { 1578 | "version": "3.0.11", 1579 | "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz", 1580 | "integrity": "sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==", 1581 | "license": "Apache-2.0", 1582 | "dependencies": { 1583 | "@smithy/service-error-classification": "^3.0.11", 1584 | "@smithy/types": "^3.7.2", 1585 | "tslib": "^2.6.2" 1586 | }, 1587 | "engines": { 1588 | "node": ">=16.0.0" 1589 | } 1590 | }, 1591 | "node_modules/@smithy/util-stream": { 1592 | "version": "3.3.4", 1593 | "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.4.tgz", 1594 | "integrity": "sha512-SGhGBG/KupieJvJSZp/rfHHka8BFgj56eek9px4pp7lZbOF+fRiVr4U7A3y3zJD8uGhxq32C5D96HxsTC9BckQ==", 1595 | "license": "Apache-2.0", 1596 | "dependencies": { 1597 | "@smithy/fetch-http-handler": "^4.1.3", 1598 | "@smithy/node-http-handler": "^3.3.3", 1599 | "@smithy/types": "^3.7.2", 1600 | "@smithy/util-base64": "^3.0.0", 1601 | "@smithy/util-buffer-from": "^3.0.0", 1602 | "@smithy/util-hex-encoding": "^3.0.0", 1603 | "@smithy/util-utf8": "^3.0.0", 1604 | "tslib": "^2.6.2" 1605 | }, 1606 | "engines": { 1607 | "node": ">=16.0.0" 1608 | } 1609 | }, 1610 | "node_modules/@smithy/util-uri-escape": { 1611 | "version": "3.0.0", 1612 | "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", 1613 | "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", 1614 | "license": "Apache-2.0", 1615 | "dependencies": { 1616 | "tslib": "^2.6.2" 1617 | }, 1618 | "engines": { 1619 | "node": ">=16.0.0" 1620 | } 1621 | }, 1622 | "node_modules/@smithy/util-utf8": { 1623 | "version": "3.0.0", 1624 | "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", 1625 | "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", 1626 | "license": "Apache-2.0", 1627 | "dependencies": { 1628 | "@smithy/util-buffer-from": "^3.0.0", 1629 | "tslib": "^2.6.2" 1630 | }, 1631 | "engines": { 1632 | "node": ">=16.0.0" 1633 | } 1634 | }, 1635 | "node_modules/@smithy/util-waiter": { 1636 | "version": "3.2.0", 1637 | "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.2.0.tgz", 1638 | "integrity": "sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==", 1639 | "license": "Apache-2.0", 1640 | "dependencies": { 1641 | "@smithy/abort-controller": "^3.1.9", 1642 | "@smithy/types": "^3.7.2", 1643 | "tslib": "^2.6.2" 1644 | }, 1645 | "engines": { 1646 | "node": ">=16.0.0" 1647 | } 1648 | }, 1649 | "node_modules/abbrev": { 1650 | "version": "2.0.0", 1651 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", 1652 | "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", 1653 | "license": "ISC", 1654 | "engines": { 1655 | "node": "^14.17.0 || ^16.13.0 || >=18.0.0" 1656 | } 1657 | }, 1658 | "node_modules/balanced-match": { 1659 | "version": "1.0.2", 1660 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1661 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1662 | "license": "MIT" 1663 | }, 1664 | "node_modules/base64-js": { 1665 | "version": "1.5.1", 1666 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 1667 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 1668 | "funding": [ 1669 | { 1670 | "type": "github", 1671 | "url": "https://github.com/sponsors/feross" 1672 | }, 1673 | { 1674 | "type": "patreon", 1675 | "url": "https://www.patreon.com/feross" 1676 | }, 1677 | { 1678 | "type": "consulting", 1679 | "url": "https://feross.org/support" 1680 | } 1681 | ], 1682 | "license": "MIT" 1683 | }, 1684 | "node_modules/bowser": { 1685 | "version": "2.11.0", 1686 | "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", 1687 | "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", 1688 | "license": "MIT" 1689 | }, 1690 | "node_modules/brace-expansion": { 1691 | "version": "1.1.11", 1692 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1693 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1694 | "license": "MIT", 1695 | "dependencies": { 1696 | "balanced-match": "^1.0.0", 1697 | "concat-map": "0.0.1" 1698 | } 1699 | }, 1700 | "node_modules/buffer": { 1701 | "version": "5.6.0", 1702 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", 1703 | "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", 1704 | "license": "MIT", 1705 | "dependencies": { 1706 | "base64-js": "^1.0.2", 1707 | "ieee754": "^1.1.4" 1708 | } 1709 | }, 1710 | "node_modules/commander": { 1711 | "version": "13.0.0", 1712 | "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", 1713 | "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", 1714 | "license": "MIT", 1715 | "engines": { 1716 | "node": ">=18" 1717 | } 1718 | }, 1719 | "node_modules/concat-map": { 1720 | "version": "0.0.1", 1721 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1722 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1723 | "license": "MIT" 1724 | }, 1725 | "node_modules/events": { 1726 | "version": "3.3.0", 1727 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 1728 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 1729 | "license": "MIT", 1730 | "engines": { 1731 | "node": ">=0.8.x" 1732 | } 1733 | }, 1734 | "node_modules/fast-xml-parser": { 1735 | "version": "4.4.1", 1736 | "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", 1737 | "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", 1738 | "funding": [ 1739 | { 1740 | "type": "github", 1741 | "url": "https://github.com/sponsors/NaturalIntelligence" 1742 | }, 1743 | { 1744 | "type": "paypal", 1745 | "url": "https://paypal.me/naturalintelligence" 1746 | } 1747 | ], 1748 | "license": "MIT", 1749 | "dependencies": { 1750 | "strnum": "^1.0.5" 1751 | }, 1752 | "bin": { 1753 | "fxparser": "src/cli/cli.js" 1754 | } 1755 | }, 1756 | "node_modules/fs.realpath": { 1757 | "version": "1.0.0", 1758 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1759 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 1760 | "license": "ISC" 1761 | }, 1762 | "node_modules/glob": { 1763 | "version": "7.2.3", 1764 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 1765 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 1766 | "deprecated": "Glob versions prior to v9 are no longer supported", 1767 | "license": "ISC", 1768 | "dependencies": { 1769 | "fs.realpath": "^1.0.0", 1770 | "inflight": "^1.0.4", 1771 | "inherits": "2", 1772 | "minimatch": "^3.1.1", 1773 | "once": "^1.3.0", 1774 | "path-is-absolute": "^1.0.0" 1775 | }, 1776 | "engines": { 1777 | "node": "*" 1778 | }, 1779 | "funding": { 1780 | "url": "https://github.com/sponsors/isaacs" 1781 | } 1782 | }, 1783 | "node_modules/ieee754": { 1784 | "version": "1.2.1", 1785 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1786 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1787 | "funding": [ 1788 | { 1789 | "type": "github", 1790 | "url": "https://github.com/sponsors/feross" 1791 | }, 1792 | { 1793 | "type": "patreon", 1794 | "url": "https://www.patreon.com/feross" 1795 | }, 1796 | { 1797 | "type": "consulting", 1798 | "url": "https://feross.org/support" 1799 | } 1800 | ], 1801 | "license": "BSD-3-Clause" 1802 | }, 1803 | "node_modules/inflight": { 1804 | "version": "1.0.6", 1805 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1806 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1807 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 1808 | "license": "ISC", 1809 | "dependencies": { 1810 | "once": "^1.3.0", 1811 | "wrappy": "1" 1812 | } 1813 | }, 1814 | "node_modules/inherits": { 1815 | "version": "2.0.4", 1816 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1817 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1818 | "license": "ISC" 1819 | }, 1820 | "node_modules/minimatch": { 1821 | "version": "3.1.2", 1822 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1823 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1824 | "license": "ISC", 1825 | "dependencies": { 1826 | "brace-expansion": "^1.1.7" 1827 | }, 1828 | "engines": { 1829 | "node": "*" 1830 | } 1831 | }, 1832 | "node_modules/minimist": { 1833 | "version": "1.2.8", 1834 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 1835 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 1836 | "license": "MIT", 1837 | "funding": { 1838 | "url": "https://github.com/sponsors/ljharb" 1839 | } 1840 | }, 1841 | "node_modules/mkdirp": { 1842 | "version": "0.5.6", 1843 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 1844 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 1845 | "license": "MIT", 1846 | "dependencies": { 1847 | "minimist": "^1.2.6" 1848 | }, 1849 | "bin": { 1850 | "mkdirp": "bin/cmd.js" 1851 | } 1852 | }, 1853 | "node_modules/node-audiorecorder": { 1854 | "version": "3.0.0", 1855 | "resolved": "https://registry.npmjs.org/node-audiorecorder/-/node-audiorecorder-3.0.0.tgz", 1856 | "integrity": "sha512-nQepUJdQT4wD985zn7113tLZqVv0dh9ly9bsB6bE3GllPordEGUL7gFVnG2GCVArz/J/K+N50wo0574bwdpOeA==", 1857 | "license": "MIT" 1858 | }, 1859 | "node_modules/node-webcam": { 1860 | "version": "0.8.2", 1861 | "resolved": "https://registry.npmjs.org/node-webcam/-/node-webcam-0.8.2.tgz", 1862 | "integrity": "sha512-cuVhDui14Mwc3BfwXMP7EPsrVt1JKns1E5hn/U88ptpdI9c/1M0wQ6nHMcPo+pBrt//kp3h3ZLx12awMdVEXCw==", 1863 | "hasInstallScript": true, 1864 | "license": "MIT", 1865 | "dependencies": { 1866 | "nopt": "*" 1867 | }, 1868 | "bin": { 1869 | "node-webcam": "bin/node-webcam.js" 1870 | } 1871 | }, 1872 | "node_modules/nopt": { 1873 | "version": "8.0.0", 1874 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.0.0.tgz", 1875 | "integrity": "sha512-1L/fTJ4UmV/lUxT2Uf006pfZKTvAgCF+chz+0OgBHO8u2Z67pE7AaAUUj7CJy0lXqHmymUvGFt6NE9R3HER0yw==", 1876 | "license": "ISC", 1877 | "dependencies": { 1878 | "abbrev": "^2.0.0" 1879 | }, 1880 | "bin": { 1881 | "nopt": "bin/nopt.js" 1882 | }, 1883 | "engines": { 1884 | "node": "^18.17.0 || >=20.5.0" 1885 | } 1886 | }, 1887 | "node_modules/once": { 1888 | "version": "1.4.0", 1889 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1890 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1891 | "license": "ISC", 1892 | "dependencies": { 1893 | "wrappy": "1" 1894 | } 1895 | }, 1896 | "node_modules/path-is-absolute": { 1897 | "version": "1.0.1", 1898 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1899 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1900 | "license": "MIT", 1901 | "engines": { 1902 | "node": ">=0.10.0" 1903 | } 1904 | }, 1905 | "node_modules/readable-stream": { 1906 | "version": "3.6.2", 1907 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 1908 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 1909 | "license": "MIT", 1910 | "dependencies": { 1911 | "inherits": "^2.0.3", 1912 | "string_decoder": "^1.1.1", 1913 | "util-deprecate": "^1.0.1" 1914 | }, 1915 | "engines": { 1916 | "node": ">= 6" 1917 | } 1918 | }, 1919 | "node_modules/rimraf": { 1920 | "version": "2.6.3", 1921 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 1922 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 1923 | "deprecated": "Rimraf versions prior to v4 are no longer supported", 1924 | "license": "ISC", 1925 | "dependencies": { 1926 | "glob": "^7.1.3" 1927 | }, 1928 | "bin": { 1929 | "rimraf": "bin.js" 1930 | } 1931 | }, 1932 | "node_modules/safe-buffer": { 1933 | "version": "5.2.1", 1934 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1935 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1936 | "funding": [ 1937 | { 1938 | "type": "github", 1939 | "url": "https://github.com/sponsors/feross" 1940 | }, 1941 | { 1942 | "type": "patreon", 1943 | "url": "https://www.patreon.com/feross" 1944 | }, 1945 | { 1946 | "type": "consulting", 1947 | "url": "https://feross.org/support" 1948 | } 1949 | ], 1950 | "license": "MIT" 1951 | }, 1952 | "node_modules/screenshot-desktop": { 1953 | "version": "1.15.1", 1954 | "resolved": "https://registry.npmjs.org/screenshot-desktop/-/screenshot-desktop-1.15.1.tgz", 1955 | "integrity": "sha512-4j9bDZSFnkRqH53bAw5W+ZhdGKiTUcUDohO6NZjL6QSC/6AXbhUqJ9YXygjHrYcSOUD4IcLty6uQ1ES7PSsomg==", 1956 | "funding": [ 1957 | { 1958 | "type": "github", 1959 | "url": "https://github.com/sponsors/bencevans" 1960 | } 1961 | ], 1962 | "license": "MIT", 1963 | "dependencies": { 1964 | "temp": "^0.9.4" 1965 | } 1966 | }, 1967 | "node_modules/stream-browserify": { 1968 | "version": "3.0.0", 1969 | "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", 1970 | "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", 1971 | "license": "MIT", 1972 | "dependencies": { 1973 | "inherits": "~2.0.4", 1974 | "readable-stream": "^3.5.0" 1975 | } 1976 | }, 1977 | "node_modules/string_decoder": { 1978 | "version": "1.3.0", 1979 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1980 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1981 | "license": "MIT", 1982 | "dependencies": { 1983 | "safe-buffer": "~5.2.0" 1984 | } 1985 | }, 1986 | "node_modules/strnum": { 1987 | "version": "1.0.5", 1988 | "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", 1989 | "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", 1990 | "license": "MIT" 1991 | }, 1992 | "node_modules/temp": { 1993 | "version": "0.9.4", 1994 | "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", 1995 | "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", 1996 | "license": "MIT", 1997 | "dependencies": { 1998 | "mkdirp": "^0.5.1", 1999 | "rimraf": "~2.6.2" 2000 | }, 2001 | "engines": { 2002 | "node": ">=6.0.0" 2003 | } 2004 | }, 2005 | "node_modules/tslib": { 2006 | "version": "2.8.1", 2007 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2008 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2009 | "license": "0BSD" 2010 | }, 2011 | "node_modules/util-deprecate": { 2012 | "version": "1.0.2", 2013 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2014 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 2015 | "license": "MIT" 2016 | }, 2017 | "node_modules/uuid": { 2018 | "version": "9.0.1", 2019 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", 2020 | "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", 2021 | "funding": [ 2022 | "https://github.com/sponsors/broofa", 2023 | "https://github.com/sponsors/ctavan" 2024 | ], 2025 | "license": "MIT", 2026 | "bin": { 2027 | "uuid": "dist/bin/uuid" 2028 | } 2029 | }, 2030 | "node_modules/wrappy": { 2031 | "version": "1.0.2", 2032 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2033 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2034 | "license": "ISC" 2035 | } 2036 | } 2037 | } 2038 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "efficient-recorder", 3 | "version": "1.0.3", 4 | "description": "Efficient audio recorder that switches between 8kHz and 44.1kHz based on volume", 5 | "main": "index.js", 6 | "bin": { 7 | "efficient-recorder": "./index.js" 8 | }, 9 | "dependencies": { 10 | "@aws-sdk/client-s3": "^3.722.0", 11 | "@aws-sdk/lib-storage": "^3.722.0", 12 | "commander": "^13.0.0", 13 | "node-audiorecorder": "^3.0.0", 14 | "node-webcam": "^0.8.2", 15 | "screenshot-desktop": "^1.15.1" 16 | }, 17 | "engines": { 18 | "node": ">=14.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /prompt.md: -------------------------------------------------------------------------------- 1 | create a node.js program that records audio in 8khz to detect if the mic is above 50db, and switches to 44.1khz otherwise. this way it can efficiently record mic audio but only when im talking. it streams the recording to s3 using the s3 uses the s3 api provided. it should be able to be ran as a cli using npx efficient-recorder --endpoint ENDPOINT --key KEY --secret SECRET 2 | 3 | https://raw.githubusercontent.com/aws/aws-sdk-js-v3/refs/heads/main/README.md 4 | https://raw.githubusercontent.com/RedKenrok/node-audiorecorder/refs/heads/master/README.md 5 | 6 | 7 | -------------------------------------------------------------------------------- /swift-version/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "efficient-recorder", 6 | platforms: [ 7 | .macOS(.v12) 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), 11 | ], 12 | targets: [ 13 | .executableTarget( 14 | name: "efficient-recorder", 15 | dependencies: [ 16 | "KeychainAccess" 17 | ], 18 | path: "Sources/EfficientRecorder", 19 | swiftSettings: [ 20 | .enableUpcomingFeature("BareSlashRegexLiterals"), 21 | .enableUpcomingFeature("ConciseMagicFile"), 22 | .enableUpcomingFeature("ExistentialAny"), 23 | .enableUpcomingFeature("ForwardTrailingClosures"), 24 | .enableUpcomingFeature("ImplicitOpenExistentials"), 25 | .enableUpcomingFeature("StrictConcurrency") 26 | ] 27 | ) 28 | ] 29 | ) -------------------------------------------------------------------------------- /swift-version/README.md: -------------------------------------------------------------------------------- 1 | > NB: this isnt' functinal yet AT ALL. just tried to write swift using claude but it didn't know enough about it to get it to work 2 | 3 | I want a macOS program that takes 1 screenshot every second and also records a separate system audio and mic audio. the screenshot doesn't need to be processed locally. the recording must be paused if db level is below 50. it must be as energy efficient as possible locally. it must stream both audio streams separately to a cloudflare r2 bucket, and it must upload the screenshots to there too (can use multipart upload). 4 | 5 | To use this, you need to: 6 | 7 | 1. Create an R2 Bucket at Cloudflare and find your S3 API URL 8 | 2. Create an API Key with read/write access ([instructions](https://developers.cloudflare.com/r2/api/s3/tokens/)) 9 | 3. When running the program, provide the S3 API URL, Access Key ID, and Secret Access Key 10 | 11 | As a follow up when I want to work on this again, I'd need to get this to work as an installable DMG MacOS application in which a user would just enter their S3 server details (would be compatible with [MinIO](https://github.com/minio/minio)) and it would start recording, always uploading whenever there's an internet connection. 12 | -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | import Cocoa 3 | 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | private var screenRecorder: ScreenRecorder? 6 | private var systemAudioRecorder: AudioRecorder? 7 | private var microphoneRecorder: AudioRecorder? 8 | private var uploadManager: UploadManager? 9 | private let statusManager = StatusManager.shared 10 | 11 | private var isRecording = false 12 | 13 | func applicationDidFinishLaunching(_ notification: Notification) { 14 | setupTerminationHandler() 15 | 16 | Task { 17 | do { 18 | try await setupRecording() 19 | } catch { 20 | statusManager.reportError(error, component: "Setup") 21 | NSApplication.shared.terminate(nil) 22 | } 23 | } 24 | } 25 | 26 | private func setupTerminationHandler() { 27 | // Handle Ctrl+C 28 | signal(SIGINT) { _ in 29 | NSApplication.shared.terminate(nil) 30 | } 31 | 32 | // Handle system shutdown 33 | signal(SIGTERM) { _ in 34 | NSApplication.shared.terminate(nil) 35 | } 36 | } 37 | 38 | private func setupRecording() async throws { 39 | // Check permissions first 40 | try await PermissionManager.shared.checkAllPermissions() 41 | 42 | // Initialize upload manager 43 | uploadManager = try UploadManager() 44 | 45 | guard let uploadManager = uploadManager else { 46 | throw NSError(domain: "com.efficient-recorder", code: -1, 47 | userInfo: [NSLocalizedDescriptionKey: "Failed to initialize upload manager"]) 48 | } 49 | 50 | // Initialize recorders 51 | screenRecorder = ScreenRecorder(uploadManager: uploadManager) 52 | systemAudioRecorder = AudioRecorder(source: .system, uploadManager: uploadManager) 53 | microphoneRecorder = AudioRecorder(source: .microphone, uploadManager: uploadManager) 54 | 55 | // Start recording 56 | try await startRecording() 57 | } 58 | 59 | private func startRecording() async throws { 60 | guard !isRecording else { return } 61 | 62 | do { 63 | try await screenRecorder?.start() 64 | try await systemAudioRecorder?.start() 65 | try await microphoneRecorder?.start() 66 | 67 | isRecording = true 68 | statusManager.recordingStarted() 69 | } catch { 70 | stopRecording() 71 | throw error 72 | } 73 | } 74 | 75 | private func stopRecording() { 76 | guard isRecording else { return } 77 | 78 | screenRecorder?.stop() 79 | systemAudioRecorder?.stop() 80 | microphoneRecorder?.stop() 81 | 82 | isRecording = false 83 | statusManager.recordingStopped() 84 | } 85 | 86 | func applicationWillTerminate(_ notification: Notification) { 87 | stopRecording() 88 | 89 | // Clean up temp files 90 | TempFileManager.shared.clearAll() 91 | } 92 | 93 | // Handle system sleep/wake 94 | func applicationProtectedDataWillBecomeUnavailable(_ notification: Notification) { 95 | stopRecording() 96 | } 97 | 98 | func applicationProtectedDataDidBecomeAvailable(_ notification: Notification) { 99 | Task { 100 | try? await startRecording() 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/AudioRecorder.swift: -------------------------------------------------------------------------------- 1 | 2 | import AVFoundation 3 | import Foundation 4 | 5 | final class AudioRecorder { 6 | enum AudioSource { 7 | case microphone 8 | case system 9 | } 10 | 11 | enum AudioError: Error { 12 | case engineSetupFailed 13 | case inputNodeMissing 14 | case monitoringFailed 15 | case recordingFailed 16 | } 17 | 18 | private let engine: AVAudioEngine 19 | private let source: AudioSource 20 | private let uploadManager: UploadManager 21 | private var isRecording = false 22 | private var lastDbLevel: Float = 0 23 | 24 | // Buffer for audio data 25 | private var audioBuffer: Data 26 | private let bufferLimit = 5 * 1024 * 1024 // 5MB 27 | 28 | init(source: AudioSource, uploadManager: UploadManager) { 29 | self.engine = AVAudioEngine() 30 | self.source = source 31 | self.uploadManager = uploadManager 32 | self.audioBuffer = Data() 33 | } 34 | 35 | func start() async throws { 36 | let session = AVAudioSession.sharedInstance() 37 | try await session.setCategory( 38 | source == .system ? .record : .playAndRecord, 39 | mode: .default, 40 | options: source == .system ? [.mixWithOthers, .defaultToSpeaker] : [] 41 | ) 42 | try await session.setActive(true) 43 | 44 | // Set up the audio format 45 | let inputNode = engine.inputNode 46 | let format = inputNode.outputFormat(forBus: 0) 47 | 48 | // Create a tap to monitor audio levels and collect data 49 | inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, time in 50 | guard let self = self else { return } 51 | self.processAudioBuffer(buffer) 52 | } 53 | 54 | try engine.start() 55 | } 56 | 57 | private func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { 58 | // Calculate dB level 59 | guard let channelData = buffer.floatChannelData?[0] else { return } 60 | let frameLength = UInt32(buffer.frameLength) 61 | 62 | var sum: Float = 0 63 | for i in 0..= ConfigManager.shared.dbThreshold else { 74 | if isRecording { 75 | flushBuffer() 76 | isRecording = false 77 | } 78 | return 79 | } 80 | 81 | isRecording = true 82 | 83 | // Convert buffer to Data and append 84 | let channelCount = Int(buffer.format.channelCount) 85 | let channels = UnsafeBufferPointer(start: buffer.floatChannelData, count: channelCount) 86 | let data = Data(bytes: channels[0], count: Int(frameLength) * MemoryLayout.size) 87 | 88 | audioBuffer.append(data) 89 | 90 | // If buffer reaches limit, flush it 91 | if audioBuffer.count >= bufferLimit { 92 | flushBuffer() 93 | } 94 | } 95 | 96 | private func flushBuffer() { 97 | guard !audioBuffer.isEmpty else { return } 98 | let dataToUpload = audioBuffer 99 | audioBuffer = Data() 100 | 101 | Task { 102 | do { 103 | try await uploadManager.uploadAudio( 104 | data: dataToUpload, 105 | source: source 106 | ) 107 | } catch { 108 | print("Failed to upload audio: \(error)") 109 | } 110 | } 111 | } 112 | 113 | func stop() { 114 | engine.stop() 115 | flushBuffer() 116 | } 117 | 118 | var currentDbLevel: Float { 119 | return lastDbLevel 120 | } 121 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/ConfigManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import KeychainAccess 3 | 4 | final class ConfigManager { 5 | static let shared = ConfigManager() 6 | 7 | private let keychain = Keychain(service: "com.efficient-recorder") 8 | private let apiKeyIdentifier = "r2_api_key" 9 | 10 | // Default configuration 11 | let screenResolution = CGSize(width: 1280, height: 720) 12 | let captureInterval: TimeInterval = 1.0 13 | let dbThreshold: Float = 50.0 14 | let audioSampleRate: Double = 44100.0 15 | 16 | private var storageConfig: StorageConfig? 17 | 18 | private init() {} 19 | 20 | func setupAPIKey(_ key: String) throws { 21 | try keychain.set(key, key: apiKeyIdentifier) 22 | // Verify we can create storage config with this key 23 | storageConfig = try StorageConfig(apiKey: key) 24 | } 25 | 26 | func getStorageConfig() throws -> StorageConfig { 27 | if let config = storageConfig { 28 | return config 29 | } 30 | 31 | guard let apiKey = try keychain.get(apiKeyIdentifier) else { 32 | throw ConfigError.invalidAPIKey 33 | } 34 | 35 | let config = try StorageConfig(apiKey: apiKey) 36 | storageConfig = config 37 | return config 38 | } 39 | 40 | func clearAPIKey() throws { 41 | try keychain.remove(apiKeyIdentifier) 42 | storageConfig = nil 43 | } 44 | 45 | func hasAPIKey() -> Bool { 46 | return (try? keychain.get(apiKeyIdentifier)) != nil 47 | } 48 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.efficient-recorder 7 | CFBundleName 8 | efficient-recorder 9 | CFBundlePackageType 10 | APPL 11 | CFBundleShortVersionString 12 | 1.0 13 | LSMinimumSystemVersion 14 | 12.0 15 | NSMicrophoneUsageDescription 16 | We need microphone access to record audio. 17 | NSScreenCaptureUsageDescription 18 | We need screen recording access to capture screenshots. 19 | NSSystemExtensionUsageDescription 20 | We need system audio capture capabilities. 21 | 22 | -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/PermissionManager.swift: -------------------------------------------------------------------------------- 1 | 2 | import AVFoundation 3 | import ScreenCaptureKit 4 | import CoreAudio 5 | import UserNotifications 6 | 7 | final class PermissionManager { 8 | static let shared = PermissionManager() 9 | 10 | private var screenCaptureAccess: Bool = false 11 | private var microphoneAccess: Bool = false 12 | private var systemAudioAccess: Bool = false 13 | 14 | private init() {} 15 | 16 | enum PermissionError: Error { 17 | case screenCaptureAccessDenied 18 | case microphoneAccessDenied 19 | case systemAudioAccessDenied 20 | } 21 | 22 | // Check all permissions at once 23 | func checkAllPermissions() async throws { 24 | try await checkScreenCaptureAccess() 25 | try await checkMicrophoneAccess() 26 | try await checkSystemAudioAccess() 27 | } 28 | 29 | // Screen Capture 30 | private func checkScreenCaptureAccess() async throws { 31 | // SCShareableContent is the modern API for screen capture 32 | let filter = SCContentFilter(.display, excluding: [], exceptingWindows: []) 33 | do { 34 | // This will trigger the permission prompt if needed 35 | let _ = try await SCShareableContent.current 36 | screenCaptureAccess = true 37 | } catch { 38 | screenCaptureAccess = false 39 | throw PermissionError.screenCaptureAccessDenied 40 | } 41 | } 42 | 43 | // Microphone 44 | private func checkMicrophoneAccess() async throws { 45 | switch AVCaptureDevice.authorizationStatus(for: .audio) { 46 | case .authorized: 47 | microphoneAccess = true 48 | case .notDetermined: 49 | microphoneAccess = await AVCaptureDevice.requestAccess(for: .audio) 50 | if !microphoneAccess { 51 | throw PermissionError.microphoneAccessDenied 52 | } 53 | default: 54 | microphoneAccess = false 55 | throw PermissionError.microphoneAccessDenied 56 | } 57 | } 58 | 59 | // System Audio 60 | private func checkSystemAudioAccess() async throws { 61 | var hasAccess = false 62 | 63 | // Use Core Audio to check system audio access 64 | var propertySize = UInt32(MemoryLayout.size) 65 | var hasPermission: UInt32 = 0 66 | 67 | let result = AudioObjectGetPropertyData( 68 | AudioObjectID(kAudioObjectSystemObject), 69 | &AudioObjectPropertyAddress( 70 | mSelector: kAudioHardwarePropertyUserSessionIsActiveOrHeadless, 71 | mScope: kAudioObjectPropertyScopeGlobal, 72 | mElement: kAudioObjectPropertyElementMain 73 | ), 74 | 0, 75 | nil, 76 | &propertySize, 77 | &hasPermission 78 | ) 79 | 80 | hasAccess = result == kAudioHardwareNoError && hasPermission == 1 81 | systemAudioAccess = hasAccess 82 | 83 | if !hasAccess { 84 | throw PermissionError.systemAudioAccessDenied 85 | } 86 | } 87 | 88 | // Helper to show instructions if permissions are missing 89 | func showPermissionInstructions() async { 90 | let center = UNUserNotificationCenter.current() 91 | let settings = await center.notificationSettings() 92 | 93 | // Only show notification if we have permission 94 | guard settings.authorizationStatus == .authorized else { 95 | print(""" 96 | Permission Instructions: 97 | 1. Screen Recording: System Settings > Privacy & Security > Screen Recording 98 | 2. Microphone: System Settings > Privacy & Security > Microphone 99 | 3. System Audio: No explicit permission needed, but audio capture must be enabled 100 | """) 101 | return 102 | } 103 | 104 | let content = UNMutableNotificationContent() 105 | content.title = "Missing Permissions" 106 | content.body = "Please check System Settings to grant necessary permissions" 107 | 108 | let request = UNNotificationRequest( 109 | identifier: UUID().uuidString, 110 | content: content, 111 | trigger: nil 112 | ) 113 | 114 | try? await center.add(request) 115 | } 116 | 117 | // Public getters for permission states 118 | var hasScreenCaptureAccess: Bool { screenCaptureAccess } 119 | var hasMicrophoneAccess: Bool { microphoneAccess } 120 | var hasSystemAudioAccess: Bool { systemAudioAccess } 121 | 122 | // Helper to check if all permissions are granted 123 | var hasAllPermissions: Bool { 124 | return screenCaptureAccess && microphoneAccess && systemAudioAccess 125 | } 126 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/ScreenRecorder.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import ScreenCaptureKit 4 | import CoreGraphics 5 | 6 | final class ScreenRecorder { 7 | enum ScreenRecorderError: Error { 8 | case captureSetupFailed 9 | case screenshotFailed 10 | case encodingFailed 11 | } 12 | 13 | private let uploadManager: UploadManager 14 | private var captureEngine: SCStream? 15 | private var isRunning = false 16 | private let config = ConfigManager.shared 17 | 18 | init(uploadManager: UploadManager) { 19 | self.uploadManager = uploadManager 20 | } 21 | 22 | func start() async throws { 23 | guard !isRunning else { return } 24 | 25 | // Get main display 26 | let content = try await SCShareableContent.current 27 | guard let display = content.displays.first else { 28 | throw ScreenRecorderError.captureSetupFailed 29 | } 30 | 31 | // Configure capture 32 | let filter = SCContentFilter(.display, excluding: [], exceptingWindows: []) 33 | 34 | let configuration = SCStreamConfiguration() 35 | configuration.width = Int(config.screenResolution.width) 36 | configuration.height = Int(config.screenResolution.height) 37 | configuration.minimumFrameInterval = CMTime(value: 1, timescale: 1) 38 | configuration.queueDepth = 1 39 | 40 | // Create and start the capture stream 41 | let stream = SCStream(filter: filter, configuration: configuration) 42 | 43 | try await stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global()) 44 | try await stream.startCapture() 45 | 46 | self.captureEngine = stream 47 | self.isRunning = true 48 | 49 | // Start capture timer 50 | startCaptureTimer() 51 | } 52 | 53 | private func startCaptureTimer() { 54 | Timer.scheduledTimer(withTimeInterval: config.captureInterval, repeats: true) { [weak self] _ in 55 | guard let self = self, self.isRunning else { return } 56 | self.captureFrame() 57 | } 58 | } 59 | 60 | private func captureFrame() { 61 | guard isRunning else { return } 62 | // Actual capture happens in stream(output:didOutput:of:) callback 63 | } 64 | 65 | func stop() { 66 | isRunning = false 67 | Task { 68 | await captureEngine?.stopCapture() 69 | captureEngine = nil 70 | } 71 | } 72 | } 73 | 74 | // SCStreamOutput implementation 75 | extension ScreenRecorder: SCStreamOutput { 76 | func stream(_ stream: SCStream, didOutput sample: CMSampleBuffer, of type: SCStreamOutputType) { 77 | guard type == .screen, 78 | let imageBuffer = sample.imageBuffer else { return } 79 | 80 | // Convert to PNG 81 | guard let data = createPNGData(from: imageBuffer) else { return } 82 | 83 | // Upload screenshot 84 | Task { 85 | do { 86 | try await uploadManager.uploadScreenshot(data: data) 87 | } catch { 88 | print("Failed to upload screenshot: \(error)") 89 | } 90 | } 91 | } 92 | 93 | private func createPNGData(from imageBuffer: CVImageBuffer) -> Data? { 94 | CVPixelBufferLockBaseAddress(imageBuffer, .readOnly) 95 | defer { CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) } 96 | 97 | let width = CVPixelBufferGetWidth(imageBuffer) 98 | let height = CVPixelBufferGetHeight(imageBuffer) 99 | let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) 100 | let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer) 101 | 102 | let context = CGContext( 103 | data: baseAddress, 104 | width: width, 105 | height: height, 106 | bitsPerComponent: 8, 107 | bytesPerRow: bytesPerRow, 108 | space: CGColorSpaceCreateDeviceRGB(), 109 | bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue 110 | ) 111 | 112 | guard let cgImage = context?.makeImage() else { return nil } 113 | return NSBitmapImageRep(cgImage: cgImage).representation(using: .png, properties: [:]) 114 | } 115 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/StatusManager.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | final class StatusManager { 5 | static let shared = StatusManager() 6 | 7 | private var recordingStartTime: Date? 8 | private var uploadStats: [String: UploadStats] = [:] 9 | 10 | private struct UploadStats { 11 | var totalBytes: Int64 = 0 12 | var uploadedFiles: Int = 0 13 | var failedUploads: Int = 0 14 | var lastUploadTime: Date? 15 | } 16 | 17 | enum Component: String { 18 | case screenshot = "Screenshots" 19 | case systemAudio = "System Audio" 20 | case micAudio = "Microphone Audio" 21 | } 22 | 23 | private init() {} 24 | 25 | // MARK: - Recording Status 26 | 27 | func recordingStarted() { 28 | recordingStartTime = Date() 29 | printStatus("Recording started") 30 | } 31 | 32 | func recordingStopped() { 33 | recordingStartTime = nil 34 | printStatus("Recording stopped") 35 | printSummary() 36 | } 37 | 38 | // MARK: - Upload Status 39 | 40 | func uploadStarted(component: Component, bytes: Int) { 41 | var stats = uploadStats[component.rawValue] ?? UploadStats() 42 | stats.totalBytes += Int64(bytes) 43 | stats.lastUploadTime = Date() 44 | uploadStats[component.rawValue] = stats 45 | 46 | printStatus("Starting upload: \(component.rawValue) (\(formatBytes(bytes)))") 47 | } 48 | 49 | func uploadCompleted(component: Component) { 50 | var stats = uploadStats[component.rawValue] ?? UploadStats() 51 | stats.uploadedFiles += 1 52 | stats.lastUploadTime = Date() 53 | uploadStats[component.rawValue] = stats 54 | 55 | printStatus("Upload completed: \(component.rawValue)") 56 | } 57 | 58 | func uploadFailed(component: Component, error: Error) { 59 | var stats = uploadStats[component.rawValue] ?? UploadStats() 60 | stats.failedUploads += 1 61 | uploadStats[component.rawValue] = stats 62 | 63 | printError("Upload failed: \(component.rawValue) - \(error.localizedDescription)") 64 | } 65 | 66 | // MARK: - Audio Levels 67 | 68 | func updateAudioLevel(component: Component, db: Float) { 69 | // Only print if significant change 70 | if db >= ConfigManager.shared.dbThreshold { 71 | printStatus("\(component.rawValue) level: \(String(format: "%.1f dB", db))") 72 | } 73 | } 74 | 75 | // MARK: - Error Reporting 76 | 77 | func reportError(_ error: Error, component: String) { 78 | printError("Error in \(component): \(error.localizedDescription)") 79 | } 80 | 81 | // MARK: - Printing Helpers 82 | 83 | private func printStatus(_ message: String) { 84 | let timestamp = DateFormatter.localizedString( 85 | from: Date(), 86 | dateStyle: .none, 87 | timeStyle: .medium 88 | ) 89 | print("[\(timestamp)] \(message)") 90 | } 91 | 92 | private func printError(_ message: String) { 93 | let timestamp = DateFormatter.localizedString( 94 | from: Date(), 95 | dateStyle: .none, 96 | timeStyle: .medium 97 | ) 98 | print("[\(timestamp)] ❌ \(message)") 99 | } 100 | 101 | private func printSummary() { 102 | guard !uploadStats.isEmpty else { return } 103 | 104 | print("\n=== Recording Session Summary ===") 105 | 106 | if let startTime = recordingStartTime { 107 | let duration = Int(-startTime.timeIntervalSinceNow) 108 | print("Duration: \(formatDuration(duration))") 109 | } 110 | 111 | for (component, stats) in uploadStats { 112 | print("\n\(component):") 113 | print(" Total Data: \(formatBytes(Int(stats.totalBytes)))") 114 | print(" Files Uploaded: \(stats.uploadedFiles)") 115 | if stats.failedUploads > 0 { 116 | print(" Failed Uploads: \(stats.failedUploads)") 117 | } 118 | } 119 | print("\n=============================") 120 | } 121 | 122 | // MARK: - Formatting Helpers 123 | 124 | private func formatBytes(_ bytes: Int) -> String { 125 | let units = ["B", "KB", "MB", "GB"] 126 | var value = Double(bytes) 127 | var unitIndex = 0 128 | 129 | while value > 1024 && unitIndex < units.count - 1 { 130 | value /= 1024 131 | unitIndex += 1 132 | } 133 | 134 | return String(format: "%.1f %@", value, units[unitIndex]) 135 | } 136 | 137 | private func formatDuration(_ seconds: Int) -> String { 138 | let hours = seconds / 3600 139 | let minutes = (seconds % 3600) / 60 140 | let seconds = seconds % 60 141 | 142 | if hours > 0 { 143 | return String(format: "%d:%02d:%02d", hours, minutes, seconds) 144 | } else { 145 | return String(format: "%d:%02d", minutes, seconds) 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/StorageConfig.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import KeychainAccess 4 | 5 | struct StorageConfig { 6 | let apiKey: String 7 | let bucketName: String 8 | let endpoint: URL 9 | let region: String 10 | 11 | // Multipart upload configuration 12 | let partSize: Int = 5 * 1024 * 1024 // 5MB default chunk size 13 | let maxRetries: Int = 3 14 | let uploadTimeout: TimeInterval = 30 15 | 16 | init(apiKey: String, bucketName: String = "recordings", 17 | endpoint: String = "https://your-endpoint.r2.cloudflarestorage.com", 18 | region: String = "auto") throws { 19 | guard !apiKey.isEmpty else { 20 | throw ConfigError.invalidAPIKey 21 | } 22 | guard let endpointURL = URL(string: endpoint) else { 23 | throw ConfigError.invalidEndpoint 24 | } 25 | 26 | self.apiKey = apiKey 27 | self.bucketName = bucketName 28 | self.endpoint = endpointURL 29 | self.region = region 30 | } 31 | } 32 | 33 | enum ConfigError: Error { 34 | case invalidAPIKey 35 | case invalidEndpoint 36 | case keychainError 37 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/TempFileManager.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | final class TempFileManager { 5 | static let shared = TempFileManager() 6 | 7 | private let fileManager = FileManager.default 8 | private let tempDirectory: URL 9 | 10 | private init() { 11 | tempDirectory = fileManager.temporaryDirectory 12 | .appendingPathComponent("efficient-recorder", isDirectory: true) 13 | 14 | try? fileManager.createDirectory(at: tempDirectory, 15 | withIntermediateDirectories: true) 16 | } 17 | 18 | func createTempFile(withExtension ext: String) throws -> URL { 19 | let fileName = UUID().uuidString + "." + ext 20 | return tempDirectory.appendingPathComponent(fileName) 21 | } 22 | 23 | func cleanOldFiles(olderThan age: TimeInterval = 3600) { 24 | guard let contents = try? fileManager.contentsOfDirectory( 25 | at: tempDirectory, 26 | includingPropertiesForKeys: [.creationDateKey] 27 | ) else { return } 28 | 29 | let oldDate = Date().addingTimeInterval(-age) 30 | 31 | for url in contents { 32 | guard let creation = try? url.resourceValues( 33 | forKeys: [.creationDateKey]).creationDate, 34 | creation < oldDate else { continue } 35 | 36 | try? fileManager.removeItem(at: url) 37 | } 38 | } 39 | 40 | func clearAll() { 41 | try? fileManager.removeItem(at: tempDirectory) 42 | try? fileManager.createDirectory(at: tempDirectory, 43 | withIntermediateDirectories: true) 44 | } 45 | 46 | deinit { 47 | clearAll() 48 | } 49 | } -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/UploadManager.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | final class UploadManager { 5 | enum UploadError: Error { 6 | case invalidConfiguration 7 | case uploadFailed(Error) 8 | case multipartUploadFailed 9 | case invalidResponse 10 | } 11 | 12 | private let tempFileManager: TempFileManager 13 | private let config: StorageConfig 14 | private let session: URLSession 15 | 16 | init() throws { 17 | self.tempFileManager = TempFileManager.shared 18 | self.config = try ConfigManager.shared.getStorageConfig() 19 | 20 | let configuration = URLSessionConfiguration.default 21 | configuration.timeoutIntervalForRequest = config.uploadTimeout 22 | configuration.httpMaximumConnectionsPerHost = 6 23 | self.session = URLSession(configuration: configuration) 24 | } 25 | 26 | // MARK: - Upload Methods 27 | 28 | func uploadScreenshot(data: Data) async throws { 29 | let fileName = "screenshot-\(Int(Date().timeIntervalSince1970)).png" 30 | try await uploadData(data, fileName: fileName) 31 | } 32 | 33 | func uploadAudio(data: Data, source: AudioRecorder.AudioSource) async throws { 34 | let prefix = source == .microphone ? "mic" : "system" 35 | let fileName = "\(prefix)-\(Int(Date().timeIntervalSince1970)).raw" 36 | try await uploadData(data, fileName: fileName) 37 | } 38 | 39 | // MARK: - Core Upload Logic 40 | 41 | private func uploadData(_ data: Data, fileName: String) async throws { 42 | // For small files, use direct upload 43 | if data.count < config.partSize { 44 | try await directUpload(data, fileName: fileName) 45 | return 46 | } 47 | 48 | // For larger files, use multipart upload 49 | try await multipartUpload(data, fileName: fileName) 50 | } 51 | 52 | private func directUpload(_ data: Data, fileName: String) async throws { 53 | var request = try createRequest(for: fileName) 54 | request.httpMethod = "PUT" 55 | 56 | let (_, response) = try await session.upload(for: request, from: data) 57 | 58 | guard let httpResponse = response as? HTTPURLResponse, 59 | (200...299).contains(httpResponse.statusCode) else { 60 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 61 | } 62 | } 63 | 64 | private func multipartUpload(_ data: Data, fileName: String) async throws { 65 | // 1. Initiate multipart upload 66 | let uploadId = try await initiateMultipartUpload(fileName: fileName) 67 | 68 | // 2. Upload parts 69 | var parts: [(partNumber: Int, etag: String)] = [] 70 | let chunks = stride(from: 0, to: data.count, by: config.partSize) 71 | 72 | for (index, offset) in chunks.enumerated() { 73 | let chunk = data[offset.. String { 107 | var request = try createRequest(for: fileName) 108 | request.httpMethod = "POST" 109 | request.url?.append(queryItems: [URLQueryItem(name: "uploads", value: "")]) 110 | 111 | let (data, response) = try await session.data(for: request) 112 | 113 | guard let httpResponse = response as? HTTPURLResponse, 114 | (200...299).contains(httpResponse.statusCode), 115 | let uploadId = String(data: data, encoding: .utf8)?.uploadIdFromXML() else { 116 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 117 | } 118 | 119 | return uploadId 120 | } 121 | 122 | private func uploadPart(_ data: Data, fileName: String, uploadId: String, partNumber: Int) async throws -> String { 123 | var request = try createRequest(for: fileName) 124 | request.httpMethod = "PUT" 125 | request.url?.append(queryItems: [ 126 | URLQueryItem(name: "partNumber", value: "\(partNumber)"), 127 | URLQueryItem(name: "uploadId", value: uploadId) 128 | ]) 129 | 130 | let (_, response) = try await session.upload(for: request, from: data) 131 | 132 | guard let httpResponse = response as? HTTPURLResponse, 133 | (200...299).contains(httpResponse.statusCode), 134 | let etag = httpResponse.allHeaderFields["ETag"] as? String else { 135 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 136 | } 137 | 138 | return etag 139 | } 140 | 141 | private func completeMultipartUpload(fileName: String, uploadId: String, parts: [(partNumber: Int, etag: String)]) async throws { 142 | var request = try createRequest(for: fileName) 143 | request.httpMethod = "POST" 144 | request.url?.append(queryItems: [URLQueryItem(name: "uploadId", value: uploadId)]) 145 | 146 | let completionXML = createCompletionXML(parts: parts) 147 | request.httpBody = completionXML.data(using: .utf8) 148 | 149 | let (_, response) = try await session.data(for: request) 150 | 151 | guard let httpResponse = response as? HTTPURLResponse, 152 | (200...299).contains(httpResponse.statusCode) else { 153 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 154 | } 155 | } 156 | 157 | private func abortMultipartUpload(fileName: String, uploadId: String) async throws { 158 | var request = try createRequest(for: fileName) 159 | request.httpMethod = "DELETE" 160 | request.url?.append(queryItems: [URLQueryItem(name: "uploadId", value: uploadId)]) 161 | 162 | let (_, response) = try await session.data(for: request) 163 | 164 | guard let httpResponse = response as? HTTPURLResponse, 165 | (200...299).contains(httpResponse.statusCode) else { 166 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 167 | } 168 | } 169 | 170 | // MARK: - Helper Methods 171 | 172 | private func createRequest(for fileName: String) throws -> URLRequest { 173 | var components = URLComponents(url: config.endpoint, resolvingAgainstBaseURL: false) 174 | components?.path = "/\(config.bucketName)/\(fileName)" 175 | 176 | guard let url = components?.url else { 177 | throw UploadError.invalidConfiguration 178 | } 179 | 180 | var request = URLRequest(url: url) 181 | request.setValue(config.apiKey, forHTTPHeaderField: "Authorization") 182 | return request 183 | } 184 | 185 | private func createCompletionXML(parts: [(partNumber: Int, etag: String)]) -> String { 186 | let partTags = parts 187 | .sorted { $0.partNumber < $1.partNumber } 188 | .map { "\($0.partNumber)\($0.etag)" } 189 | .joined() 190 | 191 | return """ 192 | 193 | 194 | \(partTags) 195 | 196 | """ 197 | } 198 | } 199 | 200 | // MARK: - String Extension for XML Parsing 201 | private extension String { 202 | func uploadIdFromXML() -> String? { 203 | guard let start = range(of: "")?.upperBound, 204 | let end = range(of: "")?.lowerBound else { 205 | return nil 206 | } 207 | return String(self[start.. 2 | 3 | 4 | 5 | com.apple.security.device.audio-input 6 | 7 | com.apple.security.device.microphone 8 | 9 | com.apple.security.screen-capture 10 | 11 | com.apple.security.automation.apple-events 12 | 13 | com.apple.security.temporary-exception.apple-events 14 | com.apple.systemevents 15 | com.apple.security.cs.allow-jit 16 | 17 | com.apple.security.cs.allow-unsigned-executable-memory 18 | 19 | com.apple.security.cs.disable-library-validation 20 | 21 | 22 | -------------------------------------------------------------------------------- /swift-version/Sources/EfficientRecorder/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppKit 3 | 4 | // Parse command line arguments 5 | let arguments = CommandLine.arguments 6 | 7 | if arguments.count > 1 { 8 | if arguments[1] == "--setup-key" { 9 | guard arguments.count == 3 else { 10 | print("Usage: ./efficient-recorder --setup-key YOUR_R2_KEY") 11 | exit(1) 12 | } 13 | 14 | do { 15 | try ConfigManager.shared.setupAPIKey(arguments[2]) 16 | print("API key successfully configured") 17 | exit(0) 18 | } catch { 19 | print("Failed to setup API key: \(error.localizedDescription)") 20 | exit(1) 21 | } 22 | } else { 23 | print("Unknown argument: \(arguments[1])") 24 | print("Usage: ./efficient-recorder [--setup-key YOUR_R2_KEY]") 25 | exit(1) 26 | } 27 | } 28 | 29 | // Check if API key is configured 30 | guard ConfigManager.shared.hasAPIKey() else { 31 | print("No API key configured. Please run with --setup-key first") 32 | print("Usage: ./efficient-recorder --setup-key YOUR_R2_KEY") 33 | exit(1) 34 | } 35 | 36 | // Create and start application 37 | let app = NSApplication.shared 38 | let delegate = AppDelegate() 39 | app.delegate = delegate 40 | app.run() -------------------------------------------------------------------------------- /swift-version/implementation.md: -------------------------------------------------------------------------------- 1 | Let's start with the core configuration files. I'll implement them in order: 2 | 3 | ```swift 4 | // StorageConfig.swift 5 | 6 | import Foundation 7 | import KeychainAccess 8 | 9 | struct StorageConfig { 10 | let apiKey: String 11 | let bucketName: String 12 | let endpoint: URL 13 | let region: String 14 | 15 | // Multipart upload configuration 16 | let partSize: Int = 5 * 1024 * 1024 // 5MB default chunk size 17 | let maxRetries: Int = 3 18 | let uploadTimeout: TimeInterval = 30 19 | 20 | init(apiKey: String, bucketName: String = "recordings", 21 | endpoint: String = "https://your-endpoint.r2.cloudflarestorage.com", 22 | region: String = "auto") throws { 23 | guard !apiKey.isEmpty else { 24 | throw ConfigError.invalidAPIKey 25 | } 26 | guard let endpointURL = URL(string: endpoint) else { 27 | throw ConfigError.invalidEndpoint 28 | } 29 | 30 | self.apiKey = apiKey 31 | self.bucketName = bucketName 32 | self.endpoint = endpointURL 33 | self.region = region 34 | } 35 | } 36 | 37 | enum ConfigError: Error { 38 | case invalidAPIKey 39 | case invalidEndpoint 40 | case keychainError 41 | } 42 | ``` 43 | 44 | ```swift 45 | // ConfigManager.swift 46 | import Foundation 47 | import KeychainAccess 48 | 49 | final class ConfigManager { 50 | static let shared = ConfigManager() 51 | 52 | private let keychain = Keychain(service: "com.efficient-recorder") 53 | private let apiKeyIdentifier = "r2_api_key" 54 | 55 | // Default configuration 56 | let screenResolution = CGSize(width: 1280, height: 720) 57 | let captureInterval: TimeInterval = 1.0 58 | let dbThreshold: Float = 50.0 59 | let audioSampleRate: Double = 44100.0 60 | 61 | private var storageConfig: StorageConfig? 62 | 63 | private init() {} 64 | 65 | func setupAPIKey(_ key: String) throws { 66 | try keychain.set(key, key: apiKeyIdentifier) 67 | // Verify we can create storage config with this key 68 | storageConfig = try StorageConfig(apiKey: key) 69 | } 70 | 71 | func getStorageConfig() throws -> StorageConfig { 72 | if let config = storageConfig { 73 | return config 74 | } 75 | 76 | guard let apiKey = try keychain.get(apiKeyIdentifier) else { 77 | throw ConfigError.invalidAPIKey 78 | } 79 | 80 | let config = try StorageConfig(apiKey: apiKey) 81 | storageConfig = config 82 | return config 83 | } 84 | 85 | func clearAPIKey() throws { 86 | try keychain.remove(apiKeyIdentifier) 87 | storageConfig = nil 88 | } 89 | 90 | func hasAPIKey() -> Bool { 91 | return (try? keychain.get(apiKeyIdentifier)) != nil 92 | } 93 | } 94 | ``` 95 | 96 | ```swift 97 | // TempFileManager.swift 98 | 99 | import Foundation 100 | 101 | final class TempFileManager { 102 | static let shared = TempFileManager() 103 | 104 | private let fileManager = FileManager.default 105 | private let tempDirectory: URL 106 | 107 | private init() { 108 | tempDirectory = fileManager.temporaryDirectory 109 | .appendingPathComponent("efficient-recorder", isDirectory: true) 110 | 111 | try? fileManager.createDirectory(at: tempDirectory, 112 | withIntermediateDirectories: true) 113 | } 114 | 115 | func createTempFile(withExtension ext: String) throws -> URL { 116 | let fileName = UUID().uuidString + "." + ext 117 | return tempDirectory.appendingPathComponent(fileName) 118 | } 119 | 120 | func cleanOldFiles(olderThan age: TimeInterval = 3600) { 121 | guard let contents = try? fileManager.contentsOfDirectory( 122 | at: tempDirectory, 123 | includingPropertiesForKeys: [.creationDateKey] 124 | ) else { return } 125 | 126 | let oldDate = Date().addingTimeInterval(-age) 127 | 128 | for url in contents { 129 | guard let creation = try? url.resourceValues( 130 | forKeys: [.creationDateKey]).creationDate, 131 | creation < oldDate else { continue } 132 | 133 | try? fileManager.removeItem(at: url) 134 | } 135 | } 136 | 137 | func clearAll() { 138 | try? fileManager.removeItem(at: tempDirectory) 139 | try? fileManager.createDirectory(at: tempDirectory, 140 | withIntermediateDirectories: true) 141 | } 142 | 143 | deinit { 144 | clearAll() 145 | } 146 | } 147 | ``` 148 | 149 | ```swift 150 | // PermissionManager.swift 151 | 152 | import AVFoundation 153 | import ScreenCaptureKit 154 | import CoreAudio 155 | import UserNotifications 156 | 157 | final class PermissionManager { 158 | static let shared = PermissionManager() 159 | 160 | private var screenCaptureAccess: Bool = false 161 | private var microphoneAccess: Bool = false 162 | private var systemAudioAccess: Bool = false 163 | 164 | private init() {} 165 | 166 | enum PermissionError: Error { 167 | case screenCaptureAccessDenied 168 | case microphoneAccessDenied 169 | case systemAudioAccessDenied 170 | } 171 | 172 | // Check all permissions at once 173 | func checkAllPermissions() async throws { 174 | try await checkScreenCaptureAccess() 175 | try await checkMicrophoneAccess() 176 | try await checkSystemAudioAccess() 177 | } 178 | 179 | // Screen Capture 180 | private func checkScreenCaptureAccess() async throws { 181 | // SCShareableContent is the modern API for screen capture 182 | let filter = SCContentFilter(.display, excluding: [], exceptingWindows: []) 183 | do { 184 | // This will trigger the permission prompt if needed 185 | let _ = try await SCShareableContent.current 186 | screenCaptureAccess = true 187 | } catch { 188 | screenCaptureAccess = false 189 | throw PermissionError.screenCaptureAccessDenied 190 | } 191 | } 192 | 193 | // Microphone 194 | private func checkMicrophoneAccess() async throws { 195 | switch AVCaptureDevice.authorizationStatus(for: .audio) { 196 | case .authorized: 197 | microphoneAccess = true 198 | case .notDetermined: 199 | microphoneAccess = await AVCaptureDevice.requestAccess(for: .audio) 200 | if !microphoneAccess { 201 | throw PermissionError.microphoneAccessDenied 202 | } 203 | default: 204 | microphoneAccess = false 205 | throw PermissionError.microphoneAccessDenied 206 | } 207 | } 208 | 209 | // System Audio 210 | private func checkSystemAudioAccess() async throws { 211 | var hasAccess = false 212 | 213 | // Use Core Audio to check system audio access 214 | var propertySize = UInt32(MemoryLayout.size) 215 | var hasPermission: UInt32 = 0 216 | 217 | let result = AudioObjectGetPropertyData( 218 | AudioObjectID(kAudioObjectSystemObject), 219 | &AudioObjectPropertyAddress( 220 | mSelector: kAudioHardwarePropertyUserSessionIsActiveOrHeadless, 221 | mScope: kAudioObjectPropertyScopeGlobal, 222 | mElement: kAudioObjectPropertyElementMain 223 | ), 224 | 0, 225 | nil, 226 | &propertySize, 227 | &hasPermission 228 | ) 229 | 230 | hasAccess = result == kAudioHardwareNoError && hasPermission == 1 231 | systemAudioAccess = hasAccess 232 | 233 | if !hasAccess { 234 | throw PermissionError.systemAudioAccessDenied 235 | } 236 | } 237 | 238 | // Helper to show instructions if permissions are missing 239 | func showPermissionInstructions() async { 240 | let center = UNUserNotificationCenter.current() 241 | let settings = await center.notificationSettings() 242 | 243 | // Only show notification if we have permission 244 | guard settings.authorizationStatus == .authorized else { 245 | print(""" 246 | Permission Instructions: 247 | 1. Screen Recording: System Settings > Privacy & Security > Screen Recording 248 | 2. Microphone: System Settings > Privacy & Security > Microphone 249 | 3. System Audio: No explicit permission needed, but audio capture must be enabled 250 | """) 251 | return 252 | } 253 | 254 | let content = UNMutableNotificationContent() 255 | content.title = "Missing Permissions" 256 | content.body = "Please check System Settings to grant necessary permissions" 257 | 258 | let request = UNNotificationRequest( 259 | identifier: UUID().uuidString, 260 | content: content, 261 | trigger: nil 262 | ) 263 | 264 | try? await center.add(request) 265 | } 266 | 267 | // Public getters for permission states 268 | var hasScreenCaptureAccess: Bool { screenCaptureAccess } 269 | var hasMicrophoneAccess: Bool { microphoneAccess } 270 | var hasSystemAudioAccess: Bool { systemAudioAccess } 271 | 272 | // Helper to check if all permissions are granted 273 | var hasAllPermissions: Bool { 274 | return screenCaptureAccess && microphoneAccess && systemAudioAccess 275 | } 276 | } 277 | ``` 278 | 279 | This implementation: 280 | 281 | 1. Uses modern async/await API 282 | 2. Handles all three required permissions: 283 | - Screen recording (ScreenCaptureKit) 284 | - Microphone (AVFoundation) 285 | - System audio (CoreAudio) 286 | 3. Provides both individual and batch permission checking 287 | 4. Includes user-friendly instruction display 288 | 5. Caches permission states 289 | 6. Uses proper error handling 290 | 291 | Key features: 292 | 293 | - Singleton pattern for global access 294 | - Async permission checks to handle UI prompts 295 | - Clear error types for each permission 296 | - Both notification and console-based instruction display 297 | - Property getters for permission states 298 | 299 | ```swift 300 | // AudioRecorder.swift 301 | 302 | import AVFoundation 303 | import Foundation 304 | 305 | final class AudioRecorder { 306 | enum AudioSource { 307 | case microphone 308 | case system 309 | } 310 | 311 | enum AudioError: Error { 312 | case engineSetupFailed 313 | case inputNodeMissing 314 | case monitoringFailed 315 | case recordingFailed 316 | } 317 | 318 | private let engine: AVAudioEngine 319 | private let source: AudioSource 320 | private let uploadManager: UploadManager 321 | private var isRecording = false 322 | private var lastDbLevel: Float = 0 323 | 324 | // Buffer for audio data 325 | private var audioBuffer: Data 326 | private let bufferLimit = 5 * 1024 * 1024 // 5MB 327 | 328 | init(source: AudioSource, uploadManager: UploadManager) { 329 | self.engine = AVAudioEngine() 330 | self.source = source 331 | self.uploadManager = uploadManager 332 | self.audioBuffer = Data() 333 | } 334 | 335 | func start() async throws { 336 | let session = AVAudioSession.sharedInstance() 337 | try await session.setCategory( 338 | source == .system ? .record : .playAndRecord, 339 | mode: .default, 340 | options: source == .system ? [.mixWithOthers, .defaultToSpeaker] : [] 341 | ) 342 | try await session.setActive(true) 343 | 344 | // Set up the audio format 345 | let inputNode = engine.inputNode 346 | let format = inputNode.outputFormat(forBus: 0) 347 | 348 | // Create a tap to monitor audio levels and collect data 349 | inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, time in 350 | guard let self = self else { return } 351 | self.processAudioBuffer(buffer) 352 | } 353 | 354 | try engine.start() 355 | } 356 | 357 | private func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { 358 | // Calculate dB level 359 | guard let channelData = buffer.floatChannelData?[0] else { return } 360 | let frameLength = UInt32(buffer.frameLength) 361 | 362 | var sum: Float = 0 363 | for i in 0..= ConfigManager.shared.dbThreshold else { 374 | if isRecording { 375 | flushBuffer() 376 | isRecording = false 377 | } 378 | return 379 | } 380 | 381 | isRecording = true 382 | 383 | // Convert buffer to Data and append 384 | let channelCount = Int(buffer.format.channelCount) 385 | let channels = UnsafeBufferPointer(start: buffer.floatChannelData, count: channelCount) 386 | let data = Data(bytes: channels[0], count: Int(frameLength) * MemoryLayout.size) 387 | 388 | audioBuffer.append(data) 389 | 390 | // If buffer reaches limit, flush it 391 | if audioBuffer.count >= bufferLimit { 392 | flushBuffer() 393 | } 394 | } 395 | 396 | private func flushBuffer() { 397 | guard !audioBuffer.isEmpty else { return } 398 | let dataToUpload = audioBuffer 399 | audioBuffer = Data() 400 | 401 | Task { 402 | do { 403 | try await uploadManager.uploadAudio( 404 | data: dataToUpload, 405 | source: source 406 | ) 407 | } catch { 408 | print("Failed to upload audio: \(error)") 409 | } 410 | } 411 | } 412 | 413 | func stop() { 414 | engine.stop() 415 | flushBuffer() 416 | } 417 | 418 | var currentDbLevel: Float { 419 | return lastDbLevel 420 | } 421 | } 422 | ``` 423 | 424 | ```swift 425 | // ScreenRecorder.swift 426 | 427 | import Foundation 428 | import ScreenCaptureKit 429 | import CoreGraphics 430 | 431 | final class ScreenRecorder { 432 | enum ScreenRecorderError: Error { 433 | case captureSetupFailed 434 | case screenshotFailed 435 | case encodingFailed 436 | } 437 | 438 | private let uploadManager: UploadManager 439 | private var captureEngine: SCStream? 440 | private var isRunning = false 441 | private let config = ConfigManager.shared 442 | 443 | init(uploadManager: UploadManager) { 444 | self.uploadManager = uploadManager 445 | } 446 | 447 | func start() async throws { 448 | guard !isRunning else { return } 449 | 450 | // Get main display 451 | let content = try await SCShareableContent.current 452 | guard let display = content.displays.first else { 453 | throw ScreenRecorderError.captureSetupFailed 454 | } 455 | 456 | // Configure capture 457 | let filter = SCContentFilter(.display, excluding: [], exceptingWindows: []) 458 | 459 | let configuration = SCStreamConfiguration() 460 | configuration.width = Int(config.screenResolution.width) 461 | configuration.height = Int(config.screenResolution.height) 462 | configuration.minimumFrameInterval = CMTime(value: 1, timescale: 1) 463 | configuration.queueDepth = 1 464 | 465 | // Create and start the capture stream 466 | let stream = SCStream(filter: filter, configuration: configuration) 467 | 468 | try await stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global()) 469 | try await stream.startCapture() 470 | 471 | self.captureEngine = stream 472 | self.isRunning = true 473 | 474 | // Start capture timer 475 | startCaptureTimer() 476 | } 477 | 478 | private func startCaptureTimer() { 479 | Timer.scheduledTimer(withTimeInterval: config.captureInterval, repeats: true) { [weak self] _ in 480 | guard let self = self, self.isRunning else { return } 481 | self.captureFrame() 482 | } 483 | } 484 | 485 | private func captureFrame() { 486 | guard isRunning else { return } 487 | // Actual capture happens in stream(output:didOutput:of:) callback 488 | } 489 | 490 | func stop() { 491 | isRunning = false 492 | Task { 493 | await captureEngine?.stopCapture() 494 | captureEngine = nil 495 | } 496 | } 497 | } 498 | 499 | // SCStreamOutput implementation 500 | extension ScreenRecorder: SCStreamOutput { 501 | func stream(_ stream: SCStream, didOutput sample: CMSampleBuffer, of type: SCStreamOutputType) { 502 | guard type == .screen, 503 | let imageBuffer = sample.imageBuffer else { return } 504 | 505 | // Convert to PNG 506 | guard let data = createPNGData(from: imageBuffer) else { return } 507 | 508 | // Upload screenshot 509 | Task { 510 | do { 511 | try await uploadManager.uploadScreenshot(data: data) 512 | } catch { 513 | print("Failed to upload screenshot: \(error)") 514 | } 515 | } 516 | } 517 | 518 | private func createPNGData(from imageBuffer: CVImageBuffer) -> Data? { 519 | CVPixelBufferLockBaseAddress(imageBuffer, .readOnly) 520 | defer { CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) } 521 | 522 | let width = CVPixelBufferGetWidth(imageBuffer) 523 | let height = CVPixelBufferGetHeight(imageBuffer) 524 | let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) 525 | let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer) 526 | 527 | let context = CGContext( 528 | data: baseAddress, 529 | width: width, 530 | height: height, 531 | bitsPerComponent: 8, 532 | bytesPerRow: bytesPerRow, 533 | space: CGColorSpaceCreateDeviceRGB(), 534 | bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue 535 | ) 536 | 537 | guard let cgImage = context?.makeImage() else { return nil } 538 | return NSBitmapImageRep(cgImage: cgImage).representation(using: .png, properties: [:]) 539 | } 540 | } 541 | ``` 542 | 543 | Key features of these implementations: 544 | 545 | AudioRecorder: 546 | 547 | 1. Handles both system and microphone audio 548 | 2. Implements dB level monitoring 549 | 3. Buffers audio data up to 5MB before uploading 550 | 4. Uses AVAudioEngine for efficient capture 551 | 5. Handles format conversion for upload 552 | 553 | ScreenRecorder: 554 | 555 | 1. Uses ScreenCaptureKit for modern screen capture 556 | 2. Implements 1fps capture using timer 557 | 3. Converts frames to PNG format 558 | 4. Handles resolution configuration 559 | 5. Efficient memory management with proper buffer handling 560 | 561 | ```swift 562 | // UploadManager.swift 563 | 564 | import Foundation 565 | 566 | final class UploadManager { 567 | enum UploadError: Error { 568 | case invalidConfiguration 569 | case uploadFailed(Error) 570 | case multipartUploadFailed 571 | case invalidResponse 572 | } 573 | 574 | private let tempFileManager: TempFileManager 575 | private let config: StorageConfig 576 | private let session: URLSession 577 | 578 | init() throws { 579 | self.tempFileManager = TempFileManager.shared 580 | self.config = try ConfigManager.shared.getStorageConfig() 581 | 582 | let configuration = URLSessionConfiguration.default 583 | configuration.timeoutIntervalForRequest = config.uploadTimeout 584 | configuration.httpMaximumConnectionsPerHost = 6 585 | self.session = URLSession(configuration: configuration) 586 | } 587 | 588 | // MARK: - Upload Methods 589 | 590 | func uploadScreenshot(data: Data) async throws { 591 | let fileName = "screenshot-\(Int(Date().timeIntervalSince1970)).png" 592 | try await uploadData(data, fileName: fileName) 593 | } 594 | 595 | func uploadAudio(data: Data, source: AudioRecorder.AudioSource) async throws { 596 | let prefix = source == .microphone ? "mic" : "system" 597 | let fileName = "\(prefix)-\(Int(Date().timeIntervalSince1970)).raw" 598 | try await uploadData(data, fileName: fileName) 599 | } 600 | 601 | // MARK: - Core Upload Logic 602 | 603 | private func uploadData(_ data: Data, fileName: String) async throws { 604 | // For small files, use direct upload 605 | if data.count < config.partSize { 606 | try await directUpload(data, fileName: fileName) 607 | return 608 | } 609 | 610 | // For larger files, use multipart upload 611 | try await multipartUpload(data, fileName: fileName) 612 | } 613 | 614 | private func directUpload(_ data: Data, fileName: String) async throws { 615 | var request = try createRequest(for: fileName) 616 | request.httpMethod = "PUT" 617 | 618 | let (_, response) = try await session.upload(for: request, from: data) 619 | 620 | guard let httpResponse = response as? HTTPURLResponse, 621 | (200...299).contains(httpResponse.statusCode) else { 622 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 623 | } 624 | } 625 | 626 | private func multipartUpload(_ data: Data, fileName: String) async throws { 627 | // 1. Initiate multipart upload 628 | let uploadId = try await initiateMultipartUpload(fileName: fileName) 629 | 630 | // 2. Upload parts 631 | var parts: [(partNumber: Int, etag: String)] = [] 632 | let chunks = stride(from: 0, to: data.count, by: config.partSize) 633 | 634 | for (index, offset) in chunks.enumerated() { 635 | let chunk = data[offset.. String { 669 | var request = try createRequest(for: fileName) 670 | request.httpMethod = "POST" 671 | request.url?.append(queryItems: [URLQueryItem(name: "uploads", value: "")]) 672 | 673 | let (data, response) = try await session.data(for: request) 674 | 675 | guard let httpResponse = response as? HTTPURLResponse, 676 | (200...299).contains(httpResponse.statusCode), 677 | let uploadId = String(data: data, encoding: .utf8)?.uploadIdFromXML() else { 678 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 679 | } 680 | 681 | return uploadId 682 | } 683 | 684 | private func uploadPart(_ data: Data, fileName: String, uploadId: String, partNumber: Int) async throws -> String { 685 | var request = try createRequest(for: fileName) 686 | request.httpMethod = "PUT" 687 | request.url?.append(queryItems: [ 688 | URLQueryItem(name: "partNumber", value: "\(partNumber)"), 689 | URLQueryItem(name: "uploadId", value: uploadId) 690 | ]) 691 | 692 | let (_, response) = try await session.upload(for: request, from: data) 693 | 694 | guard let httpResponse = response as? HTTPURLResponse, 695 | (200...299).contains(httpResponse.statusCode), 696 | let etag = httpResponse.allHeaderFields["ETag"] as? String else { 697 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 698 | } 699 | 700 | return etag 701 | } 702 | 703 | private func completeMultipartUpload(fileName: String, uploadId: String, parts: [(partNumber: Int, etag: String)]) async throws { 704 | var request = try createRequest(for: fileName) 705 | request.httpMethod = "POST" 706 | request.url?.append(queryItems: [URLQueryItem(name: "uploadId", value: uploadId)]) 707 | 708 | let completionXML = createCompletionXML(parts: parts) 709 | request.httpBody = completionXML.data(using: .utf8) 710 | 711 | let (_, response) = try await session.data(for: request) 712 | 713 | guard let httpResponse = response as? HTTPURLResponse, 714 | (200...299).contains(httpResponse.statusCode) else { 715 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 716 | } 717 | } 718 | 719 | private func abortMultipartUpload(fileName: String, uploadId: String) async throws { 720 | var request = try createRequest(for: fileName) 721 | request.httpMethod = "DELETE" 722 | request.url?.append(queryItems: [URLQueryItem(name: "uploadId", value: uploadId)]) 723 | 724 | let (_, response) = try await session.data(for: request) 725 | 726 | guard let httpResponse = response as? HTTPURLResponse, 727 | (200...299).contains(httpResponse.statusCode) else { 728 | throw UploadError.uploadFailed(URLError(.badServerResponse)) 729 | } 730 | } 731 | 732 | // MARK: - Helper Methods 733 | 734 | private func createRequest(for fileName: String) throws -> URLRequest { 735 | var components = URLComponents(url: config.endpoint, resolvingAgainstBaseURL: false) 736 | components?.path = "/\(config.bucketName)/\(fileName)" 737 | 738 | guard let url = components?.url else { 739 | throw UploadError.invalidConfiguration 740 | } 741 | 742 | var request = URLRequest(url: url) 743 | request.setValue(config.apiKey, forHTTPHeaderField: "Authorization") 744 | return request 745 | } 746 | 747 | private func createCompletionXML(parts: [(partNumber: Int, etag: String)]) -> String { 748 | let partTags = parts 749 | .sorted { $0.partNumber < $1.partNumber } 750 | .map { "\($0.partNumber)\($0.etag)" } 751 | .joined() 752 | 753 | return """ 754 | 755 | 756 | \(partTags) 757 | 758 | """ 759 | } 760 | } 761 | 762 | // MARK: - String Extension for XML Parsing 763 | private extension String { 764 | func uploadIdFromXML() -> String? { 765 | guard let start = range(of: "")?.upperBound, 766 | let end = range(of: "")?.lowerBound else { 767 | return nil 768 | } 769 | return String(self[start..= ConfigManager.shared.dbThreshold { 856 | printStatus("\(component.rawValue) level: \(String(format: "%.1f dB", db))") 857 | } 858 | } 859 | 860 | // MARK: - Error Reporting 861 | 862 | func reportError(_ error: Error, component: String) { 863 | printError("Error in \(component): \(error.localizedDescription)") 864 | } 865 | 866 | // MARK: - Printing Helpers 867 | 868 | private func printStatus(_ message: String) { 869 | let timestamp = DateFormatter.localizedString( 870 | from: Date(), 871 | dateStyle: .none, 872 | timeStyle: .medium 873 | ) 874 | print("[\(timestamp)] \(message)") 875 | } 876 | 877 | private func printError(_ message: String) { 878 | let timestamp = DateFormatter.localizedString( 879 | from: Date(), 880 | dateStyle: .none, 881 | timeStyle: .medium 882 | ) 883 | print("[\(timestamp)] ❌ \(message)") 884 | } 885 | 886 | private func printSummary() { 887 | guard !uploadStats.isEmpty else { return } 888 | 889 | print("\n=== Recording Session Summary ===") 890 | 891 | if let startTime = recordingStartTime { 892 | let duration = Int(-startTime.timeIntervalSinceNow) 893 | print("Duration: \(formatDuration(duration))") 894 | } 895 | 896 | for (component, stats) in uploadStats { 897 | print("\n\(component):") 898 | print(" Total Data: \(formatBytes(Int(stats.totalBytes)))") 899 | print(" Files Uploaded: \(stats.uploadedFiles)") 900 | if stats.failedUploads > 0 { 901 | print(" Failed Uploads: \(stats.failedUploads)") 902 | } 903 | } 904 | print("\n=============================") 905 | } 906 | 907 | // MARK: - Formatting Helpers 908 | 909 | private func formatBytes(_ bytes: Int) -> String { 910 | let units = ["B", "KB", "MB", "GB"] 911 | var value = Double(bytes) 912 | var unitIndex = 0 913 | 914 | while value > 1024 && unitIndex < units.count - 1 { 915 | value /= 1024 916 | unitIndex += 1 917 | } 918 | 919 | return String(format: "%.1f %@", value, units[unitIndex]) 920 | } 921 | 922 | private func formatDuration(_ seconds: Int) -> String { 923 | let hours = seconds / 3600 924 | let minutes = (seconds % 3600) / 60 925 | let seconds = seconds % 60 926 | 927 | if hours > 0 { 928 | return String(format: "%d:%02d:%02d", hours, minutes, seconds) 929 | } else { 930 | return String(format: "%d:%02d", minutes, seconds) 931 | } 932 | } 933 | } 934 | ``` 935 | 936 | Key features of this implementation: 937 | 938 | 1. Simple console-based status reporting 939 | 2. Tracks: 940 | - Recording duration 941 | - Upload statistics per component 942 | - Audio levels 943 | - Errors 944 | 3. Provides a summary on session end 945 | 4. Formatted output for: 946 | - Timestamps 947 | - File sizes 948 | - Duration 949 | - Error messages 950 | 5. Thread-safe singleton design 951 | 6. Minimal memory footprint 952 | 7. Clean separation of concerns 953 | 954 | I'll implement the final layer with both `AppDelegate.swift` and `main.swift`: 955 | 956 | ```swift 957 | // AppDelegate.swift 958 | 959 | import Cocoa 960 | 961 | class AppDelegate: NSObject, NSApplicationDelegate { 962 | private var screenRecorder: ScreenRecorder? 963 | private var systemAudioRecorder: AudioRecorder? 964 | private var microphoneRecorder: AudioRecorder? 965 | private var uploadManager: UploadManager? 966 | private let statusManager = StatusManager.shared 967 | 968 | private var isRecording = false 969 | 970 | func applicationDidFinishLaunching(_ notification: Notification) { 971 | setupTerminationHandler() 972 | 973 | Task { 974 | do { 975 | try await setupRecording() 976 | } catch { 977 | statusManager.reportError(error, component: "Setup") 978 | NSApplication.shared.terminate(nil) 979 | } 980 | } 981 | } 982 | 983 | private func setupTerminationHandler() { 984 | // Handle Ctrl+C 985 | signal(SIGINT) { _ in 986 | NSApplication.shared.terminate(nil) 987 | } 988 | 989 | // Handle system shutdown 990 | signal(SIGTERM) { _ in 991 | NSApplication.shared.terminate(nil) 992 | } 993 | } 994 | 995 | private func setupRecording() async throws { 996 | // Check permissions first 997 | try await PermissionManager.shared.checkAllPermissions() 998 | 999 | // Initialize upload manager 1000 | uploadManager = try UploadManager() 1001 | 1002 | guard let uploadManager = uploadManager else { 1003 | throw NSError(domain: "com.efficient-recorder", code: -1, 1004 | userInfo: [NSLocalizedDescriptionKey: "Failed to initialize upload manager"]) 1005 | } 1006 | 1007 | // Initialize recorders 1008 | screenRecorder = ScreenRecorder(uploadManager: uploadManager) 1009 | systemAudioRecorder = AudioRecorder(source: .system, uploadManager: uploadManager) 1010 | microphoneRecorder = AudioRecorder(source: .microphone, uploadManager: uploadManager) 1011 | 1012 | // Start recording 1013 | try await startRecording() 1014 | } 1015 | 1016 | private func startRecording() async throws { 1017 | guard !isRecording else { return } 1018 | 1019 | do { 1020 | try await screenRecorder?.start() 1021 | try await systemAudioRecorder?.start() 1022 | try await microphoneRecorder?.start() 1023 | 1024 | isRecording = true 1025 | statusManager.recordingStarted() 1026 | } catch { 1027 | stopRecording() 1028 | throw error 1029 | } 1030 | } 1031 | 1032 | private func stopRecording() { 1033 | guard isRecording else { return } 1034 | 1035 | screenRecorder?.stop() 1036 | systemAudioRecorder?.stop() 1037 | microphoneRecorder?.stop() 1038 | 1039 | isRecording = false 1040 | statusManager.recordingStopped() 1041 | } 1042 | 1043 | func applicationWillTerminate(_ notification: Notification) { 1044 | stopRecording() 1045 | 1046 | // Clean up temp files 1047 | TempFileManager.shared.clearAll() 1048 | } 1049 | 1050 | // Handle system sleep/wake 1051 | func applicationProtectedDataWillBecomeUnavailable(_ notification: Notification) { 1052 | stopRecording() 1053 | } 1054 | 1055 | func applicationProtectedDataDidBecomeAvailable(_ notification: Notification) { 1056 | Task { 1057 | try? await startRecording() 1058 | } 1059 | } 1060 | } 1061 | ``` 1062 | 1063 | ```swift 1064 | // main.swift 1065 | import Foundation 1066 | import AppKit 1067 | 1068 | // Parse command line arguments 1069 | let arguments = CommandLine.arguments 1070 | 1071 | if arguments.count > 1 { 1072 | if arguments[1] == "--setup-key" { 1073 | guard arguments.count == 3 else { 1074 | print("Usage: ./efficient-recorder --setup-key YOUR_R2_KEY") 1075 | exit(1) 1076 | } 1077 | 1078 | do { 1079 | try ConfigManager.shared.setupAPIKey(arguments[2]) 1080 | print("API key successfully configured") 1081 | exit(0) 1082 | } catch { 1083 | print("Failed to setup API key: \(error.localizedDescription)") 1084 | exit(1) 1085 | } 1086 | } else { 1087 | print("Unknown argument: \(arguments[1])") 1088 | print("Usage: ./efficient-recorder [--setup-key YOUR_R2_KEY]") 1089 | exit(1) 1090 | } 1091 | } 1092 | 1093 | // Check if API key is configured 1094 | guard ConfigManager.shared.hasAPIKey() else { 1095 | print("No API key configured. Please run with --setup-key first") 1096 | print("Usage: ./efficient-recorder --setup-key YOUR_R2_KEY") 1097 | exit(1) 1098 | } 1099 | 1100 | // Create and start application 1101 | let app = NSApplication.shared 1102 | let delegate = AppDelegate() 1103 | app.delegate = delegate 1104 | app.run() 1105 | ``` 1106 | 1107 | Key features of this implementation: 1108 | 1109 | 1. AppDelegate: 1110 | 1111 | - Clean initialization sequence 1112 | - Proper cleanup on termination 1113 | - Handles system signals (SIGINT, SIGTERM) 1114 | - System sleep/wake handling 1115 | - Graceful error handling 1116 | - Coordinated start/stop of all recorders 1117 | 1118 | 2. Main: 1119 | - Simple command-line argument parsing 1120 | - API key setup mode 1121 | - Clear usage instructions 1122 | - Validation before startup 1123 | - Clean application lifecycle 1124 | 1125 | The application can now be run in two ways: 1126 | 1127 | 1. Initial setup: `./efficient-recorder --setup-key YOUR_R2_KEY` 1128 | 2. Normal operation: `./efficient-recorder` 1129 | -------------------------------------------------------------------------------- /swift-version/pseudocode.md: -------------------------------------------------------------------------------- 1 | I want a macOS program that takes 1 screenshot every second and also records a separate system audio and mic audio. the screenshot doesn't need to be processed locally. the recording must be paused if db level is below 50. it must be as energy efficient as possible locally. it must stream both audio streams separately to a cloudflare r2 bucket, and it must upload the screenshots to there too (can use multipart upload). 2 | 3 | Here's the minimal set of high-impact, lower-complexity optimizations to get us to ~10% battery impact: 4 | 5 | ``` 6 | Screen Capture (Highest Power Draw, Essential to Optimize): 7 | 8 | Motion detection before capture (only save if >2% pixels changed) 9 | Reduced resolution to 1280x720 10 | Hardware-accelerated HEIC compression 11 | Dynamic capture rate: 1-3s based on changes 12 | 13 | swiftCopy// Simple pixel difference detection 14 | func shouldCapture(\_ newFrame: CGImage) -> Bool { 15 | guard let lastFrame = lastFrameData else { return true } 16 | let difference = calculateDifference(newFrame, lastFrame) 17 | return difference > 0.02 || 18 | Date().timeIntervalSince(lastCaptureTime) > 3.0 19 | } 20 | 21 | Audio Processing (Second Highest Draw): 22 | 23 | Two-tier audio sampling: 24 | 25 | 8kHz for dB monitoring 26 | 44.1kHz only when sound detected 27 | 28 | Hardware DSP for dB detection 29 | 30 | swiftCopyfunc setupAudioMonitoring() { 31 | let lowQualityFormat = AVAudioFormat( 32 | sampleRate: 8000, 33 | channels: 1 34 | ) 35 | inputNode.installTap(onBus: 0, 36 | bufferSize: 512, 37 | format: lowQualityFormat) 38 | } 39 | 40 | Network Optimization (Third Highest Draw): 41 | 42 | Simple batching (5MB chunks or 30s worth) 43 | HTTP/2 multiplexing 44 | 45 | swiftCopyclass SimpleUploadManager { 46 | private var batchSize = 5 _ 1024 _ 1024 // 5MB 47 | private var currentBatch = Data() 48 | 49 | func addToBatch(_ data: Data) { 50 | currentBatch.append(data) 51 | if currentBatch.count >= batchSize { 52 | uploadBatch() 53 | } 54 | } 55 | 56 | } 57 | ``` 58 | 59 | This minimal set: 60 | 61 | Reduces screen capture power by ~60% 62 | Reduces audio processing power by ~50% 63 | Reduces network power by ~40% 64 | 65 | Total Impact: 66 | 67 | Original: ~15-20% battery drain 68 | With these optimizations: ~8-10% battery drain 69 | Complexity: Moderate 70 | Maintenance: Reasonable 71 | 72 | Let me reformat with filenames in backticks above each codeblock. 73 | 74 | `Package.swift` 75 | 76 | ``` 77 | Set Swift 5.9+ 78 | Add dependencies: 79 | - AWSClientRuntime 80 | - AWSS3 81 | System frameworks: 82 | - AVFoundation 83 | - ScreenCaptureKit 84 | - CoreGraphics 85 | - UserNotifications 86 | - CoreMedia 87 | - VideoToolbox (for HEIC) 88 | 89 | Create executable target "efficient-recorder" 90 | ``` 91 | 92 | `Sources/EfficientRecorder/Models/StorageConfig.swift` 93 | 94 | ``` 95 | Struct containing S3 configuration: 96 | - S3 API URL (from command line) 97 | - Access Key ID (from command line) 98 | - Secret Access Key (from command line) 99 | - Default region 100 | - Multipart upload config 101 | - Bucket name 102 | 103 | Add validation methods for S3 credentials 104 | Add method to test S3 connectivity on startup 105 | ``` 106 | 107 | `Sources/EfficientRecorder/Upload/S3Manager.swift` 108 | 109 | ``` 110 | Create S3 client configuration: 111 | - Initialize with provided credentials 112 | - Set up endpoint configuration 113 | - Configure HTTP client settings 114 | - Set up retry policies 115 | 116 | Handle multipart upload orchestration: 117 | - Manage separate upload queues for screenshots and audio 118 | - Implement 5MB chunked uploads 119 | - Track ETags for multipart completion 120 | - Handle upload failures and retries 121 | - Implement cleanup for failed uploads 122 | ``` 123 | 124 | `Sources/EfficientRecorder/Permissions/PermissionManager.swift` 125 | 126 | ``` 127 | Handle macOS permissions workflow: 128 | - Screen recording (CGRequestScreenCaptureAccess) 129 | - Microphone (AVCaptureDevice.requestAccess) 130 | - System audio (CoreAudio) 131 | 132 | Show user instructions if permissions missing 133 | Cache permission states 134 | Handle permission changes during runtime 135 | ``` 136 | 137 | `Sources/EfficientRecorder/Capture/ScreenRecorder.swift` 138 | 139 | ``` 140 | Use ScreenCaptureKit for capture: 141 | - Initialize at 1280x720 142 | - Use hardware encoding 143 | - Capture every 1-3s based on: 144 | - Calculate frame difference (>2% threshold) 145 | - Time since last capture 146 | - Current battery level 147 | Convert to HEIC using VideoToolbox 148 | Send to S3Manager 149 | Handle display configuration changes 150 | ``` 151 | 152 | `Sources/EfficientRecorder/Capture/AudioRecorder.swift` 153 | 154 | ``` 155 | Manage two AVAudioEngine instances: 156 | - System audio capture 157 | - Microphone capture 158 | 159 | Each engine: 160 | - Start at 8kHz monitoring mode 161 | - Use AudioConverter for hardware DSP 162 | - Calculate dB using vDSP 163 | - When above 50dB: 164 | - Switch to 44.1kHz 165 | - Start recording 166 | - When below 50dB: 167 | - Switch back to 8kHz 168 | - Stop recording 169 | 170 | Send audio chunks to S3Manager 171 | Handle route changes (headphones etc) 172 | ``` 173 | 174 | `Sources/EfficientRecorder/Storage/TempFileManager.swift` 175 | 176 | ``` 177 | Handle temporary storage: 178 | - Create unique temp directories 179 | - Clean up old files 180 | - Monitor available space 181 | - Handle cleanup on crash 182 | Use system temp directory 183 | ``` 184 | 185 | `Sources/EfficientRecorder/Status/StatusManager.swift` 186 | 187 | ``` 188 | Simple terminal status: 189 | - Current recording state 190 | - Upload progress 191 | - Error reporting 192 | - Basic performance stats 193 | - S3 connection status 194 | Use print() with timestamps 195 | ``` 196 | 197 | `Sources/EfficientRecorder/Config/ConfigManager.swift` 198 | 199 | ``` 200 | Handle configuration: 201 | - Parse command line arguments for S3 credentials 202 | - Default settings: 203 | - Resolution: 1280x720 204 | - Initial interval: 1s 205 | - Max interval: 3s 206 | - Motion threshold: 2% 207 | - dB threshold: 50 208 | - Batch size: 5MB 209 | - Upload timeout: 30s 210 | ``` 211 | 212 | `Sources/EfficientRecorder/main.swift` 213 | 214 | ``` 215 | Entry point: 216 | 1. Parse required arguments: 217 | - --s3-url 218 | - --access-key-id 219 | - --secret-access-key 220 | 2. Validate S3 credentials 221 | 3. Test S3 connectivity 222 | 4. Check system permissions 223 | 5. Initialize capture managers 224 | 6. Start recording 225 | 7. Handle SIGINT/SIGTERM 226 | 227 | Show usage instructions if arguments missing 228 | Exit with appropriate error codes for: 229 | - Missing arguments 230 | - Invalid credentials 231 | - Connection failures 232 | ``` 233 | 234 | `Sources/EfficientRecorder/AppDelegate.swift` 235 | 236 | ``` 237 | Handle application lifecycle: 238 | - Clean startup 239 | - Clean shutdown 240 | - Permission changes 241 | - System sleep/wake 242 | - S3 connection management 243 | ``` 244 | 245 | `Build Requirements` 246 | 247 | ``` 248 | Xcode project needs: 249 | - Developer signing 250 | - Entitlements: 251 | - com.apple.security.screen-capture 252 | - com.apple.security.microphone 253 | - com.apple.security.audio-capture 254 | - Info.plist permissions 255 | - Minimum macOS 12.0 256 | ``` 257 | 258 | `Usage Instructions` 259 | 260 | ``` 261 | Required arguments: 262 | ./efficient-recorder --s3-url YOUR_S3_URL --access-key-id YOUR_ACCESS_KEY_ID --secret-access-key YOUR_SECRET_ACCESS_KEY 263 | 264 | Example: 265 | ./efficient-recorder --s3-url https://your-bucket.s3.amazonaws.com --access-key-id AKIAXXXXXXXX --secret-access-key AbCdEf123456 266 | ``` 267 | -------------------------------------------------------------------------------- /swift-version/write.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require("fs").promises; 3 | const path = require("path"); 4 | 5 | function parseMarkdownFile(markdownContent) { 6 | const codeBlockRegex = /```(?:\w*)\n([\s\S]*?)```/g; 7 | const result = { files: {} }; 8 | 9 | let match; 10 | while ((match = codeBlockRegex.exec(markdownContent)) !== null) { 11 | const codeBlock = match[1]; 12 | const lines = codeBlock.trim().split("\n"); 13 | 14 | if (lines[0].startsWith("// ")) { 15 | const path = lines[0].slice(3).trim(); 16 | const content = lines.slice(1).join("\n"); 17 | result.files[path] = { content }; 18 | } 19 | } 20 | 21 | return result; 22 | } 23 | 24 | async function createDirectories(filePath) { 25 | const dirname = path.dirname(filePath); 26 | try { 27 | await fs.access(dirname); 28 | } catch { 29 | await fs.mkdir(dirname, { recursive: true }); 30 | } 31 | } 32 | 33 | async function writeFiles(markdownContent, outputDir = "./output") { 34 | try { 35 | const parsed = parseMarkdownFile(markdownContent); 36 | await createDirectories(outputDir); 37 | 38 | const results = []; 39 | for (const [filePath, { content }] of Object.entries(parsed.files)) { 40 | const fullPath = path.join(outputDir, filePath); 41 | await createDirectories(fullPath); 42 | await fs.writeFile(fullPath, content); 43 | results.push(`Created file: ${fullPath}`); 44 | } 45 | 46 | return results; 47 | } catch (error) { 48 | throw new Error(`Error writing files: ${error.message}`); 49 | } 50 | } 51 | 52 | async function main() { 53 | const [, , inputFile] = process.argv; 54 | 55 | if (!inputFile) { 56 | console.error("Usage: node script.js "); 57 | process.exit(1); 58 | } 59 | 60 | try { 61 | const markdownContent = await fs.readFile(inputFile, "utf-8"); 62 | const results = await writeFiles(markdownContent); 63 | results.forEach((result) => console.log(result)); 64 | } catch (error) { 65 | console.error(`Error: ${error.message}`); 66 | process.exit(1); 67 | } 68 | } 69 | 70 | main(); 71 | -------------------------------------------------------------------------------- /tweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janwilmake/efficient-recorder/c85dbf83aa3fd8de6da08742155f2dcac49e38e4/tweet.png --------------------------------------------------------------------------------