imessage-kit

repository·main·Indexed 23 days ago

https://github.com/photon-hq/imessage-kit

A type-safe macOS iMessage SDK for TypeScript (version 3.0.0) that enables developers to read, send, and automate iMessage conversations. It provides tools for building AI agents and automation, featuring real-time message watching via WAL-based observation, historical message querying, and a plugin system with interrupting, sequential, and parallel hooks. The SDK supports sending text and local attachments via AppleScript and requires Full Disk Access to interact with the macOS chat.db.

Tokens
12.6K
Snippets
28
Records
55
Agent score
80%

What's inside @photon-ai/imessage-kit

  1. Understand Send vs Observe Semantics

    main

    It is important to distinguish between sending a message and observing its arrival in the database:

    • sdk.send(request): Returns a Promise<void> that resolves when the underlying osascript command exits successfully. It does not confirm the message was written to chat.db or return a Message object.
    • Observation: To confirm a message has actually landed in the database (and to track delivery status), you must use sdk.startWatching() and listen to the onFromMeMessage callback.
    // Fire-and-forget send
    await sdk.send({ to: '+1234567890', text: 'Hi' })
    
    // Observe the landed row
    await sdk.startWatching({
        onFromMeMessage: (msg) => console.log('Landed in chat.db:', msg.id, msg.isDelivered),
    })
  2. How the plugin hook system works

    main

    The SDK provides 11 hooks categorized by their execution mode and behavior when an error is thrown.

    Interrupting Hooks

    These hooks can abort the requested operation. If they throw, the operation is cancelled with a specific IMessageError code:

    • onBeforeMessageQuery: Aborts getMessages with IMessageError(DATABASE)
    • onBeforeChatQuery: Aborts listChats with IMessageError(DATABASE)
    • onBeforeSend: Aborts send with IMessageError(SEND)

    Sequential Hooks

    These run in order. If one throws, the error is routed to onError:

    • onInit
    • onDestroy
    • onError (Note: onError is not re-routed to prevent recursion)

    Parallel Hooks

    These run in parallel. Errors are routed to onError:

    • onAfterMessageQuery
    • onAfterChatQuery
    • onAfterSend (Fires only on successful AppleScript dispatch)
    • onIncomingMessage (Every incoming row observed by the watcher)
    • onFromMe (Every from-me row observed — authoritative DB-arrival signal)

    Note on Naming: onFromMe in the plugin system is distinct from DispatchEvents.onFromMeMessage used in the startWatching callback. The former is a plugin observer, while the latter is an inline handler.

  3. Initialize and use IMessageSDK

    main

    Import IMessageSDK to start interacting with iMessage. You can use standard await sdk.close() for manual teardown or use the await using (async-dispose) pattern to guarantee the SDK is closed automatically when the scope ends.

    import { IMessageSDK } from '@photon-ai/imessage-kit'
    
    const sdk = new IMessageSDK()
    
    // Send a text message
    await sdk.send({ to: '+1234567890', text: 'Hello from iMessage Kit!' })
    
    // Or use async-dispose to guarantee teardown:
    await using disposable = new IMessageSDK()
    await disposable.send({ to: '+1234567890', text: 'Hi!' })
    
    // Manual teardown
    await sdk.close()
  4. Install @photon-ai/imessage-kit

    main

    Install the SDK based on your runtime. Bun users can use the zero-dependency version, while Node.js users must also install better-sqlite3 to interact with the Messages database.

    # For Bun (zero dependencies)
    bun add @photon-ai/imessage-kit
    
    # For Node.js (requires better-sqlite3)
    npm install @photon-ai/imessage-kit better-sqlite3
  5. Run iMessage-kit examples with Bun

    main

    You can run the provided examples using Bun. Ensure you are on macOS and have granted Full Disk Access to your terminal/Bun.

    Example command to run the basic text send example:

    bun run examples/01-send-text.ts
  6. Configure IMessageSDK options

    main

    When constructing IMessageSDK, you can pass an IMessageConfig object to customize behavior. Note that numeric values outside the allowed ranges will throw an IMessageError(code: 'CONFIG') rather than being clamped. The valid ranges are available via the exported BOUNDS constant.

    interface IMessageConfig {
        databasePath?: string        // Path to Messages SQLite database (default: ~/Library/Messages/chat.db)
        maxConcurrentSends?: number  // Concurrent send cap (default: 10, range 1..50)
        sendTimeout?: number         // ms per AppleScript invocation (default: 30_000, range 1_000..300_000)
        debug?: boolean              // Verbose SDK logs
        plugins?: Plugin[]           // Plugins registered at construction; sdk.use() is also available later
    }
  7. Understand MessageSender error behavior and retries

    main

    The MessageSender implements a robust retry and error-wrapping strategy:

    1. Retry Logic: Retries are performed per-step (per AppleScript call), not end-to-end. For a message with $N$ attachments, the first call bundles the text and the first attachment. Subsequent attachments are sent via individual AppleScript calls. If a failure occurs, the system retries that specific step up to retryAttempts times.
    2. Error Guarantees: The send() method ensures that any error thrown is an IMessageError. This allows you to reliably catch and inspect error codes like CONFIG, DATABASE, or SEND.
    3. Resuming Failures: Because dispatch is non-transactional, if a batch of attachments fails, you should re-invoke send() with a slice of the remaining attachments (e.g., attachments.slice(k-1)) to resume the process.
  8. How plugin dispatch modes work

    main

    Plugins in imessage-kit operate in three distinct dispatch modes depending on the hook being used. This determines how errors are handled and whether a plugin can stop an operation.

    1. Interrupting (Sequential, Fail-fast)

    Hooks: onBeforeMessageQuery, onBeforeChatQuery, onBeforeSend.

    • Behavior: Plugins run in order (pre -> normal -> post). The first plugin to throw an error will abort the surrounding SDK operation (e.g., getMessages, listChats, or send).
    • Error Handling: Remaining plugins are not called. The caller receives an IMessageError where the code is DATABASE for queries or SEND for sends, and the cause is the plugin's original error.
    • Use Case: Use these as gates for authentication, rate limiting, or content policy enforcement.

    2. Sequential (Observing)

    Hooks: onInit, onError, onDestroy.

    • Behavior: Plugins run one at a time.
    • Error Handling: If a plugin throws, the error is captured, logged, and reported to the onError hook. The surrounding SDK lifecycle continues; a single plugin failure cannot crash the SDK.

    3. Parallel (Observing)

    Hooks: onAfterMessageQuery, onAfterChatQuery, onAfterSend, onIncomingMessage, onFromMe.

    • Behavior: All matching plugins run concurrently, and their promises are awaited as a group.
    • Error Handling: Individual failures are reported to onError, but the query result or incoming message still propagates to the caller and other plugins.

    Note: Hook return values are ignored. Plugins cannot rewrite requests or results; they can only observe or interrupt via throwing errors.

  9. Understand Attachment transfer status mapping

    main

    The imessage-kit library normalizes raw macOS transfer_state integer codes into a set of human-readable TransferStatus strings. This allows developers to handle file transfer lifecycles without managing low-level system codes.

    Normalized StatusRaw macOS CodesDescription
    pending-1, 0Archiving or waiting for acceptance
    transferring1, 2, 3, 4Accepted, preparing, transferring, or finalizing
    complete5File is available on disk
    failed6, 7Non-recoverable or recoverable error
    unknownnull or otherUnrecognized or future macOS codes
  10. How MessageDispatcher processes message batches

    main

    The MessageDispatcher processes batches of messages by partitioning them into two parallel branches: Incoming (non-from-me) and From-Me.

    • Parallelism: The two branches (incoming vs. from-me) are processed independently using Promise.all. A slow handler in the onFromMe branch will not block the delivery of incoming messages.
    • Ordering: Within each branch, messages are processed sequentially. This ensures that the order of messages within your user-provided callbacks is preserved.
    • Dispatch Logic:
      • Incoming messages are further routed based on chatKind (group or dm).
      • From-me messages are routed to the onFromMeMessage handler.

    To use the dispatcher, call the dispatch(messages: readonly Message[]) method.

  11. Use onFromMe to verify message delivery

    main
    Because onAfterSend only reports that the AppleScript dispatch succeeded, it does not guarantee the message has actually landed in the chat.db database. To authoritatively verify that a 'from-me' message has been recorded in the database, use the onFromMe hook.
  12. Grant Full Disk Access to iMessageKit

    main

    The SDK requires Full Disk Access to read the macOS chat.db file.

    1. Open System Settings → Privacy & Security → Full Disk Access.
    2. Click "+" and add your terminal or IDE (e.g., Cursor, VS Code, Terminal, Warp).