191 lines
13 KiB
Markdown
191 lines
13 KiB
Markdown
# 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
|
||
|
||
```text
|
||
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-util** — `secretbox` 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
|
||
|
||
```bash
|
||
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 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_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:** 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`, `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 `fix`→`grammar` 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 both `package.json` and `wxt.config.ts`.
|
||
- 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`.
|