Files
LexAI/CLAUDE.md
john kevin asprec 6aee260533
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 1m33s
fix: never send an API key to the provider it wasn't entered for (v1.0.2)
A stored key carried no record of which provider it belonged to. Options
saves {provider, model} without the key whenever the field is blank (which
it always is after a save, since it shows the encrypted badge instead), so
switching provider left the previous provider's key attached to the new one.
Every call then failed with that provider's own "Invalid API Key" while the
UI still showed a key as configured.

- types.ts: new `keyProvider` storage field, added to CONFIG_STORAGE_KEYS
- background.ts: keyProviderMismatch() guards the chat, COPY_AS and
  stored-key LIST_MODELS paths; absent keyProvider (pre-upgrade) is allowed
- Options.tsx: stamps keyProvider on every save; drops the encrypted badge
  and requires a new key when the saved one belongs to another provider or
  is rejected; save-time guard messages are now actually rendered (they were
  gated on modelsStatus === 'error' and never drew, so Save looked dead)
- providers.ts: providerLabel(); settings hint appended to 401/403 only;
  listModels reports keyRejected and labels errors with the display name
- Anthropic: send anthropic-dangerous-direct-browser-access on the chat path

Docs: CLAUDE.md version-bump rule corrected — wxt.config.ts reads
pkg.version, so package.json is the only place to edit.

typecheck clean, 58/58 tests, build clean (281.72 kB, manifest 1.0.2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:50:07 +08:00

14 KiB
Raw Blame History

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

  1. Evidence before inference. Read the relevant entrypoints/tests before changing them; report actual command output.
  2. Smallest useful diff. Be surgical — match existing style, don't reformat or broaden scope.
  3. Artifacts beat chat. Record state in docs/ (brief, decisions, tasks, handoff) so a fresh session resumes from files, not history.
  4. Preserve the invariants. The message contract, snapshot-before-await, data-lexai guard, and key handling break silently — see ## Lessons.
  5. Use code for deterministic work. Prefer typecheck/test/build over reasoning about correctness.
  6. 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
  MEMORY.md            # curated durable knowledge; loaded every session; capped (60 lines)
  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 in entrypoints/.
  • React 18 + TypeScript — Options and Popup pages only.
  • 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) 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. Install triggers postinstall: wxt prepare, which generates .wxt/types/typecheck fails without it (import.meta.env is typed there). If a fresh clone errors with Property 'env' does not exist on type 'ImportMeta', run npx wxt prepare.

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 (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_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. 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 both. This is a known smell — refactor tracked in docs/TASKS.md (T-04).
  • 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, but should be gated behind a DEV flag before release (T-03).

Testing notes

  • tests/unit/setup.ts mocks global.chrome. Unit tests currently exercise storage mocks rather than importing the real handlers — see docs/TASKS.md T-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 — on v*.*.* tag: build → upload → publish to Chrome Web Store.
  • Both send Telegram notifications. Secrets: GITEATOKEN, CWS_*, TELEGRAM_*.

Version bumps: edit version in package.json only — wxt.config.ts reads pkg.version, so the manifest follows automatically (T-16 done). Use npm version <x.y.z> --no-git-tag-version so package-lock.json stays in sync. A v*.*.* git tag triggers the store deploy and publishes it live (deploy-chrome.yml:91).

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, memory-sync, 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 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.

Lessons

Codebase invariants that break silently when violated (detail + evidence in docs/LESSONS_LEARNED.md):

  • Keep return true in the onMessage listener — else every async response is dropped.
  • Snapshot selection before any await; handle textarea/input and contenteditable/Range paths.
  • Keep both ANALYZE_TEXT shapes ({payload} and flat) and the fixgrammar normalization.
  • Set data-lexai="true" on every injected node; skip events on closest('[data-lexai="true"]').
  • Never log or transmit the API key except to the user's provider; keep the plaintext apiKey fallback until a migration exists.
  • Update both callX and callXWithPrompt when changing a provider's request shape.
  • Never fetch a provider from content/popup — route through the background worker.
  • Bump version in package.json only (+ lockfile); the manifest derives it via pkg.version.
  • Style inline; Tailwind classes do nothing until PostCSS is wired.

Memory protocol

Project knowledge lives in layers; write each item to exactly one and link instead of duplicating:

Layer Holds Written when
docs/MEMORY.md durable facts, conventions, environment quirks, key paths a fresh agent would waste tokens rediscovering it
docs/HANDOFF.md current state and next action only end of every substantial task
docs/DECISIONS.md why a choice was made a hard-to-reverse choice is made
docs/LESSONS_LEARNED.md + ## Lessons verified mistakes and their preventions a learning signal is verified

Context budget caps (session-start files are a recurring token tax): ## Lessons ≤ 12 rules, docs/MEMORY.md ≤ 60 entry lines, docs/HANDOFF.md ≤ 25 lines, docs/TASKS.md Active ≤ 7 contracts. When a cap is hit, run /memory-sync to consolidate/archive before adding — never grow past the cap.

State continuity

  • On a fresh/compacted/interrupted session, invoke /resume-project before planning or editing.
  • Before ending a substantial task, update docs/HANDOFF.md (verified state, changed paths, checks, risks, next action) and promote any durable new knowledge into docs/MEMORY.md per the memory protocol.
  • Run /memory-sync at a phase change, before ending a long run, or when a capped file is full.
  • After a verified recurring mistake or workflow gap, invoke /continuous-improvement; durable agent/skill changes go through system-steward.