@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 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** — love, like, dislike, laugh, emphasize, question (tapback)
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.loadConfig();
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.loadConfig();
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.loadConfig();
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 using iMessage tapbacks via the react action. Available: love (❤️), like (👍), dislike (👎), laugh (😂), emphasize (‼️), question (❓).",
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 reactionType = emojiToReaction(emoji) ?? "love";
217
- ws.send({
218
- type: "reaction",
219
- messageId,
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: reactionType }) }],
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.loadConfig();
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.formatInboundEnvelope({
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
- const cfg = runtime.config.loadConfig();
43
- const updated = {
44
- ...cfg,
45
- channels: {
46
- ...cfg.channels,
47
- "claw-messenger": {
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.15",
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.loadConfig();
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
- const cfg = runtime.config.loadConfig();
181
- const updated = {
182
- ...cfg,
183
- channels: {
184
- ...cfg.channels,
185
- "claw-messenger": {
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.15",
4
- "description": "iMessage, RCS & SMS channel plugin for OpenClaw agents, powered by Claw Messenger, the Mac-free iMessage API for AI agents. Send and receive real texts from Linux, Docker, Windows, or any cloud. No phone or Mac required.",
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",
@@ -150,8 +150,11 @@ declare module "openclaw/plugin-sdk/channel-core" {
150
150
  export interface PluginRuntime {
151
151
  version: string;
152
152
  config: {
153
- loadConfig(): OpenclawConfig;
154
- writeConfigFile(cfg: OpenclawConfig): Promise<void>;
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
- formatInboundEnvelope(ctx: any): string;
181
+ formatAgentEnvelope(ctx: any): string;
179
182
  finalizeInboundContext(ctx: any): any;
180
183
  resolveEnvelopeFormatOptions(cfg: OpenclawConfig): any;
181
184
  [key: string]: any;