From 866bea3d2b2d3e54a2bb0c9ea779ffadc1c463e5 Mon Sep 17 00:00:00 2001 From: Bun Bun Date: Sun, 14 Jun 2026 10:48:26 +0000 Subject: [PATCH] Initial MVP: Cost.dev AI Coding Cost Optimizer Chrome extension --- .gitignore | 4 + README.md | 51 ++ generate-icons.cjs | 70 ++ icons/icon128.png | Bin 0 -> 405 bytes icons/icon16.png | Bin 0 -> 78 bytes icons/icon48.png | Bin 0 -> 122 bytes package-lock.json | 1493 ++++++++++++++++++++++++++++++++++++++++++ package.json | 17 + src/background.ts | 62 ++ src/content.ts | 39 ++ src/manifest.json | 42 ++ src/popup/popup.css | 176 +++++ src/popup/popup.html | 100 +++ src/popup/popup.ts | 210 ++++++ src/storage.ts | 90 +++ src/types.ts | 56 ++ tsconfig.json | 15 + vite.config.ts | 11 + 18 files changed, 2436 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 generate-icons.cjs create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/background.ts create mode 100644 src/content.ts create mode 100644 src/manifest.json create mode 100644 src/popup/popup.css create mode 100644 src/popup/popup.html create mode 100644 src/popup/popup.ts create mode 100644 src/storage.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd6e803 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..b31a0eb --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Cost.dev โ€” AI Coding Cost Optimizer + +**v1 MVP** โ€” A Chrome extension that helps developers track, budget, and control spending on AI coding tools. + +## What it does (real features) + +- **Manual usage logging**: Log spend per tool per day with notes. +- **Monthly dashboard**: See total spend, per-tool breakdown, daily trend list, and projected month-end bill. +- **Budget & alerts**: Set a monthly budget and alert threshold. Over-budget notifications via `chrome.alarms` + `chrome.notifications` (checked hourly). +- **Tool registry**: Add your AI tools (Claude Code, GitHub Copilot, Cursor, etc.) with estimated monthly cost and billing model (fixed / usage / hybrid). +- **Local-only storage**: All data lives in `chrome.storage.local` โ€” no backend, no cloud, no accounts. +- **Empty-state handling**: Clean UI when no tools or data exist yet. + +## What is real vs. manual / deferred + +- **Real**: Manual entry, local storage, dashboard math, alarms/notifications, popup UI. +- **Manual**: All usage numbers must be entered by you. We do **not** auto-scrape billing pages because auth/DOM varies and breaks โ€” the content script is a scaffold that detects you're on a supported page and may support quick-capture in a future version. +- **Deferred**: Chrome Web Store publishing, auto-import from APIs, paid tiers, backend sync, cross-device sync. + +## Tech stack + +- Vite + `@crxjs/vite-plugin` + TypeScript +- Manifest V3 +- Permissions: `storage`, `alarms`, `notifications` +- Host permissions (content script): `cursor.com`, `github.com/copilot` (scaffold only, no auto-read in v1) + +## Build + +Requires `npm install --include=dev` because this environment runs with `NODE_ENV=production`. + +```bash +npm install --include=dev +npm run build +``` + +The build output goes to `dist/`. Confirm `dist/manifest.json` exists. + +## Load unpacked + +1. Open `chrome://extensions` +2. Enable **Developer mode** +3. Click **Load unpacked** +4. Select the `dist/` folder + +## Gitea + +```bash +git push https://git.bunbunlabs.com/bunbun/cost-dev.git HEAD:refs/heads/main +``` + +Built by Bun Bun for BunBun Labs ๐Ÿฐ diff --git a/generate-icons.cjs b/generate-icons.cjs new file mode 100644 index 0000000..f1efff9 --- /dev/null +++ b/generate-icons.cjs @@ -0,0 +1,70 @@ +// Minimal PNG generator โ€” run before build to create icon files +const { writeFileSync, mkdirSync } = require('fs') + +function crc32(buf) { + const table = new Int32Array(256) + for (let i = 0; i < 256; i++) { + let c = i + for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1) + table[i] = c + } + let crc = -1 + for (let i = 0; i < buf.length; i++) crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8) + return crc ^ -1 +} + +function pngChunk(type, data) { + const len = data.length + const chunk = new Uint8Array(4 + 4 + len + 4) + const view = new DataView(chunk.buffer) + view.setUint32(0, len, false) + chunk.set(new TextEncoder().encode(type), 4) + chunk.set(data, 8) + const crc = crc32(chunk.slice(4, 8 + len)) + view.setUint32(8 + len, crc, false) + return chunk +} + +function generatePNG(size, color) { + const header = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) + const ihdr = new Uint8Array(13) + const v = new DataView(ihdr.buffer) + v.setUint32(0, size, false) + v.setUint32(4, size, false) + v.setUint8(8, 8) + v.setUint8(9, 2) + v.setUint8(10, 0) + v.setUint8(11, 0) + v.setUint8(12, 0) + + const rowLen = 1 + size * 3 + const raw = new Uint8Array(size * rowLen) + for (let y = 0; y < size; y++) { + raw[y * rowLen] = 0 + for (let x = 0; x < size; x++) { + const off = y * rowLen + 1 + x * 3 + raw[off] = color[0] + raw[off + 1] = color[1] + raw[off + 2] = color[2] + } + } + + const compressed = new Uint8Array(require('zlib').deflateSync(raw)) + const idat = pngChunk('IDAT', compressed) + const iend = pngChunk('IEND', new Uint8Array(0)) + + const out = new Uint8Array(header.length + 4 + 4 + 13 + 4 + idat.length + iend.length) + let p = 0 + out.set(header, p); p += header.length + out.set(pngChunk('IHDR', ihdr), p); p += 4 + 4 + 13 + 4 + out.set(idat, p); p += idat.length + out.set(iend, p) + return out +} + +mkdirSync('icons', { recursive: true }) +const teal = [0, 180, 170] +writeFileSync('icons/icon16.png', generatePNG(16, teal)) +writeFileSync('icons/icon48.png', generatePNG(48, teal)) +writeFileSync('icons/icon128.png', generatePNG(128, teal)) +console.log('Icons generated') diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..ca297d4b3f12dc7da6376248bce2d59757d37da2 GIT binary patch literal 405 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V2tr}aSW-L^Y)w}FM|QkfeoM9 yH|=I%KkTk0*^vHC_9XwKKMLnY39`cAmfdCERw>hx1*X8@X7F_Nb6Mw<&;$VFcFs}& literal 0 HcmV?d00001 diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..b0733e4c362a86873fbf3611d7fd94cfb7d1afd9 GIT binary patch literal 78 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|qMj~}Ar*6y6K-tkGBH_|7;^Z- bRR)HP%s7GB`A0i|3K%?H{an^LB{Ts5k!2LX literal 0 HcmV?d00001 diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..d0fd8ec747d412da394a8110b02834fb5d698b16 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@HgTe~DWM4f{_!Pa literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e5a2d10 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1493 @@ +{ + "name": "cost-dev", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cost-dev", + "version": "1.0.0", + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "@types/chrome": "^0.0.268", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.6.1.tgz", + "integrity": "sha512-0RmYlUtQGvHXCz3B/FW7P5j+RBZUc4mk3GIRPXCx5a52N8AQ0e35GvE+Pcap/TKuI09PJgcRTdXeBOavtLVyYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "node-html-parser": "^7.0.2", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.80.0", + "rxjs": "7.5.7", + "tinyglobby": "^0.2.15" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chrome": { + "version": "0.0.268", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", + "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..453042e --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "cost-dev", + "version": "1.0.0", + "description": "AI Coding Cost Optimizer โ€” track and control AI tool spending", + "type": "module", + "scripts": { + "build": "tsc --noEmit && vite build", + "dev": "vite", + "check": "tsc --noEmit" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "@types/chrome": "^0.0.268", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } +} diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..7719c30 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,62 @@ +import { loadState, saveState, sumMonthUsages, projectedMonthEnd } from './storage' + +const ALARM_NAME = 'costdev_budget_check' + +chrome.runtime.onInstalled.addListener(async () => { + await chrome.alarms.create(ALARM_NAME, { periodInMinutes: 60 }) +}) + +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name !== ALARM_NAME) return + const state = await loadState() + if (!state.budget.enabled || state.budget.monthlyLimit <= 0) return + + const spent = sumMonthUsages(state.usages) + const limit = state.budget.monthlyLimit + const threshold = limit * state.budget.alertThreshold + + if (spent >= threshold && spent < limit) { + const last = state.lastAlertAt ?? 0 + if (Date.now() - last > 24 * 60 * 60 * 1000) { + chrome.notifications.create('costdev-warning', { + type: 'basic', + iconUrl: 'icons/icon128.png', + title: 'Cost.dev โ€” Budget Alert', + message: `You've spent $${spent.toFixed(2)} of your $${limit.toFixed(2)} monthly budget.`, + priority: 1, + }) + state.lastAlertAt = Date.now() + await saveState(state) + } + } + + if (spent >= limit) { + const last = state.lastAlertAt ?? 0 + if (Date.now() - last > 24 * 60 * 60 * 1000) { + chrome.notifications.create('costdev-over', { + type: 'basic', + iconUrl: 'icons/icon128.png', + title: 'Cost.dev โ€” Budget Overrun', + message: `You've exceeded your $${limit.toFixed(2)} monthly budget! Current spend: $${spent.toFixed(2)}.`, + priority: 2, + }) + state.lastAlertAt = Date.now() + await saveState(state) + } + } +}) + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + ;(async () => { + if (message.type === 'budget-check') { + const state = await loadState() + const spent = sumMonthUsages(state.usages) + const limit = state.budget.monthlyLimit + const projected = projectedMonthEnd(spent) + sendResponse({ spent, limit, projected, enabled: state.budget.enabled }) + } else { + sendResponse({ ok: false }) + } + })() + return true +}) diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..986ec7a --- /dev/null +++ b/src/content.ts @@ -0,0 +1,39 @@ +// Content script โ€” honest about what we can and cannot read client-side +// v1: we do NOT auto-scrape private billing pages because authentication +// and DOM structures vary and break. Instead, we watch for visible plan/usage +// text on dashboard pages and offer a "Quick Capture" to the user via a +// floating badge (not implemented in v1 to keep scope tight). +// In v1, this script is a lightweight scaffold that sends a heartbeat +// so the popup knows the user is on a supported tool page. + +interface PageInfo { + url: string + title: string + hasBillingText: boolean +} + +function detectBillingIndicators(): boolean { + const text = document.body.innerText.toLowerCase() + const indicators = [ + 'usage', + 'tokens', + 'requests', + 'credits', + 'billing', + 'subscription', + 'plan', + 'limit', + 'quota', + ] + return indicators.some((w) => text.includes(w)) +} + +const pageInfo: PageInfo = { + url: location.href, + title: document.title, + hasBillingText: detectBillingIndicators(), +} + +chrome.runtime.sendMessage({ type: 'page-info', data: pageInfo }).catch(() => { + // no-op if popup closed +}) diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..720912a --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,42 @@ +{ + "manifest_version": 3, + "name": "Cost.dev โ€” AI Coding Cost Optimizer", + "version": "1.0.0", + "description": "Track, budget, and control your AI coding tool spend. Manual entry + dashboard.", + "permissions": ["storage", "alarms", "notifications"], + "host_permissions": [ + "https://cursor.com/*", + "https://www.cursor.com/*", + "https://github.com/copilot/*", + "https://copilot.github.com/*" + ], + "background": { + "service_worker": "src/background.ts", + "type": "module" + }, + "action": { + "default_popup": "src/popup/popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "content_scripts": [ + { + "matches": [ + "https://cursor.com/*", + "https://www.cursor.com/*", + "https://github.com/copilot/*", + "https://copilot.github.com/*" + ], + "js": ["src/content.ts"], + "run_at": "document_idle" + } + ] +} diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..db0712e --- /dev/null +++ b/src/popup/popup.css @@ -0,0 +1,176 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +:root { + --bg: #0f172a; + --card: #1e293b; + --text: #f1f5f9; + --muted: #94a3b8; + --accent: #2dd4bf; + --accent-dim: #0f766e; + --danger: #f87171; + --warning: #fbbf24; + --border: #334155; + --radius: 8px; + --gap: 12px; +} +body { + width: 380px; + background: var(--bg); + color: var(--text); + font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 1.45; + overflow-x: hidden; +} +#app { padding: 16px; } +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.brand { display: flex; align-items: center; gap: 10px; } +.logo-icon { font-size: 22px; } +.brand h1 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; } +.brand p { color: var(--muted); font-size: 12px; margin-top: 2px; } +.icon-btn { + background: transparent; + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + cursor: pointer; + padding: 6px 8px; + font-size: 14px; +} +.icon-btn:hover { background: var(--card); } + +.overview .card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + margin-bottom: 16px; +} +.overview .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; } +.overview .big-number { font-size: 32px; font-weight: 800; margin: 6px 0; color: var(--accent); } +.overview .sub { color: var(--muted); font-size: 12px; margin-top: 4px; } +.progress-wrap { margin-top: 10px; height: 8px; background: #0f172a; border-radius: 4px; overflow: hidden; } +.progress-bar { height: 100%; background: var(--accent); transition: width 0.3s ease; } + +h2 { font-size: 14px; font-weight: 600; margin: 14px 0 8px; } + +.tools-section, .add-usage-section, .trend-section { margin-bottom: 14px; } + +.tools-list { display: flex; flex-direction: column; gap: 8px; } +.tool-item { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; +} +.tool-left { display: flex; flex-direction: column; gap: 2px; } +.tool-name { font-weight: 600; font-size: 13px; } +.tool-meta { color: var(--muted); font-size: 11px; } +.tool-right { text-align: right; } +.tool-spend { font-weight: 700; font-size: 14px; } +.tool-budget { font-size: 11px; color: var(--muted); } +.tool-actions { display: flex; gap: 6px; margin-top: 6px; } +.btn-small { + background: transparent; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + cursor: pointer; + padding: 4px 8px; + font-size: 11px; +} +.btn-small:hover { background: var(--border); } + +.form { display: flex; flex-direction: column; gap: 8px; } +.form.compact { flex-direction: row; flex-wrap: wrap; gap: 6px; } +.form.compact input, .form.compact select { flex: 1 1 120px; } +.form.compact button { flex: 0 0 auto; } +input, select, textarea { + background: #0f172a; + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + padding: 8px 10px; + font-size: 13px; + outline: none; + width: 100%; +} +input:focus, select:focus { border-color: var(--accent); } +.btn-primary { + background: var(--accent-dim); + color: #fff; + border: none; + border-radius: var(--radius); + padding: 8px 12px; + font-size: 13px; + cursor: pointer; + font-weight: 600; +} +.btn-primary:hover { background: var(--accent); color: #000; } +.btn-secondary { + background: transparent; + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius); + padding: 8px 12px; + font-size: 13px; + cursor: pointer; +} +.btn-danger { + background: transparent; + border: 1px solid var(--danger); + color: var(--danger); + border-radius: var(--radius); + padding: 8px 12px; + font-size: 13px; + cursor: pointer; + width: 100%; +} + +.trend-list { display: flex; flex-direction: column; gap: 6px; max-height: 180px; overflow-y: auto; } +.trend-row { + display: flex; + justify-content: space-between; + padding: 6px 8px; + background: var(--card); + border-radius: 6px; + font-size: 12px; +} +.trend-row .day { color: var(--muted); } +.trend-row .val { font-weight: 600; } + +.empty-state { text-align: center; padding: 30px 16px; color: var(--muted); } +.empty-icon { font-size: 36px; margin-bottom: 8px; } +.empty-state p { font-size: 13px; } +.hidden { display: none !important; } + +.modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} +.modal-content { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + width: 340px; +} +.modal-content h2 { margin-bottom: 12px; } +.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } +.danger-zone { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; } +.row { display: flex; align-items: center; gap: 8px; margin-top: 4px; } + +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } diff --git a/src/popup/popup.html b/src/popup/popup.html new file mode 100644 index 0000000..6a21998 --- /dev/null +++ b/src/popup/popup.html @@ -0,0 +1,100 @@ + + + + + Cost.dev + + + +
+
+
+ ๐Ÿ’ฐ +
+

Cost.dev

+

AI Coding Cost Optimizer

+
+
+
+ +
+
+ +
+
+
This Month
+
$0.00
+
Budget: $50.00
+
+
+
+
Projected: $0.00
+
+
+ +
+

Your Tools

+
+
+ + + + +
+
+ +
+

Log Usage

+
+ + + + + +
+
+ +
+

Daily Breakdown

+
+
+ + +
+ + + + + + + diff --git a/src/popup/popup.ts b/src/popup/popup.ts new file mode 100644 index 0000000..069302d --- /dev/null +++ b/src/popup/popup.ts @@ -0,0 +1,210 @@ +import type { AppState, Budget, ToolEntry, UsageRecord } from '../types' +import { + loadState, + saveState, + addTool, + addUsage, + removeTool, + removeUsage, + setBudget, + sumMonthUsages, + sumToolMonthUsages, + dailyTotals, + projectedMonthEnd, + clearAll, +} from '../storage' +import { newId, DEFAULT_BUDGET, getMonthKey, getTodayKey } from '../types' + +// --- DOM refs --- +const el = (id: string) => document.getElementById(id)! + +let state: AppState = { version: 1, tools: [], usages: [], budget: { ...DEFAULT_BUDGET } } + +async function init() { + state = await loadState() + render() + wireEvents() +} + +function render() { + const month = getMonthKey() + const total = sumMonthUsages(state.usages, month) + const limit = state.budget.monthlyLimit + const projected = projectedMonthEnd(total) + + // Overview + el('total-spend').textContent = `$${total.toFixed(2)}` + el('budget-sub').textContent = `Budget: $${limit.toFixed(2)}` + el('projected-sub').textContent = `Projected: $${projected.toFixed(2)}` + const pct = limit > 0 ? Math.min((total / limit) * 100, 100) : 0 + el('budget-bar').style.width = `${pct}%` + el('budget-bar').style.background = pct > 100 ? 'var(--danger)' : pct > 80 ? 'var(--warning)' : 'var(--accent)' + + // Tools list + const toolsList = el('tools-list') + toolsList.innerHTML = '' + if (state.tools.length === 0) { + el('empty-state').classList.remove('hidden') + el('tools-section').classList.add('hidden') + el('add-usage-section').classList.add('hidden') + el('trend-section').classList.add('hidden') + } else { + el('empty-state').classList.add('hidden') + el('tools-section').classList.remove('hidden') + el('add-usage-section').classList.remove('hidden') + el('trend-section').classList.remove('hidden') + + for (const t of state.tools) { + const toolSpent = sumToolMonthUsages(state.usages, t.id, month) + const div = document.createElement('div') + div.className = 'tool-item' + div.innerHTML = ` +
+
${escapeHtml(t.name)}
+
${escapeHtml(t.billingModel)}${t.planName ? ' ยท ' + escapeHtml(t.planName) : ''}
+
+ + +
+
+
+
$${toolSpent.toFixed(2)}
+
${t.monthlyCost > 0 ? 'Est: $' + t.monthlyCost.toFixed(2) + '/mo' : 'No estimate'}
+
+ ` + toolsList.appendChild(div) + } + } + + // Usage tool selector + const usageTool = el('usage-tool') as HTMLSelectElement + const prevVal = usageTool.value + usageTool.innerHTML = '' + for (const t of state.tools) { + const opt = document.createElement('option') + opt.value = t.id + opt.textContent = t.name + usageTool.appendChild(opt) + } + if (prevVal) usageTool.value = prevVal + + // Daily trend + const trendList = el('trend-list') + trendList.innerHTML = '' + const days = dailyTotals(state.usages, month) + if (days.length === 0) { + const empty = document.createElement('div') + empty.className = 'trend-row' + empty.innerHTML = 'No data yetโ€”' + trendList.appendChild(empty) + } else { + for (const d of days) { + const row = document.createElement('div') + row.className = 'trend-row' + row.innerHTML = `${d.day}$${d.total.toFixed(2)}` + trendList.appendChild(row) + } + } + + // Settings form values + ;(el('budget-limit') as HTMLInputElement).value = String(state.budget.monthlyLimit) + ;(el('budget-threshold') as HTMLInputElement).value = String(Math.round(state.budget.alertThreshold * 100)) + ;(el('budget-enabled') as HTMLInputElement).checked = state.budget.enabled +} + +function wireEvents() { + el('add-tool-form').addEventListener('submit', async (e) => { + e.preventDefault() + const name = (el('tool-name') as HTMLInputElement).value.trim() + const cost = parseFloat((el('tool-cost') as HTMLInputElement).value) + const model = (el('tool-model') as HTMLSelectElement).value as ToolEntry['billingModel'] + if (!name || isNaN(cost) || cost < 0) return + const tool: ToolEntry = { + id: newId(), + name, + monthlyCost: cost, + currency: 'USD', + billingModel: model, + lastUpdated: Date.now(), + } + await addTool(tool) + state = await loadState() + ;(el('add-tool-form') as HTMLFormElement).reset() + render() + }) + + el('add-usage-form').addEventListener('submit', async (e) => { + e.preventDefault() + const toolId = (el('usage-tool') as HTMLSelectElement).value + const date = (el('usage-date') as HTMLInputElement).value + const amount = parseFloat((el('usage-amount') as HTMLInputElement).value) + const note = (el('usage-note') as HTMLInputElement).value.trim() + if (!toolId || !date || isNaN(amount) || amount < 0) return + const record: UsageRecord = { + id: newId(), + toolId, + date, + amount, + note: note || undefined, + source: 'manual', + createdAt: Date.now(), + } + await addUsage(record) + state = await loadState() + ;(el('add-usage-form') as HTMLFormElement).reset() + ;(el('usage-date') as HTMLInputElement).value = getTodayKey() + render() + }) + + el('tools-list').addEventListener('click', async (e) => { + const btn = (e.target as HTMLElement).closest('button') as HTMLButtonElement | null + if (!btn) return + const action = btn.dataset.action + const toolId = btn.dataset.tool + if (!action || !toolId) return + if (action === 'delete-tool') { + await removeTool(toolId) + state = await loadState() + render() + } else if (action === 'add-usage') { + ;(el('usage-tool') as HTMLSelectElement).value = toolId + el('add-usage-section').scrollIntoView({ behavior: 'smooth' }) + } + }) + + el('btn-settings').addEventListener('click', () => { + el('settings-modal').classList.remove('hidden') + }) + el('btn-close-settings').addEventListener('click', () => { + el('settings-modal').classList.add('hidden') + }) + el('budget-form').addEventListener('submit', async (e) => { + e.preventDefault() + const limit = parseFloat((el('budget-limit') as HTMLInputElement).value) + const threshold = parseInt((el('budget-threshold') as HTMLInputElement).value, 10) / 100 + const enabled = (el('budget-enabled') as HTMLInputElement).checked + if (isNaN(limit) || isNaN(threshold)) return + const budget: Budget = { monthlyLimit: limit, currency: 'USD', alertThreshold: threshold, enabled } + await setBudget(budget) + state = await loadState() + el('settings-modal').classList.add('hidden') + render() + }) + el('btn-reset').addEventListener('click', async () => { + if (!confirm('Delete ALL data? This cannot be undone.')) return + await clearAll() + state = await loadState() + render() + }) +} + +function escapeHtml(s: string): string { + const d = document.createElement('div') + d.textContent = s + return d.innerHTML +} + +// Init default date +;(el('usage-date') as HTMLInputElement).value = getTodayKey() + +init() diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..3b48e4b --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,90 @@ +import type { AppState, Budget, ToolEntry, UsageRecord } from './types' +import { DEFAULT_BUDGET, getMonthKey } from './types' + +const STORAGE_KEY = 'costdev_state_v1' + +export async function loadState(): Promise { + const res = await chrome.storage.local.get(STORAGE_KEY) + const stored = res[STORAGE_KEY] as Partial | undefined + return { + version: 1, + tools: stored?.tools ?? [], + usages: stored?.usages ?? [], + budget: stored?.budget ?? { ...DEFAULT_BUDGET }, + lastAlertAt: stored?.lastAlertAt, + } +} + +export async function saveState(state: AppState): Promise { + await chrome.storage.local.set({ [STORAGE_KEY]: state }) +} + +export async function addTool(tool: ToolEntry): Promise { + const s = await loadState() + s.tools = [...s.tools, tool] + await saveState(s) +} + +export async function updateTool(id: string, patch: Partial): Promise { + const s = await loadState() + s.tools = s.tools.map((t) => (t.id === id ? { ...t, ...patch, lastUpdated: Date.now() } : t)) + await saveState(s) +} + +export async function removeTool(id: string): Promise { + const s = await loadState() + s.tools = s.tools.filter((t) => t.id !== id) + s.usages = s.usages.filter((u) => u.toolId !== id) + await saveState(s) +} + +export async function addUsage(record: UsageRecord): Promise { + const s = await loadState() + s.usages = [...s.usages, record] + await saveState(s) +} + +export async function removeUsage(id: string): Promise { + const s = await loadState() + s.usages = s.usages.filter((u) => u.id !== id) + await saveState(s) +} + +export async function setBudget(budget: Budget): Promise { + const s = await loadState() + s.budget = budget + await saveState(s) +} + +export function sumMonthUsages(usages: UsageRecord[], month = getMonthKey()): number { + return usages + .filter((u) => u.date.startsWith(month)) + .reduce((sum, u) => sum + u.amount, 0) +} + +export function sumToolMonthUsages(usages: UsageRecord[], toolId: string, month = getMonthKey()): number { + return usages + .filter((u) => u.toolId === toolId && u.date.startsWith(month)) + .reduce((sum, u) => sum + u.amount, 0) +} + +export function dailyTotals(usages: UsageRecord[], month = getMonthKey()): { day: string; total: number }[] { + const map = new Map() + usages + .filter((u) => u.date.startsWith(month)) + .forEach((u) => map.set(u.date, (map.get(u.date) ?? 0) + u.amount)) + return Array.from(map.entries()) + .map(([day, total]) => ({ day, total })) + .sort((a, b) => a.day.localeCompare(b.day)) +} + +export function projectedMonthEnd(currentTotal: number, today: Date = new Date()): number { + const day = today.getDate() + const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate() + if (day <= 0 || daysInMonth <= 0) return currentTotal + return (currentTotal / day) * daysInMonth +} + +export async function clearAll(): Promise { + await chrome.storage.local.remove(STORAGE_KEY) +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a29fd02 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,56 @@ +// Core types for Cost.dev + +export interface ToolEntry { + id: string + name: string + monthlyCost: number // in dollars, cents implied by decimals + currency: string + planName?: string + billingModel: 'fixed' | 'usage' | 'hybrid' + lastUpdated: number // timestamp +} + +export interface UsageRecord { + id: string + toolId: string + date: string // YYYY-MM-DD + amount: number // dollars + note?: string + source: 'manual' | 'auto' | 'import' + createdAt: number +} + +export interface Budget { + monthlyLimit: number // dollars + currency: string + alertThreshold: number // 0โ€“1 fraction of monthlyLimit + enabled: boolean +} + +export interface AppState { + tools: ToolEntry[] + usages: UsageRecord[] + budget: Budget + lastAlertAt?: number + version: 1 +} + +export const DEFAULT_BUDGET: Budget = { + monthlyLimit: 50, + currency: 'USD', + alertThreshold: 0.8, + enabled: true, +} + +export function newId(): string { + return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}` +} + +export function getMonthKey(date = new Date()): string { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}` +} + +export function getTodayKey(): string { + const d = new Date() + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..66a2080 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "types": ["chrome"] + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e4579b3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import { crx } from '@crxjs/vite-plugin' +import manifest from './src/manifest.json' with { type: 'json' } + +export default defineConfig({ + build: { + outDir: 'dist', + emptyOutDir: true, + }, + plugins: [crx({ manifest })], +})