@emotion-machine/claw-messenger 0.1.12 → 0.1.14
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 +11 -2
- package/dist/channel.d.ts +3 -3
- package/dist/channel.js +39 -21
- package/dist/inbound.d.ts +37 -0
- package/dist/inbound.js +67 -0
- package/dist/index.js +1 -1
- package/dist/outbound/send.d.ts +13 -0
- package/dist/outbound/send.js +18 -12
- package/dist/ws/client.js +13 -0
- package/package.json +7 -2
- package/src/types/openclaw.d.ts +1 -1
package/README.md
CHANGED
|
@@ -57,8 +57,17 @@ The plugin registers two tools your agent can call:
|
|
|
57
57
|
2. Create an API key from the dashboard
|
|
58
58
|
3. Install the plugin: `openclaw plugins install @emotion-machine/claw-messenger`
|
|
59
59
|
4. Add the config above with your API key
|
|
60
|
-
5.
|
|
60
|
+
5. Restart OpenClaw so the channel connects
|
|
61
|
+
6. Check the listener before the first controlled send:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl https://claw-messenger.onrender.com/api/agent/readiness \
|
|
65
|
+
-H "Authorization: Bearer ${CLAW_API_KEY}"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Continue only after the response is `{"ready":true,"action":"send_controlled_test"}`. If the action is `connect_agent`, restart OpenClaw and check again. Keep the key in the authorization header, never in the URL.
|
|
69
|
+
7. Send one message to a phone you control, reply in the same thread, and confirm the reply reaches your agent
|
|
61
70
|
|
|
62
71
|
## License
|
|
63
72
|
|
|
64
|
-
UNLICENSED
|
|
73
|
+
UNLICENSED
|
package/dist/channel.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
2
2
|
import { type ClawMessengerConfig } from "./config.js";
|
|
3
3
|
import { WsClient } from "./ws/client.js";
|
|
4
|
+
import { type SendResult } from "./outbound/send.js";
|
|
4
5
|
interface ResolvedAccount {
|
|
5
6
|
accountId: string;
|
|
6
7
|
enabled: boolean;
|
|
@@ -18,8 +19,7 @@ export declare function getConnectionStatus(): {
|
|
|
18
19
|
};
|
|
19
20
|
export declare function createGroup(to: string[], text: string): Promise<{
|
|
20
21
|
ok: boolean;
|
|
21
|
-
|
|
22
|
-
chatId: string;
|
|
23
|
-
}>;
|
|
22
|
+
} & SendResult>;
|
|
24
23
|
export declare const clawMessengerPlugin: ChannelPlugin<ResolvedAccount>;
|
|
24
|
+
export declare function handleInboundMessage(data: Record<string, unknown>, accountId: string, account: ResolvedAccount, ctx: any): Promise<void>;
|
|
25
25
|
export {};
|
package/dist/channel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as pluginSdk from "openclaw/plugin-sdk";
|
|
2
2
|
import { ClawMessengerConfigSchema } from "./config.js";
|
|
3
|
+
import { isInboundMessageEvent, normalizeInboundMessage, validateInboundRoute, } from "./inbound.js";
|
|
3
4
|
import { getRuntime } from "./runtime.js";
|
|
4
5
|
import { WsClient } from "./ws/client.js";
|
|
5
6
|
import { normalizeDirectTarget, sendText, sendMedia, sendToGroup, sendGroupMedia, sendToNewGroup, } from "./outbound/send.js";
|
|
@@ -194,6 +195,7 @@ export const clawMessengerPlugin = {
|
|
|
194
195
|
"You can react to messages using iMessage tapbacks via the react action. Available: love (❤️), like (👍), dislike (👎), laugh (😂), emphasize (‼️), question (❓).",
|
|
195
196
|
"Use reactions naturally — the way a real person would in iMessage.",
|
|
196
197
|
"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
|
+
"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.",
|
|
197
199
|
],
|
|
198
200
|
},
|
|
199
201
|
// Legacy API (OpenClaw <2026.3.22): kept for backward compatibility
|
|
@@ -369,7 +371,7 @@ export const clawMessengerPlugin = {
|
|
|
369
371
|
ctx.log?.warn?.(`[${resolvedAccountId}] Disconnected from claw-messenger`);
|
|
370
372
|
},
|
|
371
373
|
onMessage: async (data) => {
|
|
372
|
-
if (data
|
|
374
|
+
if (isInboundMessageEvent(data)) {
|
|
373
375
|
// Track last message timestamp for sync on reconnect
|
|
374
376
|
lastMessageAt.set(resolvedAccountId, new Date().toISOString());
|
|
375
377
|
await handleInboundMessage(data, resolvedAccountId, account, ctx);
|
|
@@ -437,25 +439,38 @@ export const clawMessengerPlugin = {
|
|
|
437
439
|
// versions doesn't include it. Newer OpenClaw calls this; older versions ignore it.
|
|
438
440
|
clawMessengerPlugin.describeMessageTool = describeClawMessageTool;
|
|
439
441
|
// -- Inbound message handler --
|
|
440
|
-
async function handleInboundMessage(data, accountId, account, ctx) {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const chatId =
|
|
447
|
-
const participants = data.participants ?? [];
|
|
442
|
+
export async function handleInboundMessage(data, accountId, account, ctx) {
|
|
443
|
+
const normalized = normalizeInboundMessage(data);
|
|
444
|
+
if (!normalized.ok) {
|
|
445
|
+
ctx.log?.warn?.(`[${accountId}] Dropping inbound message: ${normalized.reason}`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const { from, text, messageId, attachments, isGroup, chatId, participants, } = normalized.message;
|
|
448
449
|
const runtime = getRuntime();
|
|
449
450
|
const cfg = runtime.config.loadConfig();
|
|
450
451
|
// Resolve routing — group by chatId, DM by sender phone
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
channel
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
:
|
|
458
|
-
|
|
452
|
+
let rawRoute;
|
|
453
|
+
try {
|
|
454
|
+
rawRoute = runtime.channel.routing.resolveAgentRoute({
|
|
455
|
+
cfg,
|
|
456
|
+
channel: "claw-messenger",
|
|
457
|
+
accountId,
|
|
458
|
+
peer: isGroup
|
|
459
|
+
? { kind: "group", id: chatId }
|
|
460
|
+
: { kind: "dm", id: from },
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
ctx.log?.warn?.(`[${accountId}] Dropping inbound message from ${from}: route resolution failed (${err})`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const routeValidation = validateInboundRoute(rawRoute);
|
|
468
|
+
if (!routeValidation.ok) {
|
|
469
|
+
ctx.log?.warn?.(`[${accountId}] Dropping inbound message from ${from}: ${routeValidation.reason}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const route = routeValidation.route;
|
|
473
|
+
const routeSessionKey = route.sessionKey;
|
|
459
474
|
// Download media
|
|
460
475
|
const allMedia = [];
|
|
461
476
|
for (const attachment of attachments) {
|
|
@@ -473,7 +488,10 @@ async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
473
488
|
agentId: route.agentId,
|
|
474
489
|
});
|
|
475
490
|
const envelopeOptions = runtime.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
476
|
-
const previousTimestamp = runtime.channel.session.readSessionUpdatedAt(
|
|
491
|
+
const previousTimestamp = runtime.channel.session.readSessionUpdatedAt({
|
|
492
|
+
storePath,
|
|
493
|
+
sessionKey: routeSessionKey,
|
|
494
|
+
});
|
|
477
495
|
const rawBody = text || (allMedia.length > 0 ? "<media:image>" : "");
|
|
478
496
|
if (!rawBody)
|
|
479
497
|
return;
|
|
@@ -493,8 +511,8 @@ async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
493
511
|
CommandBody: rawBody,
|
|
494
512
|
From: `claw-messenger:${from}`,
|
|
495
513
|
To: `claw-messenger:shared`,
|
|
496
|
-
SessionKey:
|
|
497
|
-
AccountId: route.accountId,
|
|
514
|
+
SessionKey: routeSessionKey,
|
|
515
|
+
AccountId: route.accountId || accountId,
|
|
498
516
|
ChatType: isGroup ? "group" : "direct",
|
|
499
517
|
ConversationLabel: isGroup ? chatId : from,
|
|
500
518
|
SenderId: from,
|
|
@@ -515,7 +533,7 @@ async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
515
533
|
});
|
|
516
534
|
void runtime.channel.session.recordSessionMetaFromInbound({
|
|
517
535
|
storePath,
|
|
518
|
-
sessionKey: ctxPayload.SessionKey ??
|
|
536
|
+
sessionKey: ctxPayload.SessionKey ?? routeSessionKey,
|
|
519
537
|
ctx: ctxPayload,
|
|
520
538
|
}).catch(() => { });
|
|
521
539
|
const ws = wsClients.get(accountId);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface InboundAttachment {
|
|
2
|
+
url: string;
|
|
3
|
+
mimeType?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface NormalizedInboundMessage {
|
|
6
|
+
from: string;
|
|
7
|
+
text: string;
|
|
8
|
+
messageId: string;
|
|
9
|
+
attachments: InboundAttachment[];
|
|
10
|
+
isGroup: boolean;
|
|
11
|
+
chatId: string;
|
|
12
|
+
participants: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedInboundRoute {
|
|
15
|
+
sessionKey: string;
|
|
16
|
+
accountId: string;
|
|
17
|
+
agentId?: string;
|
|
18
|
+
mainSessionKey?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export type InboundMessageValidation = {
|
|
22
|
+
ok: true;
|
|
23
|
+
message: NormalizedInboundMessage;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
reason: string;
|
|
27
|
+
};
|
|
28
|
+
export type InboundRouteValidation = {
|
|
29
|
+
ok: true;
|
|
30
|
+
route: ResolvedInboundRoute;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function isInboundMessageEvent(data: Record<string, unknown>): boolean;
|
|
36
|
+
export declare function normalizeInboundMessage(data: Record<string, unknown>): InboundMessageValidation;
|
|
37
|
+
export declare function validateInboundRoute(route: unknown): InboundRouteValidation;
|
package/dist/inbound.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function nonBlankString(value) {
|
|
2
|
+
if (typeof value !== "string")
|
|
3
|
+
return null;
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
return trimmed ? trimmed : null;
|
|
6
|
+
}
|
|
7
|
+
function normalizeAttachments(value) {
|
|
8
|
+
if (!Array.isArray(value))
|
|
9
|
+
return [];
|
|
10
|
+
return value.flatMap((attachment) => {
|
|
11
|
+
if (!attachment || typeof attachment !== "object")
|
|
12
|
+
return [];
|
|
13
|
+
const url = nonBlankString(attachment.url);
|
|
14
|
+
if (!url)
|
|
15
|
+
return [];
|
|
16
|
+
const mimeType = nonBlankString(attachment.mimeType) ?? undefined;
|
|
17
|
+
return [{ url, mimeType }];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function normalizeStringArray(value) {
|
|
21
|
+
if (!Array.isArray(value))
|
|
22
|
+
return [];
|
|
23
|
+
return value.map(nonBlankString).filter((item) => Boolean(item));
|
|
24
|
+
}
|
|
25
|
+
export function isInboundMessageEvent(data) {
|
|
26
|
+
return data.type === "message";
|
|
27
|
+
}
|
|
28
|
+
export function normalizeInboundMessage(data) {
|
|
29
|
+
const isGroup = data.isGroup === true;
|
|
30
|
+
const from = nonBlankString(data.from);
|
|
31
|
+
if (!from) {
|
|
32
|
+
return { ok: false, reason: "missing sender" };
|
|
33
|
+
}
|
|
34
|
+
const chatId = nonBlankString(data.chatId) ?? "";
|
|
35
|
+
if (isGroup && !chatId) {
|
|
36
|
+
return { ok: false, reason: "missing group chatId" };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
message: {
|
|
41
|
+
from,
|
|
42
|
+
text: nonBlankString(data.text) ?? "",
|
|
43
|
+
messageId: nonBlankString(data.messageId) ?? "",
|
|
44
|
+
attachments: normalizeAttachments(data.attachments),
|
|
45
|
+
isGroup,
|
|
46
|
+
chatId,
|
|
47
|
+
participants: normalizeStringArray(data.participants),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function validateInboundRoute(route) {
|
|
52
|
+
if (!route || typeof route !== "object") {
|
|
53
|
+
return { ok: false, reason: "missing route" };
|
|
54
|
+
}
|
|
55
|
+
const sessionKey = nonBlankString(route.sessionKey);
|
|
56
|
+
if (!sessionKey) {
|
|
57
|
+
return { ok: false, reason: "missing route sessionKey" };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
route: {
|
|
62
|
+
...route,
|
|
63
|
+
sessionKey,
|
|
64
|
+
accountId: nonBlankString(route.accountId) ?? "",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -99,7 +99,7 @@ const plugin = {
|
|
|
99
99
|
const ws = getWsClient(status.accountId);
|
|
100
100
|
const diagnostics = ws?.getDiagnostics() ?? { errors: [], connectionLog: [] };
|
|
101
101
|
const report = {
|
|
102
|
-
plugin_version: "0.1.
|
|
102
|
+
plugin_version: "0.1.14",
|
|
103
103
|
node_version: process.version,
|
|
104
104
|
connected: status.connected,
|
|
105
105
|
server_url: status.serverUrl,
|
package/dist/outbound/send.d.ts
CHANGED
|
@@ -2,6 +2,19 @@ import type { WsClient } from "../ws/client.js";
|
|
|
2
2
|
export interface SendResult {
|
|
3
3
|
messageId: string;
|
|
4
4
|
chatId: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
requestedService?: string;
|
|
7
|
+
selectedService?: string;
|
|
8
|
+
fallbackAllowed?: boolean;
|
|
9
|
+
fallbackReason?: string;
|
|
10
|
+
deliveryStage?: string;
|
|
11
|
+
errorCode?: string;
|
|
12
|
+
retryable?: boolean;
|
|
13
|
+
setupProof?: {
|
|
14
|
+
required?: boolean;
|
|
15
|
+
status?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
};
|
|
5
18
|
}
|
|
6
19
|
export declare function normalizeDirectTarget(target: string): string | null;
|
|
7
20
|
export declare function sendText(ws: WsClient, to: string, text: string, service?: string): Promise<SendResult>;
|
package/dist/outbound/send.js
CHANGED
|
@@ -60,10 +60,7 @@ export async function sendMessage(ws, to, parts, service) {
|
|
|
60
60
|
...(service ? { service } : {}),
|
|
61
61
|
});
|
|
62
62
|
if (resp.ok) {
|
|
63
|
-
return
|
|
64
|
-
messageId: resp.messageId ?? "",
|
|
65
|
-
chatId: resp.chatId ?? "",
|
|
66
|
-
};
|
|
63
|
+
return sendResultFromResponse(resp);
|
|
67
64
|
}
|
|
68
65
|
throw new Error(resp.error ?? "Send failed");
|
|
69
66
|
}
|
|
@@ -86,10 +83,7 @@ async function sendGroupMessage(ws, chatId, parts, service) {
|
|
|
86
83
|
...(service ? { service } : {}),
|
|
87
84
|
});
|
|
88
85
|
if (resp.ok) {
|
|
89
|
-
return
|
|
90
|
-
messageId: resp.messageId ?? "",
|
|
91
|
-
chatId,
|
|
92
|
-
};
|
|
86
|
+
return sendResultFromResponse(resp, chatId);
|
|
93
87
|
}
|
|
94
88
|
throw new Error(resp.error ?? "Group send failed");
|
|
95
89
|
}
|
|
@@ -107,10 +101,22 @@ export async function sendToNewGroup(ws, to, text, service) {
|
|
|
107
101
|
...(service ? { service } : {}),
|
|
108
102
|
});
|
|
109
103
|
if (resp.ok) {
|
|
110
|
-
return
|
|
111
|
-
messageId: resp.messageId ?? "",
|
|
112
|
-
chatId: resp.chatId ?? "",
|
|
113
|
-
};
|
|
104
|
+
return sendResultFromResponse(resp);
|
|
114
105
|
}
|
|
115
106
|
throw new Error(resp.error ?? "Group creation failed");
|
|
116
107
|
}
|
|
108
|
+
function sendResultFromResponse(resp, fallbackChatId = "") {
|
|
109
|
+
return {
|
|
110
|
+
messageId: resp.messageId ?? "",
|
|
111
|
+
chatId: resp.chatId ?? fallbackChatId,
|
|
112
|
+
status: resp.status,
|
|
113
|
+
requestedService: resp.requestedService,
|
|
114
|
+
selectedService: resp.selectedService,
|
|
115
|
+
fallbackAllowed: resp.fallbackAllowed,
|
|
116
|
+
fallbackReason: resp.fallbackReason,
|
|
117
|
+
deliveryStage: resp.deliveryStage,
|
|
118
|
+
errorCode: resp.errorCode,
|
|
119
|
+
retryable: resp.retryable,
|
|
120
|
+
setupProof: resp.setupProof,
|
|
121
|
+
};
|
|
122
|
+
}
|
package/dist/ws/client.js
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
* against HTTP/2 servers like Render.com).
|
|
8
8
|
*/
|
|
9
9
|
import WebSocket from "ws";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
// Client fingerprint sent on the WS handshake so the server can tell which
|
|
12
|
+
// integration (and version) is connecting. Version is read at runtime from
|
|
13
|
+
// package.json; falls back to "unknown" in bundled environments without it.
|
|
14
|
+
const CLIENT_ID = `openclaw-plugin/${(() => {
|
|
15
|
+
try {
|
|
16
|
+
return createRequire(import.meta.url)("../../package.json").version;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
})()}`;
|
|
10
22
|
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
11
23
|
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
12
24
|
const MAX_DIAGNOSTIC_ENTRIES = 50;
|
|
@@ -119,6 +131,7 @@ export class WsClient {
|
|
|
119
131
|
url.pathname = url.pathname.replace(/\/$/, "") + "/ws";
|
|
120
132
|
}
|
|
121
133
|
url.searchParams.set("key", this.opts.apiKey);
|
|
134
|
+
url.searchParams.set("client", CLIENT_ID);
|
|
122
135
|
this.opts.log?.(`Connecting to ${url.origin}${url.pathname}...`);
|
|
123
136
|
// Close the previous socket if it's still open, so we don't leak
|
|
124
137
|
// connections on the server. Set this.ws to null first so the old
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emotion-machine/claw-messenger",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "iMessage, RCS & SMS channel plugin for OpenClaw
|
|
3
|
+
"version": "0.1.14",
|
|
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"openclaw",
|
|
19
19
|
"openclaw-plugin",
|
|
20
20
|
"imessage",
|
|
21
|
+
"imessage-api",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"no-mac",
|
|
24
|
+
"claw-messenger",
|
|
21
25
|
"rcs",
|
|
22
26
|
"sms",
|
|
23
27
|
"emotion-machine",
|
|
@@ -33,6 +37,7 @@
|
|
|
33
37
|
"build": "tsc",
|
|
34
38
|
"dev": "tsc --watch",
|
|
35
39
|
"clean": "rm -rf dist",
|
|
40
|
+
"test": "npm run build && node --loader ./tests/openclaw-loader.mjs --test tests/*.test.mjs",
|
|
36
41
|
"prepare": "test -d dist || npm run build",
|
|
37
42
|
"prepublishOnly": "npm run clean && npm run build"
|
|
38
43
|
},
|
package/src/types/openclaw.d.ts
CHANGED
|
@@ -199,7 +199,7 @@ declare module "openclaw/plugin-sdk" {
|
|
|
199
199
|
};
|
|
200
200
|
session: {
|
|
201
201
|
resolveStorePath(store: any, opts?: { agentId?: string }): string;
|
|
202
|
-
readSessionUpdatedAt(
|
|
202
|
+
readSessionUpdatedAt(ctx: { storePath: string; sessionKey: string }): number | null;
|
|
203
203
|
recordSessionMetaFromInbound(ctx: any): Promise<void>;
|
|
204
204
|
recordInboundSession(ctx: any): void;
|
|
205
205
|
updateLastRoute(ctx: any): void;
|