@elizaos/plugin-imessage 2.0.0-alpha.3

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/src/index.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * iMessage Plugin for ElizaOS
3
+ *
4
+ * Provides iMessage integration for ElizaOS agents on macOS.
5
+ * Uses AppleScript and/or CLI tools to send and receive messages.
6
+ */
7
+
8
+ import { platform } from "node:os";
9
+ import type { IAgentRuntime, Plugin } from "@elizaos/core";
10
+ import { logger } from "@elizaos/core";
11
+ import { sendMessage } from "./actions/index.js";
12
+ import { chatContextProvider } from "./providers/index.js";
13
+ import {
14
+ IMessageService,
15
+ parseChatsFromAppleScript,
16
+ parseMessagesFromAppleScript,
17
+ } from "./service.js";
18
+
19
+ // Re-export types and service
20
+ export * from "./types.js";
21
+ export { IMessageService, parseMessagesFromAppleScript, parseChatsFromAppleScript };
22
+ export { sendMessage };
23
+ export { chatContextProvider };
24
+
25
+ // Account management exports
26
+ export {
27
+ DEFAULT_ACCOUNT_ID,
28
+ type IMessageAccountConfig,
29
+ type IMessageGroupConfig,
30
+ type IMessageMultiAccountConfig,
31
+ isIMessageMentionRequired,
32
+ isIMessageUserAllowed,
33
+ isMultiAccountEnabled,
34
+ listEnabledIMessageAccounts,
35
+ listIMessageAccountIds,
36
+ normalizeAccountId,
37
+ type ResolvedIMessageAccount,
38
+ resolveDefaultIMessageAccountId,
39
+ resolveIMessageAccount,
40
+ resolveIMessageGroupConfig,
41
+ } from "./accounts.js";
42
+
43
+ // RPC client exports
44
+ export {
45
+ createIMessageRpcClient,
46
+ DEFAULT_PROBE_TIMEOUT_MS,
47
+ DEFAULT_REQUEST_TIMEOUT_MS,
48
+ getChatInfo,
49
+ getContactInfo,
50
+ getMessages,
51
+ type IMessageAttachment,
52
+ type IMessageChat,
53
+ type IMessageContact,
54
+ type IMessageMessage,
55
+ IMessageRpcClient,
56
+ type IMessageRpcClientOptions,
57
+ type IMessageRpcError,
58
+ type IMessageRpcNotification,
59
+ type IMessageRpcResponse,
60
+ listChats,
61
+ listContacts,
62
+ probeIMessageRpc,
63
+ sendIMessageRpc,
64
+ } from "./rpc.js";
65
+
66
+ /**
67
+ * iMessage plugin for ElizaOS agents.
68
+ */
69
+ const imessagePlugin: Plugin = {
70
+ name: "imessage",
71
+ description: "iMessage plugin for ElizaOS agents (macOS only)",
72
+
73
+ services: [IMessageService],
74
+ actions: [sendMessage],
75
+ providers: [chatContextProvider],
76
+ tests: [],
77
+
78
+ init: async (
79
+ config: Record<string, string>,
80
+ _runtime: IAgentRuntime,
81
+ ): Promise<void> => {
82
+ logger.info("Initializing iMessage plugin...");
83
+
84
+ const isMacOS = platform() === "darwin";
85
+
86
+ logger.info("iMessage plugin configuration:");
87
+ logger.info(` - Platform: ${platform()}`);
88
+ logger.info(` - macOS: ${isMacOS ? "Yes" : "No"}`);
89
+ logger.info(
90
+ ` - CLI path: ${config.IMESSAGE_CLI_PATH || process.env.IMESSAGE_CLI_PATH || "imsg (default)"}`,
91
+ );
92
+ logger.info(
93
+ ` - DM policy: ${config.IMESSAGE_DM_POLICY || process.env.IMESSAGE_DM_POLICY || "pairing"}`,
94
+ );
95
+
96
+ if (!isMacOS) {
97
+ logger.warn(
98
+ "iMessage plugin is only supported on macOS. The plugin will be inactive on this platform.",
99
+ );
100
+ }
101
+
102
+ logger.info("iMessage plugin initialized");
103
+ },
104
+ };
105
+
106
+ export default imessagePlugin;
107
+
108
+ // Channel configuration types
109
+ export type {
110
+ IMessageAccountConfig,
111
+ IMessageConfig,
112
+ IMessageReactionNotificationMode,
113
+ } from "./config.js";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Chat context provider for the iMessage plugin.
3
+ */
4
+
5
+ import type {
6
+ IAgentRuntime,
7
+ Memory,
8
+ Provider,
9
+ ProviderResult,
10
+ State,
11
+ } from "@elizaos/core";
12
+ import type { IMessageService } from "../service.js";
13
+ import { IMESSAGE_SERVICE_NAME } from "../types.js";
14
+
15
+ export const chatContextProvider: Provider = {
16
+ name: "imessageChatContext",
17
+ description: "Provides information about the current iMessage chat context",
18
+
19
+ get: async (
20
+ runtime: IAgentRuntime,
21
+ message: Memory,
22
+ state: State,
23
+ ): Promise<ProviderResult> => {
24
+ // Only provide context for iMessage messages
25
+ if (message.content.source !== "imessage") {
26
+ return {
27
+ data: {},
28
+ values: {},
29
+ text: "",
30
+ };
31
+ }
32
+
33
+ const imessageService = runtime.getService<IMessageService>(
34
+ IMESSAGE_SERVICE_NAME,
35
+ );
36
+
37
+ if (!imessageService || !imessageService.isConnected()) {
38
+ return {
39
+ data: { connected: false },
40
+ values: { connected: false },
41
+ text: "",
42
+ };
43
+ }
44
+
45
+ const agentName = state?.agentName || "The agent";
46
+ const stateData = (state?.data || {}) as Record<string, unknown>;
47
+
48
+ const handle = stateData.handle as string | undefined;
49
+ const chatId = stateData.chatId as string | undefined;
50
+ const chatType = stateData.chatType as string | undefined;
51
+ const displayName = stateData.displayName as string | undefined;
52
+
53
+ let chatDescription = "";
54
+ if (chatType === "group") {
55
+ chatDescription = displayName
56
+ ? `group chat "${displayName}"`
57
+ : "a group chat";
58
+ } else {
59
+ chatDescription = handle
60
+ ? `direct message with ${handle}`
61
+ : "a direct message";
62
+ }
63
+
64
+ const responseText =
65
+ `${agentName} is chatting via iMessage in ${chatDescription}. ` +
66
+ "iMessage supports text messages and attachments.";
67
+
68
+ return {
69
+ data: {
70
+ handle,
71
+ chatId,
72
+ chatType: chatType || "direct",
73
+ displayName,
74
+ connected: true,
75
+ platform: "imessage",
76
+ },
77
+ values: {
78
+ handle,
79
+ chatId,
80
+ chatType: chatType || "direct",
81
+ displayName,
82
+ },
83
+ text: responseText,
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * iMessage plugin providers.
3
+ */
4
+
5
+ export { chatContextProvider } from "./chatContext.js";
package/src/rpc.ts ADDED
@@ -0,0 +1,485 @@
1
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { resolve as resolvePath } from "node:path";
4
+ import { createInterface, type Interface } from "node:readline";
5
+
6
+ /**
7
+ * Default probe timeout in milliseconds
8
+ */
9
+ export const DEFAULT_PROBE_TIMEOUT_MS = 5000;
10
+
11
+ /**
12
+ * Default request timeout in milliseconds
13
+ */
14
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
15
+
16
+ /**
17
+ * iMessage RPC error structure
18
+ */
19
+ export interface IMessageRpcError {
20
+ code?: number;
21
+ message?: string;
22
+ data?: unknown;
23
+ }
24
+
25
+ /**
26
+ * iMessage RPC response structure
27
+ */
28
+ export interface IMessageRpcResponse<T> {
29
+ jsonrpc?: string;
30
+ id?: string | number | null;
31
+ result?: T;
32
+ error?: IMessageRpcError;
33
+ method?: string;
34
+ params?: unknown;
35
+ }
36
+
37
+ /**
38
+ * iMessage RPC notification structure
39
+ */
40
+ export interface IMessageRpcNotification {
41
+ method: string;
42
+ params?: unknown;
43
+ }
44
+
45
+ /**
46
+ * Options for creating an iMessage RPC client
47
+ */
48
+ export interface IMessageRpcClientOptions {
49
+ cliPath?: string;
50
+ dbPath?: string;
51
+ onNotification?: (msg: IMessageRpcNotification) => void;
52
+ onError?: (error: Error) => void;
53
+ onClose?: (code: number | null, signal: NodeJS.Signals | null) => void;
54
+ }
55
+
56
+ /**
57
+ * Pending request tracking
58
+ */
59
+ interface PendingRequest {
60
+ resolve: (value: unknown) => void;
61
+ reject: (error: Error) => void;
62
+ timer?: ReturnType<typeof setTimeout>;
63
+ }
64
+
65
+ /**
66
+ * Resolves a path with ~ expansion
67
+ */
68
+ function resolveUserPath(path: string): string {
69
+ if (path.startsWith("~/")) {
70
+ return resolvePath(homedir(), path.slice(2));
71
+ }
72
+ return resolvePath(path);
73
+ }
74
+
75
+ /**
76
+ * iMessage RPC client for communicating with the imsg CLI tool
77
+ */
78
+ export class IMessageRpcClient {
79
+ private readonly cliPath: string;
80
+ private readonly dbPath?: string;
81
+ private readonly onNotification?: (msg: IMessageRpcNotification) => void;
82
+ private readonly onError?: (error: Error) => void;
83
+ private readonly onClose?: (
84
+ code: number | null,
85
+ signal: NodeJS.Signals | null,
86
+ ) => void;
87
+ private readonly pending = new Map<string, PendingRequest>();
88
+ private readonly closedPromise: Promise<void>;
89
+ private closedResolve: (() => void) | null = null;
90
+ private child: ChildProcessWithoutNullStreams | null = null;
91
+ private reader: Interface | null = null;
92
+ private nextId = 1;
93
+ private started = false;
94
+
95
+ constructor(opts: IMessageRpcClientOptions = {}) {
96
+ this.cliPath = opts.cliPath?.trim() || "imsg";
97
+ this.dbPath = opts.dbPath?.trim()
98
+ ? resolveUserPath(opts.dbPath)
99
+ : undefined;
100
+ this.onNotification = opts.onNotification;
101
+ this.onError = opts.onError;
102
+ this.onClose = opts.onClose;
103
+ this.closedPromise = new Promise((resolve) => {
104
+ this.closedResolve = resolve;
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Starts the RPC client by spawning the CLI process
110
+ */
111
+ async start(): Promise<void> {
112
+ if (this.child) {
113
+ return;
114
+ }
115
+
116
+ const args = ["rpc"];
117
+ if (this.dbPath) {
118
+ args.push("--db", this.dbPath);
119
+ }
120
+
121
+ const child = spawn(this.cliPath, args, {
122
+ stdio: ["pipe", "pipe", "pipe"],
123
+ });
124
+
125
+ this.child = child;
126
+ this.started = true;
127
+ this.reader = createInterface({ input: child.stdout });
128
+
129
+ this.reader.on("line", (line) => {
130
+ const trimmed = line.trim();
131
+ if (!trimmed) {
132
+ return;
133
+ }
134
+ this.handleLine(trimmed);
135
+ });
136
+
137
+ child.stderr?.on("data", (chunk) => {
138
+ const lines = chunk.toString().split(/\r?\n/);
139
+ for (const line of lines) {
140
+ if (!line.trim()) {
141
+ continue;
142
+ }
143
+ this.onError?.(new Error(`imsg rpc stderr: ${line.trim()}`));
144
+ }
145
+ });
146
+
147
+ child.on("error", (err) => {
148
+ this.failAll(err instanceof Error ? err : new Error(String(err)));
149
+ this.onError?.(err instanceof Error ? err : new Error(String(err)));
150
+ this.closedResolve?.();
151
+ });
152
+
153
+ child.on("close", (code, signal) => {
154
+ if (code !== 0 && code !== null) {
155
+ const reason = signal ? `signal ${signal}` : `code ${code}`;
156
+ this.failAll(new Error(`imsg rpc exited (${reason})`));
157
+ } else {
158
+ this.failAll(new Error("imsg rpc closed"));
159
+ }
160
+ this.onClose?.(code, signal);
161
+ this.closedResolve?.();
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Stops the RPC client
167
+ */
168
+ async stop(): Promise<void> {
169
+ if (!this.child) {
170
+ return;
171
+ }
172
+
173
+ this.reader?.close();
174
+ this.reader = null;
175
+ this.child.stdin?.end();
176
+
177
+ const child = this.child;
178
+ this.child = null;
179
+
180
+ await Promise.race([
181
+ this.closedPromise,
182
+ new Promise<void>((resolve) => {
183
+ setTimeout(() => {
184
+ if (!child.killed) {
185
+ child.kill("SIGTERM");
186
+ }
187
+ resolve();
188
+ }, 500);
189
+ }),
190
+ ]);
191
+ }
192
+
193
+ /**
194
+ * Waits for the RPC client to close
195
+ */
196
+ async waitForClose(): Promise<void> {
197
+ await this.closedPromise;
198
+ }
199
+
200
+ /**
201
+ * Checks if the client is running
202
+ */
203
+ isRunning(): boolean {
204
+ return this.child !== null && this.started;
205
+ }
206
+
207
+ /**
208
+ * Makes an RPC request
209
+ */
210
+ async request<T = unknown>(
211
+ method: string,
212
+ params?: Record<string, unknown>,
213
+ opts?: { timeoutMs?: number },
214
+ ): Promise<T> {
215
+ if (!this.child || !this.child.stdin) {
216
+ throw new Error("imsg rpc not running");
217
+ }
218
+
219
+ const id = this.nextId++;
220
+ const payload = {
221
+ jsonrpc: "2.0",
222
+ id,
223
+ method,
224
+ params: params ?? {},
225
+ };
226
+ const line = `${JSON.stringify(payload)}\n`;
227
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
228
+
229
+ const response = new Promise<T>((resolve, reject) => {
230
+ const key = String(id);
231
+ const timer =
232
+ timeoutMs > 0
233
+ ? setTimeout(() => {
234
+ this.pending.delete(key);
235
+ reject(new Error(`imsg rpc timeout (${method})`));
236
+ }, timeoutMs)
237
+ : undefined;
238
+
239
+ this.pending.set(key, {
240
+ resolve: (value) => resolve(value as T),
241
+ reject,
242
+ timer,
243
+ });
244
+ });
245
+
246
+ this.child.stdin.write(line);
247
+ return await response;
248
+ }
249
+
250
+ /**
251
+ * Handles an incoming line from the RPC process
252
+ */
253
+ private handleLine(line: string): void {
254
+ let parsed: IMessageRpcResponse<unknown>;
255
+ try {
256
+ parsed = JSON.parse(line) as IMessageRpcResponse<unknown>;
257
+ } catch (err) {
258
+ const detail = err instanceof Error ? err.message : String(err);
259
+ this.onError?.(new Error(`imsg rpc: failed to parse ${line}: ${detail}`));
260
+ return;
261
+ }
262
+
263
+ // Handle response with ID
264
+ if (parsed.id !== undefined && parsed.id !== null) {
265
+ const key = String(parsed.id);
266
+ const pending = this.pending.get(key);
267
+ if (!pending) {
268
+ return;
269
+ }
270
+
271
+ if (pending.timer) {
272
+ clearTimeout(pending.timer);
273
+ }
274
+ this.pending.delete(key);
275
+
276
+ if (parsed.error) {
277
+ const baseMessage = parsed.error.message ?? "imsg rpc error";
278
+ const details = parsed.error.data;
279
+ const code = parsed.error.code;
280
+ const suffixes: string[] = [];
281
+
282
+ if (typeof code === "number") {
283
+ suffixes.push(`code=${code}`);
284
+ }
285
+ if (details !== undefined) {
286
+ const detailText =
287
+ typeof details === "string"
288
+ ? details
289
+ : JSON.stringify(details, null, 2);
290
+ if (detailText) {
291
+ suffixes.push(detailText);
292
+ }
293
+ }
294
+
295
+ const msg =
296
+ suffixes.length > 0
297
+ ? `${baseMessage}: ${suffixes.join(" ")}`
298
+ : baseMessage;
299
+ pending.reject(new Error(msg));
300
+ return;
301
+ }
302
+
303
+ pending.resolve(parsed.result);
304
+ return;
305
+ }
306
+
307
+ // Handle notification
308
+ if (parsed.method) {
309
+ this.onNotification?.({
310
+ method: parsed.method,
311
+ params: parsed.params,
312
+ });
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Fails all pending requests
318
+ */
319
+ private failAll(err: Error): void {
320
+ for (const [key, pending] of this.pending.entries()) {
321
+ if (pending.timer) {
322
+ clearTimeout(pending.timer);
323
+ }
324
+ pending.reject(err);
325
+ this.pending.delete(key);
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Creates and starts an iMessage RPC client
332
+ */
333
+ export async function createIMessageRpcClient(
334
+ opts: IMessageRpcClientOptions = {},
335
+ ): Promise<IMessageRpcClient> {
336
+ const client = new IMessageRpcClient(opts);
337
+ await client.start();
338
+ return client;
339
+ }
340
+
341
+ /**
342
+ * iMessage contact information
343
+ */
344
+ export interface IMessageContact {
345
+ id: string;
346
+ firstName?: string;
347
+ lastName?: string;
348
+ displayName?: string;
349
+ phones?: string[];
350
+ emails?: string[];
351
+ }
352
+
353
+ /**
354
+ * iMessage chat information
355
+ */
356
+ export interface IMessageChat {
357
+ id: string;
358
+ chatIdentifier: string;
359
+ displayName?: string;
360
+ isGroup: boolean;
361
+ participants: string[];
362
+ lastMessageDate?: number;
363
+ }
364
+
365
+ /**
366
+ * iMessage message information
367
+ */
368
+ export interface IMessageMessage {
369
+ id: string;
370
+ chatId: string;
371
+ text?: string;
372
+ sender: string;
373
+ isFromMe: boolean;
374
+ date: number;
375
+ dateRead?: number;
376
+ dateDelivered?: number;
377
+ attachments?: IMessageAttachment[];
378
+ }
379
+
380
+ /**
381
+ * iMessage attachment information
382
+ */
383
+ export interface IMessageAttachment {
384
+ id: string;
385
+ filename?: string;
386
+ mimeType?: string;
387
+ path?: string;
388
+ size?: number;
389
+ }
390
+
391
+ /**
392
+ * Probes the iMessage RPC to check connectivity
393
+ */
394
+ export async function probeIMessageRpc(params: {
395
+ cliPath?: string;
396
+ dbPath?: string;
397
+ timeoutMs?: number;
398
+ }): Promise<{ ok: boolean; error?: string; version?: string }> {
399
+ const client = new IMessageRpcClient({
400
+ cliPath: params.cliPath,
401
+ dbPath: params.dbPath,
402
+ });
403
+
404
+ try {
405
+ await client.start();
406
+ const result = await client.request<{ version?: string }>(
407
+ "ping",
408
+ undefined,
409
+ {
410
+ timeoutMs: params.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
411
+ },
412
+ );
413
+ await client.stop();
414
+ return { ok: true, version: result?.version };
415
+ } catch (err) {
416
+ await client.stop().catch(() => {});
417
+ return {
418
+ ok: false,
419
+ error: err instanceof Error ? err.message : String(err),
420
+ };
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Lists all contacts via iMessage RPC
426
+ */
427
+ export async function listContacts(
428
+ client: IMessageRpcClient,
429
+ ): Promise<IMessageContact[]> {
430
+ return client.request<IMessageContact[]>("listContacts");
431
+ }
432
+
433
+ /**
434
+ * Lists all chats via iMessage RPC
435
+ */
436
+ export async function listChats(
437
+ client: IMessageRpcClient,
438
+ ): Promise<IMessageChat[]> {
439
+ return client.request<IMessageChat[]>("listChats");
440
+ }
441
+
442
+ /**
443
+ * Gets recent messages from a chat
444
+ */
445
+ export async function getMessages(
446
+ client: IMessageRpcClient,
447
+ params: { chatId: string; limit?: number; before?: number },
448
+ ): Promise<IMessageMessage[]> {
449
+ return client.request<IMessageMessage[]>("getMessages", params);
450
+ }
451
+
452
+ /**
453
+ * Sends a message via iMessage RPC
454
+ */
455
+ export async function sendIMessageRpc(
456
+ client: IMessageRpcClient,
457
+ params: {
458
+ to: string;
459
+ text: string;
460
+ attachments?: string[];
461
+ service?: "iMessage" | "SMS";
462
+ },
463
+ ): Promise<{ messageId: string }> {
464
+ return client.request<{ messageId: string }>("send", params);
465
+ }
466
+
467
+ /**
468
+ * Gets chat info via iMessage RPC
469
+ */
470
+ export async function getChatInfo(
471
+ client: IMessageRpcClient,
472
+ params: { chatId: string },
473
+ ): Promise<IMessageChat | null> {
474
+ return client.request<IMessageChat | null>("getChatInfo", params);
475
+ }
476
+
477
+ /**
478
+ * Gets contact info via iMessage RPC
479
+ */
480
+ export async function getContactInfo(
481
+ client: IMessageRpcClient,
482
+ params: { identifier: string },
483
+ ): Promise<IMessageContact | null> {
484
+ return client.request<IMessageContact | null>("getContactInfo", params);
485
+ }