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 app
  • pnpm build:extension - Build the Chrome extension
  • pnpm 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 mode
  • pnpm test:coverage / pnpm coverage:report / pnpm coverage:all - Coverage run and reports
  • pnpm 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, runs pnpm build first)
  • pnpm test:e2e:debug - Run E2E tests with Playwright inspector
  • pnpm 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 via window.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

1src/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
6src/main/services/db/ # Data access layer — see "Database" below (core, migrations, per-domain repos, dbAdmin)
7src/main/services/ai/ # OpenRouter chat client + capture analysis service
8src/main/services/extraction/ # Extracted-data pipeline (source, IOC adapter, sanitizer, validators)
9src/main/ipcHandlers.ts # All IPC handler registrations
10src/main/ipcWrap.ts # handle() wrapper + IpcFailure error envelope
11src/shared/types.ts # Cross-process domain types (Case, Capture, Tag, Selector, Note, Settings, etc.)
12src/shared/ipc.ts # IPC channel definitions and payload types
13src/shared/constants.ts # Constants (CAPTURE_SERVER_PORT, MAX_MHTML_SIZE, MANIFEST_FILENAME, etc.)
14src/shared/schemas.ts # Zod schemas validating settings and imported/exported documents
15src/shared/noteDoc.ts # Rich-text note document model + body derivation
16src/shared/noteAnchor.ts # Note anchor model + text-anchor resolution
17src/shared/verify/ # Evidence-package verification (canonicalJson, manifestChain, signature, timestampToken)
18src/verifier/cli.ts # Standalone verifier CLI entry point
19src/renderer/routes/ # TanStack Router route definitions (root tree plus captures route module)
20src/renderer/stores/ # Zustand store (appStore.ts)
21src/renderer/hooks/ # React hooks for theme, search, filters, viewport, favorites, session restore, server status, etc.
22src/renderer/lib/ # React Query client, query/mutation factories, motion presets, formatting helpers
23src/renderer/components/ # UI organized by feature — see "UI components" below
24extension/src/ # Chrome extension source (background, content, popup, toast, utils/api, utils/headers)
25tests/ # Vitest unit tests
26e2e/ # Playwright E2E tests
27docs/ # 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=false
  • queries.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 exports getDb(), closeDatabase(), withTransaction(), the ImportCtx archive-import context, and ID_PROBE_TABLES / hasRowWithId() for archive-import id collision remapping.
  • migrations.ts - The whole schema history in one runMigrations(db) function: a sequence of if (version < N) blocks, each running its DDL inside a transaction that ends by setting db.pragma('user_version = N'). New schema changes append a new block and bump LATEST_SCHEMA_VERSION in core.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 live user_version alongside LATEST_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) - @theme block defines semantic color variables (canvas, text, accent, border, surface, etc.) with light/dark variants via .dark class
  • useTheme hook (src/renderer/hooks/useTheme.ts) - Manages theme state, localStorage persistence, dark class 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() and listModels(), exposed over the settings:testOpenRouter and settings:listModels channels and driven by Settings → AI.
  • src/main/services/ai/openrouter.ts - The chat client: sendPrompt() (with retry/backoff) and truncateForContext().

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 under annotation/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 in vitest.config.ts (node environment, globals enabled). Covers database, services, store, types.
  • E2E tests (e2e/) - Playwright with Electron. Config in playwright.config.ts (30s timeout, 1 worker, trace on-first-retry). Requires pnpm build first (handled by pretest:e2e script).

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.