imessage-bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 imessage-bot contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # imessage-bot
2
+
3
+ > **Use at your own risk.** This project reads from your local iMessage database and sends messages via AppleScript. The author is not liable for any unintended messages sent, data accessed, or consequences arising from use of this software. Review the code before running it.
4
+
5
+ A Node.js toolkit for reading and responding to iMessages on macOS.
6
+
7
+ I originally built this to power a weight-tracking accountability bot for a friend group — members log their weight via iMessage commands, the bot parses them and stores the data. Friends wanted to use the polling layer for their own projects, so I extracted it into this standalone toolkit.
8
+
9
+ Poll any iMessage group chat or direct message, react to commands, and send replies — all from a TypeScript script running on your Mac.
10
+
11
+ ## Requirements
12
+
13
+ - **macOS** (uses the local Messages database and AppleScript)
14
+ - **Node.js 18+**
15
+ - **Full Disk Access** granted to Terminal (or your IDE) — see below
16
+
17
+ ---
18
+
19
+ ## ⚠️ Full Disk Access — Read This First
20
+
21
+ This is the most common setup issue. Without it, Node.js cannot read `~/Library/Messages/chat.db` and you'll get a permission error immediately.
22
+
23
+ ### How to grant it
24
+
25
+ 1. Open **System Settings → Privacy & Security → Full Disk Access**
26
+ 2. Click the **+** button and add **Terminal.app** (located in `/Applications/Utilities/`)
27
+ 3. Make sure the toggle next to Terminal is **on**
28
+ 4. Fully quit and reopen Terminal
29
+
30
+ ### Known UI quirk
31
+
32
+ On some macOS versions, after adding Terminal via the **+** button it may not appear visually in the list — but it has actually been granted. This is a known display glitch.
33
+
34
+ **To verify it actually worked**, run this in Terminal:
35
+
36
+ ```bash
37
+ sqlite3 ~/Library/Messages/chat.db ".tables"
38
+ ```
39
+
40
+ - If you see a list of table names → Full Disk Access is working correctly.
41
+ - If you see `unable to open database file` → it was not granted. Try removing and re-adding Terminal, then restart your Mac.
42
+
43
+ > If you're running your bot from VS Code or another IDE instead of Terminal, you need to grant Full Disk Access to **that app** instead.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ git clone https://github.com/yourusername/imessage-bot.git
49
+ cd imessage-bot
50
+ npm install
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Find your chat GUID
56
+
57
+ ```bash
58
+ npm run find-chats
59
+ ```
60
+
61
+ This lists all your iMessage chats with their GUIDs. Copy the one you want.
62
+
63
+ ### 2. Write your bot
64
+
65
+ ```ts
66
+ // my-bot.ts
67
+ import { createPoller } from './src/index.js';
68
+
69
+ const bot = createPoller({
70
+ chatGuid: 'iMessage;+;chat123456789', // paste your GUID here
71
+ onMessage: async ({ message, reply }) => {
72
+ if (message.text === '!ping') {
73
+ await reply('pong!');
74
+ }
75
+ },
76
+ });
77
+
78
+ bot.start();
79
+ ```
80
+
81
+ ```bash
82
+ npx tsx my-bot.ts
83
+ ```
84
+
85
+ That's it. Your bot is running.
86
+
87
+ ---
88
+
89
+ ## API
90
+
91
+ ### `createPoller(options)`
92
+
93
+ The main entry point. Returns a `Poller` with `start()` and `stop()` methods.
94
+
95
+ ```ts
96
+ import { createPoller } from './src/index.js';
97
+
98
+ const bot = createPoller({
99
+ chatGuid: 'iMessage;+;chat123', // required
100
+ pollIntervalMs: 10_000, // default: 10 seconds
101
+ seedWeeksBack: 1, // how far back to look on first run (just for watermarking, not processing)
102
+ stateFile: '~/.my-bot-state.json', // where to persist the ROWID watermark
103
+
104
+ onReady: ({ chatGuid, stateFile }) => {
105
+ console.log(`Bot started, state at ${stateFile}`);
106
+ },
107
+
108
+ onMessage: async ({ message, reply, chatGuid }) => {
109
+ // message.text — the message text
110
+ // message.senderId — phone number (e.g. "+15551234567") or "Me"
111
+ // message.isFromMe — boolean
112
+ // message.date — JS timestamp in ms
113
+ // message.rowid — iMessage database row ID
114
+ // reply(text) — send a reply to the same chat
115
+ // chatGuid — the GUID of the chat
116
+ },
117
+
118
+ onError: (err) => {
119
+ console.error('Error:', err.message);
120
+ },
121
+ });
122
+
123
+ bot.start();
124
+ // bot.stop(); // gracefully stops polling
125
+ ```
126
+
127
+ ### `sendMessage(chatGuid, text)`
128
+
129
+ Send a message to any chat directly, without the poller.
130
+
131
+ ```ts
132
+ import { sendMessage } from './src/index.js';
133
+
134
+ await sendMessage('iMessage;+;chat123', 'Hello from my bot!');
135
+ ```
136
+
137
+ ### `findChats(options?)`
138
+
139
+ List chats programmatically.
140
+
141
+ ```ts
142
+ import { findChats } from './src/index.js';
143
+
144
+ const all = findChats(); // all chats
145
+ const groups = findChats({ groupOnly: true, limit: 20 }); // group chats only
146
+ ```
147
+
148
+ Returns `ChatInfo[]`:
149
+
150
+ ```ts
151
+ interface ChatInfo {
152
+ guid: string;
153
+ displayName: string | null;
154
+ chatIdentifier: string;
155
+ isGroup: boolean;
156
+ }
157
+ ```
158
+
159
+ ### `getChatParticipants(chatGuid)`
160
+
161
+ Get the phone numbers of all participants in a chat.
162
+
163
+ ```ts
164
+ import { getChatParticipants } from './src/index.js';
165
+
166
+ const numbers = getChatParticipants('iMessage;+;chat123');
167
+ // ['+15551234567', '+15559876543']
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Examples
173
+
174
+ Both examples are ready to run after replacing `YOUR_CHAT_GUID_HERE`.
175
+
176
+ | Example | Command | What it does |
177
+ |---|---|---|
178
+ | Echo bot | `npx tsx examples/echo-bot.ts` | `!echo` and `!ping` commands |
179
+ | Weight bot | `npx tsx examples/weight-bot.ts` | `/w` weight logging with history |
180
+
181
+ ---
182
+
183
+ ## Running as a Background Service (launchd)
184
+
185
+ To keep your bot running permanently on macOS, register it as a launchd agent.
186
+
187
+ Create `~/Library/LaunchAgents/com.imessage-bot.mybot.plist`:
188
+
189
+ ```xml
190
+ <?xml version="1.0" encoding="UTF-8"?>
191
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
192
+ <plist version="1.0">
193
+ <dict>
194
+ <key>Label</key>
195
+ <string>com.imessage-bot.mybot</string>
196
+ <key>ProgramArguments</key>
197
+ <array>
198
+ <string>/usr/local/bin/node</string>
199
+ <string>--import</string>
200
+ <string>tsx/esm</string>
201
+ <string>/absolute/path/to/my-bot.ts</string>
202
+ </array>
203
+ <key>RunAtLoad</key>
204
+ <true/>
205
+ <key>KeepAlive</key>
206
+ <true/>
207
+ <key>StandardOutPath</key>
208
+ <string>/tmp/imessage-bot.log</string>
209
+ <key>StandardErrorPath</key>
210
+ <string>/tmp/imessage-bot.err</string>
211
+ </dict>
212
+ </plist>
213
+ ```
214
+
215
+ ```bash
216
+ launchctl load ~/Library/LaunchAgents/com.imessage-bot.mybot.plist
217
+ launchctl start com.imessage-bot.mybot
218
+ ```
219
+
220
+ ---
221
+
222
+ ## How It Works
223
+
224
+ ```
225
+ iMessage → chat.db (SQLite, read-only)
226
+ ↓ polled every N seconds via ROWID watermark
227
+ imessage-bot
228
+ ↓ onMessage handler
229
+ your code (store data, call APIs, etc.)
230
+ ↓ reply()
231
+ AppleScript → Messages.app → iMessage reply
232
+ ```
233
+
234
+ **ROWID watermark**: The poller tracks the last-seen message row ID in a local state file. On each poll it only fetches rows newer than that ID — no duplicate processing, no re-reading old messages.
235
+
236
+ **First run**: On first start, the poller seeds the watermark from existing messages without processing them. Your bot only reacts to messages sent *after* it starts for the first time.
237
+
238
+ ---
239
+
240
+ ## Limitations
241
+
242
+ - macOS only — relies on `~/Library/Messages/chat.db` and AppleScript
243
+ - Requires the Mac to be awake and Messages.app to be running
244
+ - Sending messages via AppleScript requires Messages.app to be signed in to the Apple ID that owns the chat
245
+ - Polling is not real-time — default latency is up to 10 seconds (configurable)
246
+
247
+ ---
248
+
249
+ ## License
250
+
251
+ MIT
package/dist/db.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ export interface ChatInfo {
2
+ guid: string;
3
+ displayName: string | null;
4
+ chatIdentifier: string;
5
+ isGroup: boolean;
6
+ }
7
+ export interface RawMessage {
8
+ rowid: number;
9
+ text: string;
10
+ date: number;
11
+ senderId: string;
12
+ isFromMe: boolean;
13
+ }
14
+ /**
15
+ * List chats. By default returns all chats.
16
+ * Pass `groupOnly: true` to return only group chats.
17
+ */
18
+ export declare function findChats(options?: {
19
+ limit?: number;
20
+ groupOnly?: boolean;
21
+ }): ChatInfo[];
22
+ /**
23
+ * Fetch messages from a chat.
24
+ *
25
+ * - `lastSeenRowId > 0` → only messages newer than that ROWID (normal polling)
26
+ * - `lastSeenRowId === 0` → messages from the last `weeksBack` weeks (first-run seed)
27
+ */
28
+ export declare function getMessagesFromChat(chatGuid: string, weeksBack?: number, lastSeenRowId?: number): RawMessage[];
29
+ /**
30
+ * Get all participant phone numbers for a chat.
31
+ */
32
+ export declare function getChatParticipants(chatGuid: string): string[];
33
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAoED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,QAAQ,EAAE,CA+B3F;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAI,EACb,aAAa,SAAI,GAChB,UAAU,EAAE,CAmEd;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9D"}
package/dist/db.js ADDED
@@ -0,0 +1,170 @@
1
+ import Database from "better-sqlite3";
2
+ import { join } from "path";
3
+ import { existsSync } from "fs";
4
+ import { Unarchiver } from "node-typedstream";
5
+ const DB_PATH = join(process.env.HOME || "", "Library/Messages/chat.db");
6
+ // ─── Internal helpers ────────────────────────────────────────────────────────
7
+ function getDb() {
8
+ if (!existsSync(DB_PATH)) {
9
+ throw new Error(`iMessage database not found at ${DB_PATH}.\n` +
10
+ `Make sure Full Disk Access is enabled for Terminal (or your IDE) in:\n` +
11
+ `System Settings → Privacy & Security → Full Disk Access`);
12
+ }
13
+ return new Database(DB_PATH, { readonly: true });
14
+ }
15
+ /**
16
+ * Apple stores timestamps as nanoseconds since 2001-01-01.
17
+ * Convert to a standard JS timestamp (ms since 1970-01-01).
18
+ */
19
+ function appleTimestampToMs(ts) {
20
+ const APPLE_EPOCH_OFFSET_S = 978307200; // seconds between Unix and Apple epochs
21
+ return (ts / 1_000_000_000 + APPLE_EPOCH_OFFSET_S) * 1000;
22
+ }
23
+ /**
24
+ * Decode binary iMessage attributedBody blobs (NSAttributedString).
25
+ * Uses node-typedstream — the same approach used by BlueBubbles server.
26
+ */
27
+ function extractTextFromAttributedBody(body) {
28
+ if (!body || body.length === 0)
29
+ return null;
30
+ try {
31
+ const decoded = Unarchiver.open(body).decodeAll();
32
+ if (!decoded)
33
+ return null;
34
+ const items = (Array.isArray(decoded) ? decoded : [decoded]).flat();
35
+ for (const item of items) {
36
+ if (item && typeof item === "object") {
37
+ if ("string" in item && typeof item.string === "string" && item.string.trim()) {
38
+ return item.string.trim();
39
+ }
40
+ if ("values" in item && Array.isArray(item.values)) {
41
+ for (const val of item.values) {
42
+ if (val && typeof val === "object" && "string" in val && typeof val.string === "string" && val.string.trim()) {
43
+ return val.string.trim();
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ // ─── Public API ──────────────────────────────────────────────────────────────
56
+ /**
57
+ * List chats. By default returns all chats.
58
+ * Pass `groupOnly: true` to return only group chats.
59
+ */
60
+ export function findChats(options = {}) {
61
+ const { limit = 50, groupOnly = false } = options;
62
+ const db = getDb();
63
+ const query = `
64
+ SELECT c.guid, c.display_name, c.chat_identifier, c.style
65
+ FROM chat c
66
+ LEFT JOIN (
67
+ SELECT cmj.chat_id, MAX(m.date) AS last_date
68
+ FROM message m
69
+ INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
70
+ GROUP BY cmj.chat_id
71
+ ) last ON c.ROWID = last.chat_id
72
+ ${groupOnly ? "WHERE c.style = 43" : ""}
73
+ ORDER BY last.last_date DESC NULLS LAST
74
+ LIMIT ?
75
+ `;
76
+ const rows = db.prepare(query).all(limit);
77
+ db.close();
78
+ return rows.map((r) => ({
79
+ guid: r.guid,
80
+ displayName: r.display_name,
81
+ chatIdentifier: r.chat_identifier,
82
+ isGroup: r.style === 43,
83
+ }));
84
+ }
85
+ /**
86
+ * Fetch messages from a chat.
87
+ *
88
+ * - `lastSeenRowId > 0` → only messages newer than that ROWID (normal polling)
89
+ * - `lastSeenRowId === 0` → messages from the last `weeksBack` weeks (first-run seed)
90
+ */
91
+ export function getMessagesFromChat(chatGuid, weeksBack = 1, lastSeenRowId = 0) {
92
+ const db = getDb();
93
+ let query;
94
+ let params;
95
+ if (lastSeenRowId > 0) {
96
+ query = `
97
+ SELECT
98
+ m.ROWID AS rowid,
99
+ m.text,
100
+ m.attributedBody,
101
+ m.date,
102
+ m.is_from_me,
103
+ COALESCE(h.id, 'Me') AS sender_id
104
+ FROM message m
105
+ INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
106
+ INNER JOIN chat c ON cmj.chat_id = c.ROWID
107
+ LEFT JOIN handle h ON m.handle_id = h.ROWID
108
+ WHERE c.guid = ?
109
+ AND m.ROWID > ?
110
+ AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL)
111
+ ORDER BY m.ROWID ASC
112
+ `;
113
+ params = [chatGuid, lastSeenRowId];
114
+ }
115
+ else {
116
+ const startMs = Date.now() - weeksBack * 7 * 24 * 60 * 60 * 1000;
117
+ const APPLE_EPOCH_OFFSET_S = 978307200;
118
+ const startApple = (startMs / 1000 - APPLE_EPOCH_OFFSET_S) * 1_000_000_000;
119
+ query = `
120
+ SELECT
121
+ m.ROWID AS rowid,
122
+ m.text,
123
+ m.attributedBody,
124
+ m.date,
125
+ m.is_from_me,
126
+ COALESCE(h.id, 'Me') AS sender_id
127
+ FROM message m
128
+ INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
129
+ INNER JOIN chat c ON cmj.chat_id = c.ROWID
130
+ LEFT JOIN handle h ON m.handle_id = h.ROWID
131
+ WHERE c.guid = ?
132
+ AND m.date > ?
133
+ AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL)
134
+ ORDER BY m.ROWID ASC
135
+ `;
136
+ params = [chatGuid, startApple];
137
+ }
138
+ const rows = db.prepare(query).all(...params);
139
+ db.close();
140
+ return rows
141
+ .map((row) => {
142
+ const text = row.text || extractTextFromAttributedBody(row.attributedBody);
143
+ if (!text?.trim())
144
+ return null;
145
+ return {
146
+ rowid: row.rowid,
147
+ text: text.trim(),
148
+ date: appleTimestampToMs(row.date),
149
+ senderId: row.is_from_me ? "Me" : row.sender_id,
150
+ isFromMe: row.is_from_me === 1,
151
+ };
152
+ })
153
+ .filter((m) => m !== null);
154
+ }
155
+ /**
156
+ * Get all participant phone numbers for a chat.
157
+ */
158
+ export function getChatParticipants(chatGuid) {
159
+ const db = getDb();
160
+ const rows = db
161
+ .prepare(`SELECT h.id
162
+ FROM handle h
163
+ INNER JOIN chat_handle_join chj ON h.ROWID = chj.handle_id
164
+ INNER JOIN chat c ON chj.chat_id = c.ROWID
165
+ WHERE c.guid = ?`)
166
+ .all(chatGuid);
167
+ db.close();
168
+ return rows.map((r) => r.id);
169
+ }
170
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=find-chats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-chats.d.ts","sourceRoot":"","sources":["../src/find-chats.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * CLI utility to discover your iMessage chat GUIDs.
3
+ *
4
+ * Run with: npm run find-chats
5
+ *
6
+ * Copy the GUID of your target chat and use it as the `chatGuid` option
7
+ * when calling `createPoller()`.
8
+ *
9
+ * Note: Group chat names are only stored in chat.db when the group has been
10
+ * explicitly named inside Messages.app. Otherwise the name is NULL — this is
11
+ * a macOS limitation, not a bug. Participants are shown so you can identify
12
+ * which chat is which.
13
+ */
14
+ import { findChats, getChatParticipants } from "./db.js";
15
+ console.log("🔍 Scanning iMessage chats...\n");
16
+ const chats = findChats({ limit: 50 });
17
+ if (chats.length === 0) {
18
+ console.log("No chats found. Make sure Full Disk Access is enabled for Terminal.");
19
+ console.log("System Settings → Privacy & Security → Full Disk Access");
20
+ process.exit(1);
21
+ }
22
+ const groups = chats.filter((c) => c.isGroup);
23
+ const direct = chats.filter((c) => !c.isGroup);
24
+ if (groups.length > 0) {
25
+ console.log(`─── Group Chats (${groups.length}) ─────────────────────────`);
26
+ for (const chat of groups) {
27
+ const participants = getChatParticipants(chat.guid);
28
+ const name = chat.displayName || "(no name set in Messages.app)";
29
+ console.log(`Name : ${name}`);
30
+ console.log(`GUID : ${chat.guid}`);
31
+ console.log(`Participants : ${participants.length > 0 ? participants.join(", ") : "none found"}`);
32
+ console.log("");
33
+ }
34
+ }
35
+ if (direct.length > 0) {
36
+ console.log(`─── Direct Messages (${direct.length}) ──────────────────────`);
37
+ for (const chat of direct) {
38
+ console.log(`Contact : ${chat.chatIdentifier}`);
39
+ console.log(`GUID : ${chat.guid}`);
40
+ console.log("");
41
+ }
42
+ }
43
+ console.log("💡 Copy the GUID above and pass it as chatGuid to createPoller().");
44
+ //# sourceMappingURL=find-chats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-chats.js","sourceRoot":"","sources":["../src/find-chats.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEzD,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;AAE/C,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;AAEvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,qEAAqE,CAAC,CAAC;IACnF,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAE/C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,CAAC,MAAM,6BAA6B,CAAC,CAAC;IAC5E,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,YAAY,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,IAAI,+BAA+B,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CAAC,kBAAkB,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;QAClG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,wBAAwB,MAAM,CAAC,MAAM,0BAA0B,CAAC,CAAC;IAC7E,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,mEAAmE,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { createPoller } from "./poller.js";
2
+ export type { PollerOptions, Poller, MessageContext } from "./poller.js";
3
+ export { sendMessage } from "./messenger.js";
4
+ export { findChats, getMessagesFromChat, getChatParticipants } from "./db.js";
5
+ export type { ChatInfo, RawMessage } from "./db.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Core bot API
2
+ export { createPoller } from "./poller.js";
3
+ // Low-level messaging
4
+ export { sendMessage } from "./messenger.js";
5
+ // Database utilities
6
+ export { findChats, getMessagesFromChat, getChatParticipants } from "./db.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,sBAAsB;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Send a message to an iMessage chat via AppleScript.
3
+ *
4
+ * Requires Messages.app to be signed in and the chat to exist.
5
+ * The `chatGuid` can be obtained from `findChats()` or `npm run find-chats`.
6
+ */
7
+ export declare function sendMessage(chatGuid: string, text: string): Promise<void>;
8
+ //# sourceMappingURL=messenger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messenger.d.ts","sourceRoot":"","sources":["../src/messenger.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa/E"}
@@ -0,0 +1,22 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ const execAsync = promisify(exec);
4
+ /**
5
+ * Send a message to an iMessage chat via AppleScript.
6
+ *
7
+ * Requires Messages.app to be signed in and the chat to exist.
8
+ * The `chatGuid` can be obtained from `findChats()` or `npm run find-chats`.
9
+ */
10
+ export async function sendMessage(chatGuid, text) {
11
+ // Escape for AppleScript string literal
12
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
13
+ const script = `
14
+ tell application "Messages"
15
+ set targetChat to chat id "${chatGuid}"
16
+ send "${escaped}" to targetChat
17
+ end tell
18
+ `;
19
+ // Escape single quotes for the shell -e argument
20
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
21
+ }
22
+ //# sourceMappingURL=messenger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messenger.js","sourceRoot":"","sources":["../src/messenger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB,EAAE,IAAY;IAC9D,wCAAwC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAG;;mCAEkB,QAAQ;cAC7B,OAAO;;GAElB,CAAC;IAEF,iDAAiD;IACjD,MAAM,SAAS,CAAC,iBAAiB,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;AACrE,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { type RawMessage } from "./db.js";
2
+ export type { RawMessage };
3
+ export interface MessageContext {
4
+ /** The incoming message */
5
+ message: RawMessage;
6
+ /** The GUID of the chat this message was received in */
7
+ chatGuid: string;
8
+ /** Send a reply to the same chat */
9
+ reply: (text: string) => Promise<void>;
10
+ }
11
+ export interface PollerOptions {
12
+ /** Chat GUID to monitor. Get this by running: npm run find-chats */
13
+ chatGuid: string;
14
+ /**
15
+ * How often to poll for new messages, in milliseconds.
16
+ * @default 10000 (10 seconds)
17
+ */
18
+ pollIntervalMs?: number;
19
+ /**
20
+ * On first run, how many weeks back to seed the ROWID watermark from.
21
+ * Messages in this window are NOT processed — the poller just advances past them
22
+ * so it only reacts to messages sent after it starts.
23
+ * @default 1
24
+ */
25
+ seedWeeksBack?: number;
26
+ /**
27
+ * Where to persist the ROWID watermark between restarts.
28
+ * Defaults to ~/.imessage-bot-<hash>.json (one file per chatGuid).
29
+ */
30
+ stateFile?: string;
31
+ /**
32
+ * Called for every new message received in the chat.
33
+ * Use `context.reply(text)` to respond.
34
+ */
35
+ onMessage: (context: MessageContext) => Promise<void> | void;
36
+ /**
37
+ * Called when an error occurs during a poll cycle.
38
+ * If not provided, errors are logged to stderr.
39
+ */
40
+ onError?: (error: Error) => void;
41
+ /**
42
+ * Called once when the poller starts.
43
+ */
44
+ onReady?: (info: {
45
+ chatGuid: string;
46
+ stateFile: string;
47
+ }) => void;
48
+ }
49
+ export interface Poller {
50
+ /** Start polling. Safe to call once. */
51
+ start(): void;
52
+ /** Stop polling and clear the interval. */
53
+ stop(): void;
54
+ }
55
+ /**
56
+ * Create an iMessage bot that polls a chat for new messages.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { createPoller } from 'imessage-bot';
61
+ *
62
+ * const bot = createPoller({
63
+ * chatGuid: 'iMessage;+;chat123456',
64
+ * onMessage: async ({ message, reply }) => {
65
+ * if (message.text === '!ping') {
66
+ * await reply('pong!');
67
+ * }
68
+ * },
69
+ * });
70
+ *
71
+ * bot.start();
72
+ * ```
73
+ */
74
+ export declare function createPoller(options: PollerOptions): Poller;
75
+ //# sourceMappingURL=poller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.d.ts","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAO/D,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,UAAU,CAAC;IACpB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE7D;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,MAAM;IACrB,wCAAwC;IACxC,KAAK,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,IAAI,IAAI,IAAI,CAAC;CACd;AA4BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqF3D"}
package/dist/poller.js ADDED
@@ -0,0 +1,120 @@
1
+ import { getMessagesFromChat } from "./db.js";
2
+ import { sendMessage } from "./messenger.js";
3
+ import { readFileSync, writeFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ // ─── State helpers ───────────────────────────────────────────────────────────
6
+ function defaultStateFile(chatGuid) {
7
+ // Derive a short stable slug from the chat GUID so multiple bots don't collide
8
+ const slug = Buffer.from(chatGuid).toString("base64url").slice(0, 16);
9
+ return join(process.env.HOME || "", `.imessage-bot-${slug}.json`);
10
+ }
11
+ function loadState(stateFile) {
12
+ try {
13
+ if (existsSync(stateFile)) {
14
+ const data = JSON.parse(readFileSync(stateFile, "utf-8"));
15
+ return typeof data.lastSeenRowId === "number" ? data.lastSeenRowId : 0;
16
+ }
17
+ }
18
+ catch {
19
+ // corrupt or missing — start fresh
20
+ }
21
+ return 0;
22
+ }
23
+ function saveState(stateFile, lastSeenRowId) {
24
+ writeFileSync(stateFile, JSON.stringify({ lastSeenRowId }), "utf-8");
25
+ }
26
+ // ─── createPoller ────────────────────────────────────────────────────────────
27
+ /**
28
+ * Create an iMessage bot that polls a chat for new messages.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { createPoller } from 'imessage-bot';
33
+ *
34
+ * const bot = createPoller({
35
+ * chatGuid: 'iMessage;+;chat123456',
36
+ * onMessage: async ({ message, reply }) => {
37
+ * if (message.text === '!ping') {
38
+ * await reply('pong!');
39
+ * }
40
+ * },
41
+ * });
42
+ *
43
+ * bot.start();
44
+ * ```
45
+ */
46
+ export function createPoller(options) {
47
+ const { chatGuid, pollIntervalMs = 10_000, seedWeeksBack = 1, onMessage, onError, onReady, } = options;
48
+ const stateFile = options.stateFile ?? defaultStateFile(chatGuid);
49
+ let lastSeenRowId = loadState(stateFile);
50
+ let intervalHandle = null;
51
+ let running = false;
52
+ async function poll() {
53
+ try {
54
+ const isFirstRun = lastSeenRowId === 0;
55
+ const messages = getMessagesFromChat(chatGuid, seedWeeksBack, lastSeenRowId);
56
+ if (isFirstRun) {
57
+ if (messages.length > 0) {
58
+ lastSeenRowId = Math.max(...messages.map((m) => m.rowid));
59
+ saveState(stateFile, lastSeenRowId);
60
+ }
61
+ return;
62
+ }
63
+ if (messages.length === 0)
64
+ return;
65
+ for (const message of messages) {
66
+ const context = {
67
+ message,
68
+ chatGuid,
69
+ reply: (text) => sendMessage(chatGuid, text),
70
+ };
71
+ try {
72
+ await onMessage(context);
73
+ }
74
+ catch (err) {
75
+ const error = err instanceof Error ? err : new Error(String(err));
76
+ if (onError) {
77
+ onError(error);
78
+ }
79
+ else {
80
+ console.error(`[imessage-bot] onMessage error:`, error.message);
81
+ }
82
+ }
83
+ }
84
+ lastSeenRowId = Math.max(...messages.map((m) => m.rowid));
85
+ saveState(stateFile, lastSeenRowId);
86
+ }
87
+ catch (err) {
88
+ const error = err instanceof Error ? err : new Error(String(err));
89
+ if (onError) {
90
+ onError(error);
91
+ }
92
+ else {
93
+ console.error(`[imessage-bot] Poll error:`, error.message);
94
+ }
95
+ }
96
+ }
97
+ return {
98
+ start() {
99
+ if (running)
100
+ return;
101
+ running = true;
102
+ onReady?.({ chatGuid, stateFile });
103
+ // Immediate first poll, then on interval
104
+ poll();
105
+ intervalHandle = setInterval(poll, pollIntervalMs);
106
+ process.on("SIGINT", () => this.stop());
107
+ process.on("SIGTERM", () => this.stop());
108
+ },
109
+ stop() {
110
+ if (!running)
111
+ return;
112
+ running = false;
113
+ if (intervalHandle !== null) {
114
+ clearInterval(intervalHandle);
115
+ intervalHandle = null;
116
+ }
117
+ },
118
+ };
119
+ }
120
+ //# sourceMappingURL=poller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "imessage-bot",
3
+ "version": "0.1.0",
4
+ "description": "A Node.js toolkit for reading and responding to iMessages on macOS",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "find-chats": "npx tsx src/find-chats.ts",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": ["imessage", "bot", "macos", "messages", "automation", "applescript", "sqlite"],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18.0.0",
29
+ "os": ["darwin"]
30
+ },
31
+ "devDependencies": {
32
+ "@types/better-sqlite3": "^7.6.13",
33
+ "@types/node": "^20.11.0",
34
+ "tsx": "^4.7.0",
35
+ "typescript": "^5.3.3"
36
+ },
37
+ "dependencies": {
38
+ "better-sqlite3": "^9.4.0",
39
+ "node-typedstream": "^1.4.1"
40
+ }
41
+ }