Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.2 KiB
7.2 KiB
@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 —
MessageSourceAdapter-shaped wrapper aroundIMessageWatcherthat projects eachParsedIMessageinto the channel-agn…- imessage-adapter —
MessageSourceAdapter-shaped wrapper aroundIMessageWatcherthat projects eachParsedIMessageinto the channel-agn…
- imessage-adapter —
- error-codes — Defines the stable, fully-qualified error-code registry for
@suaveplan/imessage-adapter. Every error class in this mo…- error-codes — Defines the stable, fully-qualified error-code registry for
@suaveplan/imessage-adapter. Every error class in this mo…
- error-codes — Defines the stable, fully-qualified error-code registry for
- fixtures — Builds a real
chat.db-shaped SQLite database on a temp path using the structuralSqliteReaderseam. This is the fou…- chat-db-fixture — Builds a real
chat.db-shaped SQLite database on a temp path using the structuralSqliteReaderseam. This is the fou…
- chat-db-fixture — Builds a real
- otel — Defines the four OpenTelemetry instruments emitted by
@suaveplan/imessage-adapterand the helpers that record onto th…- metrics — Defines the four OpenTelemetry instruments emitted by
@suaveplan/imessage-adapterand the helpers that record onto th…
- metrics — Defines the four OpenTelemetry instruments emitted by
- parser — Pure-function parser that turns Apple's chat.db row shapes into the
ParsedIMessageprojection consumed by every downs…- imessage-parser — Pure-function parser that turns Apple's chat.db row shapes into the
ParsedIMessageprojection consumed by every downs…
- imessage-parser — Pure-function parser that turns Apple's chat.db row shapes into the
- schemas — Provides Zod boundary schemas for
IMessageWatcherConfigandIMessageAdapterConfig, plus theparseWatcherConfig/…- schemas — Provides Zod boundary schemas for
IMessageWatcherConfigandIMessageAdapterConfig, plus theparseWatcherConfig/…
- schemas — Provides Zod boundary schemas for
- sqlite — Production-grade
SqliteReaderbacked by Bun's built-inbun:sqlitedriver. The reader is the single seam through whi…- bun-sqlite-reader — Production-grade
SqliteReaderbacked by Bun's built-inbun:sqlitedriver. The reader is the single seam through whi…
- bun-sqlite-reader — Production-grade
- watcher — Polls Apple's
~/Library/Messages/chat.dbSQLite database on a configurable interval, projects new rows into `ParsedIM…- imessage-watcher — Polls Apple's
~/Library/Messages/chat.dbSQLite database on a configurable interval, projects new rows into `ParsedIM…
- imessage-watcher — Polls Apple's
Highlights
IMessageWatcher— Zod-validated config boundary, AbortSignal propagation, injectableSqliteReader, typedEventBussurface alongside the upstreamon/offlistener API, monotoniclastRowIdcursor (settable + readable for resume), filters forincludeFromMeandchatFilter.IMessageAdapter— wraps the watcher and projects eachParsedIMessageinto a channel-agnosticInboundMessagewith 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— productionSqliteReaderbacked by Bun'sbun:sqlite. Errors are wrapped inIMessageDbOpenFailedError.
Install
bun add @suaveplan/imessage-adapter
Usage
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.