@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/__tests__/integration.test.ts +548 -0
- package/build.ts +16 -0
- package/dist/index.js +46 -0
- package/package.json +33 -0
- package/src/accounts.ts +379 -0
- package/src/actions/index.ts +5 -0
- package/src/actions/sendMessage.ts +218 -0
- package/src/config.ts +82 -0
- package/src/index.ts +113 -0
- package/src/providers/chatContext.ts +86 -0
- package/src/providers/index.ts +5 -0
- package/src/rpc.ts +485 -0
- package/src/service.ts +589 -0
- package/src/types.ts +291 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import imessagePlugin, {
|
|
4
|
+
IMessageService,
|
|
5
|
+
chatContextProvider,
|
|
6
|
+
sendMessage,
|
|
7
|
+
// Type utilities
|
|
8
|
+
isPhoneNumber,
|
|
9
|
+
isEmail,
|
|
10
|
+
isValidIMessageTarget,
|
|
11
|
+
normalizeIMessageTarget,
|
|
12
|
+
formatPhoneNumber,
|
|
13
|
+
splitMessageForIMessage,
|
|
14
|
+
MAX_IMESSAGE_MESSAGE_LENGTH,
|
|
15
|
+
// Parsing functions
|
|
16
|
+
parseMessagesFromAppleScript,
|
|
17
|
+
parseChatsFromAppleScript,
|
|
18
|
+
// Error classes
|
|
19
|
+
IMessagePluginError,
|
|
20
|
+
IMessageConfigurationError,
|
|
21
|
+
IMessageNotSupportedError,
|
|
22
|
+
IMessageCliError,
|
|
23
|
+
// Event types
|
|
24
|
+
IMessageEventTypes,
|
|
25
|
+
IMESSAGE_SERVICE_NAME,
|
|
26
|
+
} from "../src/index";
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// Plugin exports
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
describe("iMessage plugin exports", () => {
|
|
33
|
+
it("exports plugin metadata", () => {
|
|
34
|
+
expect(imessagePlugin.name).toBe("imessage");
|
|
35
|
+
expect(imessagePlugin.description).toContain("iMessage");
|
|
36
|
+
expect(Array.isArray(imessagePlugin.actions)).toBe(true);
|
|
37
|
+
expect(Array.isArray(imessagePlugin.providers)).toBe(true);
|
|
38
|
+
expect(Array.isArray(imessagePlugin.services)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("exports actions, providers, and service", () => {
|
|
42
|
+
expect(sendMessage).toBeDefined();
|
|
43
|
+
expect(chatContextProvider).toBeDefined();
|
|
44
|
+
expect(IMessageService).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("exports parsing utility functions", () => {
|
|
48
|
+
expect(parseMessagesFromAppleScript).toBeDefined();
|
|
49
|
+
expect(parseChatsFromAppleScript).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("exports constants", () => {
|
|
53
|
+
expect(IMESSAGE_SERVICE_NAME).toBe("imessage");
|
|
54
|
+
expect(MAX_IMESSAGE_MESSAGE_LENGTH).toBe(4000);
|
|
55
|
+
expect(IMessageEventTypes.MESSAGE_RECEIVED).toBe(
|
|
56
|
+
"IMESSAGE_MESSAGE_RECEIVED",
|
|
57
|
+
);
|
|
58
|
+
expect(IMessageEventTypes.MESSAGE_SENT).toBe("IMESSAGE_MESSAGE_SENT");
|
|
59
|
+
expect(IMessageEventTypes.CONNECTION_READY).toBe(
|
|
60
|
+
"IMESSAGE_CONNECTION_READY",
|
|
61
|
+
);
|
|
62
|
+
expect(IMessageEventTypes.ERROR).toBe("IMESSAGE_ERROR");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ============================================================
|
|
67
|
+
// isPhoneNumber
|
|
68
|
+
// ============================================================
|
|
69
|
+
|
|
70
|
+
describe("isPhoneNumber", () => {
|
|
71
|
+
it("accepts valid US phone numbers", () => {
|
|
72
|
+
expect(isPhoneNumber("+15551234567")).toBe(true);
|
|
73
|
+
expect(isPhoneNumber("15551234567")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("accepts formatted phone numbers", () => {
|
|
77
|
+
expect(isPhoneNumber("1-555-123-4567")).toBe(true);
|
|
78
|
+
expect(isPhoneNumber("(555) 123-4567")).toBe(true);
|
|
79
|
+
expect(isPhoneNumber("555.123.4567")).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("accepts international phone numbers", () => {
|
|
83
|
+
expect(isPhoneNumber("+44 7700 900000")).toBe(true);
|
|
84
|
+
expect(isPhoneNumber("+61412345678")).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rejects emails", () => {
|
|
88
|
+
expect(isPhoneNumber("test@example.com")).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects too-short numbers", () => {
|
|
92
|
+
expect(isPhoneNumber("12345")).toBe(false);
|
|
93
|
+
expect(isPhoneNumber("123")).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("rejects plain text", () => {
|
|
97
|
+
expect(isPhoneNumber("hello world")).toBe(false);
|
|
98
|
+
expect(isPhoneNumber("not a phone")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("rejects empty string", () => {
|
|
102
|
+
expect(isPhoneNumber("")).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// isEmail
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
describe("isEmail", () => {
|
|
111
|
+
it("accepts valid email addresses", () => {
|
|
112
|
+
expect(isEmail("test@example.com")).toBe(true);
|
|
113
|
+
expect(isEmail("user.name@domain.co.uk")).toBe(true);
|
|
114
|
+
expect(isEmail("admin@sub.domain.org")).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects phone numbers", () => {
|
|
118
|
+
expect(isEmail("+15551234567")).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("rejects plain text", () => {
|
|
122
|
+
expect(isEmail("not an email")).toBe(false);
|
|
123
|
+
expect(isEmail("hello")).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects partial addresses", () => {
|
|
127
|
+
expect(isEmail("@domain.com")).toBe(false);
|
|
128
|
+
expect(isEmail("user@")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("rejects empty string", () => {
|
|
132
|
+
expect(isEmail("")).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ============================================================
|
|
137
|
+
// isValidIMessageTarget
|
|
138
|
+
// ============================================================
|
|
139
|
+
|
|
140
|
+
describe("isValidIMessageTarget", () => {
|
|
141
|
+
it("accepts phone numbers", () => {
|
|
142
|
+
expect(isValidIMessageTarget("+15551234567")).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("accepts email addresses", () => {
|
|
146
|
+
expect(isValidIMessageTarget("user@example.com")).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("accepts chat_id: prefixed targets", () => {
|
|
150
|
+
expect(isValidIMessageTarget("chat_id:iMessage;+;chat12345")).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rejects invalid targets", () => {
|
|
154
|
+
expect(isValidIMessageTarget("hello world")).toBe(false);
|
|
155
|
+
expect(isValidIMessageTarget("123")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("handles whitespace", () => {
|
|
159
|
+
expect(isValidIMessageTarget(" +15551234567 ")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ============================================================
|
|
164
|
+
// normalizeIMessageTarget
|
|
165
|
+
// ============================================================
|
|
166
|
+
|
|
167
|
+
describe("normalizeIMessageTarget", () => {
|
|
168
|
+
it("returns null for empty string", () => {
|
|
169
|
+
expect(normalizeIMessageTarget("")).toBeNull();
|
|
170
|
+
expect(normalizeIMessageTarget(" ")).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("preserves chat_id: prefix", () => {
|
|
174
|
+
expect(normalizeIMessageTarget("chat_id:12345")).toBe("chat_id:12345");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("strips imessage: prefix", () => {
|
|
178
|
+
const result = normalizeIMessageTarget("imessage:+15551234567");
|
|
179
|
+
expect(result).toBe("+15551234567");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("trims whitespace", () => {
|
|
183
|
+
expect(normalizeIMessageTarget(" +15551234567 ")).toBe("+15551234567");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns phone/email as-is", () => {
|
|
187
|
+
expect(normalizeIMessageTarget("+15551234567")).toBe("+15551234567");
|
|
188
|
+
expect(normalizeIMessageTarget("user@example.com")).toBe(
|
|
189
|
+
"user@example.com",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ============================================================
|
|
195
|
+
// formatPhoneNumber
|
|
196
|
+
// ============================================================
|
|
197
|
+
|
|
198
|
+
describe("formatPhoneNumber", () => {
|
|
199
|
+
it("removes formatting characters", () => {
|
|
200
|
+
expect(formatPhoneNumber("+1 (555) 123-4567")).toBe("+15551234567");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("adds + prefix for international numbers > 10 digits", () => {
|
|
204
|
+
expect(formatPhoneNumber("15551234567")).toBe("+15551234567");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("preserves existing + prefix", () => {
|
|
208
|
+
expect(formatPhoneNumber("+15551234567")).toBe("+15551234567");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does not add + for 10-digit numbers", () => {
|
|
212
|
+
expect(formatPhoneNumber("5551234567")).toBe("5551234567");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("handles dots and spaces", () => {
|
|
216
|
+
expect(formatPhoneNumber("555.123.4567")).toBe("5551234567");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ============================================================
|
|
221
|
+
// splitMessageForIMessage
|
|
222
|
+
// ============================================================
|
|
223
|
+
|
|
224
|
+
describe("splitMessageForIMessage", () => {
|
|
225
|
+
it("returns single chunk for short messages", () => {
|
|
226
|
+
const result = splitMessageForIMessage("Hello world");
|
|
227
|
+
expect(result).toEqual(["Hello world"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("returns single chunk for exactly max-length messages", () => {
|
|
231
|
+
const text = "a".repeat(MAX_IMESSAGE_MESSAGE_LENGTH);
|
|
232
|
+
const result = splitMessageForIMessage(text);
|
|
233
|
+
expect(result).toHaveLength(1);
|
|
234
|
+
expect(result[0]).toBe(text);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("splits long messages at word boundaries", () => {
|
|
238
|
+
const words = Array.from({ length: 500 }, (_, i) => `word${i}`).join(" ");
|
|
239
|
+
const result = splitMessageForIMessage(words, 100);
|
|
240
|
+
expect(result.length).toBeGreaterThan(1);
|
|
241
|
+
for (const chunk of result) {
|
|
242
|
+
expect(chunk.length).toBeLessThanOrEqual(100);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("prefers newline break points", () => {
|
|
247
|
+
const text = "a".repeat(60) + "\n" + "b".repeat(30);
|
|
248
|
+
const result = splitMessageForIMessage(text, 80);
|
|
249
|
+
expect(result).toHaveLength(2);
|
|
250
|
+
expect(result[0]).toBe("a".repeat(60));
|
|
251
|
+
expect(result[1]).toBe("b".repeat(30));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("handles text with no break points", () => {
|
|
255
|
+
const text = "a".repeat(200);
|
|
256
|
+
const result = splitMessageForIMessage(text, 100);
|
|
257
|
+
expect(result.length).toBeGreaterThan(1);
|
|
258
|
+
// All text should be preserved
|
|
259
|
+
expect(result.join("")).toBe(text);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("returns empty array for empty string", () => {
|
|
263
|
+
const result = splitMessageForIMessage("");
|
|
264
|
+
expect(result).toEqual([""]);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ============================================================
|
|
269
|
+
// parseMessagesFromAppleScript
|
|
270
|
+
// ============================================================
|
|
271
|
+
|
|
272
|
+
describe("parseMessagesFromAppleScript", () => {
|
|
273
|
+
it("parses a single message line", () => {
|
|
274
|
+
const input =
|
|
275
|
+
"msg001\tHello there\t1700000000000\t0\tchat123\t+15551234567";
|
|
276
|
+
const result = parseMessagesFromAppleScript(input);
|
|
277
|
+
|
|
278
|
+
expect(result).toHaveLength(1);
|
|
279
|
+
expect(result[0].id).toBe("msg001");
|
|
280
|
+
expect(result[0].text).toBe("Hello there");
|
|
281
|
+
expect(result[0].timestamp).toBe(1700000000000);
|
|
282
|
+
expect(result[0].isFromMe).toBe(false);
|
|
283
|
+
expect(result[0].chatId).toBe("chat123");
|
|
284
|
+
expect(result[0].handle).toBe("+15551234567");
|
|
285
|
+
expect(result[0].hasAttachments).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("parses multiple message lines", () => {
|
|
289
|
+
const input = [
|
|
290
|
+
"msg001\tHello\t1700000000000\t0\tchat1\t+15551111111",
|
|
291
|
+
"msg002\tWorld\t1700000001000\t1\tchat1\t+15552222222",
|
|
292
|
+
"msg003\tTest\t1700000002000\ttrue\tchat2\tuser@test.com",
|
|
293
|
+
].join("\n");
|
|
294
|
+
|
|
295
|
+
const result = parseMessagesFromAppleScript(input);
|
|
296
|
+
expect(result).toHaveLength(3);
|
|
297
|
+
expect(result[0].text).toBe("Hello");
|
|
298
|
+
expect(result[0].isFromMe).toBe(false);
|
|
299
|
+
expect(result[1].text).toBe("World");
|
|
300
|
+
expect(result[1].isFromMe).toBe(true);
|
|
301
|
+
expect(result[2].text).toBe("Test");
|
|
302
|
+
expect(result[2].isFromMe).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("returns empty array for empty string", () => {
|
|
306
|
+
expect(parseMessagesFromAppleScript("")).toEqual([]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("returns empty array for whitespace-only input", () => {
|
|
310
|
+
expect(parseMessagesFromAppleScript(" \n \n ")).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("skips lines with fewer than 6 fields", () => {
|
|
314
|
+
const input =
|
|
315
|
+
"partial\tdata\n" +
|
|
316
|
+
"msg001\tHello\t1700000000000\t0\tchat1\t+15551234567";
|
|
317
|
+
const result = parseMessagesFromAppleScript(input);
|
|
318
|
+
expect(result).toHaveLength(1);
|
|
319
|
+
expect(result[0].id).toBe("msg001");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("handles is_from_me variations", () => {
|
|
323
|
+
const lines = [
|
|
324
|
+
"m1\ttext\t1000\t1\tchat\tsender",
|
|
325
|
+
"m2\ttext\t1000\ttrue\tchat\tsender",
|
|
326
|
+
"m3\ttext\t1000\tTrue\tchat\tsender",
|
|
327
|
+
"m4\ttext\t1000\t0\tchat\tsender",
|
|
328
|
+
"m5\ttext\t1000\tfalse\tchat\tsender",
|
|
329
|
+
].join("\n");
|
|
330
|
+
|
|
331
|
+
const result = parseMessagesFromAppleScript(lines);
|
|
332
|
+
expect(result[0].isFromMe).toBe(true);
|
|
333
|
+
expect(result[1].isFromMe).toBe(true);
|
|
334
|
+
expect(result[2].isFromMe).toBe(true);
|
|
335
|
+
expect(result[3].isFromMe).toBe(false);
|
|
336
|
+
expect(result[4].isFromMe).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("handles invalid date by setting timestamp to 0", () => {
|
|
340
|
+
const input = "msg001\tHello\tinvalid_date\t0\tchat1\tsender";
|
|
341
|
+
const result = parseMessagesFromAppleScript(input);
|
|
342
|
+
expect(result).toHaveLength(1);
|
|
343
|
+
expect(result[0].timestamp).toBe(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("handles empty fields gracefully", () => {
|
|
347
|
+
// Use boundary placeholders so trim() doesn't strip leading/trailing tabs
|
|
348
|
+
const input = ".\t\t1000\t0\t\t.";
|
|
349
|
+
const result = parseMessagesFromAppleScript(input);
|
|
350
|
+
expect(result).toHaveLength(1);
|
|
351
|
+
expect(result[0].id).toBe(".");
|
|
352
|
+
expect(result[0].text).toBe("");
|
|
353
|
+
expect(result[0].chatId).toBe("");
|
|
354
|
+
expect(result[0].handle).toBe(".");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("returns empty for all-tab line (tabs trimmed as whitespace)", () => {
|
|
358
|
+
const input = "\t\t1000\t0\t\t";
|
|
359
|
+
const result = parseMessagesFromAppleScript(input);
|
|
360
|
+
expect(result).toHaveLength(0);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("handles extra tab-separated fields (forward compat)", () => {
|
|
364
|
+
const input =
|
|
365
|
+
"msg001\tHello\t1000\t1\tchat1\tsender\textra1\textra2";
|
|
366
|
+
const result = parseMessagesFromAppleScript(input);
|
|
367
|
+
expect(result).toHaveLength(1);
|
|
368
|
+
expect(result[0].id).toBe("msg001");
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ============================================================
|
|
373
|
+
// parseChatsFromAppleScript
|
|
374
|
+
// ============================================================
|
|
375
|
+
|
|
376
|
+
describe("parseChatsFromAppleScript", () => {
|
|
377
|
+
it("parses a single chat line", () => {
|
|
378
|
+
const input = "chat123\tWork Group\t5\t1700000000000";
|
|
379
|
+
const result = parseChatsFromAppleScript(input);
|
|
380
|
+
|
|
381
|
+
expect(result).toHaveLength(1);
|
|
382
|
+
expect(result[0].chatId).toBe("chat123");
|
|
383
|
+
expect(result[0].displayName).toBe("Work Group");
|
|
384
|
+
expect(result[0].chatType).toBe("group");
|
|
385
|
+
expect(result[0].participants).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("parses multiple chat lines", () => {
|
|
389
|
+
const input = [
|
|
390
|
+
"chat1\tWork\t5\t1700000000000",
|
|
391
|
+
"chat2\tFamily\t3\t1700000001000",
|
|
392
|
+
"chat3\t\t1\t1700000002000",
|
|
393
|
+
].join("\n");
|
|
394
|
+
|
|
395
|
+
const result = parseChatsFromAppleScript(input);
|
|
396
|
+
expect(result).toHaveLength(3);
|
|
397
|
+
expect(result[0].chatType).toBe("group");
|
|
398
|
+
expect(result[1].chatType).toBe("group");
|
|
399
|
+
expect(result[2].chatType).toBe("direct");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("returns empty array for empty string", () => {
|
|
403
|
+
expect(parseChatsFromAppleScript("")).toEqual([]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("returns empty array for whitespace-only input", () => {
|
|
407
|
+
expect(parseChatsFromAppleScript(" \n \n ")).toEqual([]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("classifies direct chats (participant_count <= 1)", () => {
|
|
411
|
+
const input = "chat1\tJohn\t1\t1700000000000";
|
|
412
|
+
const result = parseChatsFromAppleScript(input);
|
|
413
|
+
expect(result[0].chatType).toBe("direct");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("classifies group chats (participant_count > 1)", () => {
|
|
417
|
+
const input = "chat1\tTeam\t2\t1700000000000";
|
|
418
|
+
const result = parseChatsFromAppleScript(input);
|
|
419
|
+
expect(result[0].chatType).toBe("group");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("handles empty display name", () => {
|
|
423
|
+
const input = "chat1\t\t1\t1700000000000";
|
|
424
|
+
const result = parseChatsFromAppleScript(input);
|
|
425
|
+
expect(result[0].displayName).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("handles invalid participant count", () => {
|
|
429
|
+
const input = "chat1\tTest\tnotanumber\t1700000000000";
|
|
430
|
+
const result = parseChatsFromAppleScript(input);
|
|
431
|
+
expect(result).toHaveLength(1);
|
|
432
|
+
expect(result[0].chatType).toBe("direct");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("skips lines with fewer than 4 fields", () => {
|
|
436
|
+
const input =
|
|
437
|
+
"incomplete\tdata\n" + "chat1\tTest\t3\t1700000000000";
|
|
438
|
+
const result = parseChatsFromAppleScript(input);
|
|
439
|
+
expect(result).toHaveLength(1);
|
|
440
|
+
expect(result[0].chatId).toBe("chat1");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("handles extra tab-separated fields (forward compat)", () => {
|
|
444
|
+
const input = "chat1\tTest\t3\t1700000000000\textra";
|
|
445
|
+
const result = parseChatsFromAppleScript(input);
|
|
446
|
+
expect(result).toHaveLength(1);
|
|
447
|
+
expect(result[0].chatId).toBe("chat1");
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ============================================================
|
|
452
|
+
// Error classes
|
|
453
|
+
// ============================================================
|
|
454
|
+
|
|
455
|
+
describe("Error classes", () => {
|
|
456
|
+
it("IMessagePluginError has correct properties", () => {
|
|
457
|
+
const error = new IMessagePluginError("test error", "TEST_CODE", {
|
|
458
|
+
key: "value",
|
|
459
|
+
});
|
|
460
|
+
expect(error.message).toBe("test error");
|
|
461
|
+
expect(error.code).toBe("TEST_CODE");
|
|
462
|
+
expect(error.details).toEqual({ key: "value" });
|
|
463
|
+
expect(error.name).toBe("IMessagePluginError");
|
|
464
|
+
expect(error instanceof Error).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("IMessageConfigurationError sets correct code", () => {
|
|
468
|
+
const error = new IMessageConfigurationError("bad config", "cli_path");
|
|
469
|
+
expect(error.code).toBe("CONFIGURATION_ERROR");
|
|
470
|
+
expect(error.details).toEqual({ setting: "cli_path" });
|
|
471
|
+
expect(error.name).toBe("IMessageConfigurationError");
|
|
472
|
+
expect(error instanceof IMessagePluginError).toBe(true);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("IMessageNotSupportedError has default message", () => {
|
|
476
|
+
const error = new IMessageNotSupportedError();
|
|
477
|
+
expect(error.message).toBe("iMessage is only supported on macOS");
|
|
478
|
+
expect(error.code).toBe("NOT_SUPPORTED");
|
|
479
|
+
expect(error.name).toBe("IMessageNotSupportedError");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("IMessageNotSupportedError accepts custom message", () => {
|
|
483
|
+
const error = new IMessageNotSupportedError("custom msg");
|
|
484
|
+
expect(error.message).toBe("custom msg");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("IMessageCliError includes exit code", () => {
|
|
488
|
+
const error = new IMessageCliError("command failed", 1);
|
|
489
|
+
expect(error.code).toBe("CLI_ERROR");
|
|
490
|
+
expect(error.details).toEqual({ exitCode: 1 });
|
|
491
|
+
expect(error.name).toBe("IMessageCliError");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("IMessageCliError handles undefined exit code", () => {
|
|
495
|
+
const error = new IMessageCliError("command failed");
|
|
496
|
+
expect(error.details).toBeUndefined();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ============================================================
|
|
501
|
+
// Action validation
|
|
502
|
+
// ============================================================
|
|
503
|
+
|
|
504
|
+
describe("sendMessage action", () => {
|
|
505
|
+
it("has correct action metadata", () => {
|
|
506
|
+
expect(sendMessage.name).toBe("IMESSAGE_SEND_MESSAGE");
|
|
507
|
+
expect(sendMessage.description).toContain("iMessage");
|
|
508
|
+
expect(Array.isArray(sendMessage.similes)).toBe(true);
|
|
509
|
+
expect(sendMessage.similes?.length).toBeGreaterThan(0);
|
|
510
|
+
expect(Array.isArray(sendMessage.examples)).toBe(true);
|
|
511
|
+
expect(sendMessage.examples?.length).toBeGreaterThan(0);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("validate returns false for non-imessage sources", async () => {
|
|
515
|
+
const mockRuntime = {} as Parameters<
|
|
516
|
+
NonNullable<typeof sendMessage.validate>
|
|
517
|
+
>[0];
|
|
518
|
+
const mockMessage = {
|
|
519
|
+
content: { source: "discord" },
|
|
520
|
+
} as Parameters<NonNullable<typeof sendMessage.validate>>[1];
|
|
521
|
+
|
|
522
|
+
const result = await sendMessage.validate!(mockRuntime, mockMessage);
|
|
523
|
+
expect(result).toBe(false);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("validate returns true for imessage source", async () => {
|
|
527
|
+
const mockRuntime = {} as Parameters<
|
|
528
|
+
NonNullable<typeof sendMessage.validate>
|
|
529
|
+
>[0];
|
|
530
|
+
const mockMessage = {
|
|
531
|
+
content: { source: "imessage" },
|
|
532
|
+
} as Parameters<NonNullable<typeof sendMessage.validate>>[1];
|
|
533
|
+
|
|
534
|
+
const result = await sendMessage.validate!(mockRuntime, mockMessage);
|
|
535
|
+
expect(result).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// ============================================================
|
|
540
|
+
// Chat context provider
|
|
541
|
+
// ============================================================
|
|
542
|
+
|
|
543
|
+
describe("chatContextProvider", () => {
|
|
544
|
+
it("has correct provider metadata", () => {
|
|
545
|
+
expect(chatContextProvider.name).toBe("imessageChatContext");
|
|
546
|
+
expect(chatContextProvider.description).toContain("iMessage");
|
|
547
|
+
});
|
|
548
|
+
});
|
package/build.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const distDir = join(import.meta.dirname, "dist");
|
|
6
|
+
|
|
7
|
+
// Clean
|
|
8
|
+
rmSync(distDir, { recursive: true, force: true });
|
|
9
|
+
|
|
10
|
+
// Build
|
|
11
|
+
execSync("npx tsc -p tsconfig.json", {
|
|
12
|
+
cwd: import.meta.dirname,
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
console.log("Build complete: plugin-imessage");
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
import { platform } from "node:os";
|
|
8
|
+
import { logger } from "@elizaos/core";
|
|
9
|
+
import { sendMessage } from "./actions/index.js";
|
|
10
|
+
import { chatContextProvider } from "./providers/index.js";
|
|
11
|
+
import { IMessageService } from "./service.js";
|
|
12
|
+
// Re-export types and service
|
|
13
|
+
export * from "./types.js";
|
|
14
|
+
export { IMessageService };
|
|
15
|
+
export { sendMessage };
|
|
16
|
+
export { chatContextProvider };
|
|
17
|
+
// Account management exports
|
|
18
|
+
export { DEFAULT_ACCOUNT_ID, isIMessageMentionRequired, isIMessageUserAllowed, isMultiAccountEnabled, listEnabledIMessageAccounts, listIMessageAccountIds, normalizeAccountId, resolveDefaultIMessageAccountId, resolveIMessageAccount, resolveIMessageGroupConfig, } from "./accounts.js";
|
|
19
|
+
// RPC client exports
|
|
20
|
+
export { createIMessageRpcClient, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, getChatInfo, getContactInfo, getMessages, IMessageRpcClient, listChats, listContacts, probeIMessageRpc, sendIMessageRpc, } from "./rpc.js";
|
|
21
|
+
/**
|
|
22
|
+
* iMessage plugin for ElizaOS agents.
|
|
23
|
+
*/
|
|
24
|
+
const imessagePlugin = {
|
|
25
|
+
name: "imessage",
|
|
26
|
+
description: "iMessage plugin for ElizaOS agents (macOS only)",
|
|
27
|
+
services: [IMessageService],
|
|
28
|
+
actions: [sendMessage],
|
|
29
|
+
providers: [chatContextProvider],
|
|
30
|
+
tests: [],
|
|
31
|
+
init: async (config, _runtime) => {
|
|
32
|
+
logger.info("Initializing iMessage plugin...");
|
|
33
|
+
const isMacOS = platform() === "darwin";
|
|
34
|
+
logger.info("iMessage plugin configuration:");
|
|
35
|
+
logger.info(` - Platform: ${platform()}`);
|
|
36
|
+
logger.info(` - macOS: ${isMacOS ? "Yes" : "No"}`);
|
|
37
|
+
logger.info(` - CLI path: ${config.IMESSAGE_CLI_PATH || process.env.IMESSAGE_CLI_PATH || "imsg (default)"}`);
|
|
38
|
+
logger.info(` - DM policy: ${config.IMESSAGE_DM_POLICY || process.env.IMESSAGE_DM_POLICY || "pairing"}`);
|
|
39
|
+
if (!isMacOS) {
|
|
40
|
+
logger.warn("iMessage plugin is only supported on macOS. The plugin will be inactive on this platform.");
|
|
41
|
+
}
|
|
42
|
+
logger.info("iMessage plugin initialized");
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export default imessagePlugin;
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/plugin-imessage",
|
|
3
|
+
"version": "2.0.0-alpha.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun run build.ts",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"lint": "biome check --write --unsafe src"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@elizaos/core": "2.0.0-alpha.3",
|
|
14
|
+
"zod": "^4.3.6"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"typescript": "^5.6.0",
|
|
19
|
+
"vitest": "^2.0.0"
|
|
20
|
+
},
|
|
21
|
+
"milaidy": {
|
|
22
|
+
"platforms": [
|
|
23
|
+
"node"
|
|
24
|
+
],
|
|
25
|
+
"runtime": "node",
|
|
26
|
+
"platformDetails": {
|
|
27
|
+
"node": "Node.js via main entry point"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|