@emotion-machine/claw-messenger 0.1.15 → 0.1.18
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/README.md +1 -1
- package/dist/channel.d.ts +1 -1
- package/dist/channel.js +15 -30
- package/dist/index.js +18 -22
- package/dist/reactions.d.ts +21 -0
- package/dist/reactions.js +68 -0
- package/package.json +13 -2
- package/src/types/openclaw.d.ts +6 -3
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ After installing, add to your config under `channels`:
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
32
|
- **Send & receive** text messages and media (images, video, audio, documents)
|
|
33
|
-
- **iMessage reactions** —
|
|
33
|
+
- **iMessage reactions** — six standard tapbacks plus any single Unicode emoji
|
|
34
34
|
- **Group chats** — send to existing groups or create new ones
|
|
35
35
|
- **Typing indicators** — sent and received
|
|
36
36
|
- **DM security policies** — open, pairing-based approval, or allowlist
|
package/dist/channel.d.ts
CHANGED
|
@@ -21,5 +21,5 @@ export declare function createGroup(to: string[], text: string): Promise<{
|
|
|
21
21
|
ok: boolean;
|
|
22
22
|
} & SendResult>;
|
|
23
23
|
export declare const clawMessengerPlugin: ChannelPlugin<ResolvedAccount>;
|
|
24
|
-
export declare function handleInboundMessage(data: Record<string, unknown>, accountId: string, account: ResolvedAccount, ctx: any): Promise<void>;
|
|
24
|
+
export declare function handleInboundMessage(data: Record<string, unknown>, accountId: string, account: ResolvedAccount, ctx: any, wsOverride?: WsClient): Promise<void>;
|
|
25
25
|
export {};
|
package/dist/channel.js
CHANGED
|
@@ -2,19 +2,9 @@ import { DEFAULT_ACCOUNT_ID as OPENCLAW_DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MES
|
|
|
2
2
|
import { ClawMessengerConfigSchema } from "./config.js";
|
|
3
3
|
import { isInboundMessageEvent, normalizeInboundMessage, validateInboundRoute, } from "./inbound.js";
|
|
4
4
|
import { getRuntime } from "./runtime.js";
|
|
5
|
+
import { sendReaction } from "./reactions.js";
|
|
5
6
|
import { WsClient } from "./ws/client.js";
|
|
6
7
|
import { normalizeDirectTarget, sendText, sendMedia, sendToGroup, sendGroupMedia, sendToNewGroup, } from "./outbound/send.js";
|
|
7
|
-
const EMOJI_TO_REACTION = {
|
|
8
|
-
"❤️": "love", "♥️": "love", "🩷": "love", "💕": "love", "😍": "love",
|
|
9
|
-
"👍": "like", "👍🏻": "like", "👍🏼": "like", "👍🏽": "like", "👍🏾": "like", "👍🏿": "like",
|
|
10
|
-
"👎": "dislike", "👎🏻": "dislike", "👎🏼": "dislike", "👎🏽": "dislike", "👎🏾": "dislike", "👎🏿": "dislike",
|
|
11
|
-
"😂": "laugh", "🤣": "laugh", "😆": "laugh",
|
|
12
|
-
"‼️": "emphasize", "❗": "emphasize", "❕": "emphasize", "⚡": "emphasize",
|
|
13
|
-
"❓": "question", "❔": "question", "🤔": "question",
|
|
14
|
-
};
|
|
15
|
-
function emojiToReaction(emoji) {
|
|
16
|
-
return EMOJI_TO_REACTION[emoji];
|
|
17
|
-
}
|
|
18
8
|
// -- Account resolution --
|
|
19
9
|
const FALLBACK_ACCOUNT_ID = "default";
|
|
20
10
|
function hasAccountId(value) {
|
|
@@ -60,7 +50,7 @@ export function getWsClient(accountId) {
|
|
|
60
50
|
// -- Connection status helper (used by tools & commands in index.ts) --
|
|
61
51
|
export function getConnectionStatus() {
|
|
62
52
|
const runtime = getRuntime();
|
|
63
|
-
const cfg = runtime.config.
|
|
53
|
+
const cfg = runtime.config.current();
|
|
64
54
|
const account = resolveAccount(cfg);
|
|
65
55
|
const ws = wsClients.get(account.accountId);
|
|
66
56
|
return {
|
|
@@ -72,7 +62,7 @@ export function getConnectionStatus() {
|
|
|
72
62
|
}
|
|
73
63
|
export async function createGroup(to, text) {
|
|
74
64
|
const runtime = getRuntime();
|
|
75
|
-
const cfg = runtime.config.
|
|
65
|
+
const cfg = runtime.config.current();
|
|
76
66
|
const account = resolveAccount(cfg);
|
|
77
67
|
const ws = wsClients.get(account.accountId);
|
|
78
68
|
if (!ws)
|
|
@@ -86,7 +76,7 @@ export async function createGroup(to, text) {
|
|
|
86
76
|
function resolveConfig(input) {
|
|
87
77
|
if (input && "channels" in input)
|
|
88
78
|
return input;
|
|
89
|
-
return input?.cfg ?? getRuntime().config.
|
|
79
|
+
return input?.cfg ?? getRuntime().config.current();
|
|
90
80
|
}
|
|
91
81
|
function describeClawMessageTool(input) {
|
|
92
82
|
const account = resolveAccount(resolveConfig(input));
|
|
@@ -192,7 +182,7 @@ export const clawMessengerPlugin = {
|
|
|
192
182
|
},
|
|
193
183
|
agentPrompt: {
|
|
194
184
|
messageToolHints: () => [
|
|
195
|
-
"You can react to messages
|
|
185
|
+
"You can react to messages via the react action. The six iMessage tapbacks are love (❤️), like (👍), dislike (👎), laugh (😂), emphasize (‼️), and question (❓). Any other single Unicode emoji is sent as a custom reaction.",
|
|
196
186
|
"Use reactions naturally — the way a real person would in iMessage.",
|
|
197
187
|
"If you encounter connection issues, the server is WebSocket-only — do NOT test with HTTP. Full API docs and troubleshooting: https://clawmessenger.com/docs",
|
|
198
188
|
"If a user is stuck connecting, the fix is almost always: make sure the Claw Messenger plugin is enabled and the API key is set. Verified setup guides to hand them: OpenClaw setup https://www.clawmessenger.com/blog/openclaw-imessage-setup-guide, Hermes setup https://www.clawmessenger.com/blog/hermes-imessage-setup, troubleshooting https://www.clawmessenger.com/blog/openclaw-imessage-troubleshooting.",
|
|
@@ -211,17 +201,14 @@ export const clawMessengerPlugin = {
|
|
|
211
201
|
const messageId = params.messageId;
|
|
212
202
|
if (!messageId)
|
|
213
203
|
throw new Error("messageId is required for react action");
|
|
214
|
-
const emoji = params.emoji?.trim()
|
|
204
|
+
const emoji = params.emoji?.trim() || "❤️";
|
|
215
205
|
const remove = params.remove === true;
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
reactionType,
|
|
221
|
-
remove,
|
|
222
|
-
});
|
|
206
|
+
const result = await sendReaction(ws, messageId, emoji, remove);
|
|
207
|
+
const reaction = result.reactionType === "custom"
|
|
208
|
+
? result.emoji
|
|
209
|
+
: result.reactionType;
|
|
223
210
|
return {
|
|
224
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, action: remove ? "removed" : "added", reaction
|
|
211
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, action: remove ? "removed" : "added", reaction }) }],
|
|
225
212
|
};
|
|
226
213
|
}
|
|
227
214
|
if (action === "send") {
|
|
@@ -439,7 +426,7 @@ export const clawMessengerPlugin = {
|
|
|
439
426
|
// versions doesn't include it. Newer OpenClaw calls this; older versions ignore it.
|
|
440
427
|
clawMessengerPlugin.describeMessageTool = describeClawMessageTool;
|
|
441
428
|
// -- Inbound message handler --
|
|
442
|
-
export async function handleInboundMessage(data, accountId, account, ctx) {
|
|
429
|
+
export async function handleInboundMessage(data, accountId, account, ctx, wsOverride) {
|
|
443
430
|
const normalized = normalizeInboundMessage(data);
|
|
444
431
|
if (!normalized.ok) {
|
|
445
432
|
ctx.log?.warn?.(`[${accountId}] Dropping inbound message: ${normalized.reason}`);
|
|
@@ -447,7 +434,7 @@ export async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
447
434
|
}
|
|
448
435
|
const { from, text, messageId, attachments, isGroup, chatId, participants, } = normalized.message;
|
|
449
436
|
const runtime = getRuntime();
|
|
450
|
-
const cfg = runtime.config.
|
|
437
|
+
const cfg = runtime.config.current();
|
|
451
438
|
// Resolve routing — group by chatId, DM by sender phone
|
|
452
439
|
let rawRoute;
|
|
453
440
|
try {
|
|
@@ -495,13 +482,11 @@ export async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
495
482
|
const rawBody = text || (allMedia.length > 0 ? "<media:image>" : "");
|
|
496
483
|
if (!rawBody)
|
|
497
484
|
return;
|
|
498
|
-
const body = runtime.channel.reply.
|
|
485
|
+
const body = runtime.channel.reply.formatAgentEnvelope({
|
|
499
486
|
channel: "Claw Messenger",
|
|
500
487
|
from,
|
|
501
488
|
timestamp: Date.now(),
|
|
502
489
|
body: rawBody,
|
|
503
|
-
chatType: isGroup ? "group" : "direct",
|
|
504
|
-
sender: { id: from },
|
|
505
490
|
previousTimestamp,
|
|
506
491
|
envelope: envelopeOptions,
|
|
507
492
|
});
|
|
@@ -536,7 +521,7 @@ export async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
536
521
|
sessionKey: ctxPayload.SessionKey ?? routeSessionKey,
|
|
537
522
|
ctx: ctxPayload,
|
|
538
523
|
}).catch(() => { });
|
|
539
|
-
const ws = wsClients.get(accountId);
|
|
524
|
+
const ws = wsOverride ?? wsClients.get(accountId);
|
|
540
525
|
// Mark as read + start typing (DM only — skip for groups)
|
|
541
526
|
if (!isGroup && ws) {
|
|
542
527
|
ws.send({ type: "read", to: from });
|
package/dist/index.js
CHANGED
|
@@ -39,18 +39,16 @@ const plugin = {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
const runtime = getRuntime();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
...(cfg.channels?.["claw-messenger"] ?? {}),
|
|
42
|
+
await runtime.config.mutateConfigFile({
|
|
43
|
+
afterWrite: { mode: "auto" },
|
|
44
|
+
mutate(draft) {
|
|
45
|
+
draft.channels ??= {};
|
|
46
|
+
draft.channels["claw-messenger"] = {
|
|
47
|
+
...(draft.channels["claw-messenger"] ?? {}),
|
|
49
48
|
preferredService: service,
|
|
50
|
-
}
|
|
49
|
+
};
|
|
51
50
|
},
|
|
52
|
-
};
|
|
53
|
-
await runtime.config.writeConfigFile(updated);
|
|
51
|
+
});
|
|
54
52
|
return {
|
|
55
53
|
content: [{ type: "text", text: JSON.stringify({ ok: true, preferredService: service }) }],
|
|
56
54
|
};
|
|
@@ -99,7 +97,7 @@ const plugin = {
|
|
|
99
97
|
const ws = getWsClient(status.accountId);
|
|
100
98
|
const diagnostics = ws?.getDiagnostics() ?? { errors: [], connectionLog: [] };
|
|
101
99
|
const report = {
|
|
102
|
-
plugin_version: "0.1.
|
|
100
|
+
plugin_version: "0.1.18",
|
|
103
101
|
node_version: process.version,
|
|
104
102
|
connected: status.connected,
|
|
105
103
|
server_url: status.serverUrl,
|
|
@@ -111,7 +109,7 @@ const plugin = {
|
|
|
111
109
|
if (submit && status.connected) {
|
|
112
110
|
try {
|
|
113
111
|
const runtime = getRuntime();
|
|
114
|
-
const cfg = runtime.config.
|
|
112
|
+
const cfg = runtime.config.current();
|
|
115
113
|
const account = (cfg.channels?.["claw-messenger"] ?? {});
|
|
116
114
|
const apiKey = account.apiKey ?? "";
|
|
117
115
|
const serverUrl = account.serverUrl ?? "https://claw-messenger.onrender.com";
|
|
@@ -177,18 +175,16 @@ const plugin = {
|
|
|
177
175
|
return { text: `Invalid service "${arg}". Must be one of: ${VALID_SERVICES.join(", ")}` };
|
|
178
176
|
}
|
|
179
177
|
const runtime = getRuntime();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
...(cfg.channels?.["claw-messenger"] ?? {}),
|
|
178
|
+
await runtime.config.mutateConfigFile({
|
|
179
|
+
afterWrite: { mode: "auto" },
|
|
180
|
+
mutate(draft) {
|
|
181
|
+
draft.channels ??= {};
|
|
182
|
+
draft.channels["claw-messenger"] = {
|
|
183
|
+
...(draft.channels["claw-messenger"] ?? {}),
|
|
187
184
|
preferredService: match,
|
|
188
|
-
}
|
|
185
|
+
};
|
|
189
186
|
},
|
|
190
|
-
};
|
|
191
|
-
await runtime.config.writeConfigFile(updated);
|
|
187
|
+
});
|
|
192
188
|
return { text: `Preferred service switched to ${match}` };
|
|
193
189
|
},
|
|
194
190
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type StandardReactionType = "love" | "like" | "dislike" | "laugh" | "emphasize" | "question";
|
|
2
|
+
export type ReactionType = StandardReactionType | "custom";
|
|
3
|
+
export interface ReactionResult {
|
|
4
|
+
type: "reaction.result";
|
|
5
|
+
id?: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
messageId?: string;
|
|
8
|
+
reactionType?: ReactionType;
|
|
9
|
+
emoji?: string;
|
|
10
|
+
remove?: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
retryable?: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface ReactionWsClient {
|
|
16
|
+
request(message: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
export declare function isSingleEmoji(value: string): boolean;
|
|
19
|
+
export declare function reactionFrame(messageId: string, rawEmoji: string, remove?: boolean): Record<string, unknown>;
|
|
20
|
+
export declare function sendReaction(ws: ReactionWsClient, messageId: string, emoji: string, remove?: boolean): Promise<ReactionResult>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const STANDARD_EMOJI_TO_REACTION = {
|
|
2
|
+
"❤": "love",
|
|
3
|
+
"❤️": "love",
|
|
4
|
+
"♥️": "love",
|
|
5
|
+
"🩷": "love",
|
|
6
|
+
"💕": "love",
|
|
7
|
+
"😍": "love",
|
|
8
|
+
"👍": "like",
|
|
9
|
+
"👍🏻": "like",
|
|
10
|
+
"👍🏼": "like",
|
|
11
|
+
"👍🏽": "like",
|
|
12
|
+
"👍🏾": "like",
|
|
13
|
+
"👍🏿": "like",
|
|
14
|
+
"👎": "dislike",
|
|
15
|
+
"👎🏻": "dislike",
|
|
16
|
+
"👎🏼": "dislike",
|
|
17
|
+
"👎🏽": "dislike",
|
|
18
|
+
"👎🏾": "dislike",
|
|
19
|
+
"👎🏿": "dislike",
|
|
20
|
+
"😂": "laugh",
|
|
21
|
+
"🤣": "laugh",
|
|
22
|
+
"😆": "laugh",
|
|
23
|
+
"‼": "emphasize",
|
|
24
|
+
"‼️": "emphasize",
|
|
25
|
+
"❗": "emphasize",
|
|
26
|
+
"❕": "emphasize",
|
|
27
|
+
"⚡": "emphasize",
|
|
28
|
+
"❓": "question",
|
|
29
|
+
"❔": "question",
|
|
30
|
+
"🤔": "question",
|
|
31
|
+
};
|
|
32
|
+
export function isSingleEmoji(value) {
|
|
33
|
+
if (!value || value !== value.trim() || value.length > 64)
|
|
34
|
+
return false;
|
|
35
|
+
const segments = Array.from(new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(value));
|
|
36
|
+
if (segments.length !== 1)
|
|
37
|
+
return false;
|
|
38
|
+
return (/\p{Extended_Pictographic}/u.test(value)
|
|
39
|
+
|| /^\p{Regional_Indicator}{2}$/u.test(value)
|
|
40
|
+
|| /^[#*0-9]\uFE0F?\u20E3$/u.test(value));
|
|
41
|
+
}
|
|
42
|
+
export function reactionFrame(messageId, rawEmoji, remove = false) {
|
|
43
|
+
const normalizedMessageId = messageId?.trim();
|
|
44
|
+
if (!normalizedMessageId)
|
|
45
|
+
throw new Error("messageId is required for react action");
|
|
46
|
+
const emoji = rawEmoji?.trim();
|
|
47
|
+
if (!isSingleEmoji(emoji)) {
|
|
48
|
+
throw new Error("emoji must contain exactly one Unicode emoji");
|
|
49
|
+
}
|
|
50
|
+
const standardReaction = STANDARD_EMOJI_TO_REACTION[emoji];
|
|
51
|
+
return {
|
|
52
|
+
type: "reaction",
|
|
53
|
+
messageId: normalizedMessageId,
|
|
54
|
+
reactionType: standardReaction ?? "custom",
|
|
55
|
+
...(standardReaction ? {} : { emoji }),
|
|
56
|
+
remove,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function sendReaction(ws, messageId, emoji, remove = false) {
|
|
60
|
+
const response = await ws.request(reactionFrame(messageId, emoji, remove));
|
|
61
|
+
if (response.type !== "reaction.result") {
|
|
62
|
+
throw new Error("Claw Messenger returned an invalid reaction response");
|
|
63
|
+
}
|
|
64
|
+
if (response.ok !== true) {
|
|
65
|
+
throw new Error(typeof response.error === "string" ? response.error : "Reaction request failed");
|
|
66
|
+
}
|
|
67
|
+
return response;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emotion-machine/claw-messenger",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.18",
|
|
4
|
+
"description": "OpenClaw channel plugin for Claw Messenger, a managed, Mac-free iMessage, RCS, and SMS API for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
"install": {
|
|
60
60
|
"npmSpec": "@emotion-machine/claw-messenger",
|
|
61
61
|
"defaultChoice": "npm"
|
|
62
|
+
},
|
|
63
|
+
"compat": {
|
|
64
|
+
"pluginApi": ">=2026.8.1"
|
|
62
65
|
}
|
|
63
66
|
},
|
|
64
67
|
"dependencies": {
|
|
@@ -66,6 +69,14 @@
|
|
|
66
69
|
"ws": "^8.18.0",
|
|
67
70
|
"zod": "^4.3.6"
|
|
68
71
|
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"openclaw": ">=2026.8.1"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"openclaw": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
69
80
|
"devDependencies": {
|
|
70
81
|
"@types/node": "^20.14.0",
|
|
71
82
|
"@types/ws": "^8.5.0",
|
package/src/types/openclaw.d.ts
CHANGED
|
@@ -150,8 +150,11 @@ declare module "openclaw/plugin-sdk/channel-core" {
|
|
|
150
150
|
export interface PluginRuntime {
|
|
151
151
|
version: string;
|
|
152
152
|
config: {
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
current(): OpenclawConfig;
|
|
154
|
+
mutateConfigFile<T = void>(params: {
|
|
155
|
+
afterWrite: { mode: "auto" | "restart" | "none"; reason?: string };
|
|
156
|
+
mutate(draft: OpenclawConfig): T | void;
|
|
157
|
+
}): Promise<{ result?: T; [key: string]: any }>;
|
|
155
158
|
};
|
|
156
159
|
system: {
|
|
157
160
|
enqueueSystemEvent(event: any): void;
|
|
@@ -175,7 +178,7 @@ declare module "openclaw/plugin-sdk/channel-core" {
|
|
|
175
178
|
reply: {
|
|
176
179
|
dispatchReplyWithBufferedBlockDispatcher(ctx: any): Promise<any>;
|
|
177
180
|
resolveEffectiveMessagesConfig(cfg: OpenclawConfig, agentId?: string): any;
|
|
178
|
-
|
|
181
|
+
formatAgentEnvelope(ctx: any): string;
|
|
179
182
|
finalizeInboundContext(ctx: any): any;
|
|
180
183
|
resolveEnvelopeFormatOptions(cfg: OpenclawConfig): any;
|
|
181
184
|
[key: string]: any;
|