CLAUDE.md
Birdbrain
Open source web investigation & capture tool. Electron desktop app with a companion Chrome extension for capturing and analyzing web content.
Commands
pnpm dev- Start Electron app in dev mode (electron-vite)pnpm build- Build the Electron apppnpm build:extension- Build the Chrome extensionpnpm dev:extension- Build Chrome extension in watch mode (background and popup bundles only; does not watch/rebuild the content script IIFE build)pnpm build:verifier- Build the standalone verifier binary (scripts/build-verifier.mjs)pnpm test- Run tests (vitest, via Electron runtime)pnpm test:watch- Run tests in watch modepnpm test:coverage/pnpm coverage:report/pnpm coverage:all- Coverage run and reportspnpm lint- ESLint (.ts, .tsx)pnpm format- Prettier format src/ and extension/pnpm rebuild:electron- Rebuild native deps (better-sqlite3)pnpm test:e2e- Run E2E tests (Playwright + Electron, runspnpm buildfirst)pnpm test:e2e:debug- Run E2E tests with Playwright inspectorpnpm package/pnpm package:win/pnpm package:mac/pnpm package:linux- Package for distribution
Architecture
Electron + React 19 + TanStack Router + React Query + Chrome Extension + SQLite (better-sqlite3).
Process model
- Main process (
src/main/) - Electron main, SQLite database, Hono capture server, settings, export - Preload (
src/preload/) - IPC bridge exposing typed channels to renderer viawindow.birdbrain - Renderer (
src/renderer/) - React 19 + Tailwind v4 + Zustand + TanStack Router + React Query - Chrome extension (
extension/) - Content script + background service worker + popup that sends captures to the Hono server
Key directories
| 1 | src/main/services/ # Main-process services: captureServer, captureStore, storage, export, pdfExport, |
| 2 | # settings, hash, manifest, captureLifecycle, selectorLifecycle, recapture, |
| 3 | # annotations, caseArchive, zip/zipRead, timestamp/trustedTime/tsaTrust, |
| 4 | # signingKey, certification, waybackMachine, diagnostics, updater, deepLink, |
| 5 | # noteAnchorResolver, logSafe |
| 6 | src/main/services/db/ # Data access layer — see "Database" below (core, migrations, per-domain repos, dbAdmin) |
| 7 | src/main/services/ai/ # OpenRouter chat client + capture analysis service |
| 8 | src/main/services/extraction/ # Extracted-data pipeline (source, IOC adapter, sanitizer, validators) |
| 9 | src/main/ipcHandlers.ts # All IPC handler registrations |
| 10 | src/main/ipcWrap.ts # handle() wrapper + IpcFailure error envelope |
| 11 | src/shared/types.ts # Cross-process domain types (Case, Capture, Tag, Selector, Note, Settings, etc.) |
| 12 | src/shared/ipc.ts # IPC channel definitions and payload types |
| 13 | src/shared/constants.ts # Constants (CAPTURE_SERVER_PORT, MAX_MHTML_SIZE, MANIFEST_FILENAME, etc.) |
| 14 | src/shared/schemas.ts # Zod schemas validating settings and imported/exported documents |
| 15 | src/shared/noteDoc.ts # Rich-text note document model + body derivation |
| 16 | src/shared/noteAnchor.ts # Note anchor model + text-anchor resolution |
| 17 | src/shared/verify/ # Evidence-package verification (canonicalJson, manifestChain, signature, timestampToken) |
| 18 | src/verifier/cli.ts # Standalone verifier CLI entry point |
| 19 | src/renderer/routes/ # TanStack Router route definitions (root tree plus captures route module) |
| 20 | src/renderer/stores/ # Zustand store (appStore.ts) |
| 21 | src/renderer/hooks/ # React hooks for theme, search, filters, viewport, favorites, session restore, server status, etc. |
| 22 | src/renderer/lib/ # React Query client, query/mutation factories, motion presets, formatting helpers |
| 23 | src/renderer/components/ # UI organized by feature — see "UI components" below |
| 24 | extension/src/ # Chrome extension source (background, content, popup, toast, utils/api, utils/headers) |
| 25 | tests/ # Vitest unit tests |
| 26 | e2e/ # Playwright E2E tests |
| 27 | docs/ # Local working notes — see docs/README.md for layout (reference/, specs/, plans/, archive/) |
Path aliases
@main/*→src/main/*(main + preload)@shared/*→src/shared/*(all processes)@renderer/*→src/renderer/*(renderer only)
IPC pattern
All renderer↔main communication uses typed IPC channels defined in src/shared/ipc.ts. Channels follow domain:action naming (e.g., cases:create, selectors:create). Event channels (main→renderer) use event: prefix.
Domains (read IPC_CHANNELS in src/shared/ipc.ts for the authoritative list): cases, captures, recapture, tags, search, settings, export, selectors, notes, wayback, extractedData, annotations, extension, shell, app, diagnostics, updates, ai, db — plus the event: main→renderer channels.
The preload script exposes these via window.birdbrain with typed invoke/on methods.
Routing
TanStack Router (@tanstack/react-router) with the following route tree:
| 1 | / → Dashboard |
| 2 | /settings → SettingsView |
| 3 | /extension-setup → InstallExtensionGuide |
| 4 | /cases/new → NewCaseWizard |
| 5 | /cases/$caseId → CaseWorkspace (layout with tabs) |
| 6 | ├── / → redirects to /overview |
| 7 | ├── /overview → CaseOverview |
| 8 | ├── /captures → CaptureList + CaptureViewer (split view) |
| 9 | ├── /selectors → SelectorsOverview |
| 10 | ├── /notes → NotesOverview |
| 11 | ├── /tags → TagsOverview |
| 12 | └── /data → DataExplorer |
Root layout in __root.tsx renders TopBar, optional case Sidebar, main content area, CommandPalette, onboarding overlay, and devtools in development.
Data fetching
React Query (@tanstack/react-query) manages all server state. Configuration in src/renderer/lib/:
queryClient.ts- retry=false, staleTime=30s, refetchOnWindowFocus=falsequeries.ts- Query key factory, typed query options, and domain-specific mutation hooks (useCasesMutations, useCapturesMutations, etc.) with automatic cache invalidation
State management
Zustand store (src/renderer/stores/appStore.ts) for UI-only state:
- Session state (sessionActive, connectedToExtension)
- Selection state (selectedCaptureId, selectedCaptureIds)
- Search and filter state (searchQuery, activeSelectorFilters, filteredCaptureIds)
- Capture activity (captureEvents, captureStats)
Database
SQLite via better-sqlite3. The data-access layer lives in src/main/services/db/:
core.ts- Owns the connection.initDatabase()opens the file, sets the pragmas (journal_mode = WAL,foreign_keys = ON,busy_timeout), then runs migrations. Also exportsgetDb(),closeDatabase(),withTransaction(), theImportCtxarchive-import context, andID_PROBE_TABLES/hasRowWithId()for archive-import id collision remapping.migrations.ts- The whole schema history in onerunMigrations(db)function: a sequence ofif (version < N)blocks, each running its DDL inside a transaction that ends by settingdb.pragma('user_version = N'). New schema changes append a new block and bumpLATEST_SCHEMA_VERSIONincore.ts— that constant is the single source of truth for the current version, so read it rather than counting blocks.- Per-domain repos -
caseRepo.ts,captureRepo.ts,tagRepo.ts,selectorRepo.ts,noteRepo.ts,extractedDataRepo.ts,waybackRefRepo.ts. Each owns the SQL for its aggregate. dbAdmin.ts- Generic table browse/edit, vacuum, FTS rebuild, orphan cleanup, backup/restore, CSV export (backs Settings → Database).diagnosticsRepo.ts- Read-only DB facts for Settings → Diagnostics, including the liveuser_versionalongsideLATEST_SCHEMA_VERSION.
Raw connection access is lint-enforced: eslint.config.js restricts importing getDb from @main/services/db/core anywhere under src/main/ outside src/main/services/db/. New SQL belongs in a repo module; the few legacy exceptions carry an eslint-disable with a reason.
Tables and indexes: read migrations.ts — it is the only place tables and indexes are declared, in chronological order. Broadly: case/capture core plus tags, selectors and selector matches, notes, favorites, capture analyses, extracted data, annotations and annotation pins, pinned Wayback refs (table capture_archive_refs), capture texts, and FTS5 virtual tables shadowing captures, notes, and extracted data. Never assume a table exists because it appears in an early migration — later migrations drop and rebuild some (e.g. the v1 entities and case_analyses tables were dropped, and captures_fts was later dropped and recreated).
Theme system
Light/dark theme support using CSS custom properties and Tailwind v4:
- CSS tokens (
src/renderer/styles/globals.css) -@themeblock defines semantic color variables (canvas, text, accent, border, surface, etc.) with light/dark variants via.darkclass - useTheme hook (
src/renderer/hooks/useTheme.ts) - Manages theme state, localStorage persistence,darkclass on<html>, 400ms transition animations, and IPC sync to settings - Flash prevention - Inline script in HTML prevents theme flicker on load
- Component convention - Prefer semantic token classes (e.g.,
bg-canvas,text-text-primary,border-border) over raw Tailwind colors; exceptions include overlays (bg-black) and status/severity colors (bg-red-600, etc.)
Capture server
Hono HTTP server (src/main/services/captureServer.ts) on port 19845 receives captures from the Chrome extension. Supports both HTML and MHTML forensic capture formats. Captures are stored as files on disk organized by case directory with SHA-256 hash verification and hash-chained audit manifests.
Chrome extension
Located in extension/src/:
- background.ts - Service worker managing extension state and tab capture events
- content.ts - Injected into pages for HTML/screenshot capture and selector detection
- popup/ - React-based popup UI with case selector and capture controls
- utils/api.ts - HTTP client targeting
http://127.0.0.1:19845
Built separately via pnpm build:extension (uses extension/vite.config.ts).
AI services
Two OpenRouter modules, split by role:
src/main/services/openrouter.ts- Credential/catalog surface:testApiKey()andlistModels(), exposed over thesettings:testOpenRouterandsettings:listModelschannels and driven by Settings → AI.src/main/services/ai/openrouter.ts- The chat client:sendPrompt()(with retry/backoff) andtruncateForContext().
src/main/services/ai/analysisService.ts runs per-capture analysis on top of that client (analyzeCapture, saveAnalysis, getAnalysis, plus archive import/export helpers) and persists to the capture_analyses table. Handlers are registered for ai:analyze, ai:saveAnalysis, and ai:getAnalysis; the renderer surface is captures/AnalysisTab.tsx.
Note: the v1 entities and case_analyses tables were dropped in an early migration, but per-capture analysis exists again via capture_analyses — do not assume AI analysis is dead code.
Settings persist openRouterApiKey, defaultModel, and analysisSystemPrompt (defaulting to DEFAULT_ANALYSIS_SYSTEM_PROMPT in src/shared/constants.ts).
UI components
Organized by feature under src/renderer/components/:
- captures/ - Capture list/viewer workflow, details panel/rail, add-URL box, provenance, wayback/analysis/forensics tabs, MHTML viewer, download menu, inline tag/note editing hooks, verify mutation, and the annotation editor under
captures/annotation/(canvas, zoom/pan and editor hooks, pin popover, shape components underannotation/shapes/) - dashboard/ - Dashboard, CaseCard, DashboardFooter, ExtensionBanner, HeroSection, QuickStartGuide, RecentCases, plus case workspace components under
dashboard/cases/(CaseWorkspace, CreateCaseDialog, DataExplorer, ImportCaseDialog, NewCaseWizard) - export/ - ExportDialog, ExportMenu, ExportProgress, ExportComplete
- extension/ - InstallExtensionGuide, InstallExtensionStepper, installSteps.tsx
- layout/ - TopBar, Sidebar, CommandPalette, OnboardingWizard
- notes/ - AddNoteModal, CreateNoteCard, NoteCard, NoteBody, NoteEditor, NotesOverview, useNoteEditor.ts
- overview/ - CaseOverview, CaseSubhead, ActivityTimeline, MetricRow, RecentCapturesStrip, SelectorCoverageBlock, SinceLastVisitBanner, SourcesBlock, VerifyBar, overviewModel.ts
- search/ - SearchBar
- selectors/ - BulkAddSelectorsModal, CreateSelectorCard, CreateSelectorPopover, SelectorFilterFooter, SelectorTable, SelectorTableRow, SelectorsOverview, selectorUtils.ts
- settings/ - SettingsView, AIConfig, AppearanceConfig, CapturePreferences, DatabaseAdmin, DiagnosticsPanel, OperatorConfig, StorageConfig, UpdatesConfig, About, plus database utility views under
settings/db/ - status/ - CaptureHealth, ConnectionStatus, SessionControls
- tags/ - TagBadge, TagManager, TagsOverview
- ui/ - Shared primitives re-exported from
ui/index.ts: badge, button, card, dialog, input, label, scroll-area, skeleton, tabs, textarea
Documentation conventions
All design docs, specs, and implementation plans live under docs/ per the layout in docs/README.md. Canonical paths:
- Specs / design briefs / spikes →
docs/specs/YYYY-MM-DD-<slug>-design.md(or-spike.md,-brief.md,-assessment.md) — tracked - Implementation plans / checklists →
docs/plans/YYYY-MM-DD-<slug>.md— tracked (docs-only commits) - Long-lived reference →
docs/reference/<topic>.md(no date prefix) — tracked - Architecture decisions →
docs/adr/NNNN-<slug>.md— tracked - Superseded →
docs/archive/(preserve original filename) — tracked
docs/plans/ is tracked (since July 2026). Plans are still author-time working notes: they get checked off and go stale, and staleness is expected. Commit them in docs-only commits for history; never bundle them into a src/** feature PR. Tracked durable docs (specs, ADRs, reference) must be committed in their own PR — never bundled with a src/** feature change.
Override for agentic tooling: When a skill or agent specifies a different default path (e.g. Superpowers' docs/superpowers/specs/ and docs/superpowers/plans/), treat the canonical paths above as the user-preference override. Write specs to docs/specs/ and plans to docs/plans/. The legacy docs/superpowers/ tree is frozen — do not add new files there.
Testing
- Unit tests (
tests/) - Vitest running via Electron runtime (ELECTRON_RUN_AS_NODE=1). Config invitest.config.ts(node environment, globals enabled). Covers database, services, store, types. - E2E tests (
e2e/) - Playwright with Electron. Config inplaywright.config.ts(30s timeout, 1 worker, trace on-first-retry). Requirespnpm buildfirst (handled bypretest:e2escript).
Code style
- No semicolons
- Single quotes
- No trailing commas
- 100 char print width
- 2-space indent
- TypeScript strict mode
- React JSX transform (no React import needed)
- ESLint 9 flat config with TypeScript ESLint + Prettier
- Prefer semantic theme tokens over raw color values in components (exceptions: overlays, status/severity colors)
Agent skills
Issue tracker
Issues live as GitHub Issues in thebristolsound/birdbrain, accessed via the gh CLI. See docs/agents/issue-tracker.md.
Triage labels
Five canonical triage roles using their default label strings (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See docs/agents/triage-labels.md.
Domain docs
Single-context layout: CONTEXT.md + docs/adr/ at the repo root. See docs/agents/domain.md.