From 73f7b13e9d284494aa9f9a6b33c37bd204a5ac05 Mon Sep 17 00:00:00 2001 From: Bun Bun Date: Sun, 21 Jun 2026 00:22:17 +0000 Subject: [PATCH] =?UTF-8?q?ContextKeeper=20v1.0.0=20=E2=80=94=20Persistent?= =?UTF-8?q?=20AI=20coding=20session=20context=20Chrome=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + icons/icon128.png | Bin 0 -> 306 bytes icons/icon16.png | Bin 0 -> 79 bytes icons/icon48.png | Bin 0 -> 123 bytes manifest.json | 50 + package-lock.json | 2429 ++++++++++++++++++++++++++++++++++++++++ package.json | 19 + scripts/gen-icons.cjs | 52 + src/background.js | 118 ++ src/background.ts | 151 +++ src/content.js | 222 ++++ src/content.ts | 249 ++++ src/lib/detector.js | 32 + src/lib/detector.ts | 34 + src/lib/extractor.js | 78 ++ src/lib/extractor.ts | 91 ++ src/popup/popup.css | 324 ++++++ src/popup/popup.html | 47 + src/popup/popup.js | 220 ++++ src/popup/popup.ts | 253 +++++ test/detector.test.ts | 49 + test/extractor.test.ts | 86 ++ tsconfig.json | 15 + vite.config.ts | 11 + vitest.config.ts | 8 + 25 files changed, 4544 insertions(+) create mode 100644 .gitignore create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/gen-icons.cjs create mode 100644 src/background.js create mode 100644 src/background.ts create mode 100644 src/content.js create mode 100644 src/content.ts create mode 100644 src/lib/detector.js create mode 100644 src/lib/detector.ts create mode 100644 src/lib/extractor.js create mode 100644 src/lib/extractor.ts create mode 100644 src/popup/popup.css create mode 100644 src/popup/popup.html create mode 100644 src/popup/popup.js create mode 100644 src/popup/popup.ts create mode 100644 test/detector.test.ts create mode 100644 test/extractor.test.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65b1db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +.verdict +.venv/ +.DS_Store diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..bf5b07aa3b1696923cc35cf06eb220e2d356701d GIT binary patch literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRdwrKRsO>Ln`LHz39lxpultBfad|D zO$F^ewpE_2COa+PXKjqM?_g(eux4Pm#mI1hkD;NAfnf_X!va|bhPMn18ADgf?tXR4 Vd4pLFjle))@O1TaS?83{1OOsOOr8J$ literal 0 HcmV?d00001 diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..d4553010080af81b36b2d81872b7b28aeb6263d4 GIT binary patch literal 79 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|VxBIJAr*6y1#aB*5K5A`YOpHt bM-~Ia{~2Ba(yHR&Kotz0u6{1-oD!MFVdQ&MBb@0PQm+!vFvP literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..b4ba985 --- /dev/null +++ b/manifest.json @@ -0,0 +1,50 @@ +{ + "manifest_version": 3, + "name": "ContextKeeper — Stateful AI Context", + "version": "1.0.0", + "description": "Capture, persist, and restore AI coding session context across sessions. Never repeat yourself to an AI again.", + "permissions": ["storage", "activeTab", "scripting"], + "host_permissions": [ + "https://chat.openai.com/*", + "https://chatgpt.com/*", + "https://claude.ai/*", + "https://cursor.sh/*", + "https://aistudio.google.com/*", + "https://gemini.google.com/*", + "https://copilot.microsoft.com/*", + "https://github.com/copilot/*" + ], + "action": { + "default_popup": "src/popup/popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "background": { + "service_worker": "src/background.ts", + "type": "module" + }, + "content_scripts": [ + { + "matches": [ + "https://chat.openai.com/*", + "https://chatgpt.com/*", + "https://claude.ai/*", + "https://cursor.sh/*", + "https://aistudio.google.com/*", + "https://gemini.google.com/*", + "https://copilot.microsoft.com/*", + "https://github.com/copilot/*" + ], + "js": ["src/content.ts"], + "run_at": "document_idle" + } + ], + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e976722 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2429 @@ +{ + "name": "stateful-ai-agent-gap", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stateful-ai-agent-gap", + "version": "1.0.0", + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "@types/chrome": "^0.0.270", + "jsdom": "^29.1.1", + "typescript": "^5.5.0", + "vite": "^5.3.0", + "vitest": "^2.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.7.0.tgz", + "integrity": "sha512-NN98fUgJOEiVBshELJnYCW54+K2Yy067++/K+RdxclHsyG08ea+Em3lJJTubtPTCl2bECyp4+K69oT+izilnNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.3.5", + "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.1.0", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "rollup": "2.80.0", + "rxjs": "7.5.7", + "tinyglobby": "^0.2.17" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.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/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chrome": { + "version": "0.0.270", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.270.tgz", + "integrity": "sha512-ADvkowV7YnJfycZZxL2brluZ6STGW+9oKG37B422UePf2PCXuFA/XdERI0T18wtuWPx0tmFeZqq6MOXVk1IC+Q==", + "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/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/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/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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/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/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "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.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", + "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/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?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/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "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": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.3" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "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.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..653913f --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "stateful-ai-agent-gap", + "version": "1.0.0", + "description": "Persistent context layer for AI coding tools — never lose your session context again", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "test": "vitest run" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.28", + "@types/chrome": "^0.0.270", + "jsdom": "^29.1.1", + "typescript": "^5.5.0", + "vite": "^5.3.0", + "vitest": "^2.0.0" + } +} diff --git a/scripts/gen-icons.cjs b/scripts/gen-icons.cjs new file mode 100644 index 0000000..f5beda7 --- /dev/null +++ b/scripts/gen-icons.cjs @@ -0,0 +1,52 @@ +const fs = require('fs'); +const zlib = require('zlib'); + +function createPNG(width, height, r, g, b) { + const lineSize = width * 3 + 1; + const rawSize = height * lineSize; + const raw = Buffer.alloc(rawSize); + + for (let y = 0; y < height; y++) { + raw[y * lineSize] = 0; + for (let x = 0; x < width; x++) { + const offset = y * lineSize + 1 + x * 3; + raw[offset] = r; + raw[offset + 1] = g; + raw[offset + 2] = b; + } + } + + const compressed = zlib.deflateSync(raw); + + function chunk(type, data) { + const typeBuf = Buffer.from(type, 'ascii'); + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + const crc = Buffer.alloc(4); + const crcData = Buffer.concat([typeBuf, data]); + crc.writeUInt32BE(zlib.crc32(crcData), 0); + return Buffer.concat([len, typeBuf, data, crc]); + } + + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; + ihdr[9] = 2; + ihdr[10] = 0; + ihdr[11] = 0; + ihdr[12] = 0; + + const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const ihdrChunk = chunk('IHDR', ihdr); + const idatChunk = chunk('IDAT', compressed); + const iendChunk = chunk('IEND', Buffer.alloc(0)); + + return Buffer.concat([sig, ihdrChunk, idatChunk, iendChunk]); +} + +fs.mkdirSync('icons', { recursive: true }); +fs.writeFileSync('icons/icon16.png', createPNG(16, 16, 16, 185, 129)); +fs.writeFileSync('icons/icon48.png', createPNG(48, 48, 16, 185, 129)); +fs.writeFileSync('icons/icon128.png', createPNG(128, 128, 16, 185, 129)); +console.log('Icons generated'); diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..441bde4 --- /dev/null +++ b/src/background.js @@ -0,0 +1,118 @@ +/** + * Background Service Worker + * Handles context capture/restore messaging and badge updates + */ +const PLATFORM_PATTERNS = { + chatgpt: /chatgpt\.com|chat\.openai\.com/i, + claude: /claude\.ai/i, + cursor: /cursor\.sh/i, + gemini: /gemini\.google\.com|aistudio\.google\.com/i, + copilot: /copilot\.microsoft\.com|github\.com\/copilot/i, +}; +function detectPlatform(url) { + for (const [name, pattern] of Object.entries(PLATFORM_PATTERNS)) { + if (pattern.test(url)) + return name; + } + return 'unknown'; +} +async function getSessions() { + const result = await chrome.storage.local.get('sessions'); + return result.sessions || []; +} +async function saveSessions(sessions) { + await chrome.storage.local.set({ sessions }); +} +async function updateBadge(tabId, hasContext) { + const text = hasContext ? '✓' : ''; + const color = hasContext ? '#10b981' : '#6b7280'; + await chrome.action.setBadgeText({ text, tabId }); + await chrome.action.setBadgeBackgroundColor({ color, tabId }); +} +// Message handlers +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + (async () => { + if (message.type === 'CAPTURE_CONTEXT') { + const { title, url, context } = message.payload; + const platform = detectPlatform(url); + const sessions = await getSessions(); + const newSession = { + id: crypto.randomUUID(), + title: title || 'Untitled Session', + url, + platform, + context, + timestamp: Date.now(), + charCount: context.length, + }; + // Deduplicate by URL+title — keep newest + const filtered = sessions.filter((s) => !(s.url === url && s.title === title)); + filtered.unshift(newSession); + // Keep last 50 sessions + const trimmed = filtered.slice(0, 50); + await saveSessions(trimmed); + if (sender.tab?.id) { + await updateBadge(sender.tab.id, true); + } + sendResponse({ success: true, id: newSession.id, total: trimmed.length }); + } + if (message.type === 'GET_CONTEXT') { + const sessions = await getSessions(); + const { url, title } = message.payload; + const match = sessions.find((s) => s.url === url && s.title === title); + sendResponse({ success: true, found: !!match, context: match?.context || null }); + } + if (message.type === 'LIST_SESSIONS') { + const sessions = await getSessions(); + sendResponse({ success: true, sessions }); + } + if (message.type === 'DELETE_SESSION') { + const sessions = await getSessions(); + const filtered = sessions.filter((s) => s.id !== message.payload.id); + await saveSessions(filtered); + sendResponse({ success: true, total: filtered.length }); + } + if (message.type === 'RESTORE_CONTEXT') { + // Content script will handle actual injection; background just confirms + sendResponse({ success: true }); + } + if (message.type === 'CLEAR_ALL') { + await chrome.storage.local.remove('sessions'); + sendResponse({ success: true }); + } + })(); + return true; // Keep channel open for async +}); +// Update badge on tab change +chrome.tabs.onActivated.addListener(async ({ tabId }) => { + try { + const tab = await chrome.tabs.get(tabId); + if (!tab.url) + return; + const platform = detectPlatform(tab.url); + if (platform === 'unknown') { + await chrome.action.setBadgeText({ text: '', tabId }); + return; + } + const sessions = await getSessions(); + const hasContext = sessions.some((s) => s.url === tab.url); + await updateBadge(tabId, hasContext); + } + catch { + // Tab may have closed + } +}); +chrome.tabs.onUpdated.addListener(async (tabId, _changeInfo, tab) => { + if (!tab.url || !tab.active) + return; + const platform = detectPlatform(tab.url); + if (platform === 'unknown') { + await chrome.action.setBadgeText({ text: '', tabId }); + return; + } + const sessions = await getSessions(); + const hasContext = sessions.some((s) => s.url === tab.url); + await updateBadge(tabId, hasContext); +}); +// Export for testing +export { detectPlatform, getSessions, saveSessions, updateBadge }; diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..b752aae --- /dev/null +++ b/src/background.ts @@ -0,0 +1,151 @@ +/** + * Background Service Worker + * Handles context capture/restore messaging and badge updates + */ + +interface SavedSession { + id: string; + title: string; + url: string; + platform: string; + context: string; + timestamp: number; + charCount: number; +} + +const PLATFORM_PATTERNS: Record = { + chatgpt: /chatgpt\.com|chat\.openai\.com/i, + claude: /claude\.ai/i, + cursor: /cursor\.sh/i, + gemini: /gemini\.google\.com|aistudio\.google\.com/i, + copilot: /copilot\.microsoft\.com|github\.com\/copilot/i, +}; + +function detectPlatform(url: string): string { + for (const [name, pattern] of Object.entries(PLATFORM_PATTERNS)) { + if (pattern.test(url)) return name; + } + return 'unknown'; +} + +async function getSessions(): Promise { + const result = await chrome.storage.local.get('sessions'); + return (result.sessions as SavedSession[]) || []; +} + +async function saveSessions(sessions: SavedSession[]): Promise { + await chrome.storage.local.set({ sessions }); +} + +async function updateBadge(tabId: number, hasContext: boolean): Promise { + const text = hasContext ? '✓' : ''; + const color = hasContext ? '#10b981' : '#6b7280'; + await chrome.action.setBadgeText({ text, tabId }); + await chrome.action.setBadgeBackgroundColor({ color, tabId }); +} + +// Message handlers +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + (async () => { + if (message.type === 'CAPTURE_CONTEXT') { + const { title, url, context } = message.payload; + const platform = detectPlatform(url); + const sessions = await getSessions(); + + const newSession: SavedSession = { + id: crypto.randomUUID(), + title: title || 'Untitled Session', + url, + platform, + context, + timestamp: Date.now(), + charCount: context.length, + }; + + // Deduplicate by URL+title — keep newest + const filtered = sessions.filter( + (s) => !(s.url === url && s.title === title) + ); + filtered.unshift(newSession); + + // Keep last 50 sessions + const trimmed = filtered.slice(0, 50); + await saveSessions(trimmed); + + if (sender.tab?.id) { + await updateBadge(sender.tab.id, true); + } + + sendResponse({ success: true, id: newSession.id, total: trimmed.length }); + } + + if (message.type === 'GET_CONTEXT') { + const sessions = await getSessions(); + const { url, title } = message.payload; + const match = sessions.find( + (s) => s.url === url && s.title === title + ); + sendResponse({ success: true, found: !!match, context: match?.context || null }); + } + + if (message.type === 'LIST_SESSIONS') { + const sessions = await getSessions(); + sendResponse({ success: true, sessions }); + } + + if (message.type === 'DELETE_SESSION') { + const sessions = await getSessions(); + const filtered = sessions.filter((s) => s.id !== message.payload.id); + await saveSessions(filtered); + sendResponse({ success: true, total: filtered.length }); + } + + if (message.type === 'RESTORE_CONTEXT') { + // Content script will handle actual injection; background just confirms + sendResponse({ success: true }); + } + + if (message.type === 'CLEAR_ALL') { + await chrome.storage.local.remove('sessions'); + sendResponse({ success: true }); + } + })(); + + return true; // Keep channel open for async +}); + +// Update badge on tab change +chrome.tabs.onActivated.addListener(async ({ tabId }) => { + try { + const tab = await chrome.tabs.get(tabId); + if (!tab.url) return; + const platform = detectPlatform(tab.url); + if (platform === 'unknown') { + await chrome.action.setBadgeText({ text: '', tabId }); + return; + } + + const sessions = await getSessions(); + const hasContext = sessions.some((s) => s.url === tab.url); + await updateBadge(tabId, hasContext); + } catch { + // Tab may have closed + } +}); + +chrome.tabs.onUpdated.addListener(async (tabId, _changeInfo, tab) => { + if (!tab.url || !tab.active) return; + const platform = detectPlatform(tab.url); + if (platform === 'unknown') { + await chrome.action.setBadgeText({ text: '', tabId }); + return; + } + + const sessions = await getSessions(); + const hasContext = sessions.some((s) => s.url === tab.url); + await updateBadge(tabId, hasContext); +}); + +// Export for testing +export { detectPlatform, getSessions, saveSessions, updateBadge }; +export type { SavedSession }; diff --git a/src/content.js b/src/content.js new file mode 100644 index 0000000..9d1fdc1 --- /dev/null +++ b/src/content.js @@ -0,0 +1,222 @@ +/** + * Content Script — Injected into AI tool pages + * Captures conversation context and can restore it on request + */ +import { PlatformDetector } from './lib/detector'; +import { ContextExtractor } from './lib/extractor'; +const detector = new PlatformDetector(); +const extractor = new ContextExtractor(); +let captureInterval = null; +function getPageUrl() { + return window.location.href.split('?')[0]; +} +function getPageTitle() { + return document.title; +} +async function captureContext() { + const platform = detector.detect(window.location.href); + if (!platform) + return; + const context = extractor.extract(platform); + if (!context || context.trim().length < 50) + return; // Skip empty/short context + try { + await chrome.runtime.sendMessage({ + type: 'CAPTURE_CONTEXT', + payload: { + title: getPageTitle(), + url: getPageUrl(), + context, + }, + }); + } + catch { + // Extension may be reloading + } +} +function checkForRestorableContext() { + const platform = detector.detect(window.location.href); + if (!platform) + return; + chrome.runtime.sendMessage({ + type: 'GET_CONTEXT', + payload: { + title: getPageTitle(), + url: getPageUrl(), + }, + }, (response) => { + if (response?.found && response.context) { + showRestoreNotification(response.context); + } + }); +} +function showRestoreNotification(context) { + // Remove existing notification + const existing = document.getElementById('contextkeeper-restore'); + if (existing) + existing.remove(); + const el = document.createElement('div'); + el.id = 'contextkeeper-restore'; + el.innerHTML = ` +
+
+ 🧠 ContextKeeper +
+
+ Saved context found for this conversation (${context.length.toLocaleString()} chars). Restore it? +
+
+ + +
+
+ + `; + document.body.appendChild(el); + el.querySelector('#ck-restore-yes')?.addEventListener('click', () => { + restoreContextToPage(context); + el.remove(); + }); + el.querySelector('#ck-restore-no')?.addEventListener('click', () => { + el.remove(); + }); + // Auto-dismiss after 30 seconds + setTimeout(() => el.remove(), 30000); +} +function restoreContextToPage(context) { + const platform = detector.detect(window.location.href); + if (!platform) + return; + // Try to paste into the input field + const inputSelectors = { + chatgpt: ['#prompt-textarea', 'textarea[placeholder*="Message"]', 'textarea[data-id="root"]', 'div[contenteditable="true"]'], + claude: ['div[contenteditable="true"]', 'textarea[placeholder*="Message"]', 'div[role="textbox"]'], + cursor: ['textarea', 'div[contenteditable="true"]'], + gemini: ['textarea[placeholder*="Ask"]', 'div[contenteditable="true"]'], + copilot: ['textarea', 'div[contenteditable="true"]'], + }; + const selectors = inputSelectors[platform] || ['textarea', 'div[contenteditable="true"]']; + for (const selector of selectors) { + const el = document.querySelector(selector); + if (!el) + continue; + if (el instanceof HTMLTextAreaElement) { + el.value = context; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + el.focus(); + } + else if (el.isContentEditable) { + el.textContent = context; + el.dispatchEvent(new InputEvent('input', { bubbles: true })); + el.focus(); + } + break; // Stop after first successful injection + } +} +// Handle messages from popup +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message.type === 'REQUEST_CAPTURE') { + const platform = detector.detect(window.location.href); + if (!platform) { + sendResponse({ success: false, reason: 'not_ai_platform' }); + return true; + } + const context = extractor.extract(platform); + if (!context || context.length < 50) { + sendResponse({ success: false, reason: 'no_content' }); + return true; + } + captureContext().then(() => { + sendResponse({ success: true, charCount: context.length }); + }).catch(() => { + sendResponse({ success: false, reason: 'capture_failed' }); + }); + return true; + } + if (message.type === 'INJECT_CONTEXT') { + const { context } = message.payload; + restoreContextToPage(context); + sendResponse({ success: true }); + return true; + } + return true; +}); +// Initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} +else { + init(); +} +function init() { + const platform = detector.detect(window.location.href); + if (!platform) + return; + // Check for restorable context on page load + setTimeout(checkForRestorableContext, 2000); + // Periodic capture every 30 seconds (only if content changed) + let lastContext = ''; + captureInterval = setInterval(() => { + const current = extractor.extract(platform); + if (current && current !== lastContext && current.length > 50) { + lastContext = current; + captureContext(); + } + }, 30000); + // Capture before page unload + window.addEventListener('beforeunload', () => { + captureContext(); + }); +} +// Cleanup on navigation (SPA navigation) +let currentUrl = window.location.href; +const observer = new MutationObserver(() => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href; + if (captureInterval) { + clearInterval(captureInterval); + captureInterval = null; + } + init(); + } +}); +observer.observe(document.body, { childList: true, subtree: true }); diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..f1aa9c2 --- /dev/null +++ b/src/content.ts @@ -0,0 +1,249 @@ +/** + * Content Script — Injected into AI tool pages + * Captures conversation context and can restore it on request + */ + +import { PlatformDetector } from './lib/detector'; +import { ContextExtractor } from './lib/extractor'; + +const detector = new PlatformDetector(); +const extractor = new ContextExtractor(); + +let captureInterval: ReturnType | null = null; + +function getPageUrl(): string { + return window.location.href.split('?')[0]; +} + +function getPageTitle(): string { + return document.title; +} + +async function captureContext(): Promise { + const platform = detector.detect(window.location.href); + if (!platform) return; + + const context = extractor.extract(platform); + if (!context || context.trim().length < 50) return; // Skip empty/short context + + try { + await chrome.runtime.sendMessage({ + type: 'CAPTURE_CONTEXT', + payload: { + title: getPageTitle(), + url: getPageUrl(), + context, + }, + }); + } catch { + // Extension may be reloading + } +} + +function checkForRestorableContext(): void { + const platform = detector.detect(window.location.href); + if (!platform) return; + + chrome.runtime.sendMessage( + { + type: 'GET_CONTEXT', + payload: { + title: getPageTitle(), + url: getPageUrl(), + }, + }, + (response) => { + if (response?.found && response.context) { + showRestoreNotification(response.context); + } + } + ); +} + +function showRestoreNotification(context: string): void { + // Remove existing notification + const existing = document.getElementById('contextkeeper-restore'); + if (existing) existing.remove(); + + const el = document.createElement('div'); + el.id = 'contextkeeper-restore'; + el.innerHTML = ` +
+
+ 🧠 ContextKeeper +
+
+ Saved context found for this conversation (${context.length.toLocaleString()} chars). Restore it? +
+
+ + +
+
+ + `; + + document.body.appendChild(el); + + el.querySelector('#ck-restore-yes')?.addEventListener('click', () => { + restoreContextToPage(context); + el.remove(); + }); + + el.querySelector('#ck-restore-no')?.addEventListener('click', () => { + el.remove(); + }); + + // Auto-dismiss after 30 seconds + setTimeout(() => el.remove(), 30000); +} + +function restoreContextToPage(context: string): void { + const platform = detector.detect(window.location.href); + if (!platform) return; + + // Try to paste into the input field + const inputSelectors: Record = { + chatgpt: ['#prompt-textarea', 'textarea[placeholder*="Message"]', 'textarea[data-id="root"]', 'div[contenteditable="true"]'], + claude: ['div[contenteditable="true"]', 'textarea[placeholder*="Message"]', 'div[role="textbox"]'], + cursor: ['textarea', 'div[contenteditable="true"]'], + gemini: ['textarea[placeholder*="Ask"]', 'div[contenteditable="true"]'], + copilot: ['textarea', 'div[contenteditable="true"]'], + }; + + const selectors = inputSelectors[platform] || ['textarea', 'div[contenteditable="true"]']; + + for (const selector of selectors) { + const el = document.querySelector(selector) as HTMLElement; + if (!el) continue; + + if (el instanceof HTMLTextAreaElement) { + el.value = context; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + el.focus(); + } else if (el.isContentEditable) { + el.textContent = context; + el.dispatchEvent(new InputEvent('input', { bubbles: true })); + el.focus(); + } + + break; // Stop after first successful injection + } +} + +// Handle messages from popup +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message.type === 'REQUEST_CAPTURE') { + const platform = detector.detect(window.location.href); + if (!platform) { + sendResponse({ success: false, reason: 'not_ai_platform' }); + return true; + } + const context = extractor.extract(platform); + if (!context || context.length < 50) { + sendResponse({ success: false, reason: 'no_content' }); + return true; + } + captureContext().then(() => { + sendResponse({ success: true, charCount: context.length }); + }).catch(() => { + sendResponse({ success: false, reason: 'capture_failed' }); + }); + return true; + } + + if (message.type === 'INJECT_CONTEXT') { + const { context } = message.payload; + restoreContextToPage(context); + sendResponse({ success: true }); + return true; + } + + return true; +}); + +// Initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} + +function init(): void { + const platform = detector.detect(window.location.href); + if (!platform) return; + + // Check for restorable context on page load + setTimeout(checkForRestorableContext, 2000); + + // Periodic capture every 30 seconds (only if content changed) + let lastContext = ''; + captureInterval = setInterval(() => { + const current = extractor.extract(platform); + if (current && current !== lastContext && current.length > 50) { + lastContext = current; + captureContext(); + } + }, 30000); + + // Capture before page unload + window.addEventListener('beforeunload', () => { + captureContext(); + }); +} + +// Cleanup on navigation (SPA navigation) +let currentUrl = window.location.href; +const observer = new MutationObserver(() => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href; + if (captureInterval) { + clearInterval(captureInterval); + captureInterval = null; + } + init(); + } +}); + +observer.observe(document.body, { childList: true, subtree: true }); + +export {}; diff --git a/src/lib/detector.js b/src/lib/detector.js new file mode 100644 index 0000000..1397c51 --- /dev/null +++ b/src/lib/detector.js @@ -0,0 +1,32 @@ +/** + * Platform Detector — Identifies which AI tool the user is on + */ +export class PlatformDetector { + patterns = { + chatgpt: /chatgpt\.com|chat\.openai\.com/i, + claude: /claude\.ai/i, + cursor: /cursor\.sh/i, + gemini: /gemini\.google\.com|aistudio\.google\.com/i, + copilot: /copilot\.microsoft\.com|github\.com\/copilot/i, + }; + detect(url) { + for (const [name, pattern] of Object.entries(this.patterns)) { + if (pattern.test(url)) + return name; + } + return null; + } + getPlatformName(platform) { + const names = { + chatgpt: 'ChatGPT', + claude: 'Claude', + cursor: 'Cursor', + gemini: 'Gemini', + copilot: 'GitHub Copilot', + }; + return names[platform] || platform; + } + getSupportedPlatforms() { + return Object.keys(this.patterns); + } +} diff --git a/src/lib/detector.ts b/src/lib/detector.ts new file mode 100644 index 0000000..b10f4eb --- /dev/null +++ b/src/lib/detector.ts @@ -0,0 +1,34 @@ +/** + * Platform Detector — Identifies which AI tool the user is on + */ +export class PlatformDetector { + private readonly patterns: Record = { + chatgpt: /chatgpt\.com|chat\.openai\.com/i, + claude: /claude\.ai/i, + cursor: /cursor\.sh/i, + gemini: /gemini\.google\.com|aistudio\.google\.com/i, + copilot: /copilot\.microsoft\.com|github\.com\/copilot/i, + }; + + detect(url: string): string | null { + for (const [name, pattern] of Object.entries(this.patterns)) { + if (pattern.test(url)) return name; + } + return null; + } + + getPlatformName(platform: string): string { + const names: Record = { + chatgpt: 'ChatGPT', + claude: 'Claude', + cursor: 'Cursor', + gemini: 'Gemini', + copilot: 'GitHub Copilot', + }; + return names[platform] || platform; + } + + getSupportedPlatforms(): string[] { + return Object.keys(this.patterns); + } +} diff --git a/src/lib/extractor.js b/src/lib/extractor.js new file mode 100644 index 0000000..c8085a5 --- /dev/null +++ b/src/lib/extractor.js @@ -0,0 +1,78 @@ +/** + * Context Extractor — Pulls conversation context from AI tool DOMs + */ +export class ContextExtractor { + extract(platform) { + switch (platform) { + case 'chatgpt': + return this.extractChatGPT(); + case 'claude': + return this.extractClaude(); + case 'cursor': + return this.extractCursor(); + case 'gemini': + return this.extractGemini(); + case 'copilot': + return this.extractCopilot(); + default: + return ''; + } + } + extractChatGPT() { + // ChatGPT: conversation turns are in [data-testid^="conversation-turn-"] + const turns = document.querySelectorAll('[data-testid^="conversation-turn-"]'); + if (turns.length === 0) { + // Fallback: look for article elements or message divs + const articles = document.querySelectorAll('article'); + return this.extractFromElements(articles, 'user', 'assistant'); + } + return this.extractFromElements(turns, 'user', 'assistant'); + } + extractClaude() { + // Claude: messages in .message or [data-testid="message"] + const messages = document.querySelectorAll('.message, [data-testid="message"]'); + if (messages.length === 0) { + // Try content blocks + const contents = document.querySelectorAll('.prose, [data-testid="content"]'); + return this.extractFromElements(contents, 'human', 'assistant'); + } + return this.extractFromElements(messages, 'human', 'assistant'); + } + extractCursor() { + // Cursor: chat messages in the sidebar + const messages = document.querySelectorAll('.chat-message, .message-bubble'); + return this.extractFromElements(messages, 'user', 'assistant'); + } + extractGemini() { + // Gemini: message containers + const messages = document.querySelectorAll('.message-content, [data-testid="message-content"]'); + return this.extractFromElements(messages, 'user', 'model'); + } + extractCopilot() { + // Copilot: chat turns + const turns = document.querySelectorAll('.turn, .chat-turn'); + return this.extractFromElements(turns, 'user', 'assistant'); + } + extractFromElements(elements, userLabel, assistantLabel) { + if (elements.length === 0) + return ''; + const parts = []; + let turnIndex = 0; + for (const el of Array.from(elements)) { + const text = this.getTextContent(el).trim(); + if (!text || text.length < 3) + continue; + const role = turnIndex % 2 === 0 ? userLabel : assistantLabel; + parts.push(`[${role}]: ${text}`); + turnIndex++; + } + return parts.join('\n\n'); + } + getTextContent(el) { + // Clone to avoid modifying live DOM + const clone = el.cloneNode(true); + // Remove code copy buttons, timestamps, etc. + clone.querySelectorAll('button, .timestamp, .copy-button, [role="button"]').forEach((b) => b.remove()); + return clone.textContent || ''; + } +} diff --git a/src/lib/extractor.ts b/src/lib/extractor.ts new file mode 100644 index 0000000..1b12f6a --- /dev/null +++ b/src/lib/extractor.ts @@ -0,0 +1,91 @@ +/** + * Context Extractor — Pulls conversation context from AI tool DOMs + */ +export class ContextExtractor { + extract(platform: string): string { + switch (platform) { + case 'chatgpt': + return this.extractChatGPT(); + case 'claude': + return this.extractClaude(); + case 'cursor': + return this.extractCursor(); + case 'gemini': + return this.extractGemini(); + case 'copilot': + return this.extractCopilot(); + default: + return ''; + } + } + + private extractChatGPT(): string { + // ChatGPT: conversation turns are in [data-testid^="conversation-turn-"] + const turns = document.querySelectorAll('[data-testid^="conversation-turn-"]'); + if (turns.length === 0) { + // Fallback: look for article elements or message divs + const articles = document.querySelectorAll('article'); + return this.extractFromElements(articles, 'user', 'assistant'); + } + return this.extractFromElements(turns, 'user', 'assistant'); + } + + private extractClaude(): string { + // Claude: messages in .message or [data-testid="message"] + const messages = document.querySelectorAll('.message, [data-testid="message"]'); + if (messages.length === 0) { + // Try content blocks + const contents = document.querySelectorAll('.prose, [data-testid="content"]'); + return this.extractFromElements(contents, 'human', 'assistant'); + } + return this.extractFromElements(messages, 'human', 'assistant'); + } + + private extractCursor(): string { + // Cursor: chat messages in the sidebar + const messages = document.querySelectorAll('.chat-message, .message-bubble'); + return this.extractFromElements(messages, 'user', 'assistant'); + } + + private extractGemini(): string { + // Gemini: message containers + const messages = document.querySelectorAll('.message-content, [data-testid="message-content"]'); + return this.extractFromElements(messages, 'user', 'model'); + } + + private extractCopilot(): string { + // Copilot: chat turns + const turns = document.querySelectorAll('.turn, .chat-turn'); + return this.extractFromElements(turns, 'user', 'assistant'); + } + + private extractFromElements( + elements: NodeListOf, + userLabel: string, + assistantLabel: string + ): string { + if (elements.length === 0) return ''; + + const parts: string[] = []; + let turnIndex = 0; + + for (const el of Array.from(elements)) { + const text = this.getTextContent(el).trim(); + if (!text || text.length < 3) continue; + + const role = turnIndex % 2 === 0 ? userLabel : assistantLabel; + parts.push(`[${role}]: ${text}`); + turnIndex++; + } + + return parts.join('\n\n'); + } + + private getTextContent(el: Element): string { + // Clone to avoid modifying live DOM + const clone = el.cloneNode(true) as Element; + // Remove code copy buttons, timestamps, etc. + clone.querySelectorAll('button, .timestamp, .copy-button, [role="button"]').forEach((b) => b.remove()); + return clone.textContent || ''; + } +} diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..e9e6a1a --- /dev/null +++ b/src/popup/popup.css @@ -0,0 +1,324 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + width: 380px; + min-height: 400px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 16px; +} + +.header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.logo { + font-size: 28px; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + background: #1e293b; + border-radius: 12px; + border: 1px solid #334155; +} + +.title { + font-size: 16px; + font-weight: 700; + color: #f8fafc; +} + +.subtitle { + font-size: 12px; + color: #94a3b8; + margin-top: 2px; +} + +.current-page { + background: #1e293b; + border: 1px solid #334155; + border-radius: 10px; + padding: 12px; + margin-bottom: 12px; +} + +.platform-badge { + display: inline-block; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 3px 8px; + border-radius: 6px; + background: #334155; + color: #cbd5e1; + margin-bottom: 6px; +} + +.platform-badge.chatgpt { background: #10a37f20; color: #10a37f; border: 1px solid #10a37f40; } +.platform-badge.claude { background: #d9775720; color: #d97757; border: 1px solid #d9775740; } +.platform-badge.cursor { background: #6366f120; color: #6366f1; border: 1px solid #6366f140; } +.platform-badge.gemini { background: #8b5cf620; color: #8b5cf6; border: 1px solid #8b5cf640; } +.platform-badge.copilot { background: #0ea5e920; color: #0ea5e9; border: 1px solid #0ea5e940; } + +.page-title { + font-size: 13px; + font-weight: 500; + color: #f1f5f9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.actions { + display: flex; + gap: 8px; + margin-bottom: 14px; +} + +.btn { + flex: 1; + padding: 10px 14px; + border-radius: 8px; + border: none; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.btn-primary { + background: #10b981; + color: white; +} +.btn-primary:hover:not(:disabled) { background: #059669; } + +.btn-secondary { + background: #334155; + color: #e2e8f0; + border: 1px solid #475569; +} +.btn-secondary:hover:not(:disabled) { background: #475569; } + +.divider { + height: 1px; + background: #334155; + margin: 14px 0; +} + +.sessions-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.sessions-title { + font-size: 13px; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.btn-clear { + background: transparent; + border: none; + font-size: 14px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s; + padding: 4px; +} +.btn-clear:hover { opacity: 1; } + +.sessions-list { + max-height: 220px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.session-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 8px; + padding: 10px 12px; + cursor: pointer; + transition: all 0.15s; + position: relative; +} +.session-card:hover { + border-color: #475569; + background: #263348; +} + +.session-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.session-title { + font-size: 12px; + font-weight: 600; + color: #f1f5f9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.session-platform { + font-size: 10px; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; +} + +.session-meta { + font-size: 11px; + color: #64748b; + display: flex; + gap: 10px; +} + +.session-delete { + position: absolute; + top: 8px; + right: 8px; + background: transparent; + border: none; + color: #64748b; + font-size: 12px; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s; + padding: 2px 6px; + border-radius: 4px; +} +.session-card:hover .session-delete { opacity: 1; } +.session-delete:hover { color: #ef4444; background: #ef444420; } + +.empty-state { + text-align: center; + padding: 24px; + color: #64748b; + font-size: 13px; +} + +.stats { + margin-top: 10px; + text-align: center; + font-size: 11px; + color: #475569; +} + +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #64748b; } + +.toast { + position: fixed; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + background: #10b981; + color: white; + padding: 8px 16px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + animation: toast-in 0.2s ease-out; + z-index: 10000; +} + +@keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(10px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +} + +.confirm-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: fade-in 0.15s ease-out; +} + +.confirm-box { + background: #1e293b; + border: 1px solid #334155; + border-radius: 12px; + padding: 20px; + width: 280px; + text-align: center; +} + +.confirm-title { + font-size: 14px; + font-weight: 600; + color: #f8fafc; + margin-bottom: 8px; +} + +.confirm-text { + font-size: 12px; + color: #94a3b8; + margin-bottom: 16px; + line-height: 1.5; +} + +.confirm-buttons { + display: flex; + gap: 8px; +} + +.confirm-btn { + flex: 1; + padding: 8px; + border-radius: 6px; + border: none; + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.confirm-btn-danger { + background: #ef4444; + color: white; +} +.confirm-btn-danger:hover { background: #dc2626; } + +.confirm-btn-cancel { + background: #334155; + color: #e2e8f0; +} +.confirm-btn-cancel:hover { background: #475569; } + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/src/popup/popup.html b/src/popup/popup.html new file mode 100644 index 0000000..8b24467 --- /dev/null +++ b/src/popup/popup.html @@ -0,0 +1,47 @@ + + + + + + ContextKeeper + + + +
+ +
+
ContextKeeper
+
Never lose your AI context
+
+
+ +
+
+
No AI tool detected
+
+ +
+ + +
+ +
+ +
+
Saved Sessions
+ +
+ +
+
No saved sessions yet
+
+ +
+ + + + diff --git a/src/popup/popup.js b/src/popup/popup.js new file mode 100644 index 0000000..3a95130 --- /dev/null +++ b/src/popup/popup.js @@ -0,0 +1,220 @@ +/** + * Popup Controller — Manages the extension popup UI + */ +import { PlatformDetector } from '../lib/detector'; +const detector = new PlatformDetector(); +async function getCurrentTab() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + return tab || null; +} +function formatTimeAgo(timestamp) { + const diff = Date.now() - timestamp; + const mins = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + if (mins < 1) + return 'just now'; + if (mins < 60) + return `${mins}m ago`; + if (hours < 24) + return `${hours}h ago`; + return `${days}d ago`; +} +function formatChars(n) { + if (n >= 1000) + return `${(n / 1000).toFixed(1)}k chars`; + return `${n} chars`; +} +function getPlatformColorClass(platform) { + return platform.toLowerCase(); +} +async function renderSessions() { + const listEl = document.getElementById('sessions-list'); + const statsEl = document.getElementById('stats'); + const response = await chrome.runtime.sendMessage({ type: 'LIST_SESSIONS' }); + const sessions = response?.sessions || []; + if (sessions.length === 0) { + listEl.innerHTML = '
No saved sessions yet
'; + statsEl.textContent = ''; + return; + } + listEl.innerHTML = ''; + for (const session of sessions) { + const card = document.createElement('div'); + card.className = 'session-card'; + card.innerHTML = ` +
+
${escapeHtml(session.title)}
+
${escapeHtml(session.platform)}
+
+
+ ${formatTimeAgo(session.timestamp)} + ${formatChars(session.charCount)} +
+ + `; + card.addEventListener('click', (e) => { + if (e.target.classList.contains('session-delete')) + return; + restoreSession(session); + }); + card.querySelector('.session-delete')?.addEventListener('click', (e) => { + e.stopPropagation(); + deleteSession(session.id); + }); + listEl.appendChild(card); + } + statsEl.textContent = `${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${formatChars(sessions.reduce((sum, s) => sum + s.charCount, 0))} total`; +} +async function restoreSession(session) { + // Open the URL in the current or new tab + const tab = await getCurrentTab(); + if (tab?.id) { + await chrome.tabs.update(tab.id, { url: session.url }); + // After navigation, inject context via content script + setTimeout(async () => { + try { + await chrome.tabs.sendMessage(tab.id, { + type: 'INJECT_CONTEXT', + payload: { context: session.context }, + }); + } + catch { + // Page may not be loaded yet + } + }, 3000); + } + window.close(); +} +async function deleteSession(id) { + await chrome.runtime.sendMessage({ type: 'DELETE_SESSION', payload: { id } }); + await renderSessions(); + showToast('Session deleted'); +} +function showToast(message) { + const existing = document.querySelector('.toast'); + if (existing) + existing.remove(); + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); +} +function showConfirm(title, text, onConfirm) { + const overlay = document.createElement('div'); + overlay.className = 'confirm-overlay'; + overlay.innerHTML = ` +
+
${escapeHtml(title)}
+
${escapeHtml(text)}
+
+ + +
+
+ `; + document.body.appendChild(overlay); + overlay.querySelector('#confirm-yes')?.addEventListener('click', () => { + onConfirm(); + overlay.remove(); + }); + overlay.querySelector('#confirm-no')?.addEventListener('click', () => { + overlay.remove(); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) + overlay.remove(); + }); +} +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} +async function updateCurrentPage() { + const badgeEl = document.getElementById('platform-badge'); + const titleEl = document.getElementById('page-title'); + const captureBtn = document.getElementById('btn-capture'); + const restoreBtn = document.getElementById('btn-restore'); + const tab = await getCurrentTab(); + if (!tab?.url) { + badgeEl.textContent = '—'; + titleEl.textContent = 'No active page'; + captureBtn.disabled = true; + restoreBtn.disabled = true; + return; + } + const platform = detector.detect(tab.url); + if (!platform) { + badgeEl.textContent = '—'; + badgeEl.className = 'platform-badge'; + titleEl.textContent = 'No AI tool detected on this page'; + captureBtn.disabled = true; + restoreBtn.disabled = true; + return; + } + badgeEl.textContent = detector.getPlatformName(platform); + badgeEl.className = `platform-badge ${getPlatformColorClass(platform)}`; + titleEl.textContent = tab.title || 'Untitled'; + captureBtn.disabled = false; + restoreBtn.disabled = false; +} +async function captureCurrent() { + const tab = await getCurrentTab(); + if (!tab?.id) + return; + try { + const response = await chrome.tabs.sendMessage(tab.id, { type: 'REQUEST_CAPTURE' }); + if (response?.success) { + showToast('Context captured!'); + await renderSessions(); + } + else { + showToast('Nothing to capture'); + } + } + catch { + showToast('Refresh the page first'); + } +} +async function restoreCurrent() { + const tab = await getCurrentTab(); + if (!tab?.id || !tab.url) + return; + const response = await chrome.runtime.sendMessage({ + type: 'GET_CONTEXT', + payload: { title: tab.title || '', url: tab.url.split('?')[0] }, + }); + if (response?.found && response.context) { + try { + await chrome.tabs.sendMessage(tab.id, { + type: 'INJECT_CONTEXT', + payload: { context: response.context }, + }); + showToast('Context restored!'); + } + catch { + showToast('Refresh the page first'); + } + } + else { + showToast('No saved context for this page'); + } +} +async function clearAll() { + showConfirm('Clear All Sessions', 'This will permanently delete all saved context. Are you sure?', async () => { + await chrome.runtime.sendMessage({ type: 'CLEAR_ALL' }); + await renderSessions(); + showToast('All sessions cleared'); + }); +} +// Initialize +document.addEventListener('DOMContentLoaded', async () => { + await updateCurrentPage(); + await renderSessions(); + document.getElementById('btn-capture')?.addEventListener('click', captureCurrent); + document.getElementById('btn-restore')?.addEventListener('click', restoreCurrent); + document.getElementById('btn-clear')?.addEventListener('click', clearAll); +}); +export { renderSessions, updateCurrentPage, formatTimeAgo, formatChars }; diff --git a/src/popup/popup.ts b/src/popup/popup.ts new file mode 100644 index 0000000..9086ffb --- /dev/null +++ b/src/popup/popup.ts @@ -0,0 +1,253 @@ +/** + * Popup Controller — Manages the extension popup UI + */ + +import { PlatformDetector } from '../lib/detector'; + +interface SavedSession { + id: string; + title: string; + url: string; + platform: string; + context: string; + timestamp: number; + charCount: number; +} + +const detector = new PlatformDetector(); + +async function getCurrentTab(): Promise { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + return tab || null; +} + +function formatTimeAgo(timestamp: number): string { + const diff = Date.now() - timestamp; + const mins = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + if (hours < 24) return `${hours}h ago`; + return `${days}d ago`; +} + +function formatChars(n: number): string { + if (n >= 1000) return `${(n / 1000).toFixed(1)}k chars`; + return `${n} chars`; +} + +function getPlatformColorClass(platform: string): string { + return platform.toLowerCase(); +} + +async function renderSessions(): Promise { + const listEl = document.getElementById('sessions-list')!; + const statsEl = document.getElementById('stats')!; + + const response = await chrome.runtime.sendMessage({ type: 'LIST_SESSIONS' }); + const sessions: SavedSession[] = response?.sessions || []; + + if (sessions.length === 0) { + listEl.innerHTML = '
No saved sessions yet
'; + statsEl.textContent = ''; + return; + } + + listEl.innerHTML = ''; + for (const session of sessions) { + const card = document.createElement('div'); + card.className = 'session-card'; + card.innerHTML = ` +
+
${escapeHtml(session.title)}
+
${escapeHtml(session.platform)}
+
+
+ ${formatTimeAgo(session.timestamp)} + ${formatChars(session.charCount)} +
+ + `; + + card.addEventListener('click', (e) => { + if ((e.target as HTMLElement).classList.contains('session-delete')) return; + restoreSession(session); + }); + + card.querySelector('.session-delete')?.addEventListener('click', (e) => { + e.stopPropagation(); + deleteSession(session.id); + }); + + listEl.appendChild(card); + } + + statsEl.textContent = `${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${formatChars( + sessions.reduce((sum, s) => sum + s.charCount, 0) + )} total`; +} + +async function restoreSession(session: SavedSession): Promise { + // Open the URL in the current or new tab + const tab = await getCurrentTab(); + if (tab?.id) { + await chrome.tabs.update(tab.id, { url: session.url }); + // After navigation, inject context via content script + setTimeout(async () => { + try { + await chrome.tabs.sendMessage(tab.id!, { + type: 'INJECT_CONTEXT', + payload: { context: session.context }, + }); + } catch { + // Page may not be loaded yet + } + }, 3000); + } + window.close(); +} + +async function deleteSession(id: string): Promise { + await chrome.runtime.sendMessage({ type: 'DELETE_SESSION', payload: { id } }); + await renderSessions(); + showToast('Session deleted'); +} + +function showToast(message: string): void { + const existing = document.querySelector('.toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); +} + +function showConfirm(title: string, text: string, onConfirm: () => void): void { + const overlay = document.createElement('div'); + overlay.className = 'confirm-overlay'; + overlay.innerHTML = ` +
+
${escapeHtml(title)}
+
${escapeHtml(text)}
+
+ + +
+
+ `; + document.body.appendChild(overlay); + + overlay.querySelector('#confirm-yes')?.addEventListener('click', () => { + onConfirm(); + overlay.remove(); + }); + overlay.querySelector('#confirm-no')?.addEventListener('click', () => { + overlay.remove(); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +async function updateCurrentPage(): Promise { + const badgeEl = document.getElementById('platform-badge')!; + const titleEl = document.getElementById('page-title')!; + const captureBtn = document.getElementById('btn-capture') as HTMLButtonElement; + const restoreBtn = document.getElementById('btn-restore') as HTMLButtonElement; + + const tab = await getCurrentTab(); + if (!tab?.url) { + badgeEl.textContent = '—'; + titleEl.textContent = 'No active page'; + captureBtn.disabled = true; + restoreBtn.disabled = true; + return; + } + + const platform = detector.detect(tab.url); + if (!platform) { + badgeEl.textContent = '—'; + badgeEl.className = 'platform-badge'; + titleEl.textContent = 'No AI tool detected on this page'; + captureBtn.disabled = true; + restoreBtn.disabled = true; + return; + } + + badgeEl.textContent = detector.getPlatformName(platform); + badgeEl.className = `platform-badge ${getPlatformColorClass(platform)}`; + titleEl.textContent = tab.title || 'Untitled'; + captureBtn.disabled = false; + restoreBtn.disabled = false; +} + +async function captureCurrent(): Promise { + const tab = await getCurrentTab(); + if (!tab?.id) return; + + try { + const response = await chrome.tabs.sendMessage(tab.id, { type: 'REQUEST_CAPTURE' }); + if (response?.success) { + showToast('Context captured!'); + await renderSessions(); + } else { + showToast('Nothing to capture'); + } + } catch { + showToast('Refresh the page first'); + } +} + +async function restoreCurrent(): Promise { + const tab = await getCurrentTab(); + if (!tab?.id || !tab.url) return; + + const response = await chrome.runtime.sendMessage({ + type: 'GET_CONTEXT', + payload: { title: tab.title || '', url: tab.url.split('?')[0] }, + }); + + if (response?.found && response.context) { + try { + await chrome.tabs.sendMessage(tab.id, { + type: 'INJECT_CONTEXT', + payload: { context: response.context }, + }); + showToast('Context restored!'); + } catch { + showToast('Refresh the page first'); + } + } else { + showToast('No saved context for this page'); + } +} + +async function clearAll(): Promise { + showConfirm('Clear All Sessions', 'This will permanently delete all saved context. Are you sure?', async () => { + await chrome.runtime.sendMessage({ type: 'CLEAR_ALL' }); + await renderSessions(); + showToast('All sessions cleared'); + }); +} + +// Initialize +document.addEventListener('DOMContentLoaded', async () => { + await updateCurrentPage(); + await renderSessions(); + + document.getElementById('btn-capture')?.addEventListener('click', captureCurrent); + document.getElementById('btn-restore')?.addEventListener('click', restoreCurrent); + document.getElementById('btn-clear')?.addEventListener('click', clearAll); +}); + +export { renderSessions, updateCurrentPage, formatTimeAgo, formatChars }; diff --git a/test/detector.test.ts b/test/detector.test.ts new file mode 100644 index 0000000..2922a52 --- /dev/null +++ b/test/detector.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { PlatformDetector } from '../src/lib/detector'; + +describe('PlatformDetector', () => { + const detector = new PlatformDetector(); + + it('detects ChatGPT URLs', () => { + expect(detector.detect('https://chatgpt.com/c/123')).toBe('chatgpt'); + expect(detector.detect('https://chat.openai.com/chat')).toBe('chatgpt'); + }); + + it('detects Claude URLs', () => { + expect(detector.detect('https://claude.ai/chat/abc')).toBe('claude'); + }); + + it('detects Cursor URLs', () => { + expect(detector.detect('https://cursor.sh/chat')).toBe('cursor'); + }); + + it('detects Gemini URLs', () => { + expect(detector.detect('https://gemini.google.com/app')).toBe('gemini'); + expect(detector.detect('https://aistudio.google.com/app')).toBe('gemini'); + }); + + it('detects Copilot URLs', () => { + expect(detector.detect('https://copilot.microsoft.com/')).toBe('copilot'); + expect(detector.detect('https://github.com/copilot')).toBe('copilot'); + }); + + it('returns null for unrelated URLs', () => { + expect(detector.detect('https://google.com')).toBeNull(); + expect(detector.detect('https://example.com')).toBeNull(); + }); + + it('maps platform keys to display names', () => { + expect(detector.getPlatformName('chatgpt')).toBe('ChatGPT'); + expect(detector.getPlatformName('claude')).toBe('Claude'); + expect(detector.getPlatformName('unknown')).toBe('unknown'); + }); + + it('lists all supported platforms', () => { + const platforms = detector.getSupportedPlatforms(); + expect(platforms).toContain('chatgpt'); + expect(platforms).toContain('claude'); + expect(platforms).toContain('cursor'); + expect(platforms).toContain('gemini'); + expect(platforms).toContain('copilot'); + }); +}); diff --git a/test/extractor.test.ts b/test/extractor.test.ts new file mode 100644 index 0000000..e8b3a34 --- /dev/null +++ b/test/extractor.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { ContextExtractor } from '../src/lib/extractor'; + +describe('ContextExtractor', () => { + let extractor: ContextExtractor; + + beforeEach(() => { + extractor = new ContextExtractor(); + }); + + it('extracts nothing for unknown platforms', () => { + expect(extractor.extract('unknown')).toBe(''); + }); + + it('extracts ChatGPT conversation turns', () => { + document.body.innerHTML = ` +
+
Hello AI
+
+
+
Hi there!
+
+ `; + const result = extractor.extract('chatgpt'); + expect(result).toContain('[user]: Hello AI'); + expect(result).toContain('[assistant]: Hi there!'); + }); + + it('falls back to article elements for ChatGPT', () => { + document.body.innerHTML = ` +
First message
+
Second message
+ `; + const result = extractor.extract('chatgpt'); + expect(result).toContain('[user]: First message'); + expect(result).toContain('[assistant]: Second message'); + }); + + it('extracts Claude messages', () => { + document.body.innerHTML = ` +
How do I refactor this?
+
Here is a suggestion...
+ `; + const result = extractor.extract('claude'); + expect(result).toContain('[human]: How do I refactor this?'); + expect(result).toContain('[assistant]: Here is a suggestion...'); + }); + + it('extracts Cursor chat', () => { + document.body.innerHTML = ` +
Fix this bug
+
The issue is...
+ `; + const result = extractor.extract('cursor'); + expect(result).toContain('[user]: Fix this bug'); + expect(result).toContain('[assistant]: The issue is...'); + }); + + it('removes buttons and timestamps from content', () => { + document.body.innerHTML = ` +
+ Some code + + 2:30 PM +
+ `; + const result = extractor.extract('chatgpt'); + expect(result).toContain('Some code'); + expect(result).not.toContain('Copy'); + expect(result).not.toContain('2:30 PM'); + }); + + it('returns empty string when no elements found', () => { + document.body.innerHTML = '
Empty page
'; + expect(extractor.extract('chatgpt')).toBe(''); + }); + + it('skips very short text', () => { + document.body.innerHTML = ` +
Hi
+
+ `; + const result = extractor.extract('chatgpt'); + expect(result).toBe(''); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..680b889 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "types": ["chrome", "vitest/globals"] + }, + "include": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5893fb0 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import { crx } from '@crxjs/vite-plugin' +import manifest from './manifest.json' assert { type: 'json' } + +export default defineConfig({ + plugins: [crx({ manifest })], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2a2cda2 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + }, +})