diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 84bc682..08bba9f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,6 +1,7 @@ {"id":"light-session-16d","title":"Implement code review recommendations","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-08T15:51:21.70403199+03:00","updated_at":"2026-01-08T15:54:39.953267761+03:00","closed_at":"2026-01-08T15:54:39.953267761+03:00","close_reason":"Closed"} {"id":"light-session-2hd","title":"Test cache + navigation trigger for load more feature","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-09T00:46:45.660702419+03:00","updated_at":"2026-01-09T10:45:29.927755814+03:00","closed_at":"2026-01-09T10:45:29.927759601+03:00"} {"id":"light-session-4ec","title":"Fix off-by-one error in turn counting - trimMapping","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T21:21:25.453075056+03:00","updated_at":"2026-01-07T21:23:59.887784624+03:00","closed_at":"2026-01-07T21:23:59.887784624+03:00","close_reason":"Closed"} +{"id":"light-session-67h","title":"Bug: trimming not working on page reload with extension enabled","description":"Settings show keep=5 but more messages visible. Race condition or localStorage sync issue.","status":"in_progress","priority":1,"issue_type":"task","created_at":"2026-01-09T22:39:11.800084105+03:00","created_by":"mayor","updated_at":"2026-01-09T22:40:24.864320247+03:00"} {"id":"light-session-6sc","title":"Implement code review improvements","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T22:59:50.319980052+03:00","updated_at":"2026-01-07T23:06:39.948042561+03:00","closed_at":"2026-01-07T23:06:39.948042561+03:00","close_reason":"Closed"} {"id":"light-session-7sq","title":"Rename 'turns' to 'messages' in UI","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-07T21:50:55.111515332+03:00","updated_at":"2026-01-07T21:53:42.151552035+03:00","closed_at":"2026-01-07T21:53:42.151552035+03:00","close_reason":"Closed"} {"id":"light-session-8bq","title":"Fix 'Body has already been consumed' error in fetch interceptor","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T22:06:28.74686929+03:00","updated_at":"2026-01-07T22:07:29.911516018+03:00","closed_at":"2026-01-07T22:07:29.911516018+03:00","close_reason":"Fixed by extracting URL/method before nativeFetch"} @@ -9,3 +10,4 @@ {"id":"light-session-hqc","title":"Fix Firefox Xray vision bug - cloneInto for CustomEvent detail","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T21:09:56.751185775+03:00","updated_at":"2026-01-07T21:12:56.968676465+03:00","closed_at":"2026-01-07T21:12:56.968676465+03:00","close_reason":"Closed"} {"id":"light-session-kf1","title":"Fix ESLint error for cloneInto Firefox API","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-08T19:23:38.808971016+03:00","updated_at":"2026-01-08T19:24:30.315635209+03:00","closed_at":"2026-01-08T19:24:30.315635209+03:00","close_reason":"Closed"} {"id":"light-session-oho","title":"Fix: empty user nodes counted but not rendered","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-07T22:21:42.141200219+03:00","updated_at":"2026-01-07T22:44:23.70359928+03:00","closed_at":"2026-01-07T22:44:23.70359928+03:00","close_reason":"Fixed: preserve original root node as tree anchor for ChatGPT"} +{"id":"light-session-q3p","title":"Fix race condition: sync settings to localStorage for page-script","status":"in_progress","priority":1,"issue_type":"task","created_at":"2026-01-09T22:28:10.93947054+03:00","created_by":"mayor","updated_at":"2026-01-09T22:28:17.158771046+03:00"} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 516dbeb..183a66e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,23 +34,45 @@ jobs: - name: Type check run: npm run build:types - - name: Build - run: npm run build - - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - - name: Create extension zip + # ========================================================================= + # Firefox Build + # ========================================================================= + - name: Build Firefox extension + run: npm run build:prod:firefox + + - name: Create Firefox extension zip run: | cd extension - zip -r ../light-session-${{ steps.version.outputs.VERSION }}.zip \ + zip -r ../light-session-${{ steps.version.outputs.VERSION }}-firefox.zip \ manifest.json \ dist/ \ popup/ \ icons/ \ -x "*.map" + # ========================================================================= + # Chrome Build + # ========================================================================= + - name: Build Chrome extension + run: npm run build:prod:chrome + + - name: Create Chrome extension zip + run: | + cd extension + zip -r ../light-session-${{ steps.version.outputs.VERSION }}-chrome.zip \ + manifest.json \ + dist/ \ + popup/ \ + icons/ \ + -x "*.map" + + # ========================================================================= + # Source Archive + # ========================================================================= - name: Create source zip run: | zip -r light-session-${{ steps.version.outputs.VERSION }}-source.zip \ @@ -68,15 +90,20 @@ jobs: docs/ \ extension/src/ \ extension/icons/ \ - extension/manifest.json \ + extension/manifest.firefox.json \ + extension/manifest.chrome.json \ tests/ + # ========================================================================= + # GitHub Release + # ========================================================================= - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: generate_release_notes: true files: | - light-session-${{ steps.version.outputs.VERSION }}.zip + light-session-${{ steps.version.outputs.VERSION }}-firefox.zip + light-session-${{ steps.version.outputs.VERSION }}-chrome.zip light-session-${{ steps.version.outputs.VERSION }}-source.zip body: | ## LightSession v${{ steps.version.outputs.VERSION }} @@ -86,19 +113,43 @@ jobs: **Firefox Add-ons (recommended):** [Install from AMO](https://addons.mozilla.org/en-US/firefox/addon/lightsession-for-chatgpt/) - **Manual install:** - 1. Download `light-session-${{ steps.version.outputs.VERSION }}.zip` + **Chrome Web Store:** + [Install from Chrome Web Store](https://chrome.google.com/webstore/detail/lightsession-for-chatgpt/${{ secrets.CHROME_EXTENSION_ID }}) + + **Manual install (Firefox):** + 1. Download `light-session-${{ steps.version.outputs.VERSION }}-firefox.zip` 2. Open `about:debugging#/runtime/this-firefox` in Firefox 3. Click "Load Temporary Add-on" 4. Select the downloaded zip file + **Manual install (Chrome):** + 1. Download `light-session-${{ steps.version.outputs.VERSION }}-chrome.zip` + 2. Open `chrome://extensions` in Chrome + 3. Enable "Developer mode" + 4. Click "Load unpacked" and select the extracted folder + --- + # ========================================================================= + # Firefox Add-ons Publishing + # ========================================================================= - name: Publish to Firefox Add-ons uses: browser-actions/release-firefox-addon@latest with: addon-id: ${{ secrets.FIREFOX_ADDON_ID }} - addon-path: light-session-${{ steps.version.outputs.VERSION }}.zip + addon-path: light-session-${{ steps.version.outputs.VERSION }}-firefox.zip auth-api-issuer: ${{ secrets.FIREFOX_API_ISSUER }} auth-api-secret: ${{ secrets.FIREFOX_API_SECRET }} release-note: "See release notes at https://github.com/11me/light-session/releases/tag/v${{ steps.version.outputs.VERSION }}" + + # ========================================================================= + # Chrome Web Store Publishing + # ========================================================================= + - name: Publish to Chrome Web Store + uses: browser-actions/release-chrome-extension@latest + with: + extension-id: ${{ secrets.CHROME_EXTENSION_ID }} + extension-path: light-session-${{ steps.version.outputs.VERSION }}-chrome.zip + oauth-client-id: ${{ secrets.CHROME_CLIENT_ID }} + oauth-client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} + oauth-refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} diff --git a/.gitignore b/.gitignore index d9e83df..c123a67 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ extension/.dev *.tmp *.temp .serena/ +extension/manifest.json diff --git a/CLAUDE.md b/CLAUDE.md index dd4357f..74bd8be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,19 +5,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build & Test Commands ```bash -npm install # Install dependencies -npm run build # Build extension (esbuild) -npm run dev # Run in Firefox Developer Edition with auto-reload -npm run test # Run unit tests (vitest) -npm run test:watch # Run tests in watch mode -npm run lint # ESLint check -npm run lint:fix # ESLint autofix -npm run build:types # TypeScript type check (no emit) +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 dev # Run in Firefox Developer Edition +npm run watch:chrome # Watch mode for Chrome development +npm run test # Run unit tests (vitest) +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) ``` ## Architecture -**Firefox extension (Manifest V3)** that uses Fetch Proxy to trim ChatGPT conversations before React renders. +**Cross-browser extension (Manifest V3)** for Firefox and Chrome that uses Fetch Proxy to trim ChatGPT conversations before React renders. ### Core Components @@ -48,7 +51,7 @@ content.ts → dispatches settings via CustomEvent → receives status updates ### Message-Based Counting ChatGPT creates multiple nodes per assistant response (especially with Extended Thinking). -LightSession counts **messages** (role changes) instead of nodes: +LightSession Pro counts **messages** (role changes) instead of nodes: - `[user, assistant, assistant, user, assistant]` = 4 messages - Consecutive same-role nodes are aggregated as ONE message - HIDDEN_ROLES: `system`, `tool`, `thinking` excluded from count @@ -56,14 +59,18 @@ LightSession counts **messages** (role changes) instead of nodes: ## Project Structure ``` -extension/src/ -├── page/ # Page script (Fetch Proxy, runs in page context) -├── content/ # Content scripts (settings, status bar) -├── background/ # Background service worker -├── popup/ # Popup HTML/CSS/TS -└── shared/ # Types, constants, storage, logger -tests/ # Unit tests (vitest + happy-dom) -build.cjs # esbuild build script (CommonJS) +extension/ +├── manifest.json # Symlink → manifest.firefox.json (or chrome copy) +├── manifest.firefox.json # Firefox-specific manifest +├── manifest.chrome.json # Chrome-specific manifest +└── src/ + ├── page/ # Page script (Fetch Proxy, runs in page context) + ├── content/ # Content scripts (settings, status bar) + ├── background/ # Background service worker + ├── 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) ``` ## Conventions diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..366660b --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,46 @@ +# Privacy Policy + +**LightSession for ChatGPT** is a privacy-first browser extension. This policy explains what data the extension accesses and how it is handled. + +## Data Collection + +**We do not collect any data.** + +LightSession operates entirely locally in your browser. It does not: + +- Collect personal information +- Track your browsing activity +- Send data to external servers +- Use analytics or telemetry +- Store conversation content + +## Permissions Explained + +| Permission | Purpose | +|------------|---------| +| `storage` | Saves your preferences (message limit, UI settings) locally in your browser | +| `tabs` | Detects when you navigate to ChatGPT to apply settings | +| Host permissions (`chatgpt.com`, `chat.openai.com`) | Required to inject the performance optimization script on ChatGPT pages | + +## How It Works + +LightSession intercepts ChatGPT's API responses **locally in your browser** and trims the conversation data before React renders it. This keeps the UI fast without modifying your actual conversation on OpenAI's servers. + +All processing happens entirely within your browser. No data ever leaves your device. + +## Third Parties + +This extension does not share any data with third parties because it does not collect any data. + +## Open Source + +LightSession is open source. You can review the code at: +https://github.com/11me/light-session + +## Contact + +For privacy questions, open an issue on GitHub. + +--- + +*Last updated: January 2026* diff --git a/build.cjs b/build.cjs index eb4b7be..4678b92 100755 --- a/build.cjs +++ b/build.cjs @@ -1,12 +1,14 @@ #!/usr/bin/env node /** - * Build script for LightSession extension + * Build script for LightSession Pro extension * Bundles TypeScript → single JS files (no imports) for MV3 compatibility * * Usage: - * node build.js - Development build (with sourcemaps) - * node build.js --watch - Watch mode for development - * NODE_ENV=production node build.js - Production build (minified, no sourcemaps) + * 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 --watch - Watch mode for development + * NODE_ENV=production node build.cjs - Production build (minified, no sourcemaps) */ const esbuild = require('esbuild'); @@ -16,6 +18,38 @@ const path = require('path'); const isWatch = process.argv.includes('--watch'); const isProduction = process.env.NODE_ENV === 'production'; +// Parse --target=firefox|chrome (default: firefox) +const targetArg = process.argv.find((arg) => arg.startsWith('--target=')); +const target = targetArg ? targetArg.split('=')[1] : 'firefox'; +const validTargets = ['firefox', 'chrome']; +if (!validTargets.includes(target)) { + console.error(`❌ Invalid target: ${target}. Use: ${validTargets.join(', ')}`); + process.exit(1); +} + +/** + * Copy manifest for target browser + */ +function copyManifest() { + const manifestSrc = `extension/manifest.${target}.json`; + const manifestDest = 'extension/manifest.json'; + + // Always remove existing manifest.json first + if (fs.existsSync(manifestDest)) { + fs.unlinkSync(manifestDest); + } + + if (target === 'chrome') { + // For Chrome, copy manifest.chrome.json + fs.copyFileSync(manifestSrc, manifestDest); + console.log(`✓ Copied manifest.${target}.json → manifest.json`); + } else { + // For Firefox, create symlink to manifest.firefox.json + fs.symlinkSync('manifest.firefox.json', manifestDest); + console.log('✓ Created symlink manifest.json → manifest.firefox.json'); + } +} + /** * Copy static files from src to extension folder */ @@ -33,6 +67,26 @@ function copyStaticFiles() { console.log('✓ Copied static files (popup.html, popup.css)'); } +/** + * Create or remove .dev marker file for development mode detection. + * The popup checks for this file to show/hide debug options. + */ +function handleDevMarker() { + const devMarkerPath = 'extension/.dev'; + + if (isProduction) { + // Remove .dev marker in production + if (fs.existsSync(devMarkerPath)) { + fs.unlinkSync(devMarkerPath); + console.log('✓ Removed .dev marker (production build)'); + } + } else { + // Create .dev marker in development + fs.writeFileSync(devMarkerPath, 'Development build marker\n'); + console.log('✓ Created .dev marker (development build)'); + } +} + const buildOptions = { bundle: true, format: 'iife', @@ -52,7 +106,7 @@ const buildOptions = { async function build() { const mode = isProduction ? 'production' : 'development'; - console.log(`🔧 Building in ${mode} mode${isProduction ? ' (minified)' : ' (with sourcemaps)'}...\n`); + console.log(`🔧 Building for ${target.toUpperCase()} in ${mode} mode${isProduction ? ' (minified)' : ' (with sourcemaps)'}...\n`); try { await esbuild.build({ @@ -91,8 +145,10 @@ async function build() { console.log('✓ Built popup script'); copyStaticFiles(); + copyManifest(); + handleDevMarker(); - console.log(`\n✅ ${mode.charAt(0).toUpperCase() + mode.slice(1)} build complete! Extension ready for Firefox.`); + console.log(`\n✅ ${mode.charAt(0).toUpperCase() + mode.slice(1)} build complete! Extension ready for ${target.charAt(0).toUpperCase() + target.slice(1)}.`); } catch (error) { console.error('❌ Build failed:', error); process.exit(1); @@ -100,7 +156,7 @@ async function build() { } async function watch() { - console.log('👀 Watch mode enabled. Watching for changes...\n'); + console.log(`👀 Watch mode enabled for ${target.toUpperCase()}. Watching for changes...\n`); const contexts = await Promise.all([ esbuild.context({ @@ -135,7 +191,9 @@ async function watch() { await ctx.rebuild(); } copyStaticFiles(); - console.log('✅ Initial build complete.\n'); + copyManifest(); + handleDevMarker(); + console.log(`✅ Initial build complete for ${target.toUpperCase()}.\n`); // Start watching for (const ctx of contexts) { diff --git a/docs/development.md b/docs/development.md index 6072909..e6953e2 100644 --- a/docs/development.md +++ b/docs/development.md @@ -277,3 +277,65 @@ The extension uses a tiered selector approach: - Check `showStatusBar` setting is enabled - Verify content script is running (check console) - Inspect DOM for `#lightsession-status-bar` element + +## Release Process + +Releases are automated via GitHub Actions when a tag is pushed: + +```bash +# Create and push a release tag +git tag v1.2.3 +git push origin v1.2.3 +``` + +The workflow builds both Firefox and Chrome versions, creates a GitHub Release, +and publishes to both browser stores. + +### Required GitHub Secrets + +The following secrets must be configured in the repository settings: + +#### Firefox Add-ons + +| Secret | Description | +|--------|-------------| +| `FIREFOX_ADDON_ID` | AMO extension ID (e.g., `lightsession@example.com`) | +| `FIREFOX_API_ISSUER` | JWT issuer from AMO API credentials | +| `FIREFOX_API_SECRET` | JWT secret from AMO API credentials | + +#### Chrome Web Store + +| Secret | Description | +|--------|-------------| +| `CHROME_EXTENSION_ID` | Chrome extension ID (32-char alphanumeric) | +| `CHROME_CLIENT_ID` | OAuth 2.0 client ID from Google Cloud Console | +| `CHROME_CLIENT_SECRET` | OAuth 2.0 client secret | +| `CHROME_REFRESH_TOKEN` | OAuth 2.0 refresh token for Chrome Web Store API | + +### Getting Chrome Web Store Credentials + +1. **Create a Google Cloud Project** at [console.cloud.google.com](https://console.cloud.google.com) +2. **Enable the Chrome Web Store API** in the API Library +3. **Create OAuth 2.0 credentials** (Desktop application type) +4. **Get a refresh token** using the OAuth 2.0 flow: + - Use the client ID/secret to authorize + - Request scope: `https://www.googleapis.com/auth/chromewebstore` + - Exchange the authorization code for a refresh token + +For detailed instructions, see the [Chrome Web Store API documentation](https://developer.chrome.com/docs/webstore/using_webstore_api/). + +### Multi-Browser Build + +```bash +# Build for Firefox +npm run build:prod:firefox + +# Build for Chrome +npm run build:prod:chrome + +# Build both (development) +npm run build:firefox && npm run build:chrome +``` + +The build system automatically switches between `manifest.firefox.json` and +`manifest.chrome.json` based on the target. diff --git a/extension/manifest.chrome.json b/extension/manifest.chrome.json new file mode 100644 index 0000000..53f7a63 --- /dev/null +++ b/extension/manifest.chrome.json @@ -0,0 +1,48 @@ +{ + "manifest_version": 3, + "name": "LightSession Pro", + "version": "1.6.1", + "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/*"] + } + ] +} diff --git a/extension/manifest.json b/extension/manifest.firefox.json similarity index 82% rename from extension/manifest.json rename to extension/manifest.firefox.json index a1f27a0..7881096 100644 --- a/extension/manifest.json +++ b/extension/manifest.firefox.json @@ -1,6 +1,6 @@ { "manifest_version": 3, - "name": "LightSession for ChatGPT", + "name": "LightSession Pro", "version": "1.6.1", "description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.", "icons": { @@ -10,10 +10,10 @@ "128": "icons/icon-128.png" }, "action": { - "default_title": "LightSession", + "default_title": "LightSession Pro", "default_popup": "popup/popup.html" }, - "permissions": ["storage"], + "permissions": ["storage", "tabs"], "host_permissions": [ "*://chat.openai.com/*", "*://chatgpt.com/*" @@ -37,9 +37,7 @@ } ], "background": { - "scripts": ["dist/background.js"], - "service_worker": "dist/background.js", - "preferred_environment": ["service_worker", "document"] + "scripts": ["dist/background.js"] }, "web_accessible_resources": [ { diff --git a/extension/popup/popup.html b/extension/popup/popup.html index 7447ab3..8e3f029 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -3,7 +3,7 @@
-ChatGPT Performance Optimizer
ChatGPT Performance Optimizer