commit 329ddc9ced85ca39acb4b645bc6bc3fb4f20a61a Author: Meghdad Date: Sun May 17 03:53:15 2026 +0330 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b627bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# End of https://www.toptal.com/developers/gitignore/api/rust + +node_modules/ +dist/ +src-tauri/target/ +src-tauri/gen/ +*.log +.DS_Store +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8b2c18 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Codex Limit Monitor + +Windows-first desktop monitor for Codex usage and ChatGPT Codex rate-limit status. + +## What It Does + +- Reads live Codex limit percentages from `codex app-server` using `account/rateLimits/read`. +- Falls back to read-only local usage summaries from `%USERPROFILE%\.codex\state_5.sqlite` and `logs_2.sqlite`. +- Shows status in a Tauri dashboard, system tray menu, native notifications, and an always-on-top badge. + +## Development + +The current PowerShell session has Rust/Cargo available, but plain `node`, `npm`, and `codex` are not on PATH. If needed, use the configured Node/NPM path from Herd/NVM or set the Codex executable path inside the app settings. + +```powershell +npm install +npm run tauri dev +``` + +If `codex` is not on PATH, set the Codex executable in Settings. The app also tries common Windows candidates such as `%APPDATA%\npm\codex.cmd`. + +## Verification + +```powershell +cargo test --manifest-path src-tauri/Cargo.toml +npm run build +``` diff --git a/badge.html b/badge.html new file mode 100644 index 0000000..4e241e0 --- /dev/null +++ b/badge.html @@ -0,0 +1,12 @@ + + + + + + Codex Badge + + +
+ + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..1bd8617 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Codex Limit Monitor + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b849236 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2472 @@ +{ + "name": "codex-limit-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-limit-monitor", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.9.0", + "vite": "^7.2.0", + "vitest": "^4.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz", + "integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.1", + "@tauri-apps/cli-darwin-x64": "2.11.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.1", + "@tauri-apps/cli-linux-arm64-musl": "2.11.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-musl": "2.11.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.1", + "@tauri-apps/cli-win32-x64-msvc": "2.11.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", + "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz", + "integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz", + "integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz", + "integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz", + "integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz", + "integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz", + "integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz", + "integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz", + "integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz", + "integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz", + "integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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/electron-to-chromium": { + "version": "1.5.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "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.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a0d4c20 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "codex-limit-monitor", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 1420", + "build": "tsc && vite build", + "preview": "vite preview --host 127.0.0.1 --port 1420", + "tauri": "tauri", + "test": "vitest run" + }, + "dependencies": { + "@tauri-apps/api": "^2.11.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.9.0", + "vite": "^7.2.0", + "vitest": "^4.0.0" + } +} diff --git a/resume b/resume new file mode 100644 index 0000000..1bd8c1f --- /dev/null +++ b/resume @@ -0,0 +1 @@ +codex resume 019e2e41-c278-7722-9f72-500a7a88a338 \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..ab708fa --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "codex-limit-monitor" +version = "0.1.0" +description = "Windows-first desktop monitor for Codex limits and usage" +authors = ["meghdad"] +edition = "2021" + +[lib] +name = "codex_limit_monitor" +crate-type = ["rlib"] + +[[bin]] +name = "codex-limit-monitor" +path = "src/main.rs" +test = false + +[build-dependencies] +tauri-build = { version = "2.6", features = [] } + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +dirs = "6" +regex = "1" +rusqlite = { version = "0.37", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "2.11", features = ["tray-icon", "image-png"] } +tauri-plugin-notification = "2.3" +thiserror = "2" +tokio = { version = "1", features = ["io-util", "macros", "process", "sync", "time"] } +tracing = "0.1" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..261851f --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..b8a5b2f --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default permissions for Codex Limit Monitor", + "windows": ["main", "badge"], + "permissions": [ + "core:default", + "notification:default", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:allow-show" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..ea9326e Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..8c100d2 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..689ab53 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/icon-128.png b/src-tauri/icons/icon-128.png new file mode 100644 index 0000000..ea9326e Binary files /dev/null and b/src-tauri/icons/icon-128.png differ diff --git a/src-tauri/icons/icon-16.png b/src-tauri/icons/icon-16.png new file mode 100644 index 0000000..b57aa5c Binary files /dev/null and b/src-tauri/icons/icon-16.png differ diff --git a/src-tauri/icons/icon-24.png b/src-tauri/icons/icon-24.png new file mode 100644 index 0000000..87aa747 Binary files /dev/null and b/src-tauri/icons/icon-24.png differ diff --git a/src-tauri/icons/icon-256.png b/src-tauri/icons/icon-256.png new file mode 100644 index 0000000..8c100d2 Binary files /dev/null and b/src-tauri/icons/icon-256.png differ diff --git a/src-tauri/icons/icon-32.png b/src-tauri/icons/icon-32.png new file mode 100644 index 0000000..689ab53 Binary files /dev/null and b/src-tauri/icons/icon-32.png differ diff --git a/src-tauri/icons/icon-48.png b/src-tauri/icons/icon-48.png new file mode 100644 index 0000000..53b219e Binary files /dev/null and b/src-tauri/icons/icon-48.png differ diff --git a/src-tauri/icons/icon-64.png b/src-tauri/icons/icon-64.png new file mode 100644 index 0000000..3ce8b9d Binary files /dev/null and b/src-tauri/icons/icon-64.png differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..8b51676 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..8c100d2 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/codex_rpc.rs b/src-tauri/src/codex_rpc.rs new file mode 100644 index 0000000..77024f8 --- /dev/null +++ b/src-tauri/src/codex_rpc.rs @@ -0,0 +1,248 @@ +use crate::{models::RateLimitBucket, settings::AppSettings}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::{collections::BTreeMap, process::Stdio, time::Duration}; +use thiserror::Error; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + process::{ChildStdout, Command}, + time::timeout, +}; + +const RPC_TIMEOUT: Duration = Duration::from_secs(20); +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +#[derive(Debug, Error)] +pub enum CodexRpcError { + #[error("failed to start Codex app-server: {0}")] + Spawn(#[from] std::io::Error), + #[error("Codex app-server did not expose stdio")] + MissingPipe, + #[error("Codex app-server timed out")] + Timeout, + #[error("Codex app-server closed stdout")] + Closed, + #[error("invalid app-server JSON: {0}")] + InvalidJson(#[from] serde_json::Error), + #[error("app-server returned error: {0}")] + Rpc(String), + #[error("app-server response was missing result")] + MissingResult, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RateLimitsEnvelope { + rate_limits: Option, + rate_limits_by_limit_id: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawRateLimitBucket { + limit_id: String, + limit_name: Option, + primary: Option, + secondary: Option, + rate_limit_reached_type: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawLimitWindow { + used_percent: Option, + window_duration_mins: Option, + resets_at: Option, +} + +pub async fn fetch_rate_limits( + settings: &AppSettings, +) -> Result, CodexRpcError> { + let executable = settings.resolve_codex_executable(); + let mut command = Command::new(executable); + command + .arg("app-server") + .arg("--listen") + .arg("stdio://") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + #[cfg(windows)] + command.creation_flags(CREATE_NO_WINDOW); + + let mut child = command.spawn()?; + + let mut stdin = child.stdin.take().ok_or(CodexRpcError::MissingPipe)?; + let stdout = child.stdout.take().ok_or(CodexRpcError::MissingPipe)?; + let mut reader = BufReader::new(stdout).lines(); + + write_json( + &mut stdin, + json!({ + "jsonrpc": "2.0", + "method": "initialize", + "id": 0, + "params": { + "clientInfo": { + "name": "codex_limit_monitor", + "title": "Codex Limit Monitor", + "version": env!("CARGO_PKG_VERSION") + } + } + }), + ) + .await?; + + read_response(&mut reader, 0).await?; + + write_json( + &mut stdin, + json!({ + "jsonrpc": "2.0", + "method": "initialized", + "params": {} + }), + ) + .await?; + + write_json( + &mut stdin, + json!({ + "jsonrpc": "2.0", + "method": "account/rateLimits/read", + "id": 1 + }), + ) + .await?; + + let response = read_response(&mut reader, 1).await?; + let _ = child.kill().await; + + let result = response + .get("result") + .cloned() + .ok_or(CodexRpcError::MissingResult)?; + parse_rate_limits(result) +} + +async fn write_json( + stdin: &mut tokio::process::ChildStdin, + value: Value, +) -> Result<(), CodexRpcError> { + let mut line = serde_json::to_vec(&value)?; + line.push(b'\n'); + stdin.write_all(&line).await?; + stdin.flush().await?; + Ok(()) +} + +async fn read_response( + reader: &mut tokio::io::Lines>, + id: u64, +) -> Result { + loop { + let line = timeout(RPC_TIMEOUT, reader.next_line()) + .await + .map_err(|_| CodexRpcError::Timeout)? + .map_err(CodexRpcError::Spawn)? + .ok_or(CodexRpcError::Closed)?; + + let value: Value = serde_json::from_str(&line)?; + if value.get("id").and_then(Value::as_u64) != Some(id) { + continue; + } + + if let Some(error) = value.get("error") { + return Err(CodexRpcError::Rpc(error.to_string())); + } + + return Ok(value); + } +} + +fn parse_rate_limits(result: Value) -> Result, CodexRpcError> { + let envelope: RateLimitsEnvelope = serde_json::from_value(result)?; + let mut buckets: Vec = Vec::new(); + + if let Some(by_id) = envelope.rate_limits_by_limit_id { + for raw in by_id.into_values() { + buckets.push(raw.into()); + } + } else if let Some(raw) = envelope.rate_limits { + buckets.push(raw.into()); + } + + buckets.sort_by(|a, b| a.limit_id.cmp(&b.limit_id)); + Ok(buckets) +} + +impl From for RateLimitBucket { + fn from(raw: RawRateLimitBucket) -> Self { + let primary = raw.primary.unwrap_or_default(); + let secondary = raw.secondary; + Self { + limit_id: raw.limit_id, + limit_name: raw.limit_name, + used_percent: primary.used_percent.unwrap_or(0.0), + window_duration_mins: primary.window_duration_mins, + resets_at: primary.resets_at, + secondary_used_percent: secondary.as_ref().and_then(|value| value.used_percent), + secondary_resets_at: secondary.and_then(|value| value.resets_at), + reached_type: raw.rate_limit_reached_type, + } + } +} + +impl Default for RawLimitWindow { + fn default() -> Self { + Self { + used_percent: Some(0.0), + window_duration_mins: None, + resets_at: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_multi_bucket_rate_limits() { + let value = json!({ + "rateLimitsByLimitId": { + "codex": { + "limitId": "codex", + "limitName": null, + "primary": { + "usedPercent": 25, + "windowDurationMins": 15, + "resetsAt": 1730947200 + }, + "secondary": null, + "rateLimitReachedType": null + }, + "codex_other": { + "limitId": "codex_other", + "limitName": "codex_other", + "primary": { + "usedPercent": 42, + "windowDurationMins": 60, + "resetsAt": 1730950800 + } + } + } + }); + + let buckets = parse_rate_limits(value).unwrap(); + + assert_eq!(buckets.len(), 2); + assert_eq!(buckets[0].limit_id, "codex"); + assert_eq!(buckets[0].used_percent, 25.0); + assert_eq!(buckets[1].limit_id, "codex_other"); + assert_eq!(buckets[1].window_duration_mins, Some(60)); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..8958ad4 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,201 @@ +pub mod codex_rpc; +pub mod local_usage; +pub mod models; +#[cfg(not(test))] +pub mod monitor; +pub mod settings; +#[cfg(not(test))] +pub mod tray; + +#[cfg(not(test))] +use monitor::MonitorService; +#[cfg(not(test))] +use settings::{AppSettings, BadgePosition}; +#[cfg(not(test))] +use std::sync::Arc; +#[cfg(not(test))] +use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, State}; + +#[cfg(not(test))] +#[derive(Clone)] +pub struct AppState { + monitor: Arc, +} + +#[cfg(not(test))] +#[tauri::command] +async fn get_snapshot(state: State<'_, AppState>) -> Result { + state.monitor.snapshot_or_refresh(None).await +} + +#[cfg(not(test))] +#[tauri::command] +async fn refresh_now( + app: AppHandle, + state: State<'_, AppState>, +) -> Result { + state.monitor.refresh(Some(app)).await +} + +#[cfg(not(test))] +#[tauri::command] +async fn get_usage_history( + range: String, + state: State<'_, AppState>, +) -> Result { + let settings = state.monitor.settings().await; + local_usage::read_usage_history(&settings, &range).map_err(|err| err.to_string()) +} + +#[cfg(not(test))] +#[tauri::command] +async fn get_settings(state: State<'_, AppState>) -> Result { + Ok(state.monitor.settings().await) +} + +#[cfg(not(test))] +#[tauri::command] +async fn update_settings( + app: AppHandle, + settings: AppSettings, + state: State<'_, AppState>, +) -> Result { + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + apply_badge_visibility(&app, settings.badge_visible)?; + apply_badge_position(&app, &settings.badge_position)?; + let snapshot = state.monitor.refresh(Some(app.clone())).await?; + let _ = app.emit("settings_updated", &settings); + let _ = app.emit("rate_limit_updated", &snapshot); + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn toggle_badge( + app: AppHandle, + visible: bool, + state: State<'_, AppState>, +) -> Result { + let mut settings = state.monitor.settings().await; + settings.badge_visible = visible; + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + apply_badge_visibility(&app, visible)?; + let _ = app.emit("settings_updated", &settings); + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn set_badge_position( + app: AppHandle, + position: BadgePosition, + state: State<'_, AppState>, +) -> Result { + let mut settings = state.monitor.settings().await; + settings.badge_position = position; + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + apply_badge_position(&app, &settings.badge_position)?; + let _ = app.emit("settings_updated", &settings); + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn move_badge(app: AppHandle, position: BadgePosition) -> Result<(), String> { + apply_badge_position(&app, &position) +} + +#[cfg(not(test))] +#[tauri::command] +async fn pause_notifications( + until: Option, + state: State<'_, AppState>, +) -> Result { + let mut settings = state.monitor.settings().await; + settings.notifications_muted_until = until; + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn open_dashboard(app: AppHandle) -> Result<(), String> { + show_dashboard(&app) +} + +#[cfg(not(test))] +pub(crate) fn show_dashboard(app: &AppHandle) -> Result<(), String> { + if let Some(window) = app.get_webview_window("main") { + window.show().map_err(|err| err.to_string())?; + window.set_focus().map_err(|err| err.to_string())?; + } + Ok(()) +} + +#[cfg(not(test))] +pub(crate) fn apply_badge_visibility(app: &AppHandle, visible: bool) -> Result<(), String> { + if let Some(window) = app.get_webview_window("badge") { + if visible { + window.show().map_err(|err| err.to_string())?; + } else { + window.hide().map_err(|err| err.to_string())?; + } + } + Ok(()) +} + +#[cfg(not(test))] +fn apply_badge_position(app: &AppHandle, position: &BadgePosition) -> Result<(), String> { + let Some(window) = app.get_webview_window("badge") else { + return Ok(()); + }; + + window + .set_position(PhysicalPosition::new(position.x, position.y)) + .map_err(|err| err.to_string()) +} + +#[cfg(not(test))] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_notification::init()) + .setup(|app| { + let settings = AppSettings::load().unwrap_or_default(); + let monitor = Arc::new(MonitorService::new(settings.clone())); + app.manage(AppState { + monitor: monitor.clone(), + }); + + tray::setup_tray(app.handle())?; + apply_badge_visibility(app.handle(), settings.badge_visible)?; + apply_badge_position(app.handle(), &settings.badge_position)?; + if let Some(window) = app.get_webview_window("badge") { + let _ = window.set_shadow(false); + } + + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + monitor.run(app_handle).await; + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + get_snapshot, + refresh_now, + get_usage_history, + get_settings, + update_settings, + toggle_badge, + set_badge_position, + move_badge, + pause_notifications, + open_dashboard + ]) + .run(tauri::generate_context!()) + .expect("error while running Codex Limit Monitor"); +} diff --git a/src-tauri/src/local_usage.rs b/src-tauri/src/local_usage.rs new file mode 100644 index 0000000..8a2ab99 --- /dev/null +++ b/src-tauri/src/local_usage.rs @@ -0,0 +1,275 @@ +use crate::{ + models::{ModelUsage, ThreadUsage, TokenBreakdown, UsageHistory, UsagePoint, UsageSummary}, + settings::AppSettings, +}; +use chrono::{Local, TimeZone, Utc}; +use regex::Regex; +use rusqlite::{Connection, OpenFlags, Row}; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LocalUsageError { + #[error("Codex state database was not found at {0}")] + MissingStateDb(String), + #[error("SQLite error: {0}")] + Sqlite(#[from] rusqlite::Error), +} + +pub fn read_usage_summary(settings: &AppSettings) -> UsageSummary { + match read_usage_summary_inner(settings) { + Ok(summary) => summary, + Err(error) => UsageSummary { + source_path: Some(settings.codex_home.clone()), + error: Some(error.to_string()), + ..UsageSummary::default() + }, + } +} + +pub fn read_usage_history( + settings: &AppSettings, + range: &str, +) -> Result { + let state_path = state_db_path(settings); + if !state_path.exists() { + return Err(LocalUsageError::MissingStateDb( + state_path.to_string_lossy().to_string(), + )); + } + + let conn = open_read_only(&state_path)?; + let days = match range { + "24h" => 1, + "30d" => 30, + _ => 7, + }; + let since = Utc::now().timestamp() - days * 86_400; + let mut statement = conn.prepare( + "SELECT date(updated_at, 'unixepoch', 'localtime') AS bucket, + COALESCE(SUM(tokens_used), 0) AS tokens, + COUNT(*) AS threads + FROM threads + WHERE updated_at >= ?1 + GROUP BY bucket + ORDER BY bucket ASC", + )?; + + let points = statement + .query_map([since], |row| { + Ok(UsagePoint { + label: row.get(0)?, + tokens: row.get(1)?, + threads: row.get(2)?, + }) + })? + .collect::, _>>()?; + + Ok(UsageHistory { + range: range.to_string(), + points, + }) +} + +fn read_usage_summary_inner(settings: &AppSettings) -> Result { + let state_path = state_db_path(settings); + if !state_path.exists() { + return Err(LocalUsageError::MissingStateDb( + state_path.to_string_lossy().to_string(), + )); + } + + let conn = open_read_only(&state_path)?; + let today_start = local_midnight_timestamp(); + let week_start = Utc::now().timestamp() - 7 * 86_400; + + let (total_tokens, thread_count) = sum_tokens(&conn, None)?; + let (today_tokens, today_thread_count) = sum_tokens(&conn, Some(today_start))?; + let (week_tokens, week_thread_count) = sum_tokens(&conn, Some(week_start))?; + let recent_threads = recent_threads(&conn)?; + let current_thread = recent_threads.first().cloned(); + let model_breakdown = model_breakdown(&conn)?; + let token_breakdown = read_token_breakdown(settings, week_start).unwrap_or_default(); + + Ok(UsageSummary { + total_tokens, + today_tokens, + week_tokens, + thread_count, + today_thread_count, + week_thread_count, + current_thread, + recent_threads, + model_breakdown, + token_breakdown, + source_path: Some(state_path.to_string_lossy().to_string()), + error: None, + }) +} + +fn open_read_only(path: &Path) -> Result { + Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY) +} + +fn state_db_path(settings: &AppSettings) -> PathBuf { + settings.codex_home_path().join("state_5.sqlite") +} + +fn logs_db_path(settings: &AppSettings) -> PathBuf { + settings.codex_home_path().join("logs_2.sqlite") +} + +fn local_midnight_timestamp() -> i64 { + let date = Local::now().date_naive(); + let midnight = date.and_hms_opt(0, 0, 0).expect("valid midnight"); + Local + .from_local_datetime(&midnight) + .earliest() + .expect("valid local midnight") + .timestamp() +} + +fn sum_tokens(conn: &Connection, since: Option) -> Result<(i64, i64), rusqlite::Error> { + if let Some(since) = since { + conn.query_row( + "SELECT COALESCE(SUM(tokens_used), 0), COUNT(*) FROM threads WHERE updated_at >= ?1", + [since], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + } else { + conn.query_row( + "SELECT COALESCE(SUM(tokens_used), 0), COUNT(*) FROM threads", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + } +} + +fn recent_threads(conn: &Connection) -> Result, rusqlite::Error> { + let mut statement = conn.prepare( + "SELECT id, title, tokens_used, model, reasoning_effort, updated_at, cwd + FROM threads + ORDER BY updated_at DESC + LIMIT 12", + )?; + + let rows = statement + .query_map([], row_to_thread)? + .collect::, _>>(); + rows +} + +fn model_breakdown(conn: &Connection) -> Result, rusqlite::Error> { + let mut statement = conn.prepare( + "SELECT COALESCE(NULLIF(model, ''), 'unknown') AS model, + COALESCE(SUM(tokens_used), 0) AS tokens, + COUNT(*) AS threads + FROM threads + GROUP BY model + ORDER BY tokens DESC + LIMIT 8", + )?; + + let rows = statement + .query_map([], |row| { + Ok(ModelUsage { + model: row.get(0)?, + tokens: row.get(1)?, + threads: row.get(2)?, + }) + })? + .collect::, _>>(); + rows +} + +fn row_to_thread(row: &Row<'_>) -> Result { + Ok(ThreadUsage { + id: row.get(0)?, + title: row + .get::<_, String>(1) + .unwrap_or_else(|_| "Untitled".to_string()), + tokens_used: row.get(2)?, + model: row.get(3)?, + reasoning_effort: row.get(4)?, + updated_at: row.get(5)?, + cwd: row.get(6)?, + }) +} + +fn read_token_breakdown( + settings: &AppSettings, + since: i64, +) -> Result { + let logs_path = logs_db_path(settings); + if !logs_path.exists() { + return Ok(TokenBreakdown::default()); + } + + let conn = open_read_only(&logs_path)?; + let mut statement = conn.prepare( + "SELECT feedback_log_body + FROM logs + WHERE ts >= ?1 + AND feedback_log_body LIKE '%event.kind=response.completed%' + ORDER BY ts DESC + LIMIT 5000", + )?; + + let mut rows = statement.query([since])?; + let mut breakdown = TokenBreakdown::default(); + while let Some(row) = rows.next()? { + let body: Option = row.get(0)?; + if let Some(body) = body { + add_response_tokens(&mut breakdown, &body); + } + } + + Ok(breakdown) +} + +fn add_response_tokens(breakdown: &mut TokenBreakdown, body: &str) { + let regex = Regex::new(r"(input|output|cached|reasoning|tool)_token_count=(\d+)") + .expect("static token regex is valid"); + let mut matched = false; + + for capture in regex.captures_iter(body) { + let count = capture + .get(2) + .and_then(|value| value.as_str().parse::().ok()) + .unwrap_or(0); + match capture.get(1).map(|value| value.as_str()) { + Some("input") => breakdown.input += count, + Some("output") => breakdown.output += count, + Some("cached") => breakdown.cached += count, + Some("reasoning") => breakdown.reasoning += count, + Some("tool") => breakdown.tool += count, + _ => {} + } + matched = true; + } + + if matched { + breakdown.responses += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_response_token_counts() { + let mut breakdown = TokenBreakdown::default(); + add_response_tokens( + &mut breakdown, + "event.kind=response.completed input_token_count=40917 output_token_count=410 cached_token_count=39296 reasoning_token_count=32 tool_token_count=41327", + ); + + assert_eq!(breakdown.input, 40917); + assert_eq!(breakdown.output, 410); + assert_eq!(breakdown.cached, 39296); + assert_eq!(breakdown.reasoning, 32); + assert_eq!(breakdown.tool, 41327); + assert_eq!(breakdown.responses, 1); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..5d0dbfe --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + codex_limit_monitor::run(); +} diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs new file mode 100644 index 0000000..a19ac9b --- /dev/null +++ b/src-tauri/src/models.rs @@ -0,0 +1,125 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RateLimitBucket { + pub limit_id: String, + pub limit_name: Option, + pub used_percent: f64, + pub window_duration_mins: Option, + pub resets_at: Option, + pub secondary_used_percent: Option, + pub secondary_resets_at: Option, + pub reached_type: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct LiveStatus { + pub state: String, + pub detail: Option, +} + +impl LiveStatus { + pub fn online() -> Self { + Self { + state: "online".to_string(), + detail: None, + } + } + + pub fn fallback(detail: impl Into) -> Self { + Self { + state: "fallback".to_string(), + detail: Some(detail.into()), + } + } + + pub fn offline(detail: impl Into) -> Self { + Self { + state: "offline".to_string(), + detail: Some(detail.into()), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TokenBreakdown { + pub input: i64, + pub output: i64, + pub cached: i64, + pub reasoning: i64, + pub tool: i64, + pub responses: i64, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ModelUsage { + pub model: String, + pub tokens: i64, + pub threads: i64, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ThreadUsage { + pub id: String, + pub title: String, + pub tokens_used: i64, + pub model: Option, + pub reasoning_effort: Option, + pub updated_at: i64, + pub cwd: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UsageSummary { + pub total_tokens: i64, + pub today_tokens: i64, + pub week_tokens: i64, + pub thread_count: i64, + pub today_thread_count: i64, + pub week_thread_count: i64, + pub current_thread: Option, + pub recent_threads: Vec, + pub model_breakdown: Vec, + pub token_breakdown: TokenBreakdown, + pub source_path: Option, + pub error: Option, +} + +impl UsageSummary { + pub fn has_data(&self) -> bool { + self.thread_count > 0 || self.total_tokens > 0 || !self.recent_threads.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonitorSnapshot { + pub source: String, + pub fetched_at: i64, + pub live_status: LiveStatus, + pub buckets: Vec, + pub usage_summary: UsageSummary, + pub current_thread: Option, + pub message: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UsagePoint { + pub label: String, + pub tokens: i64, + pub threads: i64, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UsageHistory { + pub range: String, + pub points: Vec, +} diff --git a/src-tauri/src/monitor.rs b/src-tauri/src/monitor.rs new file mode 100644 index 0000000..f88dd77 --- /dev/null +++ b/src-tauri/src/monitor.rs @@ -0,0 +1,185 @@ +use crate::{ + codex_rpc, local_usage, + models::{LiveStatus, MonitorSnapshot, RateLimitBucket}, + settings::AppSettings, + tray, +}; +use chrono::Utc; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tauri::{AppHandle, Emitter}; +use tauri_plugin_notification::NotificationExt; +use tokio::sync::{Mutex, RwLock}; + +#[derive(Default)] +struct NotificationMemory { + seen: HashMap, +} + +pub struct MonitorService { + settings: RwLock, + snapshot: RwLock>, + notifications: Mutex, +} + +impl MonitorService { + pub fn new(settings: AppSettings) -> Self { + Self { + settings: RwLock::new(settings), + snapshot: RwLock::new(None), + notifications: Mutex::new(NotificationMemory::default()), + } + } + + pub async fn settings(&self) -> AppSettings { + self.settings.read().await.clone() + } + + pub async fn replace_settings(&self, settings: AppSettings) { + *self.settings.write().await = settings; + } + + pub async fn snapshot_or_refresh( + &self, + app: Option, + ) -> Result { + if let Some(snapshot) = self.snapshot.read().await.clone() { + return Ok(snapshot); + } + self.refresh(app).await + } + + pub async fn refresh(&self, app: Option) -> Result { + let settings = self.settings().await; + if let Some(app) = app.as_ref() { + let _ = app.emit("refresh_started", ()); + } + let usage_summary = local_usage::read_usage_summary(&settings); + let live = codex_rpc::fetch_rate_limits(&settings).await; + let fetched_at = Utc::now().timestamp(); + + let (source, live_status, buckets, message) = match live { + Ok(buckets) => ( + "codex-app-server".to_string(), + LiveStatus::online(), + buckets, + None, + ), + Err(error) if usage_summary.has_data() => ( + "local-sqlite-fallback".to_string(), + LiveStatus::fallback(error.to_string()), + Vec::new(), + Some("Live Codex limits are unavailable; showing local usage history.".to_string()), + ), + Err(error) => ( + "offline".to_string(), + LiveStatus::offline(error.to_string()), + Vec::new(), + Some("No live Codex limits or local usage data could be read.".to_string()), + ), + }; + + let current_thread = usage_summary.current_thread.clone(); + let snapshot = MonitorSnapshot { + source, + fetched_at, + live_status, + buckets, + usage_summary, + current_thread, + message, + }; + + *self.snapshot.write().await = Some(snapshot.clone()); + + if let Some(app) = app { + let _ = app.emit("rate_limit_updated", &snapshot); + let _ = app.emit("usage_updated", &snapshot.usage_summary); + tray::update_tray(&app, &snapshot); + self.maybe_notify(&app, &settings, &snapshot.buckets).await; + } + + Ok(snapshot) + } + + pub async fn run(self: Arc, app: AppHandle) { + loop { + let settings = self.settings().await; + let interval = settings.refresh_interval_secs.max(15); + let _ = self.refresh(Some(app.clone())).await; + tokio::time::sleep(Duration::from_secs(interval)).await; + } + } + + async fn maybe_notify( + &self, + app: &AppHandle, + settings: &AppSettings, + buckets: &[RateLimitBucket], + ) { + if settings + .notifications_muted_until + .is_some_and(|until| until > Utc::now().timestamp()) + { + return; + } + + let mut memory = self.notifications.lock().await; + for bucket in buckets { + let Some(level) = threshold_level(settings, bucket.used_percent) else { + continue; + }; + let reset = bucket.resets_at.unwrap_or_default(); + let key = format!("{}:{reset}", bucket.limit_id); + if memory.seen.get(&key).is_some_and(|seen| seen == level) { + continue; + } + + memory.seen.insert(key, level.to_string()); + let title = match level { + "exhausted" => "Codex limit reached", + "critical" => "Codex limit nearly reached", + _ => "Codex limit warning", + }; + let body = format!( + "{} is at {:.0}% and resets {}.", + bucket + .limit_name + .as_deref() + .unwrap_or(bucket.limit_id.as_str()), + bucket.used_percent, + bucket + .resets_at + .map(format_reset) + .unwrap_or_else(|| "soon".to_string()) + ); + + let _ = app.notification().builder().title(title).body(body).show(); + } + } +} + +fn threshold_level(settings: &AppSettings, percent: f64) -> Option<&'static str> { + if percent >= settings.thresholds.exhausted { + Some("exhausted") + } else if percent >= settings.thresholds.critical { + Some("critical") + } else if percent >= settings.thresholds.warning { + Some("warning") + } else { + None + } +} + +fn format_reset(timestamp: i64) -> String { + let now = Utc::now().timestamp(); + if timestamp <= now { + return "now".to_string(); + } + + let seconds = timestamp - now; + if seconds < 60 { + format!("in {seconds}s") + } else { + format!("in {}m", seconds / 60) + } +} diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..fd1e4d7 --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,173 @@ +use serde::{Deserialize, Serialize}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BadgePosition { + pub x: i32, + pub y: i32, +} + +impl Default for BadgePosition { + fn default() -> Self { + Self { x: 1530, y: 18 } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct NotificationThresholds { + pub warning: f64, + pub critical: f64, + pub exhausted: f64, +} + +impl Default for NotificationThresholds { + fn default() -> Self { + Self { + warning: 70.0, + critical: 90.0, + exhausted: 100.0, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AppSettings { + pub codex_executable: Option, + pub codex_home: String, + pub refresh_interval_secs: u64, + pub badge_visible: bool, + pub badge_position: BadgePosition, + pub privacy_mode: bool, + pub thresholds: NotificationThresholds, + pub notifications_muted_until: Option, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + codex_executable: None, + codex_home: default_codex_home().to_string_lossy().to_string(), + refresh_interval_secs: 60, + badge_visible: true, + badge_position: BadgePosition::default(), + privacy_mode: false, + thresholds: NotificationThresholds::default(), + notifications_muted_until: None, + } + } +} + +impl AppSettings { + pub fn load() -> io::Result { + let path = settings_path(); + if !path.exists() { + return Ok(Self::default()); + } + + let body = fs::read_to_string(path)?; + let mut settings = serde_json::from_str::(&body).unwrap_or_default(); + settings.normalize(); + Ok(settings) + } + + pub fn save(&self) -> io::Result<()> { + let path = settings_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let body = serde_json::to_string_pretty(self).map_err(io::Error::other)?; + fs::write(path, body) + } + + pub fn codex_home_path(&self) -> PathBuf { + PathBuf::from(&self.codex_home) + } + + pub fn resolve_codex_executable(&self) -> String { + if let Some(path) = self + .codex_executable + .as_ref() + .filter(|value| !value.is_empty()) + { + return path.clone(); + } + + for candidate in codex_executable_candidates() { + if candidate.exists() { + return candidate.to_string_lossy().to_string(); + } + } + + "codex".to_string() + } + + fn normalize(&mut self) { + if self.refresh_interval_secs < 15 { + self.refresh_interval_secs = 15; + } + if self.codex_home.trim().is_empty() { + self.codex_home = default_codex_home().to_string_lossy().to_string(); + } + } +} + +pub fn settings_path() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("CodexLimitMonitor") + .join("settings.json") +} + +fn default_codex_home() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".codex") +} + +fn codex_executable_candidates() -> Vec { + let mut candidates = Vec::new(); + + if let Some(app_data) = std::env::var_os("APPDATA") { + candidates.push(Path::new(&app_data).join("npm").join("codex.cmd")); + } + + candidates.push( + Path::new("C:\\Program Files") + .join("nodejs") + .join("codex.cmd"), + ); + candidates.push(Path::new("C:\\Program Files").join("nodejs").join("codex")); + + if let Some(home) = dirs::home_dir() { + candidates.push( + home.join(".config") + .join("herd") + .join("bin") + .join("nvm") + .join("v25.2.1") + .join("node_modules") + .join(".bin") + .join("codex.cmd"), + ); + candidates.push( + home.join(".config") + .join("herd") + .join("bin") + .join("nvm") + .join("v25.2.1") + .join("node_modules") + .join("@openai") + .join("codex") + .join("bin") + .join("codex.js"), + ); + } + + candidates +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs new file mode 100644 index 0000000..2faa636 --- /dev/null +++ b/src-tauri/src/tray.rs @@ -0,0 +1,120 @@ +use crate::{models::MonitorSnapshot, show_dashboard}; +use chrono::{Local, TimeZone}; +use tauri::{ + image::Image, + menu::{Menu, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Emitter, Manager, +}; + +const TRAY_ID: &str = "codex-limit-monitor"; + +pub fn setup_tray(app: &AppHandle) -> tauri::Result<()> { + let open = MenuItem::with_id(app, "open", "Open Dashboard", true, None::<&str>)?; + let refresh = MenuItem::with_id(app, "refresh", "Refresh Now", true, None::<&str>)?; + let badge = MenuItem::with_id(app, "badge", "Toggle Badge", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&open, &refresh, &badge, &quit])?; + + TrayIconBuilder::with_id(TRAY_ID) + .icon(app_tray_icon()) + .tooltip("Codex Limit Monitor") + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id().as_ref() { + "open" => { + let _ = show_dashboard(app); + } + "refresh" => { + let app = app.clone(); + let monitor = app + .try_state::() + .map(|state| state.monitor.clone()); + tauri::async_runtime::spawn(async move { + if let Some(monitor) = monitor { + let _ = monitor.refresh(Some(app.clone())).await; + } + }); + } + "badge" => { + let app = app.clone(); + let monitor = app + .try_state::() + .map(|state| state.monitor.clone()); + tauri::async_runtime::spawn(async move { + if let Some(monitor) = monitor { + let mut settings = monitor.settings().await; + settings.badge_visible = !settings.badge_visible; + if settings.save().is_ok() { + monitor.replace_settings(settings.clone()).await; + let _ = crate::apply_badge_visibility(&app, settings.badge_visible); + let _ = app.emit("settings_updated", &settings); + } + } + }); + } + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let _ = show_dashboard(tray.app_handle()); + } + }) + .build(app)?; + + Ok(()) +} + +pub fn update_tray(app: &AppHandle, snapshot: &MonitorSnapshot) { + let Some(tray) = app.tray_by_id(TRAY_ID) else { + return; + }; + + let _ = tray.set_tooltip(Some(&tooltip(snapshot))); +} + +fn app_tray_icon() -> Image<'static> { + Image::from_bytes(include_bytes!("../icons/icon.png")).expect("valid embedded tray icon") +} + +fn tooltip(snapshot: &MonitorSnapshot) -> String { + if let Some(bucket) = snapshot.buckets.first() { + let reset = bucket + .resets_at + .map(format_reset_datetime) + .map(|value| format!(", resets {value}")) + .unwrap_or_default(); + let weekly_reset = bucket + .secondary_resets_at + .map(format_reset_datetime) + .map(|value| format!(", weekly resets {value}")) + .unwrap_or_default(); + let weekly = bucket + .secondary_used_percent + .map(|percent| format!("{percent:.0}% weekly")) + .unwrap_or_else(|| "weekly unavailable".to_string()); + format!( + "Codex: {:.0}% current, {}{}{}", + bucket.used_percent, weekly, reset, weekly_reset + ) + } else { + format!( + "Codex usage: {} tokens today ({})", + snapshot.usage_summary.today_tokens, snapshot.live_status.state + ) + } +} + +fn format_reset_datetime(timestamp: i64) -> String { + Local + .timestamp_opt(timestamp, 0) + .single() + .map(|date| date.format("%b %-d, %I:%M %p").to_string()) + .unwrap_or_else(|| "unknown".to_string()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..6bffd92 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Codex Limit Monitor", + "version": "0.1.0", + "identifier": "dev.meghdad.codex-limit-monitor", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://127.0.0.1:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Codex Limit Monitor", + "width": 1180, + "height": 760, + "minWidth": 900, + "minHeight": 620, + "resizable": true, + "center": true + }, + { + "label": "badge", + "title": "Codex Badge", + "url": "badge.html", + "width": 164, + "height": 82, + "decorations": false, + "transparent": true, + "shadow": false, + "alwaysOnTop": true, + "skipTaskbar": true, + "resizable": false, + "visible": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/icon.ico", + "icons/icon.png", + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..e2c67c7 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,355 @@ +import { listen } from "@tauri-apps/api/event"; +import { useEffect, useMemo, useState } from "react"; +import { + getSettings, + getSnapshot, + getUsageHistory, + pauseNotifications, + refreshNow, + toggleBadge, + updateSettings, +} from "./api"; +import type { AppSettings, MonitorSnapshot, UsageHistory } from "./types"; +import { + currentLimitPercent, + formatCompact, + formatDateTime, + formatNumber, + statusTone, + truncateTitle, + weeklyLimitPercent, + weeklyLimitResetAt, +} from "./utils"; + +function App() { + const [snapshot, setSnapshot] = useState(null); + const [settings, setSettings] = useState(null); + const [history, setHistory] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + Promise.all([getSnapshot(), getSettings(), getUsageHistory("7d")]) + .then(([nextSnapshot, nextSettings, nextHistory]) => { + setSnapshot(nextSnapshot); + setSettings(nextSettings); + setHistory(nextHistory); + }) + .catch((err) => setError(String(err))); + + const unlisten = listen("rate_limit_updated", (event) => { + setSnapshot(event.payload); + }); + return () => { + unlisten.then((dispose) => dispose()).catch(() => undefined); + }; + }, []); + + const percent = currentLimitPercent(snapshot); + const weeklyPercent = weeklyLimitPercent(snapshot); + const weeklyReset = weeklyLimitResetAt(snapshot); + const tone = statusTone(percent); + const weeklyTone = statusTone(weeklyPercent); + const primaryBucket = snapshot?.buckets[0] ?? null; + const currentThreadTitle = useMemo(() => { + if (!snapshot?.currentThread) return "No active thread"; + if (settings?.privacyMode) return "Hidden by privacy mode"; + return truncateTitle(snapshot.currentThread.title || "Untitled thread", 88); + }, [settings?.privacyMode, snapshot?.currentThread]); + + async function runRefresh() { + setBusy(true); + setError(null); + try { + setSnapshot(await refreshNow()); + setHistory(await getUsageHistory("7d")); + } catch (err) { + setError(String(err)); + } finally { + setBusy(false); + } + } + + async function saveSettings(next: AppSettings) { + setSettings(next); + try { + setSettings(await updateSettings(next)); + } catch (err) { + setError(String(err)); + } + } + + return ( +
+ + +
+
+
+

Windows desktop monitor

+

Codex capacity at a glance

+
+
+ {snapshot?.liveStatus.detail ?? snapshot?.message ?? "Live app-server connection"} +
+
+ + {error &&
{error}
} + {snapshot?.message &&
{snapshot.message}
} + +
+
+
+
+ Codex limits +

{primaryBucket?.limitName ?? primaryBucket?.limitId ?? "Codex"}

+
+ +
+
+ + +
+
+ + + +
+
+ +
+ + + + +
+
+ +
+
+
+
+ Current thread +

{currentThreadTitle}

+
+
+
+ + + +
+
+ +
+
+
+ Token shape +

Last 7 days

+
+
+ +
+
+ +
+
+
+ Usage trend +

Daily tokens

+
+
+ +
+ + {settings && ( +
+
+
+ Settings +

Monitor behavior

+
+
+
+ + + + + +
+
+ )} +
+
+ ); +} + +function StatusPill({ state }: { state: string }) { + return
{state}
; +} + +function Metric({ + label, + value, + detail, +}: { + label: string; + value: string | number; + detail?: string; +}) { + return ( +
+ {label} + {value} + {detail && {detail}} +
+ ); +} + +function LimitGauge({ + label, + percent, + tone, + detail, +}: { + label: string; + percent: number; + tone: string; + detail: string; +}) { + const value = Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0; + return ( +
+
+
+ {Number.isFinite(percent) ? `${Math.round(percent)}%` : "--"} + used +
+
+
+ {label} + {detail} +
+
+ ); +} + +function TokenBars({ snapshot }: { snapshot: MonitorSnapshot | null }) { + const breakdown = snapshot?.usageSummary.tokenBreakdown; + const values = [ + ["Input", breakdown?.input ?? 0], + ["Output", breakdown?.output ?? 0], + ["Cached", breakdown?.cached ?? 0], + ["Reasoning", breakdown?.reasoning ?? 0], + ["Tool", breakdown?.tool ?? 0], + ] as const; + const max = Math.max(1, ...values.map(([, value]) => value)); + + return ( +
+ {values.map(([label, value]) => ( +
+ {label} +
+ +
+ {formatCompact(value)} +
+ ))} +
+ ); +} + +function Trend({ history }: { history: UsageHistory | null }) { + const points = history?.points ?? []; + const max = Math.max(1, ...points.map((point) => point.tokens)); + if (points.length === 0) { + return
No local usage history found for this range.
; + } + + return ( +
+ {points.map((point) => ( +
+ + {point.label.slice(5)} +
+ ))} +
+ ); +} + +export default App; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..9719b7b --- /dev/null +++ b/src/api.ts @@ -0,0 +1,43 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + AppSettings, + BadgePosition, + MonitorSnapshot, + UsageHistory, +} from "./types"; + +export function getSnapshot(): Promise { + return invoke("get_snapshot"); +} + +export function refreshNow(): Promise { + return invoke("refresh_now"); +} + +export function getUsageHistory(range: string): Promise { + return invoke("get_usage_history", { range }); +} + +export function getSettings(): Promise { + return invoke("get_settings"); +} + +export function updateSettings(settings: AppSettings): Promise { + return invoke("update_settings", { settings }); +} + +export function toggleBadge(visible: boolean): Promise { + return invoke("toggle_badge", { visible }); +} + +export function setBadgePosition(position: BadgePosition): Promise { + return invoke("set_badge_position", { position }); +} + +export function moveBadge(position: BadgePosition): Promise { + return invoke("move_badge", { position }); +} + +export function pauseNotifications(until: number | null): Promise { + return invoke("pause_notifications", { until }); +} diff --git a/src/badge.tsx b/src/badge.tsx new file mode 100644 index 0000000..fdea910 --- /dev/null +++ b/src/badge.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom/client"; +import { listen } from "@tauri-apps/api/event"; +import { getSettings, getSnapshot, moveBadge, setBadgePosition } from "./api"; +import type { BadgePosition, MonitorSnapshot } from "./types"; +import { + currentLimitPercent, + currentLimitResetAt, + formatCountdown, + resetWindowProgress, + statusTone, + weeklyLimitPercent, +} from "./utils"; +import "./styles.css"; + +function Badge() { + const [snapshot, setSnapshot] = useState(null); + const [position, setPosition] = useState({ x: 1530, y: 18 }); + const [refreshing, setRefreshing] = useState(true); + const [nowMs, setNowMs] = useState(Date.now()); + + useEffect(() => { + Promise.all([getSnapshot(), getSettings()]) + .then(([nextSnapshot, settings]) => { + setSnapshot(nextSnapshot); + setPosition(settings.badgePosition); + }) + .catch(() => undefined) + .finally(() => setRefreshing(false)); + const started = listen("refresh_started", () => { + setRefreshing(true); + }); + const unlisten = listen("rate_limit_updated", (event) => { + setSnapshot(event.payload); + setRefreshing(false); + }); + const timer = window.setInterval(() => setNowMs(Date.now()), 30_000); + return () => { + window.clearInterval(timer); + started.then((dispose) => dispose()).catch(() => undefined); + unlisten.then((dispose) => dispose()).catch(() => undefined); + }; + }, []); + + useEffect(() => { + const blockContextMenu = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + window.addEventListener("contextmenu", blockContextMenu, true); + return () => window.removeEventListener("contextmenu", blockContextMenu, true); + }, []); + + const currentPercent = currentLimitPercent(snapshot); + const weeklyPercent = weeklyLimitPercent(snapshot); + const resetAt = currentLimitResetAt(snapshot); + const resetProgress = resetWindowProgress(resetAt, nowMs); + const peakPercent = Math.max( + Number.isFinite(currentPercent) ? currentPercent : 0, + Number.isFinite(weeklyPercent) ? weeklyPercent : 0, + ); + const tone = statusTone(peakPercent); + + function dragBadge(event: React.PointerEvent) { + if (event.button !== 0) return; + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + + const startMouse = { x: event.screenX, y: event.screenY }; + const startPosition = position; + let latest = startPosition; + let scheduled = false; + + const calculate = (pointer: PointerEvent): BadgePosition => ({ + x: Math.round(startPosition.x + pointer.screenX - startMouse.x), + y: Math.round(startPosition.y + pointer.screenY - startMouse.y), + }); + + const onMove = (pointer: PointerEvent) => { + latest = calculate(pointer); + if (scheduled) return; + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + setPosition(latest); + moveBadge(latest).catch(() => undefined); + }); + }; + + const onUp = (pointer: PointerEvent) => { + latest = calculate(pointer); + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + setPosition(latest); + setBadgePosition(latest).catch(() => undefined); + }; + + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp, { once: true }); + } + + return ( +
+ +
+ +
+
+ + +
+
+ ); +} + +function MiniUsage({ label, percent }: { label: string; percent: number }) { + const value = Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0; + return ( +
+
+ {label} + {Number.isFinite(percent) ? `${Math.round(value)}%` : "--"} +
+
+ +
+
+ ); +} + +ReactDOM.createRoot(document.getElementById("badge-root") as HTMLElement).render( + + + , +); diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..81da346 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..af267ab --- /dev/null +++ b/src/styles.css @@ -0,0 +1,695 @@ +:root { + color-scheme: dark; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #111318; + color: #edf0f7; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: + linear-gradient(180deg, rgba(38, 44, 54, 0.98), rgba(16, 18, 24, 1) 45%), + #111318; +} + +.badge-html, +.badge-html body, +.badge-html #badge-root { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: transparent !important; +} + +button, +input { + font: inherit; +} + +button { + cursor: pointer; +} + +.app-shell { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 14px; + padding: 22px; + border-right: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(11, 13, 18, 0.72); +} + +.brand-block { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 10px; +} + +.brand-mark { + display: grid; + width: 44px; + height: 44px; + place-items: center; + border-radius: 8px; + background: #e9f6ff; + color: #101318; + font-weight: 800; +} + +.brand-block h1, +.topbar h2, +.panel h3, +.limit-panel h3, +.settings-panel h3 { + margin: 0; + letter-spacing: 0; +} + +.brand-block h1 { + font-size: 16px; +} + +.brand-block p, +.eyebrow, +.panel-header span, +.metric span, +.thread-row span, +.sidebar-note span { + color: #9ea7b8; +} + +.brand-block p { + margin: 2px 0 0; + font-size: 12px; +} + +.status-pill, +.connection { + width: fit-content; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 999px; + padding: 7px 11px; + color: #cdd5e3; + background: rgba(255, 255, 255, 0.06); + text-transform: capitalize; +} + +.status-pill.online, +.connection.online { + border-color: rgba(34, 197, 94, 0.35); + color: #a7f3d0; +} + +.status-pill.fallback, +.connection.fallback { + border-color: rgba(245, 158, 11, 0.42); + color: #fde68a; +} + +.status-pill.offline, +.connection.offline { + border-color: rgba(239, 68, 68, 0.42); + color: #fecaca; +} + +.primary-action, +.secondary-action { + min-height: 40px; + border: 0; + border-radius: 8px; + padding: 0 13px; + font-weight: 700; +} + +.primary-action { + background: #f4f7fb; + color: #111318; +} + +.primary-action:disabled { + opacity: 0.55; +} + +.secondary-action { + border: 1px solid rgba(255, 255, 255, 0.11); + background: rgba(255, 255, 255, 0.07); + color: #edf0f7; +} + +.sidebar-note { + display: grid; + gap: 4px; + margin-top: auto; + padding: 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.055); +} + +.sidebar-note strong { + overflow-wrap: anywhere; +} + +.content { + display: flex; + flex-direction: column; + gap: 18px; + padding: 26px; + min-width: 0; +} + +.topbar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.eyebrow { + margin: 0 0 5px; + font-size: 12px; + text-transform: uppercase; +} + +.topbar h2 { + font-size: 28px; +} + +.connection { + max-width: 460px; + text-transform: none; + text-align: right; +} + +.alert { + border: 1px solid rgba(245, 158, 11, 0.28); + border-radius: 8px; + padding: 11px 13px; + background: rgba(245, 158, 11, 0.09); + color: #fde68a; +} + +.alert.danger { + border-color: rgba(239, 68, 68, 0.32); + background: rgba(239, 68, 68, 0.09); + color: #fecaca; +} + +.overview-grid, +.work-grid { + display: grid; + grid-template-columns: minmax(320px, 0.92fr) minmax(340px, 1fr); + gap: 18px; +} + +.work-grid.lower { + grid-template-columns: minmax(420px, 1.1fr) minmax(320px, 0.9fr); +} + +.limit-panel, +.metric-panel, +.panel, +.settings-panel { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + background: rgba(255, 255, 255, 0.055); + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.18); +} + +.limit-panel, +.panel, +.settings-panel { + padding: 18px; +} + +.metric-panel { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + overflow: hidden; +} + +.metric-panel .metric { + padding: 18px; + background: rgba(255, 255, 255, 0.025); +} + +.panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.tone-dot { + width: 12px; + height: 12px; + border-radius: 999px; + background: #6b7280; +} + +.tone-dot.ok { + background: #22c55e; +} + +.tone-dot.warn { + background: #f59e0b; +} + +.tone-dot.danger { + background: #ef4444; +} + +.gauge { + display: grid; + place-items: center; + margin: 8px auto 18px; + width: 220px; + aspect-ratio: 1; + border-radius: 50%; + background: + radial-gradient(circle at 50% 50%, #151922 0 57%, transparent 58%), + conic-gradient(var(--accent) calc(var(--value) * 1%), rgba(255, 255, 255, 0.08) 0); +} + +.gauge.ok { + --accent: #22c55e; +} + +.gauge.warn { + --accent: #f59e0b; +} + +.gauge.danger { + --accent: #ef4444; +} + +.gauge.muted { + --accent: #64748b; +} + +.gauge strong { + display: block; + font-size: 42px; +} + +.gauge span { + color: #9ea7b8; +} + +.limit-gauge-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 18px; +} + +.limit-gauge-card { + display: grid; + justify-items: center; + gap: 10px; + min-width: 0; + padding: 14px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.035); +} + +.limit-gauge-card .gauge { + width: 132px; + margin: 0; +} + +.limit-gauge-card .gauge strong { + font-size: 28px; +} + +.limit-gauge-card > div:last-child { + display: grid; + gap: 2px; + text-align: center; +} + +.limit-gauge-card > div:last-child span { + color: #9ea7b8; +} + +.limit-meta, +.thread-current { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.metric { + display: grid; + gap: 4px; + min-width: 0; +} + +.metric strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 24px; +} + +.metric em { + color: #b6c0cf; + font-style: normal; +} + +.bars { + display: grid; + gap: 12px; +} + +.bar-row { + display: grid; + grid-template-columns: 86px minmax(0, 1fr) 70px; + gap: 10px; + align-items: center; +} + +.bar-row > span { + color: #b6c0cf; +} + +.bar-row > div { + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(255, 255, 255, 0.08); +} + +.bar-row i { + display: block; + height: 100%; + border-radius: inherit; + background: #67e8f9; +} + +.bar-row strong { + text-align: right; +} + +.thread-list { + display: grid; + gap: 10px; +} + +.thread-row { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: center; + padding: 11px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.045); + min-width: 0; +} + +.thread-row div { + min-width: 0; + flex: 1 1 auto; +} + +.thread-row strong, +.thread-row span { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.thread-row em { + color: #e0f2fe; + font-style: normal; + font-weight: 700; +} + +.trend { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(28px, 1fr); + gap: 8px; + height: 210px; + align-items: end; +} + +.trend-column { + display: grid; + grid-template-rows: 1fr 18px; + gap: 7px; + height: 100%; + align-items: end; +} + +.trend-column i { + display: block; + min-height: 8px; + border-radius: 5px 5px 2px 2px; + background: linear-gradient(180deg, #a7f3d0, #22c55e); +} + +.trend-column span { + color: #9ea7b8; + font-size: 11px; + text-align: center; +} + +.empty-state { + display: grid; + min-height: 160px; + place-items: center; + color: #9ea7b8; +} + +.settings-grid { + display: grid; + grid-template-columns: 1.2fr 1.2fr 0.6fr 0.6fr auto; + gap: 12px; + align-items: end; +} + +.settings-grid label { + display: grid; + gap: 7px; + color: #b6c0cf; +} + +.settings-grid input { + width: 100%; + min-height: 40px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 0 11px; + background: rgba(0, 0, 0, 0.22); + color: #edf0f7; +} + +.toggle-row { + grid-template-columns: 1fr auto; + align-items: center; +} + +.toggle-row input { + width: 20px; +} + +.badge-body { + width: 100vw; + height: 100vh; + min-width: 0; + min-height: 0; + overflow: hidden; + background: transparent; +} + +.badge-shell { + display: grid; + grid-template-rows: 5px minmax(0, 1fr); + align-items: stretch; + gap: 6px; + position: relative; + width: 164px; + height: 82px; + margin: 0; + border: 1px solid rgba(34, 197, 94, 0.48); + border-radius: 8px; + padding: 10px 10px 12px; + color: white; + background: rgba(17, 19, 24, 0.92); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.34); + cursor: move; + user-select: none; +} + +.badge-shell.ok { + border-color: rgba(34, 197, 94, 0.48); +} + +.badge-shell.warn { + border-color: rgba(245, 158, 11, 0.55); +} + +.badge-shell.danger { + border-color: rgba(239, 68, 68, 0.62); +} + +.badge-shell span { + color: #cbd5e1; + font-size: 12px; +} + +.badge-charts { + display: grid; + gap: 6px; + min-width: 0; +} + +.badge-chart { + display: grid; + grid-template-rows: 12px 8px; + gap: 3px; + min-width: 0; +} + +.badge-chart header { + display: flex; + justify-content: space-between; + gap: 8px; + min-width: 0; +} + +.badge-chart span, +.badge-chart strong { + color: #9ea7b8; + font-size: 10px; + font-weight: 800; + line-height: 1; +} + +.badge-chart strong { + color: #edf0f7; + text-align: right; +} + +.badge-chart-track { + height: 8px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.13); +} + +.badge-chart i { + display: block; + height: 100%; + min-width: 3px; + border-radius: inherit; + background: #22c55e; +} + +.badge-shell.warn .badge-chart i { + background: #f59e0b; +} + +.badge-shell.danger .badge-chart i { + background: #ef4444; +} + +.badge-activity { + position: absolute; + top: 0; + left: 8px; + right: 8px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 5px; + opacity: 0; + pointer-events: none; +} + +.badge-activity.active { + opacity: 1; +} + +.badge-activity i { + display: block; + height: 3px; + border-radius: 0 0 999px 999px; + background: #67e8f9; + animation: badge-pulse 0.8s ease-in-out infinite; +} + +.badge-reset { + align-self: end; + height: 5px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); +} + +.badge-reset i { + display: block; + min-width: 3px; + height: 100%; + border-radius: inherit; + background: #67e8f9; +} + +.badge-activity i:nth-child(2) { + animation-delay: 0.12s; +} + +.badge-activity i:nth-child(3) { + animation-delay: 0.24s; +} + +@keyframes badge-pulse { + 0%, + 100% { + opacity: 0.25; + transform: scaleX(0.72); + } + + 50% { + opacity: 1; + transform: scaleX(1); + } +} + +@media (max-width: 980px) { + .app-shell, + .overview-grid, + .work-grid, + .work-grid.lower { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } + + .settings-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..968e43e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,96 @@ +export type RateLimitBucket = { + limitId: string; + limitName: string | null; + usedPercent: number; + windowDurationMins: number | null; + resetsAt: number | null; + secondaryUsedPercent: number | null; + secondaryResetsAt: number | null; + reachedType: string | null; +}; + +export type LiveStatus = { + state: "online" | "fallback" | "offline" | string; + detail: string | null; +}; + +export type TokenBreakdown = { + input: number; + output: number; + cached: number; + reasoning: number; + tool: number; + responses: number; +}; + +export type ThreadUsage = { + id: string; + title: string; + tokensUsed: number; + model: string | null; + reasoningEffort: string | null; + updatedAt: number; + cwd: string | null; +}; + +export type ModelUsage = { + model: string; + tokens: number; + threads: number; +}; + +export type UsageSummary = { + totalTokens: number; + todayTokens: number; + weekTokens: number; + threadCount: number; + todayThreadCount: number; + weekThreadCount: number; + currentThread: ThreadUsage | null; + recentThreads: ThreadUsage[]; + modelBreakdown: ModelUsage[]; + tokenBreakdown: TokenBreakdown; + sourcePath: string | null; + error: string | null; +}; + +export type MonitorSnapshot = { + source: string; + fetchedAt: number; + liveStatus: LiveStatus; + buckets: RateLimitBucket[]; + usageSummary: UsageSummary; + currentThread: ThreadUsage | null; + message: string | null; +}; + +export type UsagePoint = { + label: string; + tokens: number; + threads: number; +}; + +export type UsageHistory = { + range: string; + points: UsagePoint[]; +}; + +export type BadgePosition = { + x: number; + y: number; +}; + +export type AppSettings = { + codexExecutable: string | null; + codexHome: string; + refreshIntervalSecs: number; + badgeVisible: boolean; + badgePosition: BadgePosition; + privacyMode: boolean; + thresholds: { + warning: number; + critical: number; + exhausted: number; + }; + notificationsMutedUntil: number | null; +}; diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..094bc68 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { + currentLimitPercent, + formatCountdown, + resetWindowProgress, + statusTone, + truncateTitle, + weeklyLimitPercent, + weeklyLimitResetAt, +} from "./utils"; +import type { MonitorSnapshot } from "./types"; + +describe("statusTone", () => { + it("maps percentages to tones", () => { + expect(statusTone(Number.NaN)).toBe("muted"); + expect(statusTone(10)).toBe("ok"); + expect(statusTone(70)).toBe("warn"); + expect(statusTone(90)).toBe("danger"); + }); +}); + +describe("formatCountdown", () => { + it("formats future reset timestamps", () => { + const future = Math.round(Date.now() / 1000) + 120; + expect(formatCountdown(future)).toBe("2m"); + }); +}); + +describe("limit helpers", () => { + it("separates current and weekly app-server percentages", () => { + const snapshot = { + buckets: [ + { + limitId: "codex", + limitName: null, + usedPercent: 8, + windowDurationMins: 300, + resetsAt: 100, + secondaryUsedPercent: 44, + secondaryResetsAt: 200, + reachedType: null, + }, + ], + } as MonitorSnapshot; + + expect(currentLimitPercent(snapshot)).toBe(8); + expect(weeklyLimitPercent(snapshot)).toBe(44); + expect(weeklyLimitResetAt(snapshot)).toBe(200); + }); +}); + +describe("truncateTitle", () => { + it("limits long titles", () => { + expect(truncateTitle("abcdefghijklmnopqrstuvwxyz", 10)).toBe("abcdefg..."); + }); +}); + +describe("resetWindowProgress", () => { + it("fills based on elapsed time in the five hour reset window", () => { + const resetAt = Date.UTC(2026, 0, 1, 5, 0, 0) / 1000; + const nowMs = Date.UTC(2026, 0, 1, 1, 0, 0); + + expect(resetWindowProgress(resetAt, nowMs)).toBe(20); + }); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c0ffb53 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,92 @@ +import type { MonitorSnapshot } from "./types"; + +export function formatNumber(value: number): string { + return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value); +} + +export function formatCompact(value: number): string { + return new Intl.NumberFormat(undefined, { + notation: "compact", + maximumFractionDigits: 1, + }).format(value); +} + +export function maxBucketPercent(snapshot: MonitorSnapshot | null): number { + if (!snapshot || snapshot.buckets.length === 0) return Number.NaN; + return Math.max(...snapshot.buckets.map((bucket) => bucket.usedPercent)); +} + +export function currentLimitPercent(snapshot: MonitorSnapshot | null): number { + return snapshot?.buckets[0]?.usedPercent ?? Number.NaN; +} + +export function weeklyLimitPercent(snapshot: MonitorSnapshot | null): number { + const primary = snapshot?.buckets[0]; + if (typeof primary?.secondaryUsedPercent === "number") { + return primary.secondaryUsedPercent; + } + + const weeklyBucket = snapshot?.buckets.find( + (bucket) => (bucket.windowDurationMins ?? 0) >= 10_080, + ); + return weeklyBucket?.usedPercent ?? Number.NaN; +} + +export function weeklyLimitResetAt(snapshot: MonitorSnapshot | null): number | null { + const primary = snapshot?.buckets[0]; + if (typeof primary?.secondaryResetsAt === "number") { + return primary.secondaryResetsAt; + } + + const weeklyBucket = snapshot?.buckets.find( + (bucket) => (bucket.windowDurationMins ?? 0) >= 10_080, + ); + return weeklyBucket?.resetsAt ?? null; +} + +export function currentLimitResetAt(snapshot: MonitorSnapshot | null): number | null { + return snapshot?.buckets[0]?.resetsAt ?? null; +} + +export function resetWindowProgress( + resetAt: number | null | undefined, + nowMs = Date.now(), + windowHours = 5, +): number { + if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return Number.NaN; + const windowMs = windowHours * 60 * 60 * 1000; + const resetMs = resetAt * 1000; + const startMs = resetMs - windowMs; + return Math.max(0, Math.min(100, ((nowMs - startMs) / windowMs) * 100)); +} + +export function statusTone(percent: number): "ok" | "warn" | "danger" | "muted" { + if (!Number.isFinite(percent)) return "muted"; + if (percent >= 90) return "danger"; + if (percent >= 70) return "warn"; + return "ok"; +} + +export function formatCountdown(timestamp: number): string { + const seconds = Math.max(0, Math.round(timestamp - Date.now() / 1000)); + if (seconds === 0) return "resetting"; + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.round(seconds / 60)}m`; + return `${Math.round(seconds / 3600)}h`; +} + +export function formatDateTime(timestamp: number): string { + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(timestamp * 1000)); +} + +export function truncateTitle(title: string | null | undefined, max = 74): string { + const clean = (title ?? "Untitled").trim() || "Untitled"; + if (clean.length <= max) return clean; + if (max <= 3) return clean.slice(0, max); + return `${clean.slice(0, max - 3)}...`; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..df11947 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5a4b8c1 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { resolve } from "node:path"; + +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + strictPort: true, + host: "127.0.0.1", + port: 1420, + }, + envPrefix: ["VITE_", "TAURI_"], + build: { + target: "es2022", + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + badge: resolve(__dirname, "badge.html"), + }, + }, + }, +});