Files
LexAI/CLAUDE.md
john kevin asprec f7fcd41341
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 59s
feat: implement API key resolution and live model listing for providers
2026-07-13 13:56:48 +08:00

120 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.