@elizaos/plugin-bluebubbles 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,120 @@
1
+ /**
2
+ * Environment and configuration validation for BlueBubbles plugin
3
+ */
4
+ import { z } from "zod";
5
+ import type { IAgentRuntime } from "@elizaos/core";
6
+ import type { BlueBubblesConfig, DmPolicy, GroupPolicy } from "./types";
7
+
8
+ const DmPolicySchema = z.enum(["open", "pairing", "allowlist", "disabled"]);
9
+ const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
10
+
11
+ export const BlueBubblesConfigSchema = z.object({
12
+ serverUrl: z.string().url("Server URL must be a valid URL"),
13
+ password: z.string().min(1, "Password is required"),
14
+ webhookPath: z.string().optional().default("/webhooks/bluebubbles"),
15
+ dmPolicy: DmPolicySchema.optional().default("pairing"),
16
+ groupPolicy: GroupPolicySchema.optional().default("allowlist"),
17
+ allowFrom: z.array(z.string()).optional().default([]),
18
+ groupAllowFrom: z.array(z.string()).optional().default([]),
19
+ sendReadReceipts: z.boolean().optional().default(true),
20
+ enabled: z.boolean().optional().default(true),
21
+ });
22
+
23
+ export type ValidatedBlueBubblesConfig = z.infer<typeof BlueBubblesConfigSchema>;
24
+
25
+ /**
26
+ * Validates BlueBubbles configuration
27
+ */
28
+ export function validateConfig(config: Partial<BlueBubblesConfig>): ValidatedBlueBubblesConfig {
29
+ return BlueBubblesConfigSchema.parse(config);
30
+ }
31
+
32
+ /**
33
+ * Gets BlueBubbles configuration from runtime settings
34
+ */
35
+ export function getConfigFromRuntime(runtime: IAgentRuntime): BlueBubblesConfig | null {
36
+ // Helper to safely get string settings
37
+ const getStringSetting = (key: string): string | undefined => {
38
+ const value = runtime.getSetting(key);
39
+ return typeof value === "string" ? value : undefined;
40
+ };
41
+
42
+ const serverUrl = getStringSetting("BLUEBUBBLES_SERVER_URL");
43
+ const password = getStringSetting("BLUEBUBBLES_PASSWORD");
44
+
45
+ if (!serverUrl || !password) {
46
+ return null;
47
+ }
48
+
49
+ const allowFromRaw = getStringSetting("BLUEBUBBLES_ALLOW_FROM");
50
+ const groupAllowFromRaw = getStringSetting("BLUEBUBBLES_GROUP_ALLOW_FROM");
51
+
52
+ const parseAllowList = (raw: string | undefined): string[] => {
53
+ if (!raw) return [];
54
+ return raw.split(",").map((s: string) => s.trim()).filter(Boolean);
55
+ };
56
+
57
+ return {
58
+ serverUrl,
59
+ password,
60
+ webhookPath: getStringSetting("BLUEBUBBLES_WEBHOOK_PATH") ?? "/webhooks/bluebubbles",
61
+ dmPolicy: (getStringSetting("BLUEBUBBLES_DM_POLICY") as DmPolicy) ?? "pairing",
62
+ groupPolicy: (getStringSetting("BLUEBUBBLES_GROUP_POLICY") as GroupPolicy) ?? "allowlist",
63
+ allowFrom: parseAllowList(allowFromRaw),
64
+ groupAllowFrom: parseAllowList(groupAllowFromRaw),
65
+ sendReadReceipts: getStringSetting("BLUEBUBBLES_SEND_READ_RECEIPTS") !== "false",
66
+ enabled: getStringSetting("BLUEBUBBLES_ENABLED") !== "false",
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Normalizes a phone number or email handle
72
+ */
73
+ export function normalizeHandle(handle: string): string {
74
+ const trimmed = handle.trim();
75
+
76
+ // If it looks like an email, lowercase it
77
+ if (trimmed.includes("@") && !trimmed.startsWith("+")) {
78
+ return trimmed.toLowerCase();
79
+ }
80
+
81
+ // For phone numbers, strip non-digits except leading +
82
+ const startsWithPlus = trimmed.startsWith("+");
83
+ const digits = trimmed.replace(/\D/g, "");
84
+
85
+ // Add + prefix if it was there or if we have 10+ digits (assume international)
86
+ if (startsWithPlus || digits.length >= 10) {
87
+ return `+${digits}`;
88
+ }
89
+
90
+ return digits;
91
+ }
92
+
93
+ /**
94
+ * Checks if a handle is in the allow list
95
+ */
96
+ export function isHandleAllowed(
97
+ handle: string,
98
+ allowList: string[],
99
+ policy: DmPolicy | GroupPolicy
100
+ ): boolean {
101
+ if (policy === "open") {
102
+ return true;
103
+ }
104
+
105
+ if (policy === "disabled") {
106
+ return false;
107
+ }
108
+
109
+ if (policy === "pairing" || policy === "allowlist") {
110
+ if (allowList.length === 0 && policy === "pairing") {
111
+ // Pairing mode with empty allow list allows first contact
112
+ return true;
113
+ }
114
+
115
+ const normalizedHandle = normalizeHandle(handle);
116
+ return allowList.some((allowed) => normalizeHandle(allowed) === normalizedHandle);
117
+ }
118
+
119
+ return false;
120
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * BlueBubbles Plugin for ElizaOS
3
+ *
4
+ * Provides iMessage integration via the BlueBubbles macOS app and REST API,
5
+ * supporting text messages, reactions, effects, and more.
6
+ */
7
+
8
+ import type { Plugin, IAgentRuntime } from "@elizaos/core";
9
+ import { logger } from "@elizaos/core";
10
+ import { BlueBubblesService } from "./service.js";
11
+ import { sendMessageAction, sendReactionAction } from "./actions/index.js";
12
+ import { chatContextProvider } from "./providers/index.js";
13
+ import { BLUEBUBBLES_SERVICE_NAME } from "./constants.js";
14
+
15
+ // Re-export types and service
16
+ export * from "./types.js";
17
+ export * from "./constants.js";
18
+ export { BlueBubblesService };
19
+ export { sendMessageAction, sendReactionAction };
20
+ export { chatContextProvider };
21
+
22
+ /**
23
+ * BlueBubbles plugin for ElizaOS agents.
24
+ */
25
+ const blueBubblesPlugin: Plugin = {
26
+ name: "bluebubbles",
27
+ description: "BlueBubbles iMessage bridge plugin for ElizaOS agents",
28
+
29
+ services: [BlueBubblesService],
30
+ actions: [sendMessageAction, sendReactionAction],
31
+ providers: [chatContextProvider],
32
+ tests: [],
33
+
34
+ init: async (config: Record<string, string>, runtime: IAgentRuntime): Promise<void> => {
35
+ logger.info("Initializing BlueBubbles plugin...");
36
+
37
+ const hasServerUrl = Boolean(
38
+ config.BLUEBUBBLES_SERVER_URL || process.env.BLUEBUBBLES_SERVER_URL
39
+ );
40
+ const hasPassword = Boolean(
41
+ config.BLUEBUBBLES_PASSWORD || process.env.BLUEBUBBLES_PASSWORD
42
+ );
43
+
44
+ logger.info("BlueBubbles plugin configuration:");
45
+ logger.info(` - Server URL configured: ${hasServerUrl ? "Yes" : "No"}`);
46
+ logger.info(` - Password configured: ${hasPassword ? "Yes" : "No"}`);
47
+ logger.info(
48
+ ` - DM policy: ${config.BLUEBUBBLES_DM_POLICY || process.env.BLUEBUBBLES_DM_POLICY || "pairing"}`
49
+ );
50
+ logger.info(
51
+ ` - Group policy: ${config.BLUEBUBBLES_GROUP_POLICY || process.env.BLUEBUBBLES_GROUP_POLICY || "allowlist"}`
52
+ );
53
+
54
+ if (!hasServerUrl) {
55
+ logger.warn(
56
+ "BlueBubbles server URL not configured. Set BLUEBUBBLES_SERVER_URL."
57
+ );
58
+ }
59
+
60
+ if (!hasPassword) {
61
+ logger.warn("BlueBubbles password not configured. Set BLUEBUBBLES_PASSWORD.");
62
+ }
63
+
64
+ logger.info("BlueBubbles plugin initialized");
65
+ },
66
+ };
67
+
68
+ export default blueBubblesPlugin;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Chat context provider for the BlueBubbles plugin.
3
+ */
4
+
5
+ import type {
6
+ IAgentRuntime,
7
+ Memory,
8
+ Provider,
9
+ ProviderResult,
10
+ State,
11
+ } from "@elizaos/core";
12
+ import { BLUEBUBBLES_SERVICE_NAME } from "../constants.js";
13
+ import type { BlueBubblesService } from "../service.js";
14
+
15
+ /**
16
+ * Extract handle from a chat GUID
17
+ */
18
+ function extractHandleFromChatGuid(chatGuid: string): string | null {
19
+ if (!chatGuid) return null;
20
+ // Format: iMessage;-;+1234567890 or iMessage;+;group_id
21
+ const parts = chatGuid.split(";");
22
+ if (parts.length >= 3 && parts[1] === "-") {
23
+ return parts[2];
24
+ }
25
+ return null;
26
+ }
27
+
28
+ export const chatContextProvider: Provider = {
29
+ name: "bluebubblesChatContext",
30
+ description:
31
+ "Provides information about the current BlueBubbles/iMessage chat context",
32
+
33
+ get: async (
34
+ runtime: IAgentRuntime,
35
+ message: Memory,
36
+ state: State,
37
+ ): Promise<ProviderResult> => {
38
+ // Only provide context for BlueBubbles messages
39
+ if (message.content.source !== "bluebubbles") {
40
+ return { text: "" };
41
+ }
42
+
43
+ const bbService = runtime.getService<BlueBubblesService>(
44
+ BLUEBUBBLES_SERVICE_NAME,
45
+ );
46
+
47
+ if (!bbService || !bbService.isConnected()) {
48
+ return {
49
+ values: { connected: false },
50
+ text: "",
51
+ };
52
+ }
53
+
54
+ const agentName = state.values?.agentName?.toString() || "The agent";
55
+ const stateData = (state.data || {}) as Record<string, unknown>;
56
+
57
+ const chatGuid = stateData.chatGuid as string | undefined;
58
+ const handle = stateData.handle as string | undefined;
59
+ const displayName = stateData.displayName as string | undefined;
60
+
61
+ // Determine chat type from GUID
62
+ let chatType = "direct";
63
+ let chatDescription = "";
64
+
65
+ if (chatGuid) {
66
+ if (chatGuid.includes(";+;")) {
67
+ chatType = "group";
68
+ chatDescription = displayName
69
+ ? `group chat "${displayName}"`
70
+ : "a group chat";
71
+ } else {
72
+ const extractedHandle = extractHandleFromChatGuid(chatGuid);
73
+ chatDescription = extractedHandle
74
+ ? `direct message with ${extractedHandle}`
75
+ : handle
76
+ ? `direct message with ${handle}`
77
+ : "a direct message";
78
+ }
79
+ } else if (handle) {
80
+ chatDescription = `direct message with ${handle}`;
81
+ } else {
82
+ chatDescription = "an iMessage conversation";
83
+ }
84
+
85
+ const responseText =
86
+ `${agentName} is chatting via iMessage (BlueBubbles) in ${chatDescription}. ` +
87
+ "This channel supports reactions, effects (slam, balloons, confetti, etc.), editing, and replying to messages.";
88
+
89
+ return {
90
+ values: {
91
+ chatGuid,
92
+ handle,
93
+ displayName,
94
+ chatType,
95
+ connected: true,
96
+ platform: "bluebubbles",
97
+ supportsReactions: true,
98
+ supportsEffects: true,
99
+ supportsEdit: true,
100
+ supportsReply: true,
101
+ },
102
+ text: responseText,
103
+ };
104
+ },
105
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Chat state provider for BlueBubbles
3
+ */
4
+ import {
5
+ type IAgentRuntime,
6
+ logger,
7
+ type Memory,
8
+ type Provider,
9
+ type ProviderResult,
10
+ type State,
11
+ } from "@elizaos/core";
12
+ import { BLUEBUBBLES_SERVICE_NAME } from "../constants";
13
+ import { BlueBubblesService } from "../service";
14
+ import type { BlueBubblesChatState } from "../types";
15
+
16
+ export const chatStateProvider: Provider = {
17
+ name: "BLUEBUBBLES_CHAT_STATE",
18
+ description: "Provides information about the current BlueBubbles/iMessage chat context",
19
+
20
+ get: async (
21
+ runtime: IAgentRuntime,
22
+ message: Memory,
23
+ _state: State
24
+ ): Promise<ProviderResult> => {
25
+ const service = runtime.getService<BlueBubblesService>(BLUEBUBBLES_SERVICE_NAME);
26
+
27
+ if (!service || !service.getIsRunning()) {
28
+ return { text: "" };
29
+ }
30
+
31
+ try {
32
+ const room = await runtime.getRoom(message.roomId);
33
+ if (!room?.channelId) {
34
+ return { text: "" };
35
+ }
36
+
37
+ // Only provide state for BlueBubbles channels
38
+ if (room.source !== "bluebubbles") {
39
+ return { text: "" };
40
+ }
41
+
42
+ const chatState = await service.getChatState(room.channelId);
43
+ if (!chatState) {
44
+ return { text: "" };
45
+ }
46
+
47
+ return { text: formatChatState(chatState) };
48
+ } catch (error) {
49
+ logger.debug(
50
+ `Failed to get BlueBubbles chat state: ${error instanceof Error ? error.message : String(error)}`
51
+ );
52
+ return { text: "" };
53
+ }
54
+ },
55
+ };
56
+
57
+ /**
58
+ * Formats the chat state for inclusion in prompts
59
+ */
60
+ function formatChatState(state: BlueBubblesChatState): string {
61
+ const lines: string[] = [
62
+ "# iMessage Chat Context (BlueBubbles)",
63
+ "",
64
+ `- Chat Type: ${state.isGroup ? "Group Chat" : "Direct Message"}`,
65
+ ];
66
+
67
+ if (state.displayName) {
68
+ lines.push(`- Chat Name: ${state.displayName}`);
69
+ }
70
+
71
+ if (state.isGroup) {
72
+ lines.push(`- Participants: ${state.participants.join(", ")}`);
73
+ } else {
74
+ lines.push(`- Contact: ${state.participants[0] ?? state.chatIdentifier}`);
75
+ }
76
+
77
+ if (state.lastMessageAt) {
78
+ const lastMessageDate = new Date(state.lastMessageAt);
79
+ lines.push(`- Last Message: ${lastMessageDate.toLocaleString()}`);
80
+ }
81
+
82
+ if (state.hasUnread) {
83
+ lines.push("- Has Unread Messages: Yes");
84
+ }
85
+
86
+ lines.push("");
87
+ lines.push("Note: This conversation is happening through iMessage. Be conversational and friendly.");
88
+
89
+ return lines.join("\n");
90
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * BlueBubbles providers export
3
+ */
4
+ export { chatStateProvider } from "./chatState";
5
+ export { chatContextProvider } from "./chatContext";