# @photon-ai/imessage-kit ## Introduction `@photon-ai/imessage-kit` is a type-safe, production-ready SDK for controlling macOS iMessage from Node.js and Bun runtime environments. It provides developers with a complete abstraction layer over Apple's iMessage database and messaging capabilities, enabling seamless integration of iMessage functionality into applications. The SDK uses AppleScript for message sending and direct SQLite database access for reading message history, combining native macOS technologies with modern JavaScript development practices. This SDK is designed for developers building AI agents, automation scripts, chat-first applications, and messaging bots on macOS. It features zero production dependencies when running on Bun (uses native `bun:sqlite`), full TypeScript support with perfect type inference, concurrent message sending with semaphore-based rate limiting, real-time message monitoring with webhook support, and an extensible plugin system. The SDK requires Full Disk Access permission to read the iMessage database at `~/Library/Messages/chat.db` and supports reading iMessage, SMS, and RCS messages with powerful filtering capabilities. ## API Reference ### IMessageSDK Constructor Initialize the SDK with optional configuration. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' // Basic initialization with defaults const sdk = new IMessageSDK() // Advanced configuration const sdk = new IMessageSDK({ debug: true, // Enable debug logging maxConcurrent: 10, // Max concurrent message sends (default: 5) scriptTimeout: 30000, // AppleScript timeout in ms (default: 30000) databasePath: '~/Library/Messages/chat.db', // Custom database path watcher: { pollInterval: 2000, // Poll interval for new messages (default: 2000ms) unreadOnly: false, // Watch all messages (default: false) excludeOwnMessages: true // Exclude own messages (default: true) }, webhook: { url: 'https://your-server.com/webhook', // Webhook URL for notifications headers: { 'Authorization': 'Bearer token' }, timeout: 5000 // Request timeout (default: 5000ms) }, retry: { max: 2, // Max retry attempts (default: 2) delay: 1500 // Retry delay in ms (default: 1500) }, tempFile: { maxAge: 600000, // Temp file retention (default: 10 minutes) cleanupInterval: 300000 // Cleanup interval (default: 5 minutes) } }) // Always close when done try { await sdk.send('+1234567890', 'Hello!') } finally { await sdk.close() } // Or use TypeScript 5.2+ using declaration (auto-cleanup) await using sdk = new IMessageSDK() await sdk.send('+1234567890', 'Hello!') // Automatically disposed ``` ### Reading Messages - getMessages() Query messages with flexible filters. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK() // Get all messages (excludes your own by default) const result = await sdk.getMessages() console.log(`Total: ${result.total}, Unread: ${result.unreadCount}`) for (const msg of result.messages) { console.log(`${msg.sender}: ${msg.text}`) console.log(` Service: ${msg.service}, Read: ${msg.isRead}`) console.log(` Attachments: ${msg.attachments.length}`) } // Filter by sender and time range const filtered = await sdk.getMessages({ sender: '+1234567890', since: new Date(Date.now() - 24 * 3600000), // Last 24 hours limit: 50, unreadOnly: true }) // Get messages with attachments only const withAttachments = await sdk.getMessages({ hasAttachments: true, service: 'iMessage' }) // Include your own messages in results const conversation = await sdk.getMessages({ sender: '+1234567890', excludeOwnMessages: false, // Include both sides limit: 100 }) // Filter by chat ID (for group chats) const groupMessages = await sdk.getMessages({ chatId: 'chat123456', since: new Date('2025-01-01') }) await sdk.close() ``` ### Reading Unread Messages - getUnreadMessages() Get unread messages grouped by sender. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK() // Get all unread messages grouped by sender const unread = await sdk.getUnreadMessages() for (const { sender, messages } of unread) { console.log(`\n${sender}: ${messages.length} unread messages`) for (const msg of messages) { console.log(` [${msg.date.toLocaleTimeString()}] ${msg.text}`) if (msg.attachments.length > 0) { console.log(` Attachments:`) for (const att of msg.attachments) { console.log(` - ${att.filename} (${att.mimeType}, ${att.size} bytes)`) if (att.isImage) { console.log(` Image path: ${att.path}`) } } } } } await sdk.close() ``` ### Sending Text Messages - send() Send text messages to recipients. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK({ debug: true }) try { // Send to phone number const result1 = await sdk.send('+1234567890', 'Hello, World!') console.log(`Sent at: ${result1.sentAt}`) // Send to email address const result2 = await sdk.send('user@example.com', 'Hello via email!') console.log(`Sent at: ${result2.sentAt}`) // Send with explicit text property await sdk.send('+1234567890', { text: 'Hello with object syntax' }) // Handle errors try { await sdk.send('invalid-recipient', 'This will fail') } catch (error) { console.error('Send failed:', error.message) } } finally { await sdk.close() } ``` ### Sending Images - send() with images Send images from local paths or URLs. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK() try { // Send single image await sdk.send('+1234567890', { images: ['/path/to/photo.jpg'] }) // Send multiple images await sdk.send('+1234567890', { images: ['/path/to/photo1.jpg', '/path/to/photo2.png'] }) // Send network images (auto-downloaded) await sdk.send('+1234567890', { images: ['https://example.com/image.jpg'] }) // Mix local and network images await sdk.send('+1234567890', { images: [ '/local/photo.jpg', 'https://example.com/remote.png' ] }) // Send images with text await sdk.send('+1234567890', { text: 'Check out these photos!', images: ['/path/to/vacation.jpg', '/path/to/sunset.png'] }) // Note: Incompatible formats (AVIF, WebP) are automatically converted to JPEG await sdk.send('+1234567890', { images: ['https://example.com/image.avif'] // Auto-converted to JPEG }) } finally { await sdk.close() } ``` ### Sending Files - sendFile() and sendFiles() Send documents, PDFs, CSVs, contact cards, and other files. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' import { writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' const sdk = new IMessageSDK() try { // Send single file await sdk.sendFile('+1234567890', '/path/to/document.pdf') // Send file with message await sdk.sendFile('+1234567890', '/path/to/report.pdf', 'Q4 Report attached') // Send multiple files await sdk.sendFiles('+1234567890', [ '/path/to/file1.pdf', '/path/to/data.csv', '/path/to/notes.txt' ], 'Meeting materials') // Send contact card (.vcf) const vcfContent = [ 'BEGIN:VCARD', 'VERSION:3.0', 'FN:John Doe', 'N:Doe;John;;;', 'TEL;TYPE=CELL:+1234567890', 'EMAIL:john.doe@example.com', 'ORG:Example Company', 'TITLE:Software Engineer', 'END:VCARD' ].join('\n') const vcfPath = join(tmpdir(), 'contact.vcf') writeFileSync(vcfPath, vcfContent, 'utf-8') await sdk.sendFile('+1234567890', vcfPath, 'Contact info') // Send CSV data file const csvContent = 'Name,Email,Phone\nAlice,alice@example.com,+1111111111\n' const csvPath = join(tmpdir(), 'data.csv') writeFileSync(csvPath, csvContent, 'utf-8') await sdk.sendFile('+1234567890', csvPath, 'User data') // Use send() with files parameter await sdk.send('+1234567890', { text: 'Documents attached', files: ['/path/to/doc.pdf', '/path/to/sheet.xlsx'] }) // Mix images and files await sdk.send('+1234567890', { text: 'Project files', images: ['/path/to/screenshot.png'], files: ['/path/to/report.pdf', '/path/to/data.csv'] }) } finally { await sdk.close() } ``` ### Batch Message Sending - sendBatch() Send multiple messages concurrently with automatic rate limiting. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK({ maxConcurrent: 10 // Send up to 10 messages in parallel }) try { // Batch send to multiple recipients const results = await sdk.sendBatch([ { to: '+1111111111', content: 'Message for recipient 1' }, { to: '+2222222222', content: 'Message for recipient 2' }, { to: '+3333333333', content: 'Message for recipient 3' }, { to: 'user@example.com', content: 'Message for email recipient' } ]) // Check results for (const result of results) { if (result.success) { console.log(`✓ Sent to ${result.to}`) } else { console.error(`✗ Failed to ${result.to}: ${result.error?.message}`) } } // Batch send with different content types const mixedResults = await sdk.sendBatch([ { to: '+1111111111', content: 'Text only' }, { to: '+2222222222', content: { text: 'With image', images: ['photo.jpg'] } }, { to: '+3333333333', content: { files: ['document.pdf'] } }, { to: '+4444444444', content: { text: 'Complete', images: ['img.jpg'], files: ['doc.pdf'] } } ]) const successful = mixedResults.filter(r => r.success).length const failed = mixedResults.filter(r => !r.success).length console.log(`Sent: ${successful}, Failed: ${failed}`) // Broadcast same message to multiple recipients const recipients = ['+1111111111', '+2222222222', '+3333333333'] const broadcastResults = await sdk.sendBatch( recipients.map(to => ({ to, content: 'Broadcast message' })) ) } finally { await sdk.close() } ``` ### Message Chain Processing - message() Process messages with fluent API and conditional logic. ```typescript import { IMessageSDK, type Message } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK() try { // Get some messages to process const result = await sdk.getMessages({ limit: 10 }) for (const msg of result.messages) { // Simple text matching reply await sdk.message(msg) .matchText(/hello/i) .replyText('Hi there!') .execute() // MUST call execute() // Multiple conditions await sdk.message(msg) .ifUnread() .ifFromOthers() .matchText('urgent') .replyText('Received your urgent message') .execute() // Custom predicate await sdk.message(msg) .when(m => m.sender.startsWith('+1')) .replyText('US number detected') .execute() // Reply with images await sdk.message(msg) .matchText('photo') .replyImage(['/path/to/photo1.jpg', '/path/to/photo2.jpg']) .execute() // Group chat only await sdk.message(msg) .ifGroupChat() .replyText('Thanks for adding me to the group!') .execute() // Only messages from me await sdk.message(msg) .ifFromMe() .do(async (m) => { console.log(`I sent: ${m.text}`) }) .execute() // Dynamic reply based on message content await sdk.message(msg) .ifUnread() .replyText(m => `Echo: ${m.text}`) .execute() // Custom action handler await sdk.message(msg) .when(m => m.attachments.length > 0) .do(async (m) => { console.log(`Message has ${m.attachments.length} attachments`) for (const att of m.attachments) { console.log(` - ${att.filename}`) } }) .execute() // Chain multiple conditions and actions await sdk.message(msg) .ifFromOthers() .ifUnread() .when(m => m.service === 'iMessage') .matchText(/question/i) .replyText('Thanks for your question!') .replyImage(['https://example.com/answer.jpg']) .execute() } } finally { await sdk.close() } ``` ### Real-time Message Watching - startWatching() and stopWatching() Monitor new messages in real-time with event handlers. ```typescript import { IMessageSDK, type Message } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK({ debug: true, watcher: { pollInterval: 2000, // Check every 2 seconds unreadOnly: false, // Watch all messages (not just unread) excludeOwnMessages: true // Exclude messages you send } }) // Track processed messages to avoid duplicates const processedIds = new Set() await sdk.startWatching({ onNewMessage: async (msg: Message) => { // Deduplicate if (processedIds.has(msg.id)) return processedIds.add(msg.id) console.log(`\n[${msg.date.toLocaleTimeString()}] Message from: ${msg.sender}`) console.log(`Text: ${msg.text}`) console.log(`Service: ${msg.service}, Group: ${msg.isGroupChat}`) // Auto-reply to text messages if (msg.text?.trim()) { await sdk.message(msg) .ifFromOthers() .replyText(`Thanks for your message: "${msg.text}"`) .execute() } // Echo back images if (msg.attachments.length > 0) { const images = msg.attachments .filter(a => a.isImage) .map(a => a.path) if (images.length > 0) { await sdk.send(msg.sender, { text: `Received ${images.length} image(s)`, images }) } } }, onGroupMessage: async (msg: Message) => { console.log(`\nGroup message in ${msg.chatId} from ${msg.sender}`) // Only respond if mentioned if (msg.text?.includes('@me')) { await sdk.send(msg.sender, 'You mentioned me in the group!') } }, onError: (error) => { console.error('Watcher error:', error.message) } }) console.log('Watching for messages... Press Ctrl+C to stop') // Graceful shutdown process.on('SIGINT', async () => { console.log('\nStopping watcher...') sdk.stopWatching() await sdk.close() console.log('Stopped') process.exit(0) }) // Keep process alive await new Promise(() => {}) ``` ### Webhook Integration Send HTTP notifications when new messages arrive. ```typescript import { IMessageSDK } from '@photon-ai/imessage-kit' import express from 'express' // Backend: Receive webhook notifications const app = express() app.use(express.json()) app.post('/imessage-webhook', (req, res) => { const { event, message, timestamp } = req.body console.log(`Webhook received: ${event} at ${timestamp}`) console.log(`From: ${message.sender}`) console.log(`Text: ${message.text}`) console.log(`Service: ${message.service}`) // Process the message if (message.text?.includes('urgent')) { // Send notification, update database, etc. console.log('Urgent message detected!') } res.json({ success: true }) }) app.listen(3000, () => { console.log('Webhook server listening on http://localhost:3000') }) // Frontend: Configure SDK with webhook const sdk = new IMessageSDK({ webhook: { url: 'http://localhost:3000/imessage-webhook', headers: { 'Authorization': 'Bearer secret-token', 'X-API-Key': 'your-api-key' }, timeout: 5000 // 5 second timeout }, watcher: { pollInterval: 2000 } }) await sdk.startWatching({ onError: (error) => { console.error('Error:', error) } }) console.log('Watching messages with webhook integration...') ``` ### Plugin System - use() and Plugin Hooks Extend SDK functionality with custom plugins. ```typescript import { IMessageSDK, loggerPlugin, definePlugin, type Plugin } from '@photon-ai/imessage-kit' // Use built-in logger plugin const sdk = new IMessageSDK() sdk.use(loggerPlugin({ level: 'info', colored: true, timestamp: true, prefix: '[iMessage]', logSend: true, logNewMessage: true })) // Create custom analytics plugin const analyticsPlugin = definePlugin({ name: 'analytics', version: '1.0.0', description: 'Track message statistics', onInit: async () => { console.log('Analytics plugin initialized') }, onBeforeSend: async (to, content) => { console.log(`Preparing to send to ${to}`) // Validate recipient, apply rate limiting, etc. }, onAfterSend: async (to, result) => { console.log(`Message sent to ${to} at ${result.sentAt}`) // Log to analytics service }, onNewMessage: async (message) => { console.log(`New message received: ${message.id}`) // Track incoming messages }, onError: async (error, context) => { console.error(`Error in ${context}:`, error.message) // Send to error tracking service }, onDestroy: async () => { console.log('Analytics plugin destroyed') // Cleanup resources } }) sdk.use(analyticsPlugin) // Create message filter plugin const filterPlugin: Plugin = { name: 'spam-filter', onBeforeSend: async (to, content) => { // Block certain recipients if (to.includes('spam')) { throw new Error('Recipient blocked by spam filter') } }, onNewMessage: async (message) => { // Filter spam messages if (message.text?.includes('spam-keyword')) { console.log('Spam message detected, ignoring') return // Don't process further } } } sdk.use(filterPlugin) // Register plugins during initialization const sdk2 = new IMessageSDK({ plugins: [ loggerPlugin({ level: 'debug' }), analyticsPlugin, filterPlugin ] }) try { await sdk.send('+1234567890', 'Hello!') // Plugins intercept and process this send } finally { await sdk.close() } ``` ### Error Handling Handle different error types with type guards. ```typescript import { IMessageSDK, IMessageError, SendError, DatabaseError, WebhookError, PlatformError, ConfigError } from '@photon-ai/imessage-kit' const sdk = new IMessageSDK({ debug: true }) try { await sdk.send('+1234567890', 'Hello!') } catch (error) { if (error instanceof IMessageError) { console.error(`Error code: ${error.code}`) console.error(`Message: ${error.message}`) switch (error.code) { case 'SEND': console.error('Failed to send message') console.error('Possible causes: Messages app not running, invalid recipient') break case 'DATABASE': console.error('Database access error') console.error('Check Full Disk Access permission') break case 'WEBHOOK': console.error('Webhook notification failed') console.error('Check webhook URL and network connectivity') break case 'PLATFORM': console.error('Platform error') console.error('This SDK only works on macOS') break case 'CONFIG': console.error('Configuration error') console.error('Check your configuration values') break } } else { console.error('Unexpected error:', error) } } // Specific error type handling try { await sdk.send('invalid-recipient', 'Test') } catch (error) { if (error instanceof SendError) { console.error('Send operation failed:', error.message) // Retry logic, fallback, etc. } } // Database query errors try { const messages = await sdk.getMessages() } catch (error) { if (error instanceof DatabaseError) { console.error('Database query failed:', error.message) console.error('Ensure Full Disk Access is granted') } } // Platform validation import { requireMacOS, isMacOS } from '@photon-ai/imessage-kit' if (!isMacOS()) { console.error('This SDK requires macOS') process.exit(1) } // Throws PlatformError if not macOS requireMacOS() // Validate recipients import { asRecipient } from '@photon-ai/imessage-kit' try { const recipient = asRecipient('+1234567890') // Valid await sdk.send(recipient, 'Hello') } catch (error) { console.error('Invalid recipient format') } await sdk.close() ``` ### Complete Auto-Reply Bot Example Full-featured bot with message processing and error handling. ```typescript import { IMessageSDK, type Message } from '@photon-ai/imessage-kit' const NETWORK_IMAGE = 'https://example.com/image.jpg' const processedIds = new Set() async function main() { const sdk = new IMessageSDK({ debug: true, watcher: { pollInterval: 2000, excludeOwnMessages: true }, maxConcurrent: 5 }) await sdk.startWatching({ onNewMessage: async (msg: Message) => { // Deduplicate messages if (processedIds.has(msg.id)) return processedIds.add(msg.id) // Memory management: limit cache size if (processedIds.size > 1000) { const ids = Array.from(processedIds) processedIds.clear() ids.slice(-500).forEach(id => processedIds.add(id)) } console.log(`\n[${new Date().toLocaleTimeString()}] New message from: ${msg.sender}`) try { // Handle image attachments - echo them back if (msg.attachments.length > 0) { const images = msg.attachments .filter(a => a.isImage) .map(a => a.path) for (const imagePath of images) { console.log(` Sending image back: ${imagePath}`) await sdk.send(msg.sender, { images: [imagePath] }) await new Promise(r => setTimeout(r, 500)) } } // Handle text messages if (msg.text?.trim()) { console.log(` Received: ${msg.text}`) // Process commands if (msg.text.toLowerCase().includes('help')) { await sdk.send(msg.sender, 'Available commands:\n- help: Show this message\n- status: Check bot status\n- photo: Get a photo') } else if (msg.text.toLowerCase().includes('status')) { await sdk.send(msg.sender, 'Bot is running and processing messages!') } else if (msg.text.toLowerCase().includes('photo')) { await sdk.send(msg.sender, { text: 'Here is a photo', images: [NETWORK_IMAGE] }) } else { // Default: echo with greeting const reply = `${msg.text} - Hello!` await sdk.send(msg.sender, reply) console.log(` Replied: ${reply}`) } } } catch (error) { console.error(` Error processing message: ${error}`) } }, onGroupMessage: async (msg: Message) => { console.log(`\nGroup message from ${msg.sender} in ${msg.chatId}`) // Only respond if directly mentioned if (msg.text?.includes('@bot')) { await sdk.send(msg.sender, 'Thanks for mentioning me!') } }, onError: (error) => { console.error(`Watcher error: ${error.message}`) } }) console.log('Bot started. Watching for messages...') console.log('Press Ctrl+C to stop\n') // Graceful shutdown const cleanup = async () => { console.log('\nShutting down bot...') sdk.stopWatching() await sdk.close() console.log('Bot stopped') process.exit(0) } process.on('SIGINT', cleanup) process.on('SIGTERM', cleanup) } main().catch((error) => { console.error('Fatal error:', error) process.exit(1) }) ``` ## Summary and Use Cases The `@photon-ai/imessage-kit` SDK enables a wide range of messaging automation use cases on macOS. Common applications include AI-powered chatbots that respond to incoming messages with intelligent replies, customer service automation that handles inquiries via iMessage, notification systems that alert users about important events, scheduling assistants that manage appointments through conversational interfaces, and file sharing automation for distributing documents and media. The SDK's real-time message watching capabilities make it ideal for building reactive systems that respond immediately to user input, while the batch sending features support mass communication scenarios like newsletters or announcements. Integration patterns are straightforward across different architectural approaches. The SDK works seamlessly with web applications through webhook notifications that forward messages to backend servers, fits naturally into event-driven architectures with its callback-based watcher system, integrates with AI models by piping message content to language models and sending back responses, and supports microservices patterns through its stateless design and flexible configuration. The cross-runtime support (Node.js and Bun) and zero-dependency architecture on Bun make it deployment-friendly, while the comprehensive TypeScript types enable safe integration into existing codebases. Whether building simple automation scripts or complex multi-service applications, the SDK provides the necessary primitives for robust iMessage integration with production-grade error handling, concurrent execution control, and extensible plugin architecture.