# @suaveplan/imessage-adapter - macOS iMessage SQLite chat.db watcher. Polls Apple's Messages database, decodes Apple-epoch / nanosecond timestamps, resolves handle + chat + attachment joins, and emits ParsedIMessage + InboundMessage envelopes. Structured IMESSAGE.* error registry, Zod-validated config boundary, AbortSignal cancellation, injectable SqliteReader seam (production: bun:sqlite, tests: real bun:sqlite chat.db fixture — no mocks), EventBus bridge atop @suaveplan/events, four OTel instruments. macOS iMessage SQLite chat.db watcher and `MessageSourceAdapter` implementation. Polls Apple's `~/Library/Messages/chat.db`, decodes Apple-epoch / nanosecond timestamps, resolves handle + chat + attachment joins, and emits `ParsedIMessage` and `InboundMessage` envelopes. Server-tier (Bun); the `bun:sqlite` driver is loaded lazily inside the production reader so the package type-checks + lints on every host. ## Documentation tree - [adapter](./src/adapter/imessage-adapter.md) — `MessageSourceAdapter`-shaped wrapper around `IMessageWatcher` that projects each `ParsedIMessage` into the channel-agn… - [imessage-adapter](./src/adapter/imessage-adapter.md) — `MessageSourceAdapter`-shaped wrapper around `IMessageWatcher` that projects each `ParsedIMessage` into the channel-agn… - [error-codes](./src/error-codes/error-codes.md) — Defines the stable, fully-qualified error-code registry for `@suaveplan/imessage-adapter`. Every error class in this mo… - [error-codes](./src/error-codes/error-codes.md) — Defines the stable, fully-qualified error-code registry for `@suaveplan/imessage-adapter`. Every error class in this mo… - [fixtures](./src/fixtures/chat-db-fixture.md) — Builds a real `chat.db`-shaped SQLite database on a temp path using the structural `SqliteReader` seam. This is the fou… - [chat-db-fixture](./src/fixtures/chat-db-fixture.md) — Builds a real `chat.db`-shaped SQLite database on a temp path using the structural `SqliteReader` seam. This is the fou… - [otel](./src/otel/metrics.md) — Defines the four OpenTelemetry instruments emitted by `@suaveplan/imessage-adapter` and the helpers that record onto th… - [metrics](./src/otel/metrics.md) — Defines the four OpenTelemetry instruments emitted by `@suaveplan/imessage-adapter` and the helpers that record onto th… - [parser](./src/parser/imessage-parser.md) — Pure-function parser that turns Apple's chat.db row shapes into the `ParsedIMessage` projection consumed by every downs… - [imessage-parser](./src/parser/imessage-parser.md) — Pure-function parser that turns Apple's chat.db row shapes into the `ParsedIMessage` projection consumed by every downs… - [schemas](./src/schemas/schemas.md) — Provides Zod boundary schemas for `IMessageWatcherConfig` and `IMessageAdapterConfig`, plus the `parseWatcherConfig` /… - [schemas](./src/schemas/schemas.md) — Provides Zod boundary schemas for `IMessageWatcherConfig` and `IMessageAdapterConfig`, plus the `parseWatcherConfig` /… - [sqlite](./src/sqlite/bun-sqlite-reader.md) — Production-grade `SqliteReader` backed by Bun's built-in `bun:sqlite` driver. The reader is the single seam through whi… - [bun-sqlite-reader](./src/sqlite/bun-sqlite-reader.md) — Production-grade `SqliteReader` backed by Bun's built-in `bun:sqlite` driver. The reader is the single seam through whi… - [watcher](./src/watcher/imessage-watcher.md) — Polls Apple's `~/Library/Messages/chat.db` SQLite database on a configurable interval, projects new rows into `ParsedIM… - [imessage-watcher](./src/watcher/imessage-watcher.md) — Polls Apple's `~/Library/Messages/chat.db` SQLite database on a configurable interval, projects new rows into `ParsedIM… ## Highlights - **`IMessageWatcher`** — Zod-validated config boundary, AbortSignal propagation, injectable `SqliteReader`, typed `EventBus` surface alongside the upstream `on/off` listener API, monotonic `lastRowId` cursor (settable + readable for resume), filters for `includeFromMe` and `chatFilter`. - **`IMessageAdapter`** — wraps the watcher and projects each `ParsedIMessage` into a channel-agnostic `InboundMessage` with normalised phone numbers and chat metadata. - **`IMessageParser`** — pure-function parser for chat.db row shapes; Apple-epoch decoding, attachment fallbacks, phone normalisation. - **`createChatDbFixture`** — real chat.db-shaped SQLite database builder used by tests so every test path runs against the same SQL the production watcher executes — no mocks. - **`bunSqliteReader` / `createBunSqliteReader`** — production `SqliteReader` backed by Bun's `bun:sqlite`. Errors are wrapped in `IMessageDbOpenFailedError`. ## Install ```bash bun add @suaveplan/imessage-adapter ``` ## Usage ```ts import { IMessageAdapter } from "@suaveplan/imessage-adapter"; const adapter = new IMessageAdapter(); await adapter.initialize({ dbPath: "~/Library/Messages/chat.db", pollIntervalMs: 5000, includeFromMe: false, }); for (const inbound of await adapter.poll()) { console.log(inbound.sourceAddress, inbound.content); adapter.markProcessed(inbound.id); } await adapter.close(); ``` ## Error registry (`IMESSAGE.*`) | Code | Class | When | |---|---|---| | `IMESSAGE.DB_OPEN_FAILED` | `IMessageDbOpenFailedError` | `bun:sqlite` cannot open the chat.db path | | `IMESSAGE.DB_QUERY_FAILED` | `IMessageDbQueryFailedError` | A poll-time SQL query throws | | `IMESSAGE.WATCHER_NOT_RUNNING` | `IMessageWatcherNotRunningError` | `stop()` called while not running | | `IMESSAGE.WATCHER_ALREADY_RUNNING` | `IMessageWatcherAlreadyRunningError` | `start()` called twice | | `IMESSAGE.ADAPTER_NOT_INITIALIZED` | `IMessageAdapterNotInitializedError` | `poll()` before `initialize()` | | `IMESSAGE.INVALID_CONFIG` | `IMessageInvalidConfigError` | Zod boundary rejected config; `context.issues` carries the path + reason | | `IMESSAGE.POLL_ABORTED` | `IMessagePollAbortedError` | `AbortSignal` fired | | `IMESSAGE.POLL_FAILED` | `IMessagePollFailedError` | Reserved for explicit `poll()` wrapping | | `IMESSAGE.ATTACHMENT_PARSE_FAILED` | `IMessageAttachmentParseError` | Attachment row could not be parsed | | `IMESSAGE.APPLE_DATE_PARSE_FAILED` | `IMessageAppleDateParseError` | Timestamp is NaN / non-finite | ## OpenTelemetry instruments | Instrument | Kind | Attributes | |---|---|---| | `imessage.messages_emitted_total` | Counter | `chat_type`, `from_me` | | `imessage.attachments_emitted_total` | Counter | `mime_family` | | `imessage.poll_errors_total` | Counter | `code` | | `imessage.poll_duration_ms` | Histogram | `outcome` (`ok`, `error`, `aborted`) | Call `refreshIMessageMeters()` after `metrics.setGlobalMeterProvider` to bind subsequent records to the chosen provider. ## Subpath exports - `@suaveplan/imessage-adapter/error-codes` - `@suaveplan/imessage-adapter/schemas` - `@suaveplan/imessage-adapter/parser` - `@suaveplan/imessage-adapter/watcher` - `@suaveplan/imessage-adapter/adapter` - `@suaveplan/imessage-adapter/sqlite/bun-sqlite-reader` - `@suaveplan/imessage-adapter/otel` - `@suaveplan/imessage-adapter/fixtures` ## Tier Server-tier (Bun on macOS). The `bun:sqlite` import is reached via `createRequire(import.meta.url)` so the static module graph never attempts to resolve the specifier on a non-Bun runner.