@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.
@@ -0,0 +1,379 @@
1
+ import type { IAgentRuntime } from "@elizaos/core";
2
+
3
+ /**
4
+ * Default account identifier used when no specific account is configured
5
+ */
6
+ export const DEFAULT_ACCOUNT_ID = "default";
7
+
8
+ /**
9
+ * Group-specific configuration
10
+ */
11
+ export interface IMessageGroupConfig {
12
+ /** If false, ignore messages from this group */
13
+ enabled?: boolean;
14
+ /** Allowlist for users in this group */
15
+ allowFrom?: Array<string | number>;
16
+ /** Require bot mention to respond */
17
+ requireMention?: boolean;
18
+ /** Custom system prompt for this group */
19
+ systemPrompt?: string;
20
+ /** Skills enabled for this group */
21
+ skills?: string[];
22
+ }
23
+
24
+ /**
25
+ * Configuration for a single iMessage account
26
+ */
27
+ export interface IMessageAccountConfig {
28
+ /** Optional display name for this account */
29
+ name?: string;
30
+ /** If false, do not start this iMessage account */
31
+ enabled?: boolean;
32
+ /** Path to the iMessage CLI tool */
33
+ cliPath?: string;
34
+ /** Path to the iMessage database */
35
+ dbPath?: string;
36
+ /** iMessage service type (iMessage or SMS) */
37
+ service?: "iMessage" | "SMS";
38
+ /** Phone number region code */
39
+ region?: string;
40
+ /** Allowlist for DM senders */
41
+ allowFrom?: Array<string | number>;
42
+ /** Allowlist for groups */
43
+ groupAllowFrom?: Array<string | number>;
44
+ /** DM access policy */
45
+ dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
46
+ /** Group message access policy */
47
+ groupPolicy?: "open" | "allowlist" | "disabled";
48
+ /** Whether to include attachments */
49
+ includeAttachments?: boolean;
50
+ /** Max media size in MB */
51
+ mediaMaxMb?: number;
52
+ /** Text chunk limit for messages */
53
+ textChunkLimit?: number;
54
+ /** Group-specific configurations */
55
+ groups?: Record<string, IMessageGroupConfig>;
56
+ }
57
+
58
+ /**
59
+ * Multi-account iMessage configuration structure
60
+ */
61
+ export interface IMessageMultiAccountConfig {
62
+ /** Default/base configuration applied to all accounts */
63
+ enabled?: boolean;
64
+ cliPath?: string;
65
+ dbPath?: string;
66
+ service?: "iMessage" | "SMS";
67
+ region?: string;
68
+ dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
69
+ groupPolicy?: "open" | "allowlist" | "disabled";
70
+ includeAttachments?: boolean;
71
+ mediaMaxMb?: number;
72
+ textChunkLimit?: number;
73
+ /** Per-account configuration overrides */
74
+ accounts?: Record<string, IMessageAccountConfig>;
75
+ /** Group configurations at base level */
76
+ groups?: Record<string, IMessageGroupConfig>;
77
+ }
78
+
79
+ /**
80
+ * Resolved iMessage account with all configuration merged
81
+ */
82
+ export interface ResolvedIMessageAccount {
83
+ accountId: string;
84
+ enabled: boolean;
85
+ name?: string;
86
+ cliPath: string;
87
+ dbPath?: string;
88
+ configured: boolean;
89
+ config: IMessageAccountConfig;
90
+ }
91
+
92
+ /**
93
+ * Normalizes an account ID, returning the default if not provided
94
+ */
95
+ export function normalizeAccountId(accountId?: string | null): string {
96
+ if (!accountId || typeof accountId !== "string") {
97
+ return DEFAULT_ACCOUNT_ID;
98
+ }
99
+ const trimmed = accountId.trim().toLowerCase();
100
+ if (!trimmed || trimmed === "default") {
101
+ return DEFAULT_ACCOUNT_ID;
102
+ }
103
+ return trimmed;
104
+ }
105
+
106
+ /**
107
+ * Gets the multi-account configuration from runtime settings
108
+ */
109
+ export function getMultiAccountConfig(
110
+ runtime: IAgentRuntime,
111
+ ): IMessageMultiAccountConfig {
112
+ const characterIMessage = runtime.character?.settings?.imessage as
113
+ | IMessageMultiAccountConfig
114
+ | undefined;
115
+
116
+ return {
117
+ enabled: characterIMessage?.enabled,
118
+ cliPath: characterIMessage?.cliPath,
119
+ dbPath: characterIMessage?.dbPath,
120
+ service: characterIMessage?.service,
121
+ region: characterIMessage?.region,
122
+ dmPolicy: characterIMessage?.dmPolicy,
123
+ groupPolicy: characterIMessage?.groupPolicy,
124
+ includeAttachments: characterIMessage?.includeAttachments,
125
+ mediaMaxMb: characterIMessage?.mediaMaxMb,
126
+ textChunkLimit: characterIMessage?.textChunkLimit,
127
+ accounts: characterIMessage?.accounts,
128
+ groups: characterIMessage?.groups,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Lists all configured account IDs
134
+ */
135
+ export function listIMessageAccountIds(runtime: IAgentRuntime): string[] {
136
+ const config = getMultiAccountConfig(runtime);
137
+ const accounts = config.accounts;
138
+
139
+ if (!accounts || typeof accounts !== "object") {
140
+ return [DEFAULT_ACCOUNT_ID];
141
+ }
142
+
143
+ const ids = Object.keys(accounts).filter(Boolean);
144
+ if (ids.length === 0) {
145
+ return [DEFAULT_ACCOUNT_ID];
146
+ }
147
+
148
+ return ids.slice().sort((a: string, b: string) => a.localeCompare(b));
149
+ }
150
+
151
+ /**
152
+ * Resolves the default account ID to use
153
+ */
154
+ export function resolveDefaultIMessageAccountId(
155
+ runtime: IAgentRuntime,
156
+ ): string {
157
+ const ids = listIMessageAccountIds(runtime);
158
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
159
+ return DEFAULT_ACCOUNT_ID;
160
+ }
161
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
162
+ }
163
+
164
+ /**
165
+ * Gets the account-specific configuration
166
+ */
167
+ function getAccountConfig(
168
+ runtime: IAgentRuntime,
169
+ accountId: string,
170
+ ): IMessageAccountConfig | undefined {
171
+ const config = getMultiAccountConfig(runtime);
172
+ const accounts = config.accounts;
173
+
174
+ if (!accounts || typeof accounts !== "object") {
175
+ return undefined;
176
+ }
177
+
178
+ return accounts[accountId];
179
+ }
180
+
181
+ /**
182
+ * Merges base configuration with account-specific overrides
183
+ */
184
+ function mergeIMessageAccountConfig(
185
+ runtime: IAgentRuntime,
186
+ accountId: string,
187
+ ): IMessageAccountConfig {
188
+ const multiConfig = getMultiAccountConfig(runtime);
189
+ const { accounts: _ignored, ...baseConfig } = multiConfig;
190
+ const accountConfig = getAccountConfig(runtime, accountId) ?? {};
191
+
192
+ // Get environment/runtime settings for the base config
193
+ const envCliPath = runtime.getSetting("IMESSAGE_CLI_PATH") as
194
+ | string
195
+ | undefined;
196
+ const envDbPath = runtime.getSetting("IMESSAGE_DB_PATH") as
197
+ | string
198
+ | undefined;
199
+ const envDmPolicy = runtime.getSetting("IMESSAGE_DM_POLICY") as
200
+ | string
201
+ | undefined;
202
+ const envGroupPolicy = runtime.getSetting("IMESSAGE_GROUP_POLICY") as
203
+ | string
204
+ | undefined;
205
+
206
+ const envConfig: IMessageAccountConfig = {
207
+ cliPath: envCliPath || undefined,
208
+ dbPath: envDbPath || undefined,
209
+ dmPolicy: envDmPolicy as IMessageAccountConfig["dmPolicy"] | undefined,
210
+ groupPolicy: envGroupPolicy as
211
+ | IMessageAccountConfig["groupPolicy"]
212
+ | undefined,
213
+ };
214
+
215
+ // Merge order: env defaults < base config < account config
216
+ return {
217
+ ...envConfig,
218
+ ...baseConfig,
219
+ ...accountConfig,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Resolves a complete iMessage account configuration
225
+ */
226
+ export function resolveIMessageAccount(
227
+ runtime: IAgentRuntime,
228
+ accountId?: string | null,
229
+ ): ResolvedIMessageAccount {
230
+ const normalizedAccountId = normalizeAccountId(accountId);
231
+ const multiConfig = getMultiAccountConfig(runtime);
232
+
233
+ const baseEnabled = multiConfig.enabled !== false;
234
+ const merged = mergeIMessageAccountConfig(runtime, normalizedAccountId);
235
+ const accountEnabled = merged.enabled !== false;
236
+ const enabled = baseEnabled && accountEnabled;
237
+
238
+ const cliPath = merged.cliPath?.trim() || "imsg";
239
+
240
+ // Determine if this account is actually configured
241
+ const configured = Boolean(
242
+ merged.cliPath?.trim() ||
243
+ merged.dbPath?.trim() ||
244
+ merged.service ||
245
+ merged.region?.trim() ||
246
+ (merged.allowFrom && merged.allowFrom.length > 0) ||
247
+ (merged.groupAllowFrom && merged.groupAllowFrom.length > 0) ||
248
+ merged.dmPolicy ||
249
+ merged.groupPolicy ||
250
+ typeof merged.includeAttachments === "boolean" ||
251
+ typeof merged.mediaMaxMb === "number" ||
252
+ typeof merged.textChunkLimit === "number" ||
253
+ (merged.groups && Object.keys(merged.groups).length > 0),
254
+ );
255
+
256
+ return {
257
+ accountId: normalizedAccountId,
258
+ enabled,
259
+ name: merged.name?.trim() || undefined,
260
+ cliPath,
261
+ dbPath: merged.dbPath?.trim() || undefined,
262
+ configured,
263
+ config: merged,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Lists all enabled iMessage accounts
269
+ */
270
+ export function listEnabledIMessageAccounts(
271
+ runtime: IAgentRuntime,
272
+ ): ResolvedIMessageAccount[] {
273
+ return listIMessageAccountIds(runtime)
274
+ .map((accountId) => resolveIMessageAccount(runtime, accountId))
275
+ .filter((account) => account.enabled);
276
+ }
277
+
278
+ /**
279
+ * Checks if multi-account mode is enabled
280
+ */
281
+ export function isMultiAccountEnabled(runtime: IAgentRuntime): boolean {
282
+ const accounts = listEnabledIMessageAccounts(runtime);
283
+ return accounts.length > 1;
284
+ }
285
+
286
+ /**
287
+ * Resolves group configuration for a specific group
288
+ */
289
+ export function resolveIMessageGroupConfig(
290
+ runtime: IAgentRuntime,
291
+ accountId: string,
292
+ groupId: string,
293
+ ): IMessageGroupConfig | undefined {
294
+ const multiConfig = getMultiAccountConfig(runtime);
295
+ const accountConfig = getAccountConfig(runtime, accountId);
296
+
297
+ // Check account-level groups first
298
+ const accountGroup = accountConfig?.groups?.[groupId];
299
+ if (accountGroup) {
300
+ return accountGroup;
301
+ }
302
+
303
+ // Fall back to base-level groups
304
+ return multiConfig.groups?.[groupId];
305
+ }
306
+
307
+ /**
308
+ * Checks if a user is allowed based on policy and allowlist
309
+ */
310
+ export function isIMessageUserAllowed(params: {
311
+ identifier: string;
312
+ accountConfig: IMessageAccountConfig;
313
+ isGroup: boolean;
314
+ groupId?: string;
315
+ groupConfig?: IMessageGroupConfig;
316
+ }): boolean {
317
+ const { identifier, accountConfig, isGroup, groupConfig } = params;
318
+
319
+ if (isGroup) {
320
+ const policy = accountConfig.groupPolicy ?? "allowlist";
321
+ if (policy === "disabled") {
322
+ return false;
323
+ }
324
+
325
+ if (policy === "open") {
326
+ return true;
327
+ }
328
+
329
+ // Check group-specific allowlist first
330
+ if (groupConfig?.allowFrom?.length) {
331
+ return groupConfig.allowFrom.some(
332
+ (allowed) => String(allowed) === identifier,
333
+ );
334
+ }
335
+
336
+ // Check account-level group allowlist
337
+ if (accountConfig.groupAllowFrom?.length) {
338
+ return accountConfig.groupAllowFrom.some(
339
+ (allowed) => String(allowed) === identifier,
340
+ );
341
+ }
342
+
343
+ return policy !== "allowlist";
344
+ }
345
+
346
+ // DM handling
347
+ const policy = accountConfig.dmPolicy ?? "pairing";
348
+ if (policy === "disabled") {
349
+ return false;
350
+ }
351
+
352
+ if (policy === "open") {
353
+ return true;
354
+ }
355
+
356
+ if (policy === "pairing") {
357
+ return true;
358
+ }
359
+
360
+ // Allowlist policy
361
+ if (accountConfig.allowFrom?.length) {
362
+ return accountConfig.allowFrom.some(
363
+ (allowed) => String(allowed) === identifier,
364
+ );
365
+ }
366
+
367
+ return false;
368
+ }
369
+
370
+ /**
371
+ * Checks if mention is required in a group
372
+ */
373
+ export function isIMessageMentionRequired(params: {
374
+ accountConfig: IMessageAccountConfig;
375
+ groupConfig?: IMessageGroupConfig;
376
+ }): boolean {
377
+ const { groupConfig } = params;
378
+ return groupConfig?.requireMention ?? false;
379
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * iMessage plugin actions.
3
+ */
4
+
5
+ export { sendMessage } from "./sendMessage.js";
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Send message action for the iMessage plugin.
3
+ */
4
+
5
+ import type {
6
+ Action,
7
+ ActionResult,
8
+ HandlerCallback,
9
+ IAgentRuntime,
10
+ Memory,
11
+ State,
12
+ } from "@elizaos/core";
13
+ import {
14
+ composePromptFromState,
15
+ logger,
16
+ ModelType,
17
+ parseJSONObjectFromText,
18
+ } from "@elizaos/core";
19
+ import type { IMessageService } from "../service.js";
20
+ import {
21
+ IMESSAGE_SERVICE_NAME,
22
+ isValidIMessageTarget,
23
+ normalizeIMessageTarget,
24
+ } from "../types.js";
25
+
26
+ const SEND_MESSAGE_TEMPLATE = `# Task: Extract iMessage parameters
27
+
28
+ Based on the conversation, determine what message to send and to whom.
29
+
30
+ Recent conversation:
31
+ {{recentMessages}}
32
+
33
+ Extract the following:
34
+ 1. text: The message content to send
35
+ 2. to: The recipient (phone number, email, or "current" to reply)
36
+
37
+ Respond with a JSON object:
38
+ \`\`\`json
39
+ {
40
+ "text": "message to send",
41
+ "to": "phone/email or 'current'"
42
+ }
43
+ \`\`\`
44
+ `;
45
+
46
+ interface SendMessageParams {
47
+ text: string;
48
+ to: string;
49
+ }
50
+
51
+ export const sendMessage: Action = {
52
+ name: "IMESSAGE_SEND_MESSAGE",
53
+ similes: ["SEND_IMESSAGE", "IMESSAGE_TEXT", "TEXT_IMESSAGE", "SEND_IMSG"],
54
+ description: "Send a text message via iMessage (macOS only)",
55
+
56
+ validate: async (
57
+ _runtime: IAgentRuntime,
58
+ message: Memory,
59
+ _state?: State,
60
+ ): Promise<boolean> => {
61
+ return message.content.source === "imessage";
62
+ },
63
+
64
+ handler: async (
65
+ runtime: IAgentRuntime,
66
+ message: Memory,
67
+ state: State | undefined,
68
+ _options?: Record<string, unknown>,
69
+ callback?: HandlerCallback,
70
+ ): Promise<ActionResult> => {
71
+ const imessageService = runtime.getService<IMessageService>(
72
+ IMESSAGE_SERVICE_NAME,
73
+ );
74
+
75
+ if (!imessageService || !imessageService.isConnected()) {
76
+ if (callback) {
77
+ callback({
78
+ text: "iMessage service is not available.",
79
+ source: "imessage",
80
+ });
81
+ }
82
+ return { success: false, error: "iMessage service not available" };
83
+ }
84
+
85
+ if (!imessageService.isMacOS()) {
86
+ if (callback) {
87
+ callback({
88
+ text: "iMessage is only available on macOS.",
89
+ source: "imessage",
90
+ });
91
+ }
92
+ return { success: false, error: "iMessage requires macOS" };
93
+ }
94
+
95
+ // Compose state if not provided
96
+ const currentState = state ?? (await runtime.composeState(message));
97
+
98
+ // Extract parameters using LLM
99
+ const prompt = await composePromptFromState({
100
+ template: SEND_MESSAGE_TEMPLATE,
101
+ state: currentState,
102
+ });
103
+
104
+ let msgInfo: SendMessageParams | null = null;
105
+
106
+ for (let attempt = 0; attempt < 3; attempt++) {
107
+ const response = await runtime.useModel(ModelType.TEXT_SMALL, {
108
+ prompt,
109
+ });
110
+
111
+ const parsed = parseJSONObjectFromText(response);
112
+ if (parsed?.text) {
113
+ msgInfo = {
114
+ text: String(parsed.text),
115
+ to: String(parsed.to || "current"),
116
+ };
117
+ break;
118
+ }
119
+ }
120
+
121
+ if (!msgInfo || !msgInfo.text) {
122
+ if (callback) {
123
+ callback({
124
+ text: "I couldn't understand what message you want me to send. Please try again.",
125
+ source: "imessage",
126
+ });
127
+ }
128
+ return { success: false, error: "Could not extract message parameters" };
129
+ }
130
+
131
+ // Determine target
132
+ let targetId: string | undefined;
133
+
134
+ if (msgInfo.to && msgInfo.to !== "current") {
135
+ const normalized = normalizeIMessageTarget(msgInfo.to);
136
+ if (normalized && isValidIMessageTarget(normalized)) {
137
+ targetId = normalized;
138
+ }
139
+ }
140
+
141
+ // Fall back to current chat
142
+ if (!targetId) {
143
+ const stateData = (currentState.data || {}) as Record<string, unknown>;
144
+ targetId = (stateData.chatId as string) || (stateData.handle as string);
145
+ }
146
+
147
+ if (!targetId) {
148
+ if (callback) {
149
+ callback({
150
+ text: "I couldn't determine who to send the message to. Please specify a phone number or email.",
151
+ source: "imessage",
152
+ });
153
+ }
154
+ return { success: false, error: "Could not determine recipient" };
155
+ }
156
+
157
+ // Send message
158
+ const result = await imessageService.sendMessage(targetId, msgInfo.text);
159
+
160
+ if (!result.success) {
161
+ if (callback) {
162
+ callback({
163
+ text: `Failed to send message: ${result.error}`,
164
+ source: "imessage",
165
+ });
166
+ }
167
+ return { success: false, error: result.error };
168
+ }
169
+
170
+ logger.debug(`Sent iMessage to ${targetId}`);
171
+
172
+ if (callback) {
173
+ callback({
174
+ text: "Message sent successfully.",
175
+ source: message.content.source as string,
176
+ });
177
+ }
178
+
179
+ return {
180
+ success: true,
181
+ data: {
182
+ to: targetId,
183
+ messageId: result.messageId,
184
+ },
185
+ };
186
+ },
187
+
188
+ examples: [
189
+ [
190
+ {
191
+ name: "{{user1}}",
192
+ content: { text: "Send them a message saying 'Hello!'" },
193
+ },
194
+ {
195
+ name: "{{agent}}",
196
+ content: {
197
+ text: "I'll send that message via iMessage.",
198
+ actions: ["IMESSAGE_SEND_MESSAGE"],
199
+ },
200
+ },
201
+ ],
202
+ [
203
+ {
204
+ name: "{{user1}}",
205
+ content: {
206
+ text: "Text +1234567890 saying 'I'll be there in 10 minutes'",
207
+ },
208
+ },
209
+ {
210
+ name: "{{agent}}",
211
+ content: {
212
+ text: "I'll send that text.",
213
+ actions: ["IMESSAGE_SEND_MESSAGE"],
214
+ },
215
+ },
216
+ ],
217
+ ],
218
+ };
package/src/config.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * iMessage plugin configuration types.
3
+ *
4
+ * These types define the configuration schema for the iMessage plugin.
5
+ * Shared base types are imported from @elizaos/core.
6
+ */
7
+
8
+ import type {
9
+ BlockStreamingCoalesceConfig,
10
+ ChannelHeartbeatVisibilityConfig,
11
+ DmConfig,
12
+ DmPolicy,
13
+ GroupPolicy,
14
+ MarkdownConfig,
15
+ } from "@elizaos/core";
16
+
17
+ // ============================================================
18
+ // Reaction Configuration
19
+ // ============================================================
20
+
21
+ export type IMessageReactionNotificationMode = "off" | "own" | "all" | "allowlist";
22
+
23
+ // ============================================================
24
+ // Account Configuration
25
+ // ============================================================
26
+
27
+ export type IMessageAccountConfig = {
28
+ /** Optional display name for this account (used in CLI/UI lists). */
29
+ name?: string;
30
+ /** Optional provider capability tags used for agent/runtime guidance. */
31
+ capabilities?: string[];
32
+ /** Markdown formatting overrides (tables). */
33
+ markdown?: MarkdownConfig;
34
+ /** Allow channel-initiated config writes (default: true). */
35
+ configWrites?: boolean;
36
+ /** If false, do not start this iMessage account. Default: true. */
37
+ enabled?: boolean;
38
+ /** Direct message access policy (default: pairing). */
39
+ dmPolicy?: DmPolicy;
40
+ /** Optional allowlist for iMessage senders (phone E.164 or iCloud email). */
41
+ allowFrom?: Array<string | number>;
42
+ /** Optional allowlist for iMessage group senders. */
43
+ groupAllowFrom?: Array<string | number>;
44
+ /**
45
+ * Controls how group messages are handled:
46
+ * - "open": groups bypass allowFrom, no extra gating
47
+ * - "disabled": block all group messages
48
+ * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
49
+ */
50
+ groupPolicy?: GroupPolicy;
51
+ /** Max group messages to keep as history context (0 disables). */
52
+ historyLimit?: number;
53
+ /** Max DM turns to keep as history context. */
54
+ dmHistoryLimit?: number;
55
+ /** Per-DM config overrides keyed by user ID. */
56
+ dms?: Record<string, DmConfig>;
57
+ /** Outbound text chunk size (chars). Default: 4000. */
58
+ textChunkLimit?: number;
59
+ /** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
60
+ chunkMode?: "length" | "newline";
61
+ /** Disable block streaming for this account. */
62
+ blockStreaming?: boolean;
63
+ /** Merge streamed block replies before sending. */
64
+ blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
65
+ /** Maximum media file size in MB. Default: 100. */
66
+ mediaMaxMb?: number;
67
+ /** Reaction notification mode (off|own|all|allowlist). Default: own. */
68
+ reactionNotifications?: IMessageReactionNotificationMode;
69
+ /** Allowlist for reaction notifications when mode is allowlist. */
70
+ reactionAllowlist?: Array<string | number>;
71
+ /** Heartbeat visibility settings for this channel. */
72
+ heartbeat?: ChannelHeartbeatVisibilityConfig;
73
+ };
74
+
75
+ // ============================================================
76
+ // Main iMessage Configuration
77
+ // ============================================================
78
+
79
+ export type IMessageConfig = {
80
+ /** Optional per-account iMessage configuration (multi-account). */
81
+ accounts?: Record<string, IMessageAccountConfig>;
82
+ } & IMessageAccountConfig;