@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
package/src/types.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the iMessage plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Service } from "@elizaos/core";
|
|
6
|
+
|
|
7
|
+
/** Maximum message length for iMessage */
|
|
8
|
+
export const MAX_IMESSAGE_MESSAGE_LENGTH = 4000;
|
|
9
|
+
|
|
10
|
+
/** Default poll interval in ms */
|
|
11
|
+
export const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
12
|
+
|
|
13
|
+
/** iMessage service name constant */
|
|
14
|
+
export const IMESSAGE_SERVICE_NAME = "imessage";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Event types emitted by the iMessage plugin
|
|
18
|
+
*/
|
|
19
|
+
export const IMessageEventTypes = {
|
|
20
|
+
MESSAGE_RECEIVED: "IMESSAGE_MESSAGE_RECEIVED",
|
|
21
|
+
MESSAGE_SENT: "IMESSAGE_MESSAGE_SENT",
|
|
22
|
+
CONNECTION_READY: "IMESSAGE_CONNECTION_READY",
|
|
23
|
+
ERROR: "IMESSAGE_ERROR",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type IMessageEventType =
|
|
27
|
+
(typeof IMessageEventTypes)[keyof typeof IMessageEventTypes];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* iMessage chat types
|
|
31
|
+
*/
|
|
32
|
+
export type IMessageChatType = "direct" | "group";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configuration settings for the iMessage plugin
|
|
36
|
+
*/
|
|
37
|
+
export interface IMessageSettings {
|
|
38
|
+
/** Path to iMessage CLI tool */
|
|
39
|
+
cliPath: string;
|
|
40
|
+
/** Path to iMessage database */
|
|
41
|
+
dbPath?: string;
|
|
42
|
+
/** Polling interval in ms */
|
|
43
|
+
pollIntervalMs: number;
|
|
44
|
+
/** DM policy */
|
|
45
|
+
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
|
46
|
+
/** Group policy */
|
|
47
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
48
|
+
/** Handles/phone numbers for allowlist */
|
|
49
|
+
allowFrom: string[];
|
|
50
|
+
/** Enable/disable the plugin */
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* iMessage contact
|
|
56
|
+
*/
|
|
57
|
+
export interface IMessageContact {
|
|
58
|
+
/** Handle (phone number or email) */
|
|
59
|
+
handle: string;
|
|
60
|
+
/** Display name */
|
|
61
|
+
displayName?: string;
|
|
62
|
+
/** Is this a phone number? */
|
|
63
|
+
isPhoneNumber: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* iMessage chat
|
|
68
|
+
*/
|
|
69
|
+
export interface IMessageChat {
|
|
70
|
+
/** Chat ID */
|
|
71
|
+
chatId: string;
|
|
72
|
+
/** Chat type */
|
|
73
|
+
chatType: IMessageChatType;
|
|
74
|
+
/** Display name */
|
|
75
|
+
displayName?: string;
|
|
76
|
+
/** Participants */
|
|
77
|
+
participants: IMessageContact[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* iMessage message
|
|
82
|
+
*/
|
|
83
|
+
export interface IMessageMessage {
|
|
84
|
+
/** Message ID (ROWID) */
|
|
85
|
+
id: string;
|
|
86
|
+
/** Message text */
|
|
87
|
+
text: string;
|
|
88
|
+
/** Sender handle */
|
|
89
|
+
handle: string;
|
|
90
|
+
/** Chat ID */
|
|
91
|
+
chatId: string;
|
|
92
|
+
/** Timestamp */
|
|
93
|
+
timestamp: number;
|
|
94
|
+
/** Is from me */
|
|
95
|
+
isFromMe: boolean;
|
|
96
|
+
/** Has attachments */
|
|
97
|
+
hasAttachments: boolean;
|
|
98
|
+
/** Attachment paths */
|
|
99
|
+
attachmentPaths?: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Options for sending a message
|
|
104
|
+
*/
|
|
105
|
+
export interface IMessageSendOptions {
|
|
106
|
+
/** Media URL or path to attach */
|
|
107
|
+
mediaUrl?: string;
|
|
108
|
+
/** Max bytes for media */
|
|
109
|
+
maxBytes?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Result from sending a message
|
|
114
|
+
*/
|
|
115
|
+
export interface IMessageSendResult {
|
|
116
|
+
success: boolean;
|
|
117
|
+
messageId?: string;
|
|
118
|
+
chatId?: string;
|
|
119
|
+
error?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Service interface for iMessage
|
|
124
|
+
*/
|
|
125
|
+
export interface IIMessageService extends Service {
|
|
126
|
+
/** Check if the service is connected */
|
|
127
|
+
isConnected(): boolean;
|
|
128
|
+
|
|
129
|
+
/** Check if running on macOS */
|
|
130
|
+
isMacOS(): boolean;
|
|
131
|
+
|
|
132
|
+
/** Send a message */
|
|
133
|
+
sendMessage(
|
|
134
|
+
to: string,
|
|
135
|
+
text: string,
|
|
136
|
+
options?: IMessageSendOptions,
|
|
137
|
+
): Promise<IMessageSendResult>;
|
|
138
|
+
|
|
139
|
+
/** Get recent messages */
|
|
140
|
+
getRecentMessages(limit?: number): Promise<IMessageMessage[]>;
|
|
141
|
+
|
|
142
|
+
/** Get chats */
|
|
143
|
+
getChats(): Promise<IMessageChat[]>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* iMessage plugin errors
|
|
148
|
+
*/
|
|
149
|
+
export class IMessagePluginError extends Error {
|
|
150
|
+
constructor(
|
|
151
|
+
message: string,
|
|
152
|
+
public readonly code: string,
|
|
153
|
+
public readonly details?: Record<string, unknown>,
|
|
154
|
+
) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = "IMessagePluginError";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class IMessageConfigurationError extends IMessagePluginError {
|
|
161
|
+
constructor(message: string, setting?: string) {
|
|
162
|
+
super(message, "CONFIGURATION_ERROR", setting ? { setting } : undefined);
|
|
163
|
+
this.name = "IMessageConfigurationError";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class IMessageNotSupportedError extends IMessagePluginError {
|
|
168
|
+
constructor(message: string = "iMessage is only supported on macOS") {
|
|
169
|
+
super(message, "NOT_SUPPORTED");
|
|
170
|
+
this.name = "IMessageNotSupportedError";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class IMessageCliError extends IMessagePluginError {
|
|
175
|
+
constructor(message: string, exitCode?: number) {
|
|
176
|
+
super(
|
|
177
|
+
message,
|
|
178
|
+
"CLI_ERROR",
|
|
179
|
+
exitCode !== undefined ? { exitCode } : undefined,
|
|
180
|
+
);
|
|
181
|
+
this.name = "IMessageCliError";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Utility functions
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a string looks like a phone number
|
|
189
|
+
*/
|
|
190
|
+
export function isPhoneNumber(input: string): boolean {
|
|
191
|
+
// Remove common formatting
|
|
192
|
+
const cleaned = input.replace(/[\s\-().]/g, "");
|
|
193
|
+
// Check if it's a phone number pattern
|
|
194
|
+
return /^\+?\d{10,15}$/.test(cleaned);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if a string looks like an email
|
|
199
|
+
*/
|
|
200
|
+
export function isEmail(input: string): boolean {
|
|
201
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if a string is a valid iMessage target (phone or email)
|
|
206
|
+
*/
|
|
207
|
+
export function isValidIMessageTarget(target: string): boolean {
|
|
208
|
+
const trimmed = target.trim();
|
|
209
|
+
return (
|
|
210
|
+
isPhoneNumber(trimmed) || isEmail(trimmed) || trimmed.startsWith("chat_id:")
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Normalize an iMessage target
|
|
216
|
+
*/
|
|
217
|
+
export function normalizeIMessageTarget(target: string): string | null {
|
|
218
|
+
const trimmed = target.trim();
|
|
219
|
+
if (!trimmed) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle chat_id: prefix
|
|
224
|
+
if (trimmed.startsWith("chat_id:")) {
|
|
225
|
+
return trimmed;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle imessage: prefix
|
|
229
|
+
if (trimmed.toLowerCase().startsWith("imessage:")) {
|
|
230
|
+
return trimmed.slice(9).trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Return as-is for phone numbers and emails
|
|
234
|
+
return trimmed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format a phone number for iMessage
|
|
239
|
+
*/
|
|
240
|
+
export function formatPhoneNumber(phone: string): string {
|
|
241
|
+
// Remove formatting
|
|
242
|
+
let cleaned = phone.replace(/[\s\-().]/g, "");
|
|
243
|
+
|
|
244
|
+
// Add + prefix if missing for international
|
|
245
|
+
if (cleaned.length > 10 && !cleaned.startsWith("+")) {
|
|
246
|
+
cleaned = `+${cleaned}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return cleaned;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Split text for iMessage
|
|
254
|
+
*/
|
|
255
|
+
export function splitMessageForIMessage(
|
|
256
|
+
text: string,
|
|
257
|
+
maxLength: number = MAX_IMESSAGE_MESSAGE_LENGTH,
|
|
258
|
+
): string[] {
|
|
259
|
+
if (text.length <= maxLength) {
|
|
260
|
+
return [text];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const chunks: string[] = [];
|
|
264
|
+
let remaining = text;
|
|
265
|
+
|
|
266
|
+
while (remaining.length > 0) {
|
|
267
|
+
if (remaining.length <= maxLength) {
|
|
268
|
+
chunks.push(remaining);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let breakPoint = maxLength;
|
|
273
|
+
|
|
274
|
+
// Try newline first
|
|
275
|
+
const newlineIdx = remaining.lastIndexOf("\n", maxLength);
|
|
276
|
+
if (newlineIdx > maxLength / 2) {
|
|
277
|
+
breakPoint = newlineIdx + 1;
|
|
278
|
+
} else {
|
|
279
|
+
// Try space
|
|
280
|
+
const spaceIdx = remaining.lastIndexOf(" ", maxLength);
|
|
281
|
+
if (spaceIdx > maxLength / 2) {
|
|
282
|
+
breakPoint = spaceIdx + 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
chunks.push(remaining.slice(0, breakPoint).trimEnd());
|
|
287
|
+
remaining = remaining.slice(breakPoint).trimStart();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return chunks;
|
|
291
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
+
}
|