photon-hq/imessage-kit: A TypeScript iMessage SDK for macOS
A type-safe, elegant iMessage SDK for macOS with zero dependencies
At a glance
- What is it?
- A type-safe wrapper around chat.db and AppleScript for reading, sending and watching iMessage conversations. The README is unusually honest about what a send does and does not confirm.
- Who is it for?
- Adopt it if you are building a macOS-only TypeScript or Bun service that needs to read chat.db and fire messages out through Messages.app, and you can grant Full Disk Access to the process. Skip it if you need delivery receipts from the send call, cross-platform deployment, or SQL-level full-text search over message bodies.
- 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 115 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 October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The gap between Messages.app and a scriptable API
macOS ships no supported iMessage API. What exists instead is a SQLite database at ~/Library/Messages/chat.db and the osascript binary, and every iMessage automation project is some arrangement of those two things. The awkward parts are the same each time: chat.db stores message bodies in a binary attributedBody column rather than plain text, reading it requires Full Disk Access, and sending goes through AppleScript with no return value that tells you whether the message actually landed.
@photon-ai/imessage-kit packages that arrangement behind a typed surface. The target user is a developer building an agent, a notification relay, or a chat-first application on a Mac that is already signed into Messages. It is not a hosted service and not a cross-platform library. The package description calls it a "Type-safe macOS iMessage SDK for TypeScript", and the repository topics list agent and ai alongside apple and imessage, which matches where the demand is coming from.
What a send actually confirms, and what it does not
This is the design decision worth understanding before anything else. The README states plainly that sdk.send(request) returns a 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.
So a resolved promise means the AppleScript invocation finished, not that a human received anything. If you want to correlate a send with a row in the database, the documented route is to subscribe to onFromMeMessage through the watcher. That callback fires for every from-me row the SDK observes, whether the row was authored by this SDK, by another Apple client, or by a person typing in Messages.app. The consequence is that you cannot attribute a row to your own send by callback alone; you need your own correlation logic on top.
The SDK exposes this rather than hiding it, which is the right call. A library that returned a fake Message object from send() would be lying, and the fire-and-forget semantics are a property of osascript, not of this wrapper.
Installing it and sending a first message
The install path depends on your runtime. Bun is the zero-dependency route; Node.js pulls in better-sqlite3, which package.json lists under optionalDependencies rather than dependencies.
# For Bun (zero dependencies)
bun add @photon-ai/imessage-kit
# For Node.js (requires better-sqlite3)
npm install @photon-ai/imessage-kit better-sqlite3Before any of this works, the process running your code needs Full Disk Access, because reading chat.db is gated. The README gives the steps: open System Settings, go to Privacy & Security, then Full Disk Access, click the plus button and add your IDE or terminal, naming Cursor, VS Code, Terminal and Warp as examples. Granting it to a terminal grants it to everything you launch from that terminal.
With that in place, constructing the SDK and sending is two lines. The README also shows an async-dispose form using await using, which guarantees teardown, and a manual sdk.close() for the ordinary case.
import { IMessageSDK } from '@photon-ai/imessage-kit'
const sdk = new IMessageSDK()
await sdk.send({ to: '+1234567890', text: 'Hello from iMessage Kit!' })
await sdk.close()If the promise resolves, osascript exited cleanly. To see the row appear, start the watcher and log onFromMeMessage. That is the first real use, and it also teaches you the send/observe split in one step.
Attachments are non-transactional, and the README says so
A send with text and multiple attachments is not a single operation. According to the README, the first osascript call bundles the text with attachments[0], and each later attachment becomes its own call with roughly 500ms of pacing between steps. A failure partway through is labelled "attachment N/total".
That is a partial-send failure mode you have to handle in your own code. There is no rollback described, and the README does not document one. If you are sending a message plus five images, you can end up with the text and two images delivered and the rest not, and your error handling needs a story for that. The pacing also means a multi-attachment send takes real wall-clock time, which matters if you are inside a request handler.
Attachments must be local absolute paths. Remote URLs are rejected, so downloading a remote asset first is your responsibility. The README is explicit: "Local file paths only. Download remote URLs yourself first."
Configuration, bounds and the throwing behaviour
The config interface covers databasePath (defaulting to ~/Library/Messages/chat.db), maxConcurrentSends (default 10, range 1 to 50), sendTimeout in milliseconds per AppleScript invocation (default 30_000, range 1_000 to 300_000), debug, and plugins.
The interesting part is what happens when you go out of range. The README states that out-of-range numeric values throw IMessageError with code 'CONFIG' at construction, and that they are not silently clamped. The accepted ranges are exported from the package root as a BOUNDS constant, so you can read them programmatically instead of hardcoding numbers. Throwing at construction is better than clamping, because a maxConcurrentSends of 500 silently becoming 50 is the kind of thing you discover in production under load.
maxConcurrentSends defaulting to 10 is a hint about the underlying constraint. Each send is an osascript process, and AppleScript against Messages.app does not parallelise gracefully. The cap is there to stop you from spawning processes faster than Messages can absorb them.
Querying messages, and where search is slower than you expect
getMessages() takes a wide filter set: chatId, participant, service (iMessage, SMS or RCS), isFromMe, isRead, hasAttachments, excludeReactions, since, before, search, limit and offset. The tri-state booleans are a nice touch; omitting isFromMe returns both directions rather than defaulting to one.
The limitation to know about is search. The README says it runs in the application layer over decoded attributedBody, and that there is no SQL LIKE index behind it. Practically, that means a search query has to decode message bodies before it can match anything, and the cost scales with how many rows the other filters let through. The documented advice is to narrow with chatId or date bounds first. If your use case is full-text search across years of history, this SDK is the wrong layer for it; you want to index the decoded text yourself into something built for search.
excludeReactions is worth enabling by default if you are feeding messages to an agent. Tapback and sticker rows will otherwise appear as messages and confuse any downstream model.
Photon Spectrum is the paid-tier answer to the obvious gaps
The README's own note points elsewhere for threaded replies, tapbacks, message editing, unsending and live typing indicators, directing readers to Photon Spectrum at photon.codes. That is the same organisation, so this is a tier boundary rather than a competitor.
The real alternative from outside the project is writing the SQLite and AppleScript layer yourself, which is what most people do before finding a library like this. The difference in approach is that a hand-rolled version lets you control the polling strategy, the attributedBody decoding, and the exact AppleScript you emit, at the cost of maintaining all of it against macOS updates. This SDK centralises that work behind types and a documented config surface.
A second alternative, if you need a network-reachable interface rather than an in-process library, is to run something that exposes iMessage over HTTP and call it from any language. This package is a library, not a gateway; it has no server component and no HTTP surface in the README or the repository layout. Pick based on whether your consumer is a TypeScript process on the same Mac or something else entirely.
Licence, maintenance and what an upgrade costs you
The licence is MIT, stated in package.json and the README badge. MIT permits commercial use and modification with the licence text retained; that is a summary of the licence, not legal advice, and the LICENSE file in the repository root is the authority.
On maintenance: the repository is not archived, and the last push was on 2026-06-08. The most recent release is v3.0.0 from 2026-04-20, following v2.1.2 in January 2026 and v2.1.1 earlier that month. A major version bump in April means breaking changes landed recently, so pin your version and read the release notes before moving across a major boundary.
Upgrade cost is dominated by two things. First, macOS itself: chat.db schema and AppleScript behaviour change with OS releases, and a library that reads attributedBody is exposed to that. Second, the runtime split. If you run on Bun you avoid the better-sqlite3 native module; on Node you carry a native dependency that needs to compile or match a prebuilt binary for your Node version. That is a real operational difference between two otherwise identical deployments.
Editorial conclusion
Adopt it if you are building a macOS-only TypeScript or Bun service that needs to read chat.db and fire messages out through Messages.app, and you can grant Full Disk Access to the process. Skip it if you need delivery receipts from the send call, cross-platform deployment, or SQL-level full-text search over message bodies. Before committing, verify that your target machine can run osascript under the account your service uses, and check whether the watcher's polling interval is acceptable for your latency budget.
Frequently asked questions
How do I install @photon-ai/imessage-kit?
On Bun, run bun add @photon-ai/imessage-kit, which the README describes as the zero-dependency path. On Node.js, run npm install @photon-ai/imessage-kit better-sqlite3, because better-sqlite3 is an optional dependency rather than a bundled one.
Does @photon-ai/imessage-kit need Full Disk Access?
Yes. The README states that reading chat.db requires Full Disk Access, granted through System Settings, Privacy & Security, Full Disk Access, where you add your IDE or terminal. The examples given include Cursor, VS Code, Terminal and Warp.
Does sdk.send() confirm that the message was delivered?
No. The README says the promise resolves when osascript exits successfully, and that it does not confirm the message landed in chat.db or return a Message object. To see the landed row, subscribe to onFromMeMessage through the watcher.
Can @photon-ai/imessage-kit send attachments from a URL?
No. The README specifies local absolute paths for attachments and states that remote URLs are rejected, so you download the file yourself first. A send with text plus multiple attachments is also non-transactional, with each attachment after the first sent in its own osascript call.
What happens if I set maxConcurrentSends outside the allowed range?
The SDK throws IMessageError with code 'CONFIG' at construction rather than clamping the value. The README gives the default as 10 with a range of 1 to 50, and the accepted ranges are exported from the package root as the BOUNDS constant.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/photon-hq-imessage-kit)