Chrome extension (MV3) + FastAPI backend. Captures chat context from AI chat tools, cleans it via LLM, stores in Supabase, and injects into other AI chats.
Version: 0.1.0
- ChatGPT (
chatgpt.com,chat.openai.com) - Claude (
claude.ai) - Perplexity (
www.perplexity.ai,perplexity.ai) - Gemini (
gemini.google.com,bard.google.com) - Copilot (
copilot.microsoft.com) - Grok (
grok.com) - Mistral (
chat.mistral.ai)
- Node.js and npm
- Python 3.12+
- Chrome or Chromium-based browser
- Supabase project
- OpenRouter API key
Create .env from .env.example:
SUPABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
OPENROUTER_API_KEY=
LLM_API_BASE=https://openrouter.ai/api/v1
LLM_MODEL=openai/gpt-4o-mini
ALLOWED_EXTENSION_ORIGINS=chrome-extension://<your-extension-id>python -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
npm installnpm run build- Open
chrome://extensions - Enable Developer mode
- Click "Load unpacked"
- Select the
dist/folder - Copy the extension ID
- Add
chrome-extension://<id>to.envALLOWED_EXTENSION_ORIGINS - Restart backend after changing
.env
npm run backendBackend starts at http://127.0.0.1:8000. Logs write to uvicorn.log.
| Command | Purpose |
|---|---|
npm run build |
Compile TypeScript and copy static files to dist/ |
npm run backend |
Start FastAPI backend on 127.0.0.1:8000 |
npm test |
Run Python unittest discover |
npm run check:python |
py_compile on api/core.py and api/index.py |
npm run check |
Build + test + python check |
npm run generate-icons |
Generate PNG icons from SVG (requires sharp) |
api/
core.py # validation, truncation, prompts, parsing, hashing
index.py # FastAPI app, routes, OpenRouter, Supabase
mcp_server.py # MCP server (FastMCP) at /mcp
extension/
manifest.json # MV3 manifest
popup.html # popup shell
popup.css # popup styles
injected.css # content-script injected styles
assets/ # icons and logo
src/
background.ts # service worker message router
client.ts # backend fetch client, per-browser client id
config.ts # backend URL and supported hosts
content.ts # page scraping, save button, import button, context injection
popup.ts # popup UI logic
types.ts # shared TypeScript interfaces
tests/
test_core.py # Python unittest coverage for api/core.py
scripts/
start-backend.py # uvicorn launcher
copy-static.js # copies extension files into dist/
generate-icons.js # generates PNG icons from SVG
Build output goes to dist/. Chrome loads extension files from dist/, not src/ or extension/.
- Content script runs on supported AI chat sites
- Injects a "Save Context" button at bottom-right
- User clicks button (or uses popup save)
- Content script scrapes visible chat text
- Background service worker sends raw chat to backend with
X-Relay-Client-Id - Backend validates, truncates large chats, sends to LLM for cleanup
- Backend parses title/content, hashes, dedupes by client + hash
- Cleaned context stored in Supabase
- Popup lists saved contexts
- User selects one → extension injects it into current chat input
Injected format:
You are continuing an existing project.
Here is the full working context:
--- CONTEXT START ---
<saved context>
--- CONTEXT END ---
First, acknowledge you understand this context. Then wait for my next instruction.
All context endpoints require:
X-Relay-Client-Id: <client-id>
Content-Type: application/jsonReturns API metadata.
Returns config health:
{
"ok": true,
"supabase_configured": true,
"llm_configured": true
}Body:
{ "raw_chat": "..." }Response:
{
"id": 1,
"title": "Project Context",
"created_at": "2026-05-19T00:00:00Z",
"deduped": false,
"truncated": false
}Returns latest 20 contexts for current client id.
Returns full context. Missing context returns 404.
Mounted at /mcp on the backend (Streamable HTTP transport). Built with FastMCP.
All tools require a client_id parameter:
| Tool | Description |
|---|---|
list_contexts |
List up to 20 contexts for a client |
get_context |
Get full context content by id |
capture_context |
Save new context from raw chat text (async, uses LLM cleanup) |
{
"mcpServers": {
"relay": {
"url": "http://localhost:8000/mcp"
}
}
}npx -y @modelcontextprotocol/inspectorConnect to http://localhost:8000/mcp.
| Variable | Description |
|---|---|
SUPABASE_URL |
Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY |
Supabase service role secret |
OPENROUTER_API_KEY |
OpenRouter API key |
LLM_API_BASE |
LLM API base URL (default: https://openrouter.ai/api/v1) |
LLM_MODEL |
Model to use for cleanup (default: openai/gpt-4o-mini) |
ALLOWED_EXTENSION_ORIGINS |
Comma-separated extension origins for CORS |
Run supabase_schema.sql in Supabase SQL editor.
Schema:
create table if not exists public.contexts (
id bigserial primary key,
client_id text not null,
title text not null,
content text not null,
content_hash text not null,
created_at timestamptz not null default now()
);Indexes:
contexts_client_hash_idxon(client_id, content_hash)for dedupecontexts_client_created_idxon(client_id, created_at desc)for listing
Security: RLS enabled, anon and authenticated privileges revoked, backend uses service role key (bypasses RLS).
Implemented in api/core.py.
Truncation: Raw chats over 60K chars retain first 10K + last 50K with a mid-truncation note.
Cleanup prompt asks model to return:
Title: <short specific title, max 8 words>
Project goal:
...
Constraints:
...
Decisions made:
...
Important information:
...
Open items:
...
Parsing: If output starts with Title:, title extracted (max 120 chars). Fallback: Context - <Mon DD>.
Dedupe: SHA-256 hash of cleaned content, matched by client_id + content_hash.
- Keep
SUPABASE_SERVICE_ROLE_KEYonly on backend - Never put service role key in extension code
- Do not commit
.env - Contexts are scoped by generated client id, not user auth
- Client id lives in Chrome local extension storage
- No user accounts or auth beyond client id
- No delete or edit/rename endpoint
- No pagination beyond latest 20 contexts
- Scraping depends on third-party site DOM structure
- Backend URL hardcoded in
src/config.ts - Large chats lose middle content after 60K chars
- No linter/formatter configured
No license file is currently included.