AGENTS.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, archive, 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,archiveRefRepo.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, 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, archive/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.