imessage-kit: a TypeScript wrapper around chat.db and osascript
A type-safe, elegant iMessage SDK for macOS with zero dependencies
At a glance
- What is it?
- Photon's MIT-licensed SDK reads the Messages SQLite database and sends through AppleScript. It is a small, typed surface over two macOS mechanisms, and the interesting parts are the boundaries it draws between them.
- Who is it for?
- Adopt imessage-kit if you are building a macOS-only agent or automation that needs typed reads from chat.db plus basic outbound sends, and you accept that sending is fire-and-forget through osascript. Do not adopt it if you need threaded replies, tapbacks, message editing, unsending, or typing indicators, since the README routes those to Photon Spectrum.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 99 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap between AppleScript sending and SQLite reading
Sending an iMessage from a script and reading what was sent are two unrelated problems on macOS. AppleScript can drive Messages.app to compose and send, but it gives you no handle on the resulting row. The Messages database at ~/Library/Messages/chat.db holds the truth, including delivery state and reactions, but it is a SQLite file with an attributedBody column that is not plain text. imessage-kit sits across both: sdk.send() shells out to osascript, and sdk.getMessages() queries chat.db. The README is explicit that these are separate paths, and the package is aimed at people building AI agents, automation tools, and chat-first applications on macOS rather than cross-platform messaging. The topics list (agent, ai, apple, imessage, sms, typescript) matches that framing. If your target is a Linux server or a web backend, this is the wrong shape of tool.
Send is a promise about osascript, not about delivery
The most important design decision in the README is stated in a short section titled Send vs Observe Semantics. sdk.send(request) returns Promise<void> that resolves when osascript exits successfully. It does not confirm the message landed in chat.db, and it does not return a Message object. To know whether a row appeared, you subscribe to onFromMeMessage through sdk.startWatching(), which the README says fires for every from-me row observed, whether authored by this SDK, another Apple client, or Messages.app. That last clause matters: the watcher is not a receipt channel for your own sends, it is a stream of database rows, and you have to do the correlation yourself. Anyone expecting send-then-confirm semantics from a single await will build on a false assumption. The documentation gives a two-step example (send, then start watching) but no correlation key beyond the message id observed on the other side.
Configuration values throw instead of clamping
IMessageConfig exposes databasePath (default ~/Library/Messages/chat.db), maxConcurrentSends (default 10, range 1 to 50), sendTimeout (milliseconds per AppleScript invocation, default 30_000, range 1_000 to 300_000), debug, and plugins. The README states that out-of-range numeric values throw IMessageError with code 'CONFIG' at construction rather than being silently clamped, and that the accepted ranges are exported as a BOUNDS constant from the package root. That is a deliberate choice and a defensible one: a typo in sendTimeout fails at startup instead of producing mysterious timeouts later. It also means you cannot pass a computed value without checking it against BOUNDS first, which is a small amount of friction for callers who generate configuration dynamically. The maxConcurrentSends cap of 50 is a hard ceiling, so bulk-send workloads need their own queue above the SDK.
Installation, permissions, and the two runtime paths
Installation differs by runtime. For Bun the README gives bun add @photon-ai/imessage-kit and calls it zero dependencies. For Node.js it gives npm install @photon-ai/imessage-kit better-sqlite3, so the zero-dependency claim applies to the Bun path specifically; Node users pull in a native SQLite binding. Reading chat.db requires Full Disk Access, and the README walks through System Settings, Privacy & Security, Full Disk Access, then adding your IDE or terminal (it names Cursor, VS Code, Terminal, and Warp as examples). That step is per-application, not per-project, and it is the most common place for a working script to fail on a colleague's machine. Basic usage is new IMessageSDK() followed by await sdk.send({ to: '+1234567890', text: 'Hello from iMessage Kit!' }), with either await using for async disposal or an explicit await sdk.close() for manual teardown. The presence of both disposal paths suggests the SDK holds resources (a database handle, a watcher) that outlive a single call.
Multi-attachment sends are non-transactional by design
The attachment example documents behaviour that is easy to miss. When you pass text plus multiple attachments, the first osascript call bundles the text with attachments[0], and each later attachment becomes its own call with roughly 500ms of inter-step pacing. A failure partway through is labelled "attachment N/total" in the error. So a three-attachment send is three AppleScript invocations, not one, and there is no rollback: earlier attachments may already be delivered when a later one fails. The README also notes that attachments must be local absolute paths and that remote URLs are rejected, so downloading is the caller's job. This is a case where the API shape (one send call) hides a multi-step process with partial-failure semantics. If you need all-or-nothing delivery, this SDK does not provide it and the error label is your only signal that you are mid-batch.
Querying is rich, but search runs in the application layer
getMessages() accepts chatId, participant, service ('iMessage', 'SMS', or 'RCS'), tri-state filters isFromMe, isRead, and hasAttachments (omit for both), excludeReactions to drop tapback and sticker rows, since and before dates, search, limit, and offset. The README states plainly that search runs in the application layer over decoded attributedBody and that there is no SQL LIKE index, so it is a substring scan over decoded text rather than an indexed lookup. The practical consequence is that search cost scales with how many rows survive your other filters, and the documentation's own advice is to narrow with chatId first. That is a real limitation, not a footnote: on a large chat.db, an unqualified search string is the slow path. The tri-state boolean filters are a nicer detail than they look, since omitting a field genuinely means both values rather than defaulting to false.
Where Spectrum begins and what that means for scope
A note near the top of the README points readers who want threaded replies, tapbacks, message editing, unsending, and live typing indicators to Photon Spectrum, described as Photon's open-source multi-channel agent framework covering iMessage, SMS, email, Slack, Discord, and voice. That tells you the boundary of this package: it is the single-channel, send-and-read layer, and the richer messaging features live elsewhere. The comparison that matters is not against another iMessage library but against driving osascript and sqlite3 yourself. Doing it manually means writing the AppleScript, handling attributedBody decoding, and managing concurrency and timeouts on your own. imessage-kit packages those decisions, including the throw-on-out-of-range config behaviour and the plugin hook via sdk.use(), at the cost of accepting its particular split between send and observe. If your needs stop at one script that sends a message, the SDK is more machinery than the task requires.
Licence, maintenance, and what to verify before adopting
The package is MIT licensed, which permits commercial use and modification subject to the licence terms; that is a statement about the licence text, not legal advice, and you should read LICENSE and your own obligations. The release history shows v3.0.0 in April 2026, preceded by v2.1.2 and v2.1.1 earlier that year, and the last push to the repository was 2026-06-08, so the project is active rather than archived. A major version bump from 2.x to 3.0.0 signals breaking changes somewhere in the API, and the README's configuration interface carries a note that readonly modifiers were omitted for readability and that src/types/config.ts is authoritative, so pinning a version and reading the types at that tag is the reliable way to know what you are calling. The plugin system via sdk.use() and the plugins array in config are the extension point to check if you plan to add behaviour. Nothing in the supplied material describes upgrade tooling or migration notes between majors, so treat a 2.x to 3.x move as a manual review of src/types rather than a drop-in.
Editorial conclusion
Adopt imessage-kit if you are building a macOS-only agent or automation that needs typed reads from chat.db plus basic outbound sends, and you accept that sending is fire-and-forget through osascript. Do not adopt it if you need threaded replies, tapbacks, message editing, unsending, or typing indicators, since the README routes those to Photon Spectrum. Before committing, verify Full Disk Access is granted to the exact process that will run the SDK, confirm whether you are on Bun or need better-sqlite3, and decide how you will correlate sends with chat.db rows, because sdk.send() resolves on osascript exit and returns no Message object.
Community notes