Files
LexAI/CLAUDE.md
john kevin asprec c2ca659a2b
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 1m15s
feat: Enhance project memory and skills documentation; add memory-sync skill for knowledge consolidation
2026-07-15 16:01:13 +08:00

191 lines
13 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
> 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`.