- Added a new "Prompt Builder" tab in the Popup for generating AI prompts with customizable parameters. - Introduced new state variables for managing prompt styles, personas, formats, and models. - Enhanced the Options page to fetch and display models based on the provided API key. - Updated the actions and types to include the new 'prompt' action and its associated parameters. - Implemented migration logic for legacy plaintext API keys to encrypted storage. - Updated the getSystemPrompt function to incorporate prompt parameters for better instruction generation. - Added tests for the new functionality, including context menu entries and prompt generation logic.
12 KiB
CLAUDE.md — LexAI
Operating guide and control plane for Claude Code in this repo. Keep it durable and current, not a diary. Architecture/convention detail lives here and in
docs/.
0. Project contract
| Field | Value |
|---|---|
| Project | LexAI — Grammarly-like Chrome extension (Manifest V3), BYO-LLM-key |
| Outcome | Select text on any page → AI action (fix/rephrase/shorten/expand/explain) → Replace or Copy, with no backend and no subscription |
| Non-goals | No backend/account/subscription; no telemetry; no transmission of text/key except to the user's chosen provider; not a full editor |
| Primary user | People who hold an LLM API key and want inline writing help without a SaaS subscription |
| Acceptance tests | npm run typecheck + npm test -- --run pass; npm run build yields a loadable .output/chrome-mv3/; Replace works on textarea/input and contenteditable; key uses the encrypted path and is never logged/exfiltrated |
| Constraints | WXT ^0.20 + React 18 + TS; Node 22; Tailwind inactive (inline styles); <all_urls> today; Gitea CI + Chrome Web Store |
| Source of truth | This file + docs/; issue tracking in Plane (LEXAI) |
| Commands | install: npm install · test: npm test -- --run · typecheck: npm run typecheck · build: npm run build |
Definition of done
Done means: the change is implemented, typecheck/test/build pass, behavior is verified (for DOM/selection/replace changes, a real-page load-unpacked check — unit tests don't cover DOM timing), the codebase invariants in ## Lessons are preserved, and docs/HANDOFF.md states what changed and how it was tested. Do not claim success from code inspection alone.
1. Operating principles
- Evidence before inference. Read the relevant entrypoints/tests before changing them; report actual command output.
- Smallest useful diff. Be surgical — match existing style, don't reformat or broaden scope.
- Artifacts beat chat. Record state in
docs/(brief, decisions, tasks, handoff) so a fresh session resumes from files, not history. - Preserve the invariants. The message contract, snapshot-before-await,
data-lexaiguard, and key handling break silently — see## Lessons. - Use code for deterministic work. Prefer
typecheck/test/buildover reasoning about correctness. - Escalate intentionally. Surface anything touching manifest permissions, storage schema, the key path, or a release.
2. Files that preserve context
docs/
PROJECT_BRIEF.md # outcome, non-goals, constraints
ARCHITECTURE.md # the three-context system and boundaries
DECISIONS.md # ADRs: BYO-key, inline styles, no shadow DOM, key story, Gitea CI
TASKS.md # active contracts derived from RECOMMENDATIONS.md
EVALS.md # standing gates + failure-derived checks
LESSONS_LEARNED.md # codebase invariants (detail behind the ## Lessons list)
HANDOFF.md # current state, risks, next action
SELF_MODEL.md # operator/project model, kept honest by audit
attacksurface.md # exposure inventory (permissions, key storage, CI)
Supporting: README.md (public), PHASE1_SUMMARY.md (build history), RECOMMENDATIONS.md (source of the task list), src/utils/REFERENCE_NOTES.md (reference-project patterns).
What LexAI is
A Grammarly-like Chrome Extension (Manifest V3) providing 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 service worker calls the user's chosen provider directly.
- Providers: OpenAI, Anthropic, Groq, OpenRouter (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 inentrypoints/. - React 18 + TypeScript — Options and Popup pages only.
- Zustand — a dependency, but state is currently local; not yet wired into a store.
- tweetnacl / tweetnacl-util —
secretboxsymmetric encryption for the API key. - Vitest (jsdom) unit tests, Playwright 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 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 * — 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 (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. See docs/ARCHITECTURE.md for the component table.
Message contract
ANALYZE_TEXTaccepts both{ payload: {text, action, style} }(content/popup) and flat{ text, action, style }. Keep both shapes working if you touch the handler.actionvalues:grammar,rephrase,shorten,expand,explain. The context menu and popup emitfix, whichgetSystemPromptnormalizes togrammar.- The listener returns
trueto 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 checktarget.closest('[data-lexai="true"]')to avoid self-triggering. Preserve it on any new injected element.- Selection is captured eagerly (on
mouseupand on buttonmousedown) because focus shifts and the live selection is gone by the time an async response returns. 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 acallXWithPrompt(arbitrary system prompt, used by COPY_AS). A change to request shape usually needs both. This is a known smell — refactor tracked indocs/TASKS.md(T-04). - API-key handling: prefer the encrypted path (
apiKeyEnc+encKey); plaintextapiKeyis 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
apiKeyfallback without a migration. - Console
[LexAI …]debug logs exist in content.ts's replace path — intentional for now, but should be gated behind a DEV flag before release (T-03).
Testing notes
tests/unit/setup.tsmocksglobal.chrome. Unit tests currently exercise storage mocks rather than importing the real handlers — seedocs/TASKS.mdT-08 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 (T-09). - Standing gates and how to run them:
docs/EVALS.md.
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— onv*.*.*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. (Single-sourcing tracked in T-16.)
Orchestration & agents
For coordinated work, run Fable as the main session (claude --agent fable-orchestrator) and let it route to specialists; see .claude/AGENTS.md for the roster. For LexAI code (entrypoints/, src/), prefer the lexai-extension-dev specialist over the generic builder — it knows the message contract, snapshot pattern, and key rules. Security-relevant changes (permissions, key path, dependencies) go through security-auditor.
On-demand skills in .claude/skills/: resume-project, continuous-improvement, dev-loop, attack-surface, prompt-injection-audit, self-model-audit. Invoke by name; keep each run bounded.
Quality gates by risk
| Risk | Examples in LexAI | Required gates |
|---|---|---|
| Low | docs, inline-style tweaks, copy | contract + self-check (typecheck) |
| Medium | new action, provider request change, Options/Popup UI | typecheck + test -- --run + build + a separate verifier |
| High | manifest permissions, key handling/storage schema, release/version, CWS listing | written plan + security-auditor + independent critic + real-page verification + explicit owner authorization before release |
When making changes
- After editing an entrypoint, run
npm run typecheckandnpm test -- --run. - For behavior changes,
npm run buildand 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.
Lessons
Codebase invariants that break silently when violated (detail + evidence in docs/LESSONS_LEARNED.md):
- Keep
return truein theonMessagelistener — else every async response is dropped. - Snapshot selection before any
await; handle textarea/input and contenteditable/Range paths. - Keep both
ANALYZE_TEXTshapes ({payload}and flat) and thefix→grammarnormalization. - Set
data-lexai="true"on every injected node; skip events onclosest('[data-lexai="true"]'). - Never log or transmit the API key except to the user's provider; keep the plaintext
apiKeyfallback until a migration exists. - Update both
callXandcallXWithPromptwhen changing a provider's request shape. - Never
fetcha provider from content/popup — route through the background worker. - Bump
versionin bothpackage.jsonandwxt.config.ts. - Style inline; Tailwind classes do nothing until PostCSS is wired.
State continuity
- On a fresh/compacted/interrupted session, invoke
/resume-projectbefore planning or editing. - Before ending a substantial task, update
docs/HANDOFF.md(verified state, changed paths, checks, risks, next action). - After a verified recurring mistake or workflow gap, invoke
/continuous-improvement; durable agent/skill changes go throughsystem-steward.