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

6.2 KiB
Raw Blame History

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-utilsecretbox 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

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 buildchrome://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.