120 lines
6.2 KiB
Markdown
120 lines
6.2 KiB
Markdown
# CLAUDE.md — LexAI
|
||
|
||
Guidance for Claude Code when working in this repository.
|
||
|
||
## What LexAI is
|
||
|
||
A Grammarly-like **Chrome Extension (Manifest V3)** that provides AI writing assistance
|
||
(grammar fix, rephrase, shorten, expand, explain) on any webpage. Users bring **their own
|
||
LLM API key** — there is no LexAI backend. The extension's service worker calls the user's
|
||
chosen provider directly.
|
||
|
||
- **Providers:** OpenAI, Anthropic, Groq, OpenRouter (all configured in `entrypoints/background.ts`).
|
||
- **No subscription, no server.** The API key lives encrypted in `chrome.storage.local`.
|
||
|
||
## Tech stack
|
||
|
||
- **WXT** `^0.20` — extension framework (wraps Vite). Entrypoints live in `entrypoints/`.
|
||
- **React 18** + TypeScript — used only for the Options and Popup pages.
|
||
- **Zustand** — a dependency, but state is currently local; not yet wired into a store.
|
||
- **tweetnacl** / **tweetnacl-util** — `secretbox` symmetric encryption for the API key.
|
||
- **Vitest** (jsdom) for unit tests, **Playwright** for e2e.
|
||
- **Tailwind** is in devDependencies but **not active** — all UI uses inline style objects
|
||
(WXT PostCSS was never wired up). Do not assume Tailwind classes work.
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
npm install # first-time setup (node_modules is gitignored; not present by default)
|
||
npm run dev # WXT dev server with hot reload
|
||
npm run build # production build → .output/chrome-mv3/
|
||
npm run zip # package for Chrome Web Store
|
||
npm test # vitest (add `-- --run` for one-shot, non-watch)
|
||
npm run test:e2e # Playwright (requires a prior `npm run build`)
|
||
npm run typecheck # tsc --noEmit
|
||
```
|
||
|
||
**Prerequisite:** Node. CI pins **Node 22** (`node:22-bookworm`). Run `npm install` before any
|
||
`npm run *` script — the binaries (`tsc`, `vitest`) come from `node_modules/.bin`.
|
||
|
||
**Load unpacked in Chrome:** `npm run build` → `chrome://extensions` → Developer Mode →
|
||
Load unpacked → select `.output/chrome-mv3`.
|
||
|
||
## Architecture
|
||
|
||
Three cooperating contexts, message-passed over `chrome.runtime`:
|
||
|
||
```
|
||
entrypoints/content.ts (content script, injected into <all_urls>)
|
||
• Detects text selection: textarea/input (selectionStart/End) vs contenteditable/DOM (Range API)
|
||
• Renders the floating toolbar + result modal + toasts (all inline-styled, appended to document.body)
|
||
• Snapshots selection state BEFORE any async call, then Replace uses the snapshot
|
||
• Sends { type: 'ANALYZE_TEXT', payload: {text, action, style} } to the background
|
||
|
||
entrypoints/background.ts (service worker — the LLM proxy)
|
||
• onMessage: ANALYZE_TEXT and COPY_AS
|
||
• Reads provider/apiKey/apiKeyEnc/encKey/model from chrome.storage.local
|
||
• Decrypts the key (tweetnacl secretbox), routes to the correct provider's fetch call
|
||
• Registers right-click context menus (action × style) on install
|
||
|
||
entrypoints/options/Options.tsx (settings page, React)
|
||
• Provider + model + API key form; encrypts the key and writes apiKeyEnc/encKey to storage
|
||
|
||
entrypoints/popup/Popup.tsx (toolbar popup, React)
|
||
• Standalone text box → same ANALYZE_TEXT flow; shows config status; links to Options
|
||
```
|
||
|
||
Content script and popup **must not** call provider APIs directly — CORS and key handling
|
||
belong in the background service worker. Route everything through `ANALYZE_TEXT`/`COPY_AS`.
|
||
|
||
### Message contract
|
||
|
||
- `ANALYZE_TEXT` accepts **both** `{ payload: {text, action, style} }` (content/popup) and
|
||
flat `{ text, action, style }`. Keep both shapes working if you touch the handler.
|
||
- `action` values: `grammar`, `rephrase`, `shorten`, `expand`, `explain`. The context menu
|
||
and popup emit `fix`, which `getSystemPrompt` normalizes to `grammar`.
|
||
- The listener returns `true` to keep the async channel open — **required**; removing it
|
||
silently breaks every response.
|
||
|
||
## Key conventions & gotchas
|
||
|
||
- **`data-lexai="true"`** is set on every LexAI-injected DOM node. Selection/click handlers
|
||
check `target.closest('[data-lexai="true"]')` to avoid self-triggering. Preserve it on any
|
||
new injected element.
|
||
- **Selection is captured eagerly** (on `mouseup` and on button `mousedown`) because focus
|
||
shifts and the live selection is gone by the time an async response returns. When editing
|
||
content.ts, keep the snapshot-before-await pattern intact.
|
||
- **`z-index: 2147483647`** (max) on toolbar/modal so they sit above host-page UI.
|
||
- **Provider code is duplicated**: each provider has a `callX` (system-prompt from action)
|
||
and a `callXWithPrompt` (arbitrary system prompt, used by COPY_AS). A change to request
|
||
shape usually needs to be made in both. See "Recommendations" below — this is a known smell.
|
||
- **API-key handling:** prefer the encrypted path (`apiKeyEnc` + `encKey`); plaintext `apiKey`
|
||
is legacy/back-compat only. Never log the key. Never add code that transmits it anywhere
|
||
except the user's chosen provider endpoint.
|
||
- **Backward compat:** don't drop the plaintext `apiKey` fallback without a migration.
|
||
- Console `[LexAI …]` debug logs exist in content.ts's replace path — intentional for now.
|
||
|
||
## Testing notes
|
||
|
||
- `tests/unit/setup.ts` mocks `global.chrome`. Unit tests currently exercise storage mocks
|
||
rather than importing the real handlers — see Recommendations for the gap.
|
||
- Playwright e2e loads the built extension via `--load-extension=.output/chrome-mv3`; the
|
||
test files still contain `[EXTENSION_ID]` placeholders and won't pass as-is.
|
||
|
||
## CI / release (Gitea, not GitHub Actions)
|
||
|
||
Workflows live in `.gitea/workflows/`:
|
||
- `ci.yml` — typecheck → test → build → publish zip to Gitea package registry (on push to main/develop, PRs).
|
||
- `deploy-chrome.yml` — on `v*.*.*` tag: build → upload → publish to Chrome Web Store.
|
||
- Both send Telegram notifications. Secrets: `GITEATOKEN`, `CWS_*`, `TELEGRAM_*`.
|
||
|
||
**Version bumps:** update `version` in **both** `package.json` and `wxt.config.ts` (the
|
||
manifest version comes from wxt.config.ts). A `v*.*.*` git tag triggers the store deploy.
|
||
|
||
## When making changes
|
||
|
||
- After editing an entrypoint, run `npm run typecheck` and `npm test -- --run`.
|
||
- For behavior changes, `npm run build` and load unpacked to verify in a real page — the
|
||
selection/replace logic is DOM-timing-sensitive and unit tests don't cover it.
|
||
- Keep UI styling inline (no Tailwind) unless you're intentionally wiring PostCSS.
|