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 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, 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=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, 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 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, 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) - @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, 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 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.