From d0eabbfd113e9cab4b14d89afcbb3fd2ace2f6f8 Mon Sep 17 00:00:00 2001 From: Dvir Zagury-Grynbaum Date: Sun, 22 Feb 2026 15:39:47 -0500 Subject: [PATCH 1/2] add Safari build target and manifest Co-authored-by: Cursor --- CLAUDE.md | 9 ++-- build.cjs | 9 ++-- extension/manifest.safari.json | 53 ++++++++++++++++++++++++ extension/src/shared/browser-polyfill.ts | 1 + package.json | 3 ++ 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 extension/manifest.safari.json diff --git a/CLAUDE.md b/CLAUDE.md index 74bd8be..9d62198 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ npm install # Install dependencies npm run build # Build for Firefox (default) npm run build:firefox # Build for Firefox npm run build:chrome # Build for Chrome +npm run build:safari # Build for Safari npm run dev # Run in Firefox Developer Edition npm run watch:chrome # Watch mode for Chrome development npm run test # Run unit tests (vitest) @@ -16,11 +17,12 @@ npm run lint # ESLint check npm run build:types # TypeScript type check npm run package # Package for Firefox (web-ext-artifacts/) npm run package:chrome # Package for Chrome (ZIP) +npm run package:safari # Build for Safari + run safari-web-extension-converter (outputs to web-ext-artifacts/safari/) ``` ## Architecture -**Cross-browser extension (Manifest V3)** for Firefox and Chrome that uses Fetch Proxy to trim ChatGPT conversations before React renders. +**Cross-browser extension (Manifest V3)** for Firefox, Chrome, and Safari that uses Fetch Proxy to trim ChatGPT conversations before React renders. ### Core Components @@ -60,9 +62,10 @@ LightSession Pro counts **messages** (role changes) instead of nodes: ``` extension/ -├── manifest.json # Symlink → manifest.firefox.json (or chrome copy) +├── manifest.json # Symlink → manifest.firefox.json (or chrome/safari copy) ├── manifest.firefox.json # Firefox-specific manifest ├── manifest.chrome.json # Chrome-specific manifest +├── manifest.safari.json # Safari-specific manifest (MV3, no declarativeContent) └── src/ ├── page/ # Page script (Fetch Proxy, runs in page context) ├── content/ # Content scripts (settings, status bar) @@ -70,7 +73,7 @@ extension/ ├── popup/ # Popup HTML/CSS/TS └── shared/ # Types, constants, storage, logger tests/ # Unit tests (vitest + happy-dom) -build.cjs # esbuild build script (supports --target=firefox|chrome) +build.cjs # esbuild build script (supports --target=firefox|chrome|safari) ``` ## Conventions diff --git a/build.cjs b/build.cjs index 4678b92..379d533 100755 --- a/build.cjs +++ b/build.cjs @@ -7,6 +7,7 @@ * node build.cjs - Development build for Firefox (default) * node build.cjs --target=firefox - Build for Firefox * node build.cjs --target=chrome - Build for Chrome + * node build.cjs --target=safari - Build for Safari * node build.cjs --watch - Watch mode for development * NODE_ENV=production node build.cjs - Production build (minified, no sourcemaps) */ @@ -18,10 +19,10 @@ const path = require('path'); const isWatch = process.argv.includes('--watch'); const isProduction = process.env.NODE_ENV === 'production'; -// Parse --target=firefox|chrome (default: firefox) +// Parse --target=firefox|chrome|safari (default: firefox) const targetArg = process.argv.find((arg) => arg.startsWith('--target=')); const target = targetArg ? targetArg.split('=')[1] : 'firefox'; -const validTargets = ['firefox', 'chrome']; +const validTargets = ['firefox', 'chrome', 'safari']; if (!validTargets.includes(target)) { console.error(`❌ Invalid target: ${target}. Use: ${validTargets.join(', ')}`); process.exit(1); @@ -39,8 +40,8 @@ function copyManifest() { fs.unlinkSync(manifestDest); } - if (target === 'chrome') { - // For Chrome, copy manifest.chrome.json + if (target === 'chrome' || target === 'safari') { + // For Chrome/Safari, copy the target-specific manifest fs.copyFileSync(manifestSrc, manifestDest); console.log(`✓ Copied manifest.${target}.json → manifest.json`); } else { diff --git a/extension/manifest.safari.json b/extension/manifest.safari.json new file mode 100644 index 0000000..732c761 --- /dev/null +++ b/extension/manifest.safari.json @@ -0,0 +1,53 @@ +{ + "manifest_version": 3, + "name": "LightSession Pro for ChatGPT", + "version": "1.7.0", + "description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.", + "icons": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "action": { + "default_title": "LightSession Pro", + "default_popup": "popup/popup.html" + }, + "permissions": ["storage", "tabs"], + "host_permissions": [ + "*://chat.openai.com/*", + "*://chatgpt.com/*" + ], + "content_scripts": [ + { + "matches": [ + "*://chat.openai.com/*", + "*://chatgpt.com/*" + ], + "js": ["dist/page-inject.js"], + "run_at": "document_start" + }, + { + "matches": [ + "*://chat.openai.com/*", + "*://chatgpt.com/*" + ], + "js": ["dist/content.js"], + "run_at": "document_idle" + } + ], + "background": { + "service_worker": "dist/background.js" + }, + "web_accessible_resources": [ + { + "resources": ["dist/page-script.js", ".dev"], + "matches": ["*://chat.openai.com/*", "*://chatgpt.com/*"] + } + ], + "browser_specific_settings": { + "safari": { + "strict_min_version": "15.4" + } + } +} diff --git a/extension/src/shared/browser-polyfill.ts b/extension/src/shared/browser-polyfill.ts index 6a5dbbc..ccdc382 100644 --- a/extension/src/shared/browser-polyfill.ts +++ b/extension/src/shared/browser-polyfill.ts @@ -4,6 +4,7 @@ * Cross-browser compatibility layer for WebExtension APIs. * - Firefox: uses global `browser` object (Promise-based) * - Chrome: uses global `chrome` object (callback-based, but MV3 supports Promises) + * - Safari: Web Extensions API (browser/chrome); MV3 from Safari 15.4+ * * Modern Chrome (MV3) supports Promise-based APIs similar to Firefox, * so we can use `chrome` directly as a drop-in replacement for `browser`. diff --git a/package.json b/package.json index 0440b37..c379a7a 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "build": "node build.cjs", "build:firefox": "node build.cjs --target=firefox", "build:chrome": "node build.cjs --target=chrome", + "build:safari": "node build.cjs --target=safari", "build:types": "tsc --noEmit", "build:prod": "rm -f extension/.dev && NODE_ENV=production node build.cjs", "build:prod:firefox": "rm -f extension/.dev && NODE_ENV=production node build.cjs --target=firefox", "build:prod:chrome": "rm -f extension/.dev && NODE_ENV=production node build.cjs --target=chrome", + "build:prod:safari": "rm -f extension/.dev && NODE_ENV=production node build.cjs --target=safari", "watch": "node build.cjs --watch", "watch:chrome": "node build.cjs --target=chrome --watch", "lint": "eslint extension/src", @@ -27,6 +29,7 @@ "dev:stable": "npm run build:firefox && web-ext run --source-dir=extension --firefox=firefox --start-url='https://chat.openai.com'", "package": "npm run build:prod:firefox && web-ext build --source-dir=extension --artifacts-dir=web-ext-artifacts", "package:chrome": "npm run build:prod:chrome && cd extension && zip -r ../web-ext-artifacts/lightsession-chrome.zip . -x '*.ts' -x 'src/*' -x 'manifest.*.json'", + "package:safari": "npm run build:prod:safari && xcrun safari-web-extension-converter extension --project-location web-ext-artifacts/safari --app-name 'LightSession Pro' --bundle-identifier com.lightsession.pro --copy-resources --no-open", "clean": "rm -rf extension/dist extension/popup/popup.js web-ext-artifacts" }, "keywords": [ From f79badf22dd5bb52b210764b9725d1980f737a08 Mon Sep 17 00:00:00 2001 From: Dvir Zagury-Grynbaum Date: Sun, 22 Feb 2026 15:47:57 -0500 Subject: [PATCH 2/2] add safari support --- package-lock.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index bbf1dcc..35b8884 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1290,7 +1290,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1357,7 +1356,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -1672,7 +1670,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2963,7 +2960,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5253,7 +5249,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5317,7 +5312,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5455,7 +5449,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5549,7 +5542,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" },