commit aecb5face50bcc15a4725b3d521c98d09086a4b1 Author: Bun Bun Date: Mon Jun 15 18:31:50 2026 +0000 RhythmCal MVP: Energy-Aware AI Scheduler Chrome extension diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b9d0c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.verdict +*.local diff --git a/generate-icons.cjs b/generate-icons.cjs new file mode 100644 index 0000000..761c3e6 --- /dev/null +++ b/generate-icons.cjs @@ -0,0 +1,26 @@ +const fs = require('fs'); +const path = require('path'); + +// Minimal valid 1x1 PNG (gray pixel) for placeholders +// Real icons would be proper assets, but these satisfy manifest requirements for MVP +const minimalPng = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk length + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, // 8-bit RGB, compression + 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, // IDAT chunk + 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, // compressed data (gray pixel) + 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xFE, // + 0xD7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, // IEND chunk + 0x44, 0xAE, 0x42, 0x60, 0x82 +]); + +const iconDir = path.join(__dirname, 'src', 'icons'); +fs.mkdirSync(iconDir, { recursive: true }); + +const sizes = [16, 48, 128]; +for (const size of sizes) { + fs.writeFileSync(path.join(iconDir, `icon${size}.png`), minimalPng); +} + +console.log('Icons generated:', sizes.map(s => `icon${s}.png`).join(', ')); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8827090 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1524 @@ +{ + "name": "rhythmcal", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rhythmcal", + "version": "1.0.0", + "dependencies": { + "date-fns": "^3.6.0" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "@types/chrome": "^0.0.268", + "@types/node": "^20.14.0", + "typescript": "^5.4.0", + "vite": "^5.2.0" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.6.1.tgz", + "integrity": "sha512-0RmYlUtQGvHXCz3B/FW7P5j+RBZUc4mk3GIRPXCx5a52N8AQ0e35GvE+Pcap/TKuI09PJgcRTdXeBOavtLVyYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "node-html-parser": "^7.0.2", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.80.0", + "rxjs": "7.5.7", + "tinyglobby": "^0.2.15" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chrome": { + "version": "0.0.268", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", + "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9f9b66 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "rhythmcal", + "version": "1.0.0", + "description": "Energy-Aware AI Scheduler for Chrome", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "test": "node --test dist/test/**/*.test.js", + "lint": "tsc --noEmit" + }, + "dependencies": { + "date-fns": "^3.6.0" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "@types/chrome": "^0.0.268", + "@types/node": "^20.14.0", + "typescript": "^5.4.0", + "vite": "^5.2.0" + } +} diff --git a/public/icon128.png b/public/icon128.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/public/icon128.png differ diff --git a/public/icon16.png b/public/icon16.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/public/icon16.png differ diff --git a/public/icon48.png b/public/icon48.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/public/icon48.png differ diff --git a/public/icons/icon128.png b/public/icons/icon128.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/public/icons/icon128.png differ diff --git a/public/icons/icon16.png b/public/icons/icon16.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/public/icons/icon16.png differ diff --git a/public/icons/icon48.png b/public/icons/icon48.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/public/icons/icon48.png differ diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..7ea9150 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,86 @@ +import { getStorage, setStorage } from './lib/storage.js'; +import { DEFAULT_PROFILE, type EnergyProfile } from './lib/energy.js'; +import { findFreeSlots, suggestSlot, type ScheduleSuggestion } from './lib/scheduler.js'; +import { listPrimaryEvents, createPrimaryEvent, type CalendarEvent } from './lib/calendar.js'; + +chrome.runtime.onInstalled.addListener(async (details) => { + if (details.reason === 'install') { + const existing = await getStorage('energy_profile'); + if (!existing) { + await setStorage('energy_profile', DEFAULT_PROFILE); + } + chrome.alarms.create('energy-check', { periodInMinutes: 60 }); + } +}); + +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === 'energy-check') { + const now = new Date(); + const hour = now.getHours(); + const profile = await getStorage('energy_profile'); + if (!profile) return; + + const window = profile.windows.find(w => { + if (w.startHour <= w.endHour) { + return hour >= w.startHour && hour < w.endHour; + } + return hour >= w.startHour || hour < w.endHour; + }); + + if (window && window.isRecovery && profile.protectRecovery) { + chrome.notifications.create('recovery-' + Date.now(), { + type: 'basic', + iconUrl: 'icons/icon128.png', + title: 'RhythmCal — Recovery Time', + message: `It's ${window.label}. Protect this window — avoid scheduling meetings now.`, + priority: 1, + }); + } + } +}); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + (async () => { + try { + if (request.action === 'getProfile') { + const profile = await getStorage('energy_profile') || DEFAULT_PROFILE; + sendResponse({ success: true, profile }); + } else if (request.action === 'saveProfile') { + await setStorage('energy_profile', request.profile); + sendResponse({ success: true }); + } else if (request.action === 'getEvents') { + const { timeMin, timeMax } = request; + const events = await listPrimaryEvents(timeMin, timeMax); + sendResponse({ success: true, events }); + } else if (request.action === 'suggestSlot') { + const { title, duration, date, timeMin, timeMax } = request; + const events = await listPrimaryEvents(timeMin, timeMax); + const profile = await getStorage('energy_profile') || DEFAULT_PROFILE; + const suggestion = suggestSlot(title, duration, events, profile, new Date(date)); + sendResponse({ success: true, suggestion }); + } else if (request.action === 'scheduleEvent') { + const { title, start, end, description } = request; + const event = await createPrimaryEvent({ + summary: title, + start: { dateTime: start }, + end: { dateTime: end }, + description, + }); + sendResponse({ success: true, event }); + } else if (request.action === 'getFreeSlots') { + const { date, timeMin, timeMax, minDuration } = request; + const events = await listPrimaryEvents(timeMin, timeMax); + const profile = await getStorage('energy_profile') || DEFAULT_PROFILE; + const slots = findFreeSlots(events, profile, new Date(date), minDuration); + sendResponse({ success: true, slots }); + } else { + sendResponse({ success: false, error: 'Unknown action' }); + } + } catch (err: any) { + sendResponse({ success: false, error: err.message || String(err) }); + } + })(); + return true; +}); + +console.log('RhythmCal background service worker started'); diff --git a/src/icons/icon128.png b/src/icons/icon128.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/src/icons/icon128.png differ diff --git a/src/icons/icon16.png b/src/icons/icon16.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/src/icons/icon16.png differ diff --git a/src/icons/icon48.png b/src/icons/icon48.png new file mode 100644 index 0000000..4daa1f4 Binary files /dev/null and b/src/icons/icon48.png differ diff --git a/src/lib/calendar.ts b/src/lib/calendar.ts new file mode 100644 index 0000000..3dc32a3 --- /dev/null +++ b/src/lib/calendar.ts @@ -0,0 +1,103 @@ +import { getStorage, setStorage, removeStorage } from './storage.js'; + +const CALENDAR_SCOPES = 'https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events'; + +export interface CalendarEvent { + id: string; + summary: string; + start: { dateTime?: string; date?: string }; + end: { dateTime?: string; date?: string }; + description?: string; + location?: string; + status?: string; +} + +export async function getAuthToken(): Promise { + return new Promise((resolve, reject) => { + chrome.identity.getAuthToken({ interactive: true }, (token) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(token ?? null); + } + }); + }); +} + +export async function revokeToken(): Promise { + const token = await getStorage('gapi_token'); + if (token) { + await new Promise((resolve) => { + chrome.identity.removeCachedAuthToken({ token }, () => { + chrome.identity.launchWebAuthFlow({ + url: `https://accounts.google.com/o/oauth2/revoke?token=${token}`, + interactive: false, + }, () => resolve()); + }); + }); + await removeStorage('gapi_token'); + } +} + +async function gapiFetch(path: string, token: string, options: RequestInit = {}): Promise { + const url = `https://www.googleapis.com/calendar/v3${path}`; + const res = await fetch(url, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + if (!res.ok) { + if (res.status === 401) { + await revokeToken(); + } + throw new Error(`Calendar API ${res.status}: ${await res.text()}`); + } + return res.json(); +} + +export async function listCalendars(): Promise<{ id: string; summary: string }[]> { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + const data = await gapiFetch('/users/me/calendarList', token); + return (data.items || []).map((c: any) => ({ id: c.id, summary: c.summary })); +} + +export async function listEvents(calendarId: string, timeMin: string, timeMax: string): Promise { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + const params = new URLSearchParams({ timeMin, timeMax, singleEvents: 'true', orderBy: 'startTime' }); + const data = await gapiFetch(`/calendars/${encodeURIComponent(calendarId)}/events?${params}`, token); + return data.items || []; +} + +export async function createEvent(calendarId: string, event: Partial): Promise { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + const data = await gapiFetch(`/calendars/${encodeURIComponent(calendarId)}/events`, token, { + method: 'POST', + body: JSON.stringify(event), + }); + return data; +} + +export async function deleteEvent(calendarId: string, eventId: string): Promise { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + await gapiFetch(`/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`, token, { method: 'DELETE' }); +} + +export async function listPrimaryEvents(timeMin: string, timeMax: string): Promise { + const calendars = await listCalendars(); + const primary = calendars.find(c => c.id === 'primary') || calendars[0]; + if (!primary) return []; + return listEvents(primary.id, timeMin, timeMax); +} + +export async function createPrimaryEvent(event: Partial): Promise { + return createEvent('primary', event); +} + + diff --git a/src/lib/energy.ts b/src/lib/energy.ts new file mode 100644 index 0000000..5db76ce --- /dev/null +++ b/src/lib/energy.ts @@ -0,0 +1,86 @@ +export interface EnergyWindow { + startHour: number; // 0-23 + endHour: number; // 0-23 + energyLevel: number; // 1-5, 5 = peak energy + label: string; + isRecovery: boolean; +} + +export interface EnergyProfile { + windows: EnergyWindow[]; + workDays: number[]; // 0-6, 0 = Sunday + defaultEventDuration: number; // minutes + preferredMeetingDuration: number; // minutes + minRecoveryBlock: number; // minutes + protectRecovery: boolean; +} + +export const DEFAULT_PROFILE: EnergyProfile = { + windows: [ + { startHour: 6, endHour: 9, energyLevel: 4, label: 'Morning Ramp', isRecovery: false }, + { startHour: 9, endHour: 12, energyLevel: 5, label: 'Deep Work Peak', isRecovery: false }, + { startHour: 12, endHour: 13, energyLevel: 2, label: 'Lunch / Recovery', isRecovery: true }, + { startHour: 13, endHour: 15, energyLevel: 3, label: 'Light Work', isRecovery: false }, + { startHour: 15, endHour: 16, energyLevel: 2, label: 'Afternoon Dip', isRecovery: true }, + { startHour: 16, endHour: 18, energyLevel: 3, label: 'Second Wind', isRecovery: false }, + { startHour: 18, endHour: 22, energyLevel: 2, label: 'Evening Wind-down', isRecovery: true }, + { startHour: 22, endHour: 6, energyLevel: 1, label: 'Sleep', isRecovery: true }, + ], + workDays: [1, 2, 3, 4, 5], // Mon-Fri + defaultEventDuration: 60, + preferredMeetingDuration: 30, + minRecoveryBlock: 30, + protectRecovery: true, +}; + +export function getEnergyLevelAtHour(profile: EnergyProfile, hour: number): number { + const window = profile.windows.find(w => { + if (w.startHour <= w.endHour) { + return hour >= w.startHour && hour < w.endHour; + } + // Overnight window (e.g. 22-6) + return hour >= w.startHour || hour < w.endHour; + }); + return window ? window.energyLevel : 1; +} + +export function isRecoveryWindow(profile: EnergyProfile, hour: number): boolean { + const window = profile.windows.find(w => { + if (w.startHour <= w.endHour) { + return hour >= w.startHour && hour < w.endHour; + } + return hour >= w.startHour || hour < w.endHour; + }); + return window ? window.isRecovery : false; +} + +export function getWindowLabel(profile: EnergyProfile, hour: number): string { + const window = profile.windows.find(w => { + if (w.startHour <= w.endHour) { + return hour >= w.startHour && hour < w.endHour; + } + return hour >= w.startHour || hour < w.endHour; + }); + return window ? window.label : 'Unknown'; +} + +export function energyScore(profile: EnergyProfile, startHour: number, durationHours: number): number { + let total = 0; + let count = 0; + for (let h = 0; h < durationHours; h++) { + const hour = (startHour + h) % 24; + total += getEnergyLevelAtHour(profile, hour); + count++; + } + return count > 0 ? total / count : 0; +} + +export function blocksRecovery(profile: EnergyProfile, startHour: number, durationMinutes: number): boolean { + if (!profile.protectRecovery) return false; + const durationHours = Math.ceil(durationMinutes / 60); + for (let h = 0; h <= durationHours; h++) { + const hour = (startHour + h) % 24; + if (isRecoveryWindow(profile, hour)) return true; + } + return false; +} diff --git a/src/lib/scheduler.ts b/src/lib/scheduler.ts new file mode 100644 index 0000000..8e0ac3b --- /dev/null +++ b/src/lib/scheduler.ts @@ -0,0 +1,135 @@ +import { EnergyProfile, energyScore, blocksRecovery, getEnergyLevelAtHour, isRecoveryWindow, getWindowLabel } from './energy.js'; +import { CalendarEvent, listPrimaryEvents, createPrimaryEvent } from './calendar.js'; +import { getStorage, setStorage } from './storage.js'; + +export interface Slot { + start: Date; + end: Date; + energyScore: number; + blocksRecovery: boolean; + conflicts: CalendarEvent[]; + label: string; +} + +export interface ScheduleSuggestion { + title: string; + suggestedStart: Date; + suggestedEnd: Date; + energyScore: number; + reason: string; + recoveryWarning: string | null; +} + +export function findFreeSlots( + events: CalendarEvent[], + profile: EnergyProfile, + date: Date, + minDurationMinutes: number = 30 +): Slot[] { + const slots: Slot[] = []; + const dayStart = new Date(date); + dayStart.setHours(6, 0, 0, 0); + const dayEnd = new Date(date); + dayEnd.setHours(22, 0, 0, 0); + + const occupied = events + .filter(e => e.start?.dateTime) + .map(e => ({ + start: new Date(e.start.dateTime!), + end: new Date(e.end?.dateTime || e.start.dateTime!), + })) + .sort((a, b) => a.start.getTime() - b.start.getTime()); + + let cursor = dayStart.getTime(); + + for (const block of occupied) { + if (block.start.getTime() > cursor + minDurationMinutes * 60 * 1000) { + const slotStart = new Date(cursor); + const slotEnd = new Date(block.start.getTime()); + const hour = slotStart.getHours(); + const durationHours = (slotEnd.getTime() - slotStart.getTime()) / 3600000; + const score = energyScore(profile, hour, durationHours); + slots.push({ + start: slotStart, + end: slotEnd, + energyScore: score, + blocksRecovery: blocksRecovery(profile, hour, (slotEnd.getTime() - slotStart.getTime()) / 60000), + conflicts: [], + label: getWindowLabel(profile, hour), + }); + } + cursor = Math.max(cursor, block.end.getTime()); + } + + if (dayEnd.getTime() > cursor + minDurationMinutes * 60 * 1000) { + const hour = new Date(cursor).getHours(); + slots.push({ + start: new Date(cursor), + end: new Date(dayEnd.getTime()), + energyScore: energyScore(profile, hour, (dayEnd.getTime() - cursor) / 3600000), + blocksRecovery: blocksRecovery(profile, hour, (dayEnd.getTime() - cursor) / 60000), + conflicts: [], + label: getWindowLabel(profile, hour), + }); + } + + return slots; +} + +export function suggestSlot( + title: string, + durationMinutes: number, + events: CalendarEvent[], + profile: EnergyProfile, + date: Date +): ScheduleSuggestion | null { + const slots = findFreeSlots(events, profile, date, durationMinutes); + if (slots.length === 0) return null; + + const valid = slots.filter(s => { + const ms = s.end.getTime() - s.start.getTime(); + return ms >= durationMinutes * 60 * 1000; + }); + + if (valid.length === 0) return null; + + const candidates = valid.filter(s => !s.blocksRecovery); + const pool = candidates.length > 0 ? candidates : valid; + + const best = pool.reduce((a, b) => (a.energyScore > b.energyScore ? a : b)); + const start = new Date(best.start); + const end = new Date(start.getTime() + durationMinutes * 60 * 1000); + + let reason: string; + if (best.energyScore >= 4.5) { + reason = `Peak energy window (${best.label}) — ideal for deep work.`; + } else if (best.energyScore >= 3) { + reason = `Moderate energy (${best.label}) — good for focused tasks.`; + } else { + reason = `Low energy (${best.label}) — consider a shorter session or lighter task.`; + } + + let recoveryWarning: string | null = null; + if (best.blocksRecovery) { + const recoveryLabel = profile.windows.find(w => w.isRecovery && start.getHours() >= w.startHour && start.getHours() < w.endHour)?.label; + recoveryWarning = `This slot overlaps a recovery window (${recoveryLabel || 'rest period'}). Consider protecting your energy.`; + } + + return { title, suggestedStart: start, suggestedEnd: end, energyScore: best.energyScore, reason, recoveryWarning }; +} + +export function energyOverlayForDay( + profile: EnergyProfile, + date: Date +): { hour: number; level: number; label: string; isRecovery: boolean }[] { + const result = []; + for (let h = 0; h < 24; h++) { + result.push({ + hour: h, + level: getEnergyLevelAtHour(profile, h), + label: getWindowLabel(profile, h), + isRecovery: isRecoveryWindow(profile, h), + }); + } + return result; +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..0da8d83 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,19 @@ +export async function getStorage(key: string): Promise { + return new Promise((resolve) => { + chrome.storage.local.get([key], (result) => { + resolve(result[key] ?? null); + }); + }); +} + +export async function setStorage(key: string, value: T): Promise { + return new Promise((resolve) => { + chrome.storage.local.set({ [key]: value }, () => resolve()); + }); +} + +export async function removeStorage(key: string): Promise { + return new Promise((resolve) => { + chrome.storage.local.remove(key, () => resolve()); + }); +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..db724d9 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,40 @@ +{ + "manifest_version": 3, + "name": "RhythmCal — Energy-Aware Scheduler", + "version": "1.0.0", + "description": "AI calendar assistant that respects your energy rhythms and protects recovery windows", + "permissions": [ + "identity", + "storage", + "alarms", + "notifications" + ], + "host_permissions": [ + "https://www.googleapis.com/*", + "https://accounts.google.com/*" + ], + "action": { + "default_popup": "src/popup/popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "options_page": "src/options/options.html", + "background": { + "service_worker": "src/background.ts", + "type": "module" + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "web_accessible_resources": [ + { + "resources": ["src/popup/*.css", "src/options/*.css"], + "matches": [""] + } + ] +} diff --git a/src/options/options.html b/src/options/options.html new file mode 100644 index 0000000..b5280bb --- /dev/null +++ b/src/options/options.html @@ -0,0 +1,67 @@ + + + + + RhythmCal — Energy Profile + + + +

RhythmCal Energy Profile

+

Define your daily energy windows so RhythmCal can protect your recovery and schedule deep work at your peak.

+
+
Peak energy
+
Work window
+
Recovery
+
+
+ +
+ + +
+
+ + min +
+
+ + +
+
+ + + Saved! +
+ + + diff --git a/src/options/options.ts b/src/options/options.ts new file mode 100644 index 0000000..e313ca3 --- /dev/null +++ b/src/options/options.ts @@ -0,0 +1,121 @@ +import { DEFAULT_PROFILE, type EnergyProfile, type EnergyWindow } from '../lib/energy.js'; + +const container = document.getElementById('windows-container')!; +const btnAdd = document.getElementById('btn-add')!; +const btnSave = document.getElementById('btn-save')!; +const btnReset = document.getElementById('btn-reset')!; +const savedMsg = document.getElementById('saved-msg')!; +const workDaysInput = document.getElementById('work-days') as HTMLInputElement; +const minRecoveryInput = document.getElementById('min-recovery') as HTMLInputElement; +const protectRecoveryInput = document.getElementById('protect-recovery') as HTMLInputElement; + +let windows: EnergyWindow[] = []; + +async function loadProfile() { + const res: any = await new Promise((resolve) => { + chrome.runtime.sendMessage({ action: 'getProfile' }, (r) => resolve(r)); + }); + const profile: EnergyProfile = res?.profile || DEFAULT_PROFILE; + windows = JSON.parse(JSON.stringify(profile.windows)); + renderWindows(); + workDaysInput.value = profile.workDays.map(d => ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d]).join(','); + minRecoveryInput.value = String(profile.minRecoveryBlock); + protectRecoveryInput.checked = profile.protectRecovery; +} + +function renderWindows() { + container.innerHTML = ''; + windows.forEach((w, i) => { + const card = document.createElement('div'); + card.className = 'window-card ' + (w.isRecovery ? 'recovery' : w.energyLevel >= 4 ? 'peak' : 'work'); + card.innerHTML = ` +

Window ${i + 1}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ `; + container.appendChild(card); + }); + + document.querySelectorAll('.window-card input, .window-card select').forEach(el => { + el.addEventListener('change', updateFromUI); + }); + document.querySelectorAll('.window-card .danger').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt((e.target as HTMLElement).getAttribute('data-idx')!, 10); + windows.splice(idx, 1); + renderWindows(); + }); + }); +} + +function updateFromUI() { + document.querySelectorAll('.window-card').forEach((card, i) => { + const w = windows[i]; + if (!w) return; + w.label = card.querySelector('.label')!.value; + w.startHour = parseInt(card.querySelector('.start')!.value, 10); + w.endHour = parseInt(card.querySelector('.end')!.value, 10); + w.energyLevel = parseInt(card.querySelector('.energy')!.value, 10); + w.isRecovery = card.querySelector('.recovery')!.checked; + }); +} + +btnAdd.addEventListener('click', () => { + updateFromUI(); + windows.push({ startHour: 9, endHour: 10, energyLevel: 3, label: 'New Window', isRecovery: false }); + renderWindows(); +}); + +btnSave.addEventListener('click', async () => { + updateFromUI(); + const dayMap: Record = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }; + const workDays = workDaysInput.value.split(',').map(s => s.trim()).filter(Boolean).map(s => dayMap[s] ?? 1).filter(n => n >= 0 && n <= 6); + const profile: EnergyProfile = { + windows: windows.sort((a, b) => a.startHour - b.startHour), + workDays: workDays.length ? workDays : [1, 2, 3, 4, 5], + defaultEventDuration: 60, + preferredMeetingDuration: 30, + minRecoveryBlock: parseInt(minRecoveryInput.value, 10) || 30, + protectRecovery: protectRecoveryInput.checked, + }; + await new Promise((resolve) => { + chrome.runtime.sendMessage({ action: 'saveProfile', profile }, () => resolve()); + }); + savedMsg.classList.add('show'); + setTimeout(() => savedMsg.classList.remove('show'), 2000); +}); + +btnReset.addEventListener('click', () => { + windows = JSON.parse(JSON.stringify(DEFAULT_PROFILE.windows)); + renderWindows(); + workDaysInput.value = 'Mon,Tue,Wed,Thu,Fri'; + minRecoveryInput.value = '30'; + protectRecoveryInput.checked = true; +}); + +loadProfile(); diff --git a/src/popup/popup.html b/src/popup/popup.html new file mode 100644 index 0000000..fb23a3b --- /dev/null +++ b/src/popup/popup.html @@ -0,0 +1,90 @@ + + + + + RhythmCal + + + +
+

RhythmCal

+

Energy-aware AI scheduling

+
+
+
+

Connect Google Calendar to start scheduling

+ +
+
+ + + + diff --git a/src/popup/popup.ts b/src/popup/popup.ts new file mode 100644 index 0000000..3a18e42 --- /dev/null +++ b/src/popup/popup.ts @@ -0,0 +1,198 @@ +import { DEFAULT_PROFILE, type EnergyProfile } from '../lib/energy.js'; +import type { ScheduleSuggestion, Slot } from '../lib/scheduler.js'; + +const authSection = document.getElementById('auth-section')!; +const mainSection = document.getElementById('main-section')!; +const btnAuth = document.getElementById('btn-auth')!; +const btnSuggest = document.getElementById('btn-suggest')!; +const btnRefresh = document.getElementById('btn-refresh')!; +const btnOptions = document.getElementById('btn-options')!; +const taskTitle = document.getElementById('task-title') as HTMLInputElement; +const taskDuration = document.getElementById('task-duration') as HTMLInputElement; +const taskDate = document.getElementById('task-date') as HTMLSelectElement; +const suggestionResult = document.getElementById('suggestion-result')!; +const slotList = document.getElementById('slot-list')!; +const energyStrip = document.getElementById('energy-strip')!; + +function formatTime(d: Date): string { + return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +function formatDate(d: Date): string { + return d.toISOString().split('T')[0]; +} + +function toISOStringLocal(d: Date): string { + const offset = d.getTimezoneOffset() * 60000; + return new Date(d.getTime() - offset).toISOString().slice(0, -1); +} + +async function checkAuth(): Promise { + return new Promise((resolve) => { + chrome.runtime.sendMessage({ action: 'getEvents', timeMin: new Date().toISOString(), timeMax: new Date().toISOString() }, (res) => { + resolve(res?.success === true); + }); + }); +} + +async function init() { + const isAuth = await checkAuth(); + if (isAuth) { + authSection.style.display = 'none'; + mainSection.style.display = 'block'; + await loadDatePicker(); + await renderEnergyStrip(); + await loadSlots(); + } else { + authSection.style.display = 'block'; + mainSection.style.display = 'none'; + } +} + +btnAuth.addEventListener('click', async () => { + btnAuth.setAttribute('disabled', 'true'); + btnAuth.textContent = 'Connecting...'; + const ok = await checkAuth(); + if (ok) { + await init(); + } else { + btnAuth.removeAttribute('disabled'); + btnAuth.textContent = 'Connect Calendar'; + suggestionResult.innerHTML = '
Could not connect. Try again.
'; + } +}); + +async function loadDatePicker() { + taskDate.innerHTML = ''; + const today = new Date(); + for (let i = 0; i < 7; i++) { + const d = new Date(today); + d.setDate(today.getDate() + i); + const opt = document.createElement('option'); + opt.value = formatDate(d); + opt.textContent = i === 0 ? 'Today' : d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }); + taskDate.appendChild(opt); + } +} + +async function renderEnergyStrip() { + const profile: EnergyProfile = await new Promise((resolve) => { + chrome.runtime.sendMessage({ action: 'getProfile' }, (res) => resolve(res?.profile || DEFAULT_PROFILE)); + }); + energyStrip.innerHTML = ''; + for (let h = 6; h <= 21; h++) { + const window = profile.windows.find(w => { + if (w.startHour <= w.endHour) return h >= w.startHour && h < w.endHour; + return h >= w.startHour || h < w.endHour; + }); + const bar = document.createElement('div'); + bar.className = 'energy-bar'; + if (window) { + bar.classList.add(window.isRecovery ? 'recovery' : `l${window.energyLevel}`); + bar.title = `${window.label} (${h}:00-${h+1}:00)`; + } + energyStrip.appendChild(bar); + } +} + +async function loadSlots() { + slotList.innerHTML = '
Loading slots...
'; + const date = new Date(taskDate.value || formatDate(new Date())); + const timeMin = toISOStringLocal(new Date(date.setHours(0,0,0,0))) + 'Z'; + const timeMax = toISOStringLocal(new Date(date.setHours(23,59,59,0))) + 'Z'; + + chrome.runtime.sendMessage({ action: 'getFreeSlots', date: date.toISOString(), timeMin, timeMax, minDuration: 30 }, (res) => { + if (!res?.success) { + slotList.innerHTML = '
Could not load calendar.
'; + return; + } + const slots: Slot[] = res.slots; + if (slots.length === 0) { + slotList.innerHTML = '
No free slots found today.
'; + return; + } + slotList.innerHTML = ''; + slots.forEach(slot => { + const div = document.createElement('div'); + div.className = 'slot-item'; + const start = new Date(slot.start); + const end = new Date(slot.end); + const scoreClass = slot.energyScore >= 4 ? 'score-high' : slot.energyScore >= 2.5 ? 'score-mid' : 'score-low'; + const recoveryText = slot.blocksRecovery ? ' ⚠️ Recovery' : ''; + div.innerHTML = ` +
+
${formatTime(start)} — ${formatTime(end)}
+
Energy: ${slot.energyScore.toFixed(1)}/5 ${recoveryText}
+
+ `; + slotList.appendChild(div); + }); + }); +} + +btnSuggest.addEventListener('click', async () => { + const title = taskTitle.value.trim(); + const duration = parseInt(taskDuration.value, 10) || 60; + if (!title) { + suggestionResult.innerHTML = '
Enter a task title.
'; + return; + } + btnSuggest.setAttribute('disabled', 'true'); + btnSuggest.textContent = 'Finding...'; + suggestionResult.innerHTML = ''; + + const date = new Date(taskDate.value || formatDate(new Date())); + const timeMin = toISOStringLocal(new Date(date.setHours(0,0,0,0))) + 'Z'; + const timeMax = toISOStringLocal(new Date(date.setHours(23,59,59,0))) + 'Z'; + + chrome.runtime.sendMessage({ action: 'suggestSlot', title, duration, date: date.toISOString(), timeMin, timeMax }, (res) => { + btnSuggest.removeAttribute('disabled'); + btnSuggest.textContent = 'Find Best Slot'; + if (!res?.success) { + suggestionResult.innerHTML = `
${res?.error || 'Failed to find slot'}
`; + return; + } + const s: ScheduleSuggestion = res.suggestion; + if (!s) { + suggestionResult.innerHTML = '
No suitable slots found today.
'; + return; + } + const div = document.createElement('div'); + div.className = 'suggestion'; + div.innerHTML = ` +

Best slot: ${formatTime(s.suggestedStart)} — ${formatTime(s.suggestedEnd)}

+

${s.reason}

+

Energy score: ${s.energyScore.toFixed(1)}/5

+ ${s.recoveryWarning ? `
${s.recoveryWarning}
` : ''} + `; + const btnSchedule = document.createElement('button'); + btnSchedule.textContent = 'Schedule It'; + btnSchedule.style.marginTop = '8px'; + btnSchedule.addEventListener('click', () => { + btnSchedule.setAttribute('disabled', 'true'); + btnSchedule.textContent = 'Scheduling...'; + chrome.runtime.sendMessage({ + action: 'scheduleEvent', + title: s.title, + start: s.suggestedStart.toISOString(), + end: s.suggestedEnd.toISOString(), + description: `Scheduled by RhythmCal — ${s.reason}`, + }, (res2) => { + if (res2?.success) { + div.innerHTML += '
Scheduled successfully!
'; + loadSlots(); + } else { + div.innerHTML += `
${res2?.error || 'Failed to schedule'}
`; + } + }); + }); + div.appendChild(btnSchedule); + suggestionResult.appendChild(div); + }); +}); + +taskDate.addEventListener('change', () => loadSlots()); +btnRefresh.addEventListener('click', () => loadSlots()); +btnOptions.addEventListener('click', () => chrome.runtime.openOptionsPage()); + +init(); diff --git a/src/test/scheduler.test.ts b/src/test/scheduler.test.ts new file mode 100644 index 0000000..734380e --- /dev/null +++ b/src/test/scheduler.test.ts @@ -0,0 +1,83 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { DEFAULT_PROFILE, getEnergyLevelAtHour, isRecoveryWindow, energyScore, blocksRecovery, getWindowLabel } from '../lib/energy.js'; +import { findFreeSlots, suggestSlot, energyOverlayForDay } from '../lib/scheduler.js'; +import type { CalendarEvent } from '../lib/calendar.js'; + +describe('energy', () => { + it('returns correct energy level at hour 10 (deep work peak)', () => { + assert.strictEqual(getEnergyLevelAtHour(DEFAULT_PROFILE, 10), 5); + }); + + it('returns recovery at hour 12 (lunch)', () => { + assert.strictEqual(isRecoveryWindow(DEFAULT_PROFILE, 12), true); + }); + + it('returns recovery at hour 3 (overnight sleep)', () => { + assert.strictEqual(isRecoveryWindow(DEFAULT_PROFILE, 3), true); + }); + + it('energyScore for 9-12 window is 5.0', () => { + assert.strictEqual(energyScore(DEFAULT_PROFILE, 9, 3), 5.0); + }); + + it('blocksRecovery for 60 min starting at 11:30 overlaps lunch recovery', () => { + assert.strictEqual(blocksRecovery(DEFAULT_PROFILE, 11, 60), true); + }); + + it('does not block recovery at 9am for 60 min', () => { + assert.strictEqual(blocksRecovery(DEFAULT_PROFILE, 9, 60), false); + }); + + it('getWindowLabel returns Deep Work Peak at 10', () => { + assert.strictEqual(getWindowLabel(DEFAULT_PROFILE, 10), 'Deep Work Peak'); + }); +}); + +describe('scheduler', () => { + const events: CalendarEvent[] = [ + { id: '1', summary: 'Meeting', start: { dateTime: '2024-06-15T09:00:00' }, end: { dateTime: '2024-06-15T10:00:00' } }, + { id: '2', summary: 'Lunch', start: { dateTime: '2024-06-15T12:00:00' }, end: { dateTime: '2024-06-15T13:00:00' } }, + ]; + + it('findFreeSlots returns slots between 6am and 9am, and 10am-12pm', () => { + const slots = findFreeSlots(events, DEFAULT_PROFILE, new Date('2024-06-15'), 30); + assert.ok(slots.length >= 2, `Expected at least 2 slots, got ${slots.length}`); + const firstSlot = slots[0]; + assert.strictEqual(firstSlot.start.getHours(), 6); + assert.strictEqual(firstSlot.end.getHours(), 9); + }); + + it('suggestSlot picks highest energy non-recovery slot for 60 min', () => { + const suggestion = suggestSlot('Deep Work', 60, events, DEFAULT_PROFILE, new Date('2024-06-15')); + assert.ok(suggestion, 'Expected a suggestion'); + if (!suggestion) return; + assert.ok(suggestion.energyScore >= 3, `Expected decent energy score, got ${suggestion.energyScore}`); + assert.ok(suggestion.suggestedStart.getHours() >= 6, 'Should start after 6am'); + }); + + it('suggestSlot returns null when no free time exists', () => { + const allDay: CalendarEvent[] = [ + { id: 'x', summary: 'All Day', start: { dateTime: '2024-06-15T06:00:00' }, end: { dateTime: '2024-06-15T22:00:00' } }, + ]; + const suggestion = suggestSlot('Task', 60, allDay, DEFAULT_PROFILE, new Date('2024-06-15')); + assert.strictEqual(suggestion, null); + }); + + it('energyOverlayForDay returns 24 items', () => { + const overlay = energyOverlayForDay(DEFAULT_PROFILE, new Date('2024-06-15')); + assert.strictEqual(overlay.length, 24); + assert.strictEqual(overlay[10].level, 5); + assert.strictEqual(overlay[12].isRecovery, true); + }); + + it('findFreeSlots respects minDuration', () => { + const shortEvents: CalendarEvent[] = [ + { id: '1', summary: 'A', start: { dateTime: '2024-06-15T06:00:00' }, end: { dateTime: '2024-06-15T06:45:00' } }, + { id: '2', summary: 'B', start: { dateTime: '2024-06-15T06:50:00' }, end: { dateTime: '2024-06-15T22:00:00' } }, + ]; + const slots = findFreeSlots(shortEvents, DEFAULT_PROFILE, new Date('2024-06-15'), 30); + // 5 min gap between 6:45 and 6:50 is less than 30 min minDuration, so should be 0 slots + assert.strictEqual(slots.length, 0, 'Should not return sub-minDuration gap'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ee840ff --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["chrome", "node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..a8209f8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import { crx } from '@crxjs/vite-plugin' +import manifest from './src/manifest.json' with { type: 'json' } + +export default defineConfig({ + build: { + outDir: 'dist', + }, + plugins: [crx({ manifest })], +})