@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.
- package/__tests__/integration.test.ts +260 -0
- package/build.ts +16 -0
- package/dist/index.js +46 -0
- package/package.json +33 -0
- package/src/actions/index.ts +5 -0
- package/src/actions/sendMessage.ts +175 -0
- package/src/actions/sendReaction.ts +186 -0
- package/src/client.ts +389 -0
- package/src/constants.ts +41 -0
- package/src/environment.ts +120 -0
- package/src/index.ts +68 -0
- package/src/providers/chatContext.ts +105 -0
- package/src/providers/chatState.ts +90 -0
- package/src/providers/index.ts +5 -0
- package/src/service.ts +502 -0
- package/src/types.ts +165 -0
- package/tsconfig.json +22 -0
|
@@ -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
|
+
}
|