Covers building, testing, and extending both the backend and the browser extension.
- Python 3.12+,
uvpackage manager - Running Ollama instance (for AI features)
cd backend
uv sync # Install dependencies
uv run pre-commit install # Enable git hooks (ruff, mypy)Create backend/.env:
DATABASE_URL=sqlite:///data/mindcache.db
FAISS_INDEX_PATH=data/faiss_index.bin
BM25_INDEX_PATH=data/bm25_index.pkl
EMBEDDING_PROVIDER=ollama
EMBEDDING_MODEL_NAME=embeddinggemma:300m
EMBEDDING_DIMENSION=768
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen3.5:2b
LOG_LEVEL=INFORuntime .env (current overrides):
OLLAMA_MODEL=gemma4:31b-cloud
EMBEDDING_PROVIDER=ollama
EMBEDDING_MODEL_NAME=embeddinggemma:300mThe EMBEDDING_DIMENSION is auto-detected by querying Ollama /api/embed on startup, falling back to settings.EMBEDDING_DIMENSION (384) if detection fails.
uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000API docs at http://127.0.0.1:8000/docs (auto-generated by FastAPI).
docker compose up --build -dThe Docker setup:
- Builds from
python:3.12-slimwithuvfor fast dependency installation - Mounts
./data:/app/datafor persistent SQLite/FAISS/BM25 files - Connects to host Ollama via
host.docker.internal:11434 - Pre-downloads SentenceTransformer model for zero-network cold start
MindCache uses Alembic for schema versioning. The initial schema is created programmatically in main.py lifespan, with dynamic PRAGMA-based ALTER TABLE for existing databases.
uv run alembic revision --autogenerate -m "describe_changes"
uv run alembic upgrade head
uv run alembic downgrade -1The migrations/versions/ directory is intentionally empty — the app creates tables on startup if they don't exist.
uv run pytest # All tests
uv run pytest -s # With logs/prints
uv run pytest --cov=app tests/ # With coverage
uv run pytest tests/test_api.py -k "test_visit_and_search_flow" # Single testTests use an in-memory SQLite database (sqlite+aiosqlite:///:memory:) and mock all AI services (embedding, keyword extraction, entity extraction, Ollama). This means tests run in milliseconds with no external dependencies.
Test structure:
tests/conftest.py— Fixtures: event loop, in-memory DB, mocked AI services, FAISS/BM25 temp filestests/test_api.py— 9 tests: health, visit+search flow, custom title, time filtering, noise skipping, platform search, all URLs, click analytics, search evaluation datasettests/test_extractors.py— 14 tests: factory selection, YouTube (success + missing transcript), X (success + partial fallback + fxtwitter), GitHub, Google Search (success + DDG fallback), Reddit (post + fallback + JSON fallback), pipeline integrationtests/test_services.py— 9 tests: BS4 fallback extraction, ISO date parsing, URL validation, embedding generation, keyword extraction, vector service (add/search/delete), Ollama summary, BM25 tokenization/search, quality score calculationtests/search_evaluation.json— 4 regression queries: "stop ai slop", "karpathy coding rules", "fastapi web framework", "rust programming language"
uv run ruff check . # Lint with auto-fix
uv run ruff format --check . # Format check
uv run mypy . # Static type checkingPre-commit hooks run ruff lint and format automatically on git commit.
Code style (pyproject.toml): line-length 120, target Python 3.12, selects E/W/F/I/B/C4/UP rules.
backend/
├── app/
│ ├── api/ # FastAPI routers
│ │ ├── __init__.py
│ │ ├── visit.py # POST /visit
│ │ ├── search.py # POST /search, POST /search/click
│ │ ├── documents.py # CRUD /documents, GET /graph
│ │ └── health.py # GET /health
│ ├── core/
│ │ ├── config.py # Pydantic settings
│ │ ├── exceptions.py # Custom HTTP exceptions
│ │ └── logging.py # Logging configuration
│ ├── db/
│ │ ├── base.py # SQLAlchemy Base
│ │ └── session.py # async engine + session factory
│ ├── models/
│ │ └── document.py # ORM models (Document, Keyword, Entity, etc.)
│ ├── repositories/
│ │ └── document_repository.py # Async CRUD operations
│ ├── schemas/
│ │ └── document.py # Pydantic V2 request/response schemas
│ ├── services/
│ │ ├── __init__.py
│ │ ├── document_processor.py # Ingestion orchestration
│ │ ├── embedding_service.py # Ollama embedding generation
│ │ ├── ollama_service.py # Ollama LLM communication
│ │ ├── vector_service.py # FAISS management
│ │ ├── bm25_service.py # BM25 lexical search
│ │ ├── search_service.py # Hybrid search pipeline
│ │ ├── keyword_extractor.py # Keyword extraction (Ollama + TF)
│ │ ├── entity_extractor.py # NER (Ollama + regex)
│ │ └── extractors/ # Platform-specific extractors
│ │ ├── base.py # Abstract base + ExtractionResult
│ │ ├── factory.py # ExtractorFactory registry
│ │ ├── generic.py # Trafilatura + BS4
│ │ ├── youtube.py # yt-dlp + captions
│ │ ├── x.py # fixupx.com syndication API
│ │ ├── github.py # Repo metadata + README
│ │ ├── reddit.py # old.reddit.com comments
│ │ ├── pdf.py # pypdf + pspdfkit
│ │ └── google_search.py # SERP + DuckDuckGo
│ └── main.py # App entry, lifespan, CORS
├── data/ # SQLite DB + FAISS + BM25 (gitignored)
├── migrations/
│ ├── env.py # Alembic async config
│ └── script.py.mako # Migration template
├── tests/ # Pytest test suite
├── Dockerfile # Production image
├── docker-compose.yml # Container orchestration
└── pyproject.toml # Dependencies + tool config
- Node.js 18+, npm 9+
- Running backend server
cd extension
npm installnpm run devServes popup and settings pages in a browser tab at a local Vite dev URL. Background worker cannot be fully tested here since it needs Chrome extension APIs (tabs, storage, contextMenus). For background testing, build and load the extension in a browser.
npm run buildThe build.js script runs a three-stage production build:
- Popup & Settings — Standard Vite build (ESM,
dist/assets/) - content.js (459KB) — Vite IIFE build with Defuddle + Turndown bundled
- background.js (28KB) — Vite IIFE build with blacklist constants inlined
All three stages set process.env.NODE_ENV = 'production'.
npm run test # Run once
npm run test:watch # Watch modeTests use vitest with jsdom environment and mocked Chrome APIs:
tests/setup.ts— Mockschrome.storage.local,chrome.tabs,chrome.runtime,global.fetchtests/service.test.ts— 4 tests: health check (success + offline), visit submission, semantic searchtests/store.test.ts— 6 tests: settings defaults, modifications, domain exclusion; search query + recent searches constraints; connection state transitions
- Go to
chrome://extensions(orbrave://extensions) - Enable Developer mode
- Click Load unpacked → select
extension/dist/
npm run build
cd dist
zip -r ../mindcache-extension.zip .extension/
├── public/manifest.json # Manifest V3: permissions, commands, content scripts
├── src/
│ ├── index.css # Global Tailwind + custom styles
│ ├── background/index.ts # Service worker: tab tracking, context menus, messaging
│ ├── content/
│ │ ├── index.ts # Content script entry: message handlers, overlay init
│ │ ├── extract-page.ts # Defuddle extraction pipeline + Turndown
│ │ └── SearchOverlay.tsx # Spotlight-style search overlay (Shadow DOM)
│ ├── popup/
│ │ ├── main.tsx # Popup React root
│ │ └── App.tsx # Popup UI: status, preview, capture, diagnostics
│ ├── settings/
│ │ ├── main.tsx # Settings React root
│ │ └── App.tsx # Full dashboard: 5 tabs, TanStack Query, filters
│ ├── components/
│ │ ├── DocumentDetail.tsx # Document detail view with AI summary
│ │ └── InteractiveKnowledgeGraph.tsx # Force-directed graph (Canvas)
│ ├── services/
│ │ └── BackendClient.ts # API client with retry + validation
│ ├── store/
│ │ ├── settingsStore.ts # Persisted settings (Zustand + chrome.storage)
│ │ ├── searchStore.ts # Search state + recent queries
│ │ └── connectionStore.ts # Backend health status (transient)
│ ├── types/
│ │ ├── index.ts # All TypeScript interfaces
│ │ └── turndown.d.ts # Turndown type declarations
│ └── utils/
│ ├── error.ts # Error message formatting
│ └── storage.ts # chrome.storage.local adapter
├── tests/ # Vitest test suite
├── build.js # 3-stage Vite build script
├── vite.config.ts # Dev server config
├── vitest.config.ts # Test config
├── tailwind.config.js # Tailwind CSS config
└── tsconfig.json # TypeScript strict config
All hardcoded numeric values in the settings dashboard are extracted to named constants in src/settings/App.tsx:
| Constant | Default | Purpose |
|---|---|---|
DEFAULT_RESULT_LIMIT |
10 | Default search result count |
MAX_DOCUMENTS_FETCH |
100 | Max documents in list query |
SEARCH_DEBOUNCE_DELAY |
250ms | Search input debounce |
SAVE_SUCCESS_DURATION |
1500ms | Success toast duration |
RECENT_ACTIVITY_COUNT |
7 | Dashboard recent items |
MAX_KEYWORDS_DISPLAY |
4 | Max keyword tags per result |
GRAPH_DISPLAY_MAX |
50 | Graph node display cap |
BEST_MATCH_THRESHOLD |
65 | "Best Match" badge threshold |
STRONG_MATCH_THRESHOLD |
55 | "Strong Match" badge threshold |
DEFAULT_MIN_SCORE |
0 | Default min match score filter |
RESULT_LIMIT_OPTIONS |
[5, 10, 20, 30] | Result limit dropdown options |
Background worker inlines:
BLACKLISTED_EXTENSIONS— file extensions to skip (images, media, archives, binaries, docs)BLACKLISTED_PATHS— URL path patterns to skip (login, signup, admin, auth, etc.)DWELL_TIME_THRESHOLD_SEC— 10s before auto-indexingMIN_DWELL_TIME_SEC— 5s minimum dwell to index on deactivationEXTRACTION_TIMEOUT_MS— 2s content script timeout
Content script defines:
EXCLUDED_TAGS— elements stripped during noise removalBLACKLISTED_PATHS— same path blacklist for URL validation
- Add
chrome.contextMenus.create()increateContextMenus()insrc/background/index.ts. - Add a handler in the
chrome.contextMenus.onClickedlistener. - Implement capture logic (reuse
captureCurrentPage,captureUrl, orcaptureSelection). - Add message handler in
src/content/index.tsif DOM access is needed.
- Register the command in
extension/public/manifest.jsonunder"commands". - Add a handler in
chrome.commands.onCommandinsrc/background/index.ts. - Update the settings page Shortcuts section.
- Start the backend.
- Load the extension in the browser.
- Visit any webpage, wait 10 seconds.
- Check backend logs for
POST /visittransaction. - Or check
GET http://127.0.0.1:8000/documents.
- Turn off Auto extraction in extension Settings → Privacy.
- Visit a page.
- Use
Ctrl+Shift+S, right-click → "Save this page to MindCache", or click the popup button. - Verify the visit appears in
GET /documents.
- Open any webpage.
- Press
Ctrl+Shift+K(orMacCtrl+Shift+Kon macOS). - The spotlight overlay appears with tabs and memory search modes.
- Test
Tabmode switch,Ctrl+T/Ctrl+Mshortcuts, and Arrow/Enter navigation. - Verify backend connection shows in the footer badge.
| Package | Purpose |
|---|---|
@tanstack/react-query |
Data fetching (search, documents, dashboard) |
defuddle |
Page content extraction (Obsidian Web Clipper) |
turndown |
HTML-to-Markdown conversion |
marked |
Markdown-to-HTML rendering (DocumentDetail) |
lucide-react |
Icon library (50+ icons used) |
zustand |
State management with chrome.storage persistence |
clsx + tailwind-merge |
Class name utilities |
@tailwindcss/typography |
Prose styling for document content |
vitest + @testing-library/react |
Testing framework |