Files
skills/docs/communication/messaging/imessage-adapter/README.md
2026-05-29 08:20:56 +01:00

121 lines
7.2 KiB
Markdown

# @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.
<!-- begin:auto-docs -->
## 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…
<!-- end:auto-docs -->
## 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.