feat: Implement Prompt Builder functionality in Popup and Options
Some checks failed
CI — Test & Build / Test & Build (push) Failing after 39s
Some checks failed
CI — Test & Build / Test & Build (push) Failing after 39s
- 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.
This commit is contained in:
72
docs/ARCHITECTURE.md
Normal file
72
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Architecture — LexAI
|
||||
|
||||
> The current system and its important boundaries. Update in the same change that alters behavior; log the reason in `DECISIONS.md`.
|
||||
|
||||
## System at a glance
|
||||
|
||||
- **Shape:** Browser extension (Chrome, Manifest V3) with three cooperating contexts message-passed over `chrome.runtime`. No backend.
|
||||
- **Stack:** WXT `^0.20` (wraps Vite), React 18 + TypeScript (Options/Popup pages only), tweetnacl/tweetnacl-util for key encryption, Zustand installed but unused.
|
||||
- **Data stores:** `chrome.storage.local` (provider, model, `apiKeyEnc`, `encKey`, legacy plaintext `apiKey`). No server, no DB.
|
||||
- **Hosting / deploy:** Chrome Web Store. Build output `.output/chrome-mv3/`.
|
||||
- **Build / release:** Gitea CI (`.gitea/workflows/`) → typecheck → test → build → zip to Gitea registry; `v*.*.*` tag → Chrome Web Store deploy.
|
||||
|
||||
## Component map
|
||||
|
||||
```text
|
||||
entrypoints/content.ts (content script, injected into <all_urls>)
|
||||
• Detects selection: textarea/input (selectionStart/End) vs contenteditable/DOM (Range API)
|
||||
• Renders floating toolbar + result modal + toasts (inline-styled, appended to document.body)
|
||||
• Snapshots selection state BEFORE any async call; Replace uses the snapshot
|
||||
• Sends { type: 'ANALYZE_TEXT', payload: {text, action, style} } to background
|
||||
|
||||
entrypoints/background.ts (service worker — the LLM proxy)
|
||||
• onMessage: ANALYZE_TEXT and COPY_AS (returns true to keep async channel open)
|
||||
• Reads provider/apiKey/apiKeyEnc/encKey/model from chrome.storage.local
|
||||
• Decrypts key (tweetnacl secretbox), routes to the correct provider fetch
|
||||
• Registers right-click context menus (action × style) on install
|
||||
|
||||
entrypoints/options/Options.tsx (React settings page)
|
||||
• Provider + model + API key form; encrypts key → apiKeyEnc/encKey in storage
|
||||
|
||||
entrypoints/popup/Popup.tsx (React toolbar popup)
|
||||
• Standalone text box → same ANALYZE_TEXT flow; shows config status; links to Options
|
||||
```
|
||||
|
||||
| Component | Responsibility | Owns (paths) | Talks to | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| content script | selection, toolbar/modal UI, replace | `entrypoints/content.ts` | background via messages | DOM-timing-sensitive; snapshot before await |
|
||||
| background SW | LLM proxy, key decrypt, routing, context menus | `entrypoints/background.ts` | provider APIs, storage | only context allowed to fetch providers |
|
||||
| options page | provider/model/key config + encrypt | `entrypoints/options/Options.tsx` | storage | React |
|
||||
| popup | standalone analyze + status | `entrypoints/popup/Popup.tsx` | background via messages | React |
|
||||
| shared utils | reference notes, future shared code | `src/utils/**` | — | see REFERENCE_NOTES.md |
|
||||
|
||||
## Boundaries and contracts
|
||||
|
||||
- **CORS/key boundary:** Content script and popup **must not** call provider APIs. All provider `fetch` and key handling live in `background.ts`. Route through `ANALYZE_TEXT` / `COPY_AS`.
|
||||
- **Message contract:**
|
||||
- `ANALYZE_TEXT` accepts **both** `{ payload: {text, action, style} }` and flat `{ text, action, style }` — keep both if you touch the handler.
|
||||
- `action` ∈ `grammar | rephrase | shorten | expand | explain`. Context menu/popup emit `fix`, normalized to `grammar` by `getSystemPrompt`.
|
||||
- The `onMessage` listener **must `return true`** to keep the async channel open; removing it silently breaks every response.
|
||||
- **Provider layer:** Each provider is duplicated — `callX` (system prompt from action) and `callXWithPrompt` (arbitrary prompt, used by COPY_AS). Request-shape changes usually need both. (Known smell — see DECISIONS + TASKS.)
|
||||
- **DOM guard:** Every injected node carries `data-lexai="true"`; handlers check `closest('[data-lexai="true"]')` to avoid self-triggering. `z-index: 2147483647` keeps UI above host pages.
|
||||
|
||||
## Data model (essentials)
|
||||
|
||||
- **Storage keys:** `provider`, `model`, `apiKeyEnc`, `encKey`, `apiKey` (legacy plaintext, back-compat only).
|
||||
- **Sensitive data:** the LLM API key. Prefer the encrypted path; never log it; never transmit except to the user's selected provider. Don't drop the plaintext fallback without a migration.
|
||||
|
||||
## Cross-cutting concerns
|
||||
|
||||
- **Config/secrets:** provider list + default models + endpoints currently duplicated across `Options.tsx` and `background.ts` (drift risk — see TASKS).
|
||||
- **Observability:** intentional `[LexAI …]` console logs in content.ts replace path (should be gated behind a DEV flag — see TASKS).
|
||||
- **Testing:** Vitest (jsdom) unit + Playwright e2e. Unit tests currently exercise the `chrome.storage` mock rather than importing real handlers; e2e has `[EXTENSION_ID]` placeholders and won't pass as-is (see TASKS/EVALS).
|
||||
|
||||
## Known constraints and debt
|
||||
|
||||
- Tailwind inactive; all UI is inline style objects (WXT PostCSS never wired). Do not assume Tailwind classes work.
|
||||
- No shadow DOM; UI injected directly into `document.body`, isolated only by `data-lexai` + max z-index.
|
||||
- Version bump is a manual two-file edit (`package.json` + `wxt.config.ts`).
|
||||
|
||||
---
|
||||
|
||||
*Record non-obvious choices in `DECISIONS.md`; keep exposure current in `attacksurface.md`.*
|
||||
78
docs/DECISIONS.md
Normal file
78
docs/DECISIONS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Decisions — LexAI
|
||||
|
||||
> Lightweight ADRs. One entry per material decision: what, why, what was rejected, when. Newest at top. Supersede rather than rewrite.
|
||||
|
||||
## Log
|
||||
|
||||
### D-2026-03-06-01 — BYO-LLM-key, no backend
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Hosted writing assistants cost a subscription and route user text through a third party. Target users already hold LLM API keys.
|
||||
- **Decision:** No LexAI server. The background service worker calls the user's chosen provider (OpenAI/Anthropic/Groq/OpenRouter) directly with the user's key.
|
||||
- **Alternatives considered:** A thin proxy backend (rejected: adds cost, privacy surface, and an account system); on-device browser AI only (rejected: too limited across providers — noted as a Proofly reference pattern).
|
||||
- **Consequences:** Zero server cost and strong privacy story; shifts key handling and CORS entirely into the extension; no server-side rate limiting or abuse controls.
|
||||
- **Verification:** Manual — confirm no network calls leave the extension except to the selected provider endpoint.
|
||||
- **Owner / date:** Phase 1, 2026-03-06
|
||||
|
||||
### D-2026-03-06-02 — Inline styles, no Tailwind
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Content-script UI must not be broken by host-page CSS; WXT PostCSS/Tailwind integration was not wired.
|
||||
- **Decision:** Style all UI with inline style objects (`Object.assign(el.style, …)` / `style={{…}}`), using a dark Catppuccin-ish palette.
|
||||
- **Alternatives considered:** Tailwind (left in devDependencies but inactive); shadow DOM + stylesheet (deferred — see D-...-03).
|
||||
- **Consequences:** Reliable rendering on any host page; palette/button styles get duplicated across content/Options/Popup (refactor tracked in TASKS #5).
|
||||
- **Verification:** Visual check on multiple sites.
|
||||
- **Owner / date:** Phase 1, 2026-03-06
|
||||
|
||||
### D-2026-03-06-03 — No shadow DOM; `data-lexai` guard instead
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Injected toolbar/modal could collide with host-page styles or re-trigger LexAI's own handlers.
|
||||
- **Decision:** Inject directly into `document.body`; mark every LexAI node `data-lexai="true"` and skip events whose target is `closest('[data-lexai="true"]')`; use max `z-index` (2147483647).
|
||||
- **Alternatives considered:** Shadow DOM (rejected for now: added complexity; revisit if style isolation issues appear).
|
||||
- **Consequences:** Simple and working; weaker isolation than shadow DOM; highly customized editors (e.g. Google Docs) may not accept programmatic replace.
|
||||
- **Verification:** Manual across textarea/input/contenteditable sites.
|
||||
- **Owner / date:** Phase 1, 2026-03-06
|
||||
|
||||
### D-2026-03-06-04 — Eager selection snapshot before async
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Focus shifts to the toolbar and the live selection is gone by the time an async provider response returns.
|
||||
- **Decision:** Capture the active element + selection offsets eagerly (on `mouseup` and on button `mousedown`) and snapshot before any `await`; Replace uses the snapshot. Handle both textarea/input (`selectionStart/End`) and contenteditable/DOM (`Range` API).
|
||||
- **Alternatives considered:** Re-reading selection after the response (rejected: selection no longer exists).
|
||||
- **Consequences:** Replace works reliably; the pattern is fragile — editing content.ts must preserve snapshot-before-await. Zero automated coverage today (TASKS #10).
|
||||
- **Owner / date:** Phase 1, 2026-03-06
|
||||
|
||||
### D-2026-03-06-05 — Dual `ANALYZE_TEXT` message shapes; `return true` listener
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Content script/popup send `{ payload: {...} }`; other call sites send flat `{ text, action, style }`. Async responses need the message channel held open.
|
||||
- **Decision:** The handler accepts both shapes; the `onMessage` listener returns `true`. `fix` normalizes to `grammar`.
|
||||
- **Consequences:** Flexible but must be preserved in both forms; removing `return true` silently breaks all responses.
|
||||
- **Owner / date:** Phase 1, 2026-03-06
|
||||
|
||||
### D-2026-xx-xx-06 — tweetnacl secretbox for the API key (obfuscation, not protection)
|
||||
|
||||
- **Status:** accepted — flagged for revisit
|
||||
- **Context:** Storing the raw key in `chrome.storage.local` looked bad; added tweetnacl `secretbox` encryption (`apiKeyEnc` + `encKey`).
|
||||
- **Decision:** Prefer the encrypted path; keep plaintext `apiKey` as back-compat until a migration exists.
|
||||
- **Known weakness:** `encKey` is stored next to `apiKeyEnc`, so anyone who can read storage can decrypt. This is obfuscation, not protection (RECOMMENDATIONS #2).
|
||||
- **Alternatives to consider:** derive the key from `chrome.storage.session` / WebCrypto / a user passphrase; and be honest in the UI ("stored locally, obscured"). Tracked in TASKS #2.
|
||||
- **Owner / date:** post-Phase 1
|
||||
|
||||
### D-2026-03-06-07 — Gitea CI + Chrome Web Store deploy
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Project hosts CI on Gitea, not GitHub Actions.
|
||||
- **Decision:** `.gitea/workflows/ci.yml` (typecheck→test→build→zip to registry) and `deploy-chrome.yml` (on `v*.*.*` tag → CWS). Telegram notifications. Version must match in `package.json` and `wxt.config.ts`.
|
||||
- **Known weakness:** workflows `git clone` into `/tmp` and set `http.sslVerify false` (RECOMMENDATIONS #17). Revisit for speed/security.
|
||||
- **Owner / date:** Phase 1, 2026-03-06
|
||||
|
||||
## Open / proposed
|
||||
|
||||
### D-PROPOSED — Narrow host permissions from `<all_urls>`
|
||||
|
||||
- **Status:** proposed (decide before serious Web Store push)
|
||||
- **Context:** Content script injects into every frame of every site, including banking/email/internal apps; also the #1 CWS review slowdown (RECOMMENDATIONS #1).
|
||||
- **Options:** `activeTab` + on-demand injection, or a user-configurable allowlist.
|
||||
- **Verification:** confirm actions still work after narrowing; measure review outcome.
|
||||
48
docs/EVALS.md
Normal file
48
docs/EVALS.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Project evaluations — LexAI
|
||||
|
||||
> Small, repeatable checks. Prefer a deterministic command or test over a prose reminder. The standing checks below are the baseline gates for any change.
|
||||
|
||||
## Standing gates (run on every change)
|
||||
|
||||
### E-BASE-01 — Typecheck
|
||||
|
||||
- **How to run:** `npm run typecheck`
|
||||
- **Pass condition:** `tsc --noEmit` exits 0.
|
||||
- **Cost:** fast.
|
||||
|
||||
### E-BASE-02 — Unit tests
|
||||
|
||||
- **How to run:** `npm test -- --run`
|
||||
- **Pass condition:** vitest exits 0.
|
||||
- **Note:** current unit tests exercise the `chrome.storage` mock, not the real handlers — passing does **not** prove provider routing or key decrypt. See TASKS #8.
|
||||
|
||||
### E-BASE-03 — Production build
|
||||
|
||||
- **How to run:** `npm run build`
|
||||
- **Pass condition:** builds to `.output/chrome-mv3/`; bundle roughly ~166 KB baseline.
|
||||
- **Cost:** fast (~3s).
|
||||
|
||||
### E-BASE-04 — Manual real-page check (behavior changes)
|
||||
|
||||
- **How to run:** `npm run build` → load unpacked `.output/chrome-mv3` → select text on a textarea and a contenteditable site → run an action → Replace.
|
||||
- **Pass condition:** toolbar shows, result modal returns, Replace edits both target types.
|
||||
- **Why manual:** selection/replace is DOM-timing-sensitive and has no automated coverage.
|
||||
|
||||
## Active failure-derived checks
|
||||
|
||||
_No failure-derived checks yet. Add one here when a verified regression gives a deterministic trigger — e.g. a guard that fails if the built `content.js` still contains `[LexAI` logs (T-03), or a test asserting the `onMessage` listener returns `true`._
|
||||
|
||||
## Eval template
|
||||
|
||||
```markdown
|
||||
### E-YYYY-MM-DD-NN — [short check name]
|
||||
- **Prevents:** [lesson ID and failure mode]
|
||||
- **How to run:** `[exact command or steps]`
|
||||
- **Pass condition:** [observable]
|
||||
- **Cost:** fast | moderate | expensive
|
||||
- **Last verified:** [date + result]
|
||||
```
|
||||
|
||||
## Retired checks
|
||||
|
||||
_Move obsolete checks here with the reason and the lesson they covered._
|
||||
27
docs/HANDOFF.md
Normal file
27
docs/HANDOFF.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Handoff — LexAI
|
||||
|
||||
## Current state
|
||||
|
||||
- **Outcome:** Phase 1 complete (7 workitems, 2026-03-06). Codebase reviewed 2026-07-13 (`RECOMMENDATIONS.md`). Operating-system docs + agent roster aligned to the template 2026-07-15.
|
||||
- **Delivered:** working MV3 extension — selection detection (textarea/input/contenteditable), floating toolbar, background LLM proxy with OpenAI/Anthropic/Groq/OpenRouter, Options page, result modal with Replace/Copy. Build ~166 KB.
|
||||
- **Verified (Phase 1):** `npm run build` clean. Note: unit tests currently exercise the storage mock, not the real handlers; e2e has `[EXTENSION_ID]` placeholders and won't pass as-is.
|
||||
- **Verified (2026-07-15 finalize/build):** `npm run typecheck` clean, `npm test -- --run` 46/46 passing (actions, messaging, crypto, providers), `npm run build` clean → `.output/chrome-mv3/` (265.82 kB) ready for load-unpacked testing. Recent refactor series (crypto consolidation, provider adapter table, context-menu registry, dev-gated debug logs) all pass gates; real-page Replace verification still pending on the user's load-unpacked check.
|
||||
- **Fix (2026-07-15):** OpenAI adapter now sends `max_completion_tokens` instead of the legacy `max_tokens` (newer OpenAI models reject it) and omits `temperature` for reasoning models (`o*`/`gpt-5*`, which only accept the default). Groq/OpenRouter unchanged (they still expect `max_tokens`). Pinned tests updated + new reasoning-model test; typecheck/47 tests/build all green; rebuilt `.output/chrome-mv3/`.
|
||||
- **Feature (2026-07-15):** Options form reordered to Provider → API Key → Model (model list is fetched with the key). New `prompt` action ("Make Prompt", prompt-engineer): added to `ACTIONS`/labels (context menus follow automatically), prompt-engineer system prompt in `getSystemPrompt` with a prompt-directed style modifier, toolbar button (🪄 Prompt) in content.ts, popup button. Tests updated (registry count now derived; new getSystemPrompt cases); typecheck/48 tests/build green.
|
||||
- **Feature (2026-07-15, Prompt Builder):** dedicated popup section for the `prompt` action with its own parameters — Prompt Style (Auto/Instructional/Role-play/Step-by-step/Few-shot/Structured), Persona (presets + Custom free text + None), Output Format (Auto/Plain/Markdown/Bulleted/Numbered/JSON/Table). Constants in `src/lib/actions.ts`, `PromptParams` added to `AnalyzePayload` (both message shapes still supported), composed into the system prompt by `promptParamModifiers` in providers.ts (prompt action only; 'Auto' = no-op). Params persist in `chrome.storage.local`. Toolbar 🪄 Prompt keeps Auto defaults. typecheck/49 tests/build green.
|
||||
- **Feature (2026-07-15, popup tabs + model picker):** popup restructured into two tabs — "✍ Writing" (style + fix/rephrase/shorten/expand) and "🪄 Prompt Builder" (prompt params + Make Prompt). Prompt Builder gained a Model picker: fetched via `LIST_MODELS` on first tab open, '' = configured default; selection sent as new optional `model` override on `AnalyzePayload` (both message shapes), applied in background's `handleAnalyzeText`. Tab + model persist in storage. typecheck/49 tests/build green.
|
||||
- **Tweak (2026-07-15):** model picker's "Default (model)" label → plain "Default" (badge already shows the model). Toolbar/context-menu `prompt` requests now inherit the saved Prompt Builder settings: background's `handleAnalyzeText` loads `promptStyle/promptPersona/customPersona/promptFormat/promptModel` from storage when the payload has no `promptParams` (popup still sends explicit ones); persona resolution shared via `resolvePromptPersona` in actions.ts. typecheck/49 tests/build green.
|
||||
- **Feature (2026-07-15, in-page Prompt Builder dialog):** toolbar 🪄 Prompt and context-menu "Make Prompt" now open an on-page dialog (content.ts `showPromptBuilderDialog`) with the popup's parameters (Style/Persona+custom/Format/Model); selections persist to the shared storage keys, then the request runs without explicit params (background applies saved). Context menu: prompt is now a single item (no style children — registry excludes it; tests updated). `LIST_MODELS` resolves provider from storage when omitted. Toolbar re-clamps position using its real width so the last buttons stay on-screen. typecheck/49 tests/build green. Needs a real-page check (dialog + replace are DOM-timing-sensitive).
|
||||
- **Fix (2026-07-15, prompt UX chain):** Make Prompt no longer closes the dialog silently — the dialog becomes a "⟳ Building your prompt…" spinner and `runAction` closes it (`closePromptBuilder`) at every completion path (success, error, invalidated-context). Prompt result modal is prompt-specific: "🪄 Engineered Prompt" title, and the writing-style selector row is replaced by "✎ Edit Parameters" (reopens the builder dialog) + Regenerate (re-runs with saved builder params). typecheck/49 tests/build green.
|
||||
- **Security/perf pass (2026-07-15, goal-driven audit):** reviewed key safety, user-text privacy, and content-script performance; fixed:
|
||||
- `src/lib/crypto.ts` — new `migratePlaintextApiKey()`: background auto-encrypts a legacy plaintext `apiKey` on worker start and removes the plaintext (write-and-await key material before delete; verify-decrypt before dropping plaintext when an encrypted key already exists; re-check for a concurrent Options save before writing). Read-path plaintext fallback in `resolveApiKey` retained per invariant.
|
||||
- `entrypoints/background.ts` — calls the migration on startup; `sender.id !== chrome.runtime.id` guard on `onMessage` (defense-in-depth; internal senders unaffected).
|
||||
- `entrypoints/content.ts` — fixed unbounded document-listener leak: `showToolbar` added `click`/`scroll` listeners per selection and never removed them; now unregistered in `hideToolbar` (also closes an orphaned More-menu). Error toast gained `data-lexai`; mouseup threshold now uses `MIN_SELECTION_LENGTH` (was hardcoded `<= 10`, which ate exactly-10-char selections); same sender guard on its listener.
|
||||
- `src/lib/providers.ts` — `callProvider` guards `res.json()` so non-JSON gateway errors (HTML 502) surface as `"<Provider> error: HTTP <status>"` instead of a raw SyntaxError.
|
||||
- Verified: typecheck clean, 54/54 tests (6 new: 4 migration, 1 non-JSON error, plus existing), build clean (280.3 kB). `security-auditor` reviewed the key-path diff: PASS; its two P3 hardening notes (await encKey persistence, concurrent-save re-check) were implemented and re-gated. User-text privacy audited clean: no persistence of analyzed text, debug logs dev-gated, key/text travel only to the chosen provider. Real-page load-unpacked check of toolbar/replace still recommended (DOM-timing paths untouched except listener cleanup).
|
||||
- **Changed paths (this alignment):** added `docs/` (brief, architecture, decisions, tasks, evals, lessons, handoff, self-model, attacksurface), ported `.claude/agents/*` roster + `.claude/skills/*`, kept `lexai-extension-dev`, updated `.claude/AGENTS.md`. `CLAUDE.md` restructured to the operating-system format (all original LexAI rules preserved).
|
||||
- **Open risks (ranked):**
|
||||
1. `<all_urls>` host permission — privacy surface + CWS review blocker (TASKS #1).
|
||||
2. Key "encryption" is obfuscation (`encKey` co-located) — TASKS #2.
|
||||
3. Tests don't cover real code paths (TASKS #8) or DOM replace (TASKS #10).
|
||||
- **Next smallest action:** run the quick wins in order — T-03 (gate debug logs), then T-06/T-09/T-15/T-16 — each is small and independent. Do T-01/T-02 before any Chrome Web Store push.
|
||||
70
docs/LESSONS_LEARNED.md
Normal file
70
docs/LESSONS_LEARNED.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Lessons learned — LexAI
|
||||
|
||||
> Evidence-backed invariants and guardrails for this codebase. Not a transcript or issue tracker. The one-line active rules live in `CLAUDE.md` → `## Lessons`; the detail lives here.
|
||||
|
||||
## Active guardrails
|
||||
|
||||
`CLAUDE.md` → `## Lessons` is the canonical active list loaded every session. The entries below are the established codebase invariants (from Phase 1 and the 2026-07-13 code read) that break things silently when violated.
|
||||
|
||||
### L-CORE-01 — `onMessage` listener must `return true`
|
||||
|
||||
- **Root cause / failure boundary:** async provider responses need the message channel held open; a listener that doesn't `return true` drops every response with no error.
|
||||
- **Prevention:** never remove `return true` from the `chrome.runtime.onMessage` handler in `background.ts`.
|
||||
- **Eval:** manual (candidate: a unit test asserting the listener returns `true`).
|
||||
|
||||
### L-CORE-02 — Snapshot selection before any `await`
|
||||
|
||||
- **Root cause / failure boundary:** focus shifts to the toolbar and the live selection is gone by the time an async response returns.
|
||||
- **Prevention:** in `content.ts`, capture active element + offsets eagerly (mouseup + button mousedown) and snapshot before awaiting; Replace uses the snapshot. Handle textarea/input (`selectionStart/End`) **and** contenteditable/DOM (`Range` API).
|
||||
- **Eval:** E-BASE-04 manual; DOM test tracked in TASKS #10.
|
||||
|
||||
### L-CORE-03 — Keep both `ANALYZE_TEXT` message shapes
|
||||
|
||||
- **Root cause / failure boundary:** callers send both `{ payload: {…} }` and flat `{ text, action, style }`; dropping either breaks a call path. `fix` normalizes to `grammar`.
|
||||
- **Prevention:** if you touch the handler, keep both shapes and the action normalization.
|
||||
|
||||
### L-CORE-04 — Preserve the `data-lexai="true"` guard
|
||||
|
||||
- **Root cause / failure boundary:** without it, LexAI's own injected UI re-triggers selection/click handlers.
|
||||
- **Prevention:** set `data-lexai="true"` on every injected node; handlers skip `target.closest('[data-lexai="true"]')`.
|
||||
|
||||
### L-CORE-05 — Never expose the API key; keep the plaintext fallback
|
||||
|
||||
- **Root cause / failure boundary:** the key is a user secret; and legacy installs still have plaintext `apiKey`.
|
||||
- **Prevention:** prefer `apiKeyEnc` + `encKey`; never log the key; never send it anywhere except the user's selected provider endpoint; don't drop the plaintext `apiKey` fallback without a migration.
|
||||
|
||||
### L-CORE-06 — Provider code is duplicated (`callX` + `callXWithPrompt`)
|
||||
|
||||
- **Root cause / failure boundary:** each provider has two near-identical functions; a request-shape change to one silently diverges from the other.
|
||||
- **Prevention:** update both until the layer is refactored (TASKS #4). Keep error handling uniform (network → friendly string; `!res.ok` → provider message; empty → explicit message).
|
||||
|
||||
### L-CORE-07 — Content script / popup must not call providers
|
||||
|
||||
- **Root cause / failure boundary:** CORS and key handling belong in the service worker; a direct provider `fetch` from content/popup leaks the key path and fails CORS.
|
||||
- **Prevention:** route everything through `ANALYZE_TEXT` / `COPY_AS` to `background.ts`.
|
||||
|
||||
### L-CORE-08 — Version lives in two files
|
||||
|
||||
- **Root cause / failure boundary:** manifest version comes from `wxt.config.ts`; `package.json` has its own — they drift and have caused git churn.
|
||||
- **Prevention:** bump `version` in **both** `package.json` and `wxt.config.ts` (until T-16 single-sources it). A `v*.*.*` tag triggers the CWS deploy.
|
||||
|
||||
### L-CORE-09 — UI is inline styles; Tailwind is inactive
|
||||
|
||||
- **Root cause / failure boundary:** Tailwind is installed but WXT PostCSS was never wired; Tailwind classes silently do nothing.
|
||||
- **Prevention:** style with inline objects and the existing dark palette; don't add Tailwind classes unless the task is explicitly to wire PostCSS.
|
||||
|
||||
## Recording policy
|
||||
|
||||
Add a lesson only after a material, evidenced learning signal (correction, unexpected failure, regression, rejected review, proven wrong assumption). Each needs a durable prevention; link a deterministic eval when possible. No secrets, credentials, personal data, or raw transcripts.
|
||||
|
||||
## Lesson template
|
||||
|
||||
```markdown
|
||||
### L-YYYY-MM-DD-NN — [short imperative guardrail]
|
||||
- **Status:** active | archived | superseded by [ID]
|
||||
- **Trigger / Root cause / Prevention / Evidence / Eval / Owner-review**
|
||||
```
|
||||
|
||||
## Archive
|
||||
|
||||
_Historical lessons move here with their original IDs and a one-line archival reason._
|
||||
52
docs/PROJECT_BRIEF.md
Normal file
52
docs/PROJECT_BRIEF.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Project brief — LexAI
|
||||
|
||||
> Source of truth for *what* LexAI is and *why*. Keep it under two screens; link out for detail.
|
||||
|
||||
## Outcome
|
||||
|
||||
- **One-line product:** A Grammarly-like Chrome extension (Manifest V3) that gives AI writing help — grammar fix, rephrase, shorten, expand, explain — on any webpage, using the user's own LLM API key.
|
||||
- **Measurable outcome:** A user can select text on any page, pick an action from the floating toolbar (or right-click menu / popup), and replace or copy an AI-improved version — with no LexAI backend and no subscription.
|
||||
- **Primary user:** Individuals who already hold an LLM API key (OpenAI / Anthropic / Groq / OpenRouter) and want inline writing assistance without paying a SaaS subscription or sending text through a third-party server.
|
||||
- **Why now:** BYO-key removes the cost and privacy objections to hosted writing assistants; MV3 + WXT makes a lightweight, serverless extension practical.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No LexAI backend, account system, or subscription. The extension talks directly to the user's chosen provider.
|
||||
- Not a full document editor; it augments existing page inputs (textarea/input/contenteditable).
|
||||
- No telemetry or transmission of user text anywhere except the user-selected provider endpoint.
|
||||
- Not (yet) streaming, autocomplete, tone profiles, or custom style profiles — those are roadmap.
|
||||
|
||||
## Acceptance tests
|
||||
|
||||
1. `npm run typecheck` and `npm test -- --run` pass.
|
||||
2. `npm run build` produces a loadable `.output/chrome-mv3/` bundle (~166 KB baseline).
|
||||
3. Loaded unpacked, selecting text on a page shows the toolbar; an action returns a result modal; Replace edits both textarea/input and contenteditable targets.
|
||||
4. API key is stored via the encrypted path (`apiKeyEnc` + `encKey`) and never logged or sent anywhere but the provider endpoint.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Stack:** WXT `^0.20` (Vite), React 18 + TypeScript (Options/Popup only), tweetnacl for key encryption. Tailwind is installed but **inactive** — all UI is inline styles.
|
||||
- **Runtime:** Node 22 (CI pins `node:22-bookworm`). `npm install` required before any `npm run *`.
|
||||
- **Security/compliance:** Handles a user secret (LLM API key) and reads page-selected text. Manifest currently requests `<all_urls>` — a Chrome Web Store review risk (see `attacksurface.md`).
|
||||
- **Release:** CI is **Gitea** (`.gitea/workflows/`), not GitHub Actions. Version must match in `package.json` and `wxt.config.ts`; a `v*.*.*` tag deploys to the Chrome Web Store.
|
||||
|
||||
## Stakeholders
|
||||
|
||||
| Role | Who | Decision authority |
|
||||
| --- | --- | --- |
|
||||
| Owner / maintainer | John Kevin Asprec | scope, priorities, release |
|
||||
| Project tracking | Plane (LEXAI project) | https://plane-pro.juankibin.space |
|
||||
|
||||
## Unknowns
|
||||
|
||||
- Whether to narrow host permissions to `activeTab`/allowlist before a serious Web Store push (see Decisions + attack surface).
|
||||
- Whether the current tweetnacl approach should be replaced given `encKey` is co-located with the ciphertext (it is obfuscation, not protection).
|
||||
|
||||
## Source of truth
|
||||
|
||||
- **Issue tracker:** Plane — LEXAI project (link above).
|
||||
- **This repo:** entrypoints in `entrypoints/`, shared code in `src/`, tests in `tests/`. `CLAUDE.md` is the working guide for architecture and conventions.
|
||||
|
||||
---
|
||||
|
||||
*Related: `ARCHITECTURE.md`, `DECISIONS.md`, `TASKS.md` (from RECOMMENDATIONS), `attacksurface.md`, `SELF_MODEL.md`.*
|
||||
38
docs/SELF_MODEL.md
Normal file
38
docs/SELF_MODEL.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Self-model — LexAI
|
||||
|
||||
> What the harness believes about the operator and this project. Kept honest by `self-model-audit`. No secrets or sensitive personal data.
|
||||
|
||||
## Operator
|
||||
|
||||
- **Who I'm building for:** John Kevin Asprec — owner/maintainer of LexAI.
|
||||
- **Working style:** ships in focused phases (Phase 1 delivered 7 workitems to a deadline); values concise, direct output over verbose explanation; comfortable with the code and the toolchain.
|
||||
- **Communication preferences:** concise and direct; minimal formatting; prefers the point over the preamble.
|
||||
- **Technical depth:** high — WXT/MV3, TypeScript, React, CI/CD. Wants surgical diffs and real verification, not hand-holding.
|
||||
- **Decision authority kept:** manifest permission changes, key-handling changes, releases (version bump + `v*.*.*` tag), and anything touching the Web Store listing.
|
||||
|
||||
## Project intent (the real one)
|
||||
|
||||
- **Optimizing for:** a genuinely useful, private, subscription-free writing assistant that runs on the user's own key — shipped to the Chrome Web Store.
|
||||
- **What "good" means here:** typecheck + tests + build green, real-page behavior verified, minimal diffs, invariants preserved (see LESSONS_LEARNED), key never exposed.
|
||||
- **Non-negotiable constraints:** no backend; never transmit user text or key anywhere but the chosen provider; inline styles until PostCSS is deliberately wired.
|
||||
|
||||
## Voice (if the harness writes as the operator)
|
||||
|
||||
- **Sounds like:** direct, technical, no filler.
|
||||
- **Never sounds like:** marketing fluff, over-hedged, or padded with obvious restatement.
|
||||
|
||||
## Known drift risks
|
||||
|
||||
- "API key is encrypted" — the current tweetnacl approach is obfuscation, not protection; don't let docs or UI over-claim (see attacksurface + TASKS #2).
|
||||
- Phase-1 framing may go stale as recommendations land; re-read `TASKS.md` state before assuming what's done.
|
||||
- Tailwind is present but inactive — don't infer a Tailwind workflow from its presence in devDependencies.
|
||||
|
||||
## Change log
|
||||
|
||||
| Date | What changed in this model | Evidence |
|
||||
| --- | --- | --- |
|
||||
| 2026-07-15 | Initial capture from README, CLAUDE.md, PHASE1_SUMMARY, RECOMMENDATIONS | repo docs |
|
||||
|
||||
---
|
||||
|
||||
*Update via `self-model-audit` when behavior and this file diverge. Never store credentials, financial/health data, or anything not agreed to persist.*
|
||||
76
docs/TASKS.md
Normal file
76
docs/TASKS.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Tasks — LexAI
|
||||
|
||||
> Active task contracts, derived from `RECOMMENDATIONS.md` (full read 2026-07-13). Task numbers match the recommendation numbers for traceability. Completed contracts move to `HANDOFF.md`; durable choices move to `DECISIONS.md`.
|
||||
|
||||
## Suggested order (from RECOMMENDATIONS)
|
||||
|
||||
Quick wins first: **T-03, T-06, T-09, T-11, T-15, T-16** (all small, mostly independent). Then structural refactors **T-04, T-05, T-08**. Do **T-01 / T-02** (permissions + key story) before any serious Chrome Web Store push. Save **T-10, T-12, T-13** for a focused Phase 2.
|
||||
|
||||
## Active (next up — fully specified)
|
||||
|
||||
### T-03 — Gate debug logging behind a DEV flag
|
||||
|
||||
- **Status:** ready · **Owner:** lexai-extension-dev · **Effort:** S
|
||||
- **Goal:** Stop leaking selection text/element values to the host-page console in production.
|
||||
- **In scope:** `entrypoints/content.ts` `[LexAI …]` logs (captureForButton, Replace paths).
|
||||
- **Out of scope:** removing logs entirely; other files.
|
||||
- **Constraints:** keep logs available in dev; no behavior change.
|
||||
- **Deliverable:** logs wrapped in `import.meta.env.DEV` (or a `__DEV__` guard).
|
||||
- **Verification:** `npm run build` then grep the built `content.js` for `[LexAI` — none present; `npm run dev` still logs.
|
||||
- **Stop condition:** production bundle has no LexAI console output.
|
||||
|
||||
### T-06 — Remove or wire dead dependencies
|
||||
|
||||
- **Status:** ready · **Owner:** builder · **Effort:** S
|
||||
- **Goal:** Drop confusion and install weight from unused deps.
|
||||
- **In scope:** `zustand` (no store exists), `tailwindcss` + `autoprefixer` (inactive).
|
||||
- **Constraints:** if kept, they must be actually wired; otherwise remove from `package.json`.
|
||||
- **Deliverable:** updated `package.json` + lockfile, or a documented decision to wire them.
|
||||
- **Verification:** `npm install` && `npm run typecheck` && `npm run build` clean.
|
||||
- **Stop condition:** no installed-but-unused runtime deps remain unexplained.
|
||||
|
||||
### T-09 — Fix or quarantine the e2e suite
|
||||
|
||||
- **Status:** ready · **Owner:** lexai-extension-dev · **Effort:** S
|
||||
- **Goal:** Make CI green mean something.
|
||||
- **In scope:** `tests/e2e/extension.test.ts` hard-coded `chrome-extension://[EXTENSION_ID]/…`.
|
||||
- **Deliverable:** resolve the extension ID at runtime (from the service-worker target), or `.skip` the suite with a TODO until fixed.
|
||||
- **Verification:** `npm run build` && `npm run test:e2e` — passes or is cleanly skipped, not failing.
|
||||
- **Stop condition:** e2e no longer red for the placeholder reason.
|
||||
|
||||
### T-15 / T-16 — Pin toolchain & single-source the version
|
||||
|
||||
- **Status:** ready · **Owner:** builder · **Effort:** S
|
||||
- **Goal:** Prevent `npm run *` failing with no version guard, and prevent shipping mismatched versions.
|
||||
- **In scope:** add `engines`/confirm `.nvmrc` (Node 22) + `packageManager` field; make `wxt.config.ts` read `version` from `package.json` (or a bump script that writes both).
|
||||
- **Verification:** bump once; confirm `package.json` and the built `manifest.json` version match.
|
||||
- **Stop condition:** version is a single edit; toolchain pinned to CI's Node 22.
|
||||
|
||||
## Backlog (ready, from RECOMMENDATIONS)
|
||||
|
||||
| ID | Task | Theme | Effort |
|
||||
| --- | --- | --- | --- |
|
||||
| T-01 | Narrow host permissions from `<all_urls>` (activeTab / allowlist) — do before CWS push | Security | M |
|
||||
| T-02 | Fix the key story: don't co-locate `encKey` with ciphertext; be honest in UI ("stored locally, obscured") | Security | M |
|
||||
| T-04 | Collapse duplicated provider layer into `callProvider(config, messages/system, text)` + per-provider adapter | Maintainability | M |
|
||||
| T-05 | Extract shared theme/styles into `src/ui/theme.ts` (palette used across content/Options/Popup) | Maintainability | M |
|
||||
| T-07 | Centralize provider/model/endpoint config in one shared module (Options + background drift) | Maintainability | S |
|
||||
| T-08 | Unit-test real code: extract `getSystemPrompt`, `decryptApiKey`, provider router; test prompt normalization, encrypt→decrypt round-trip, routing, error extraction | Testing | M |
|
||||
| T-10 | Add content-script DOM test for selection→snapshot→replace (textarea + contenteditable) | Testing | L |
|
||||
| T-11 | Make `max_tokens` adaptive (scale with input length or expose in settings) — currently hard-coded 1024 | UX | S |
|
||||
| T-12 | Add response streaming into the modal | UX | L |
|
||||
| T-13 | Accessibility: aria-labels, focus management, focus trap on modal, keyboard nav | UX | M |
|
||||
| T-14 | React error boundaries + graceful storage-failure handling on Options/Popup | UX | S |
|
||||
| T-17 | CI: use checked-out workspace instead of `git clone` into /tmp; stop disabling TLS verification | Build/release | S |
|
||||
|
||||
## Task contract format
|
||||
|
||||
```markdown
|
||||
### T-NN — [verb + concrete deliverable]
|
||||
- **Status:** ready | in progress | blocked | in review | done · **Owner:** [agent] · **Effort:** S/M/L
|
||||
- **Goal / In scope / Out of scope / Constraints / Deliverable / Verification / Stop condition**
|
||||
```
|
||||
|
||||
## Done (recent)
|
||||
|
||||
- Phase 1 (2026-03-06): 7 workitems — WXT setup, selection detection, floating toolbar, SW LLM proxy, OpenAI+Anthropic+Groq+OpenRouter providers, Options page, result modal with Replace. See `PHASE1_SUMMARY.md`.
|
||||
48
docs/attacksurface.md
Normal file
48
docs/attacksurface.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Attack surface — LexAI
|
||||
|
||||
> Living inventory of LexAI's exposure. Updated whenever manifest/permissions, storage, or provider handling changes, and before any Chrome Web Store push. Contains **no secrets** — only references. Maintained via the `attack-surface` skill; security review via `security-auditor`.
|
||||
|
||||
## Assets
|
||||
|
||||
| Asset | Type | Tech | Hosted | Auth in | Exposure | Defenses | Review cadence |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Content script | injected code | WXT/TS | client | n/a | **`<all_urls>`, all frames** | `data-lexai` guard; inline styles; max z-index | every manifest/permission change |
|
||||
| Background service worker | LLM proxy | WXT/TS | client | user's provider key | reachable only via extension messages | key never logged; provider-only fetch | every key/provider change |
|
||||
| `chrome.storage.local` | local store | Chrome | client | extension-only | holds `apiKeyEnc`+`encKey` (+ legacy plaintext `apiKey`) | tweetnacl secretbox (see weakness) | every key-handling change |
|
||||
| Provider endpoints | 3rd-party API | HTTPS | OpenAI/Anthropic/Groq/OpenRouter | user's API key | outbound only, user-initiated | HTTPS; key in header only | on provider add/change |
|
||||
| Gitea CI | pipeline | Gitea workflows | self/3p | `GITEATOKEN`, `CWS_*`, `TELEGRAM_*` | build + publish to CWS | secrets in Gitea; **but** `http.sslVerify false` (see gap) | on workflow change |
|
||||
|
||||
## Per-asset notes
|
||||
|
||||
### Content script — `<all_urls>`
|
||||
- **Exposure:** injects into every frame of every site, including banking, email, internal apps. Biggest privacy surface and the #1 Chrome Web Store review slowdown.
|
||||
- **Mitigation (proposed):** narrow to `activeTab` + on-demand injection, or a user allowlist (TASKS #1 / D-PROPOSED). Decide before a serious CWS push.
|
||||
|
||||
### API-key storage — obfuscation, not protection
|
||||
- **Exposure:** `encKey` is stored in `chrome.storage.local` next to `apiKeyEnc`; anyone who can read storage can decrypt. The "encrypted" claim over-promises.
|
||||
- **Secrets location:** `chrome.storage.local` (user's own browser). Never in repo, never logged.
|
||||
- **Mitigation (proposed):** derive the key from `chrome.storage.session` / WebCrypto / a passphrase, and describe it honestly in the UI (TASKS #2 / D-...-06).
|
||||
|
||||
### Debug logging leak
|
||||
- **Exposure:** `content.ts` logs selection text and element values to the host-page console — readable by the page.
|
||||
- **Mitigation:** gate behind `import.meta.env.DEV` (TASKS #3).
|
||||
|
||||
### CI TLS verification disabled
|
||||
- **Exposure:** both Gitea workflows set `http.sslVerify false` and `git clone` into `/tmp`.
|
||||
- **Mitigation:** use the checked-out workspace and restore TLS verification (TASKS #17).
|
||||
|
||||
## Model / harness input surface (prompt-injection)
|
||||
|
||||
The extension sends **user-selected page text** to the chosen LLM with a fixed system prompt. Page-controlled text is untrusted input to the provider call.
|
||||
|
||||
| Input avenue | Consuming model | Reachable actions | Exposure | Defense in place |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Selected page text → `ANALYZE_TEXT` | user's provider | returns text shown in modal; user chooses Replace/Copy | injected instructions in page text could steer the model's output | user reviews output before Replace; no tool-calling; output is inert text |
|
||||
|
||||
- **Note:** exposure is low because the model output is inert (no tool execution) and the user gates Replace. Run `prompt-injection-audit` if LexAI ever adds auto-apply, tool use, or agentic actions.
|
||||
|
||||
## Gaps / unknowns
|
||||
|
||||
- Host-permission narrowing not yet decided (TASKS #1).
|
||||
- Key-derivation redesign not yet done (TASKS #2).
|
||||
- No automated check that production builds exclude debug logs (TASKS #3).
|
||||
Reference in New Issue
Block a user