@emotion-machine/claw-messenger 0.1.12 → 0.1.13
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/dist/channel.d.ts +2 -3
- package/dist/channel.js +35 -20
- package/dist/inbound.d.ts +37 -0
- package/dist/inbound.js +67 -0
- 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 +2 -1
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,6 @@ 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>;
|
|
25
24
|
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);
|
|
@@ -438,24 +440,37 @@ export const clawMessengerPlugin = {
|
|
|
438
440
|
clawMessengerPlugin.describeMessageTool = describeClawMessageTool;
|
|
439
441
|
// -- Inbound message handler --
|
|
440
442
|
async function handleInboundMessage(data, accountId, account, ctx) {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const chatId =
|
|
447
|
-
const participants = data.participants ?? [];
|
|
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,7 @@ 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(storePath,
|
|
491
|
+
const previousTimestamp = runtime.channel.session.readSessionUpdatedAt(storePath, routeSessionKey);
|
|
477
492
|
const rawBody = text || (allMedia.length > 0 ? "<media:image>" : "");
|
|
478
493
|
if (!rawBody)
|
|
479
494
|
return;
|
|
@@ -493,8 +508,8 @@ async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
493
508
|
CommandBody: rawBody,
|
|
494
509
|
From: `claw-messenger:${from}`,
|
|
495
510
|
To: `claw-messenger:shared`,
|
|
496
|
-
SessionKey:
|
|
497
|
-
AccountId: route.accountId,
|
|
511
|
+
SessionKey: routeSessionKey,
|
|
512
|
+
AccountId: route.accountId || accountId,
|
|
498
513
|
ChatType: isGroup ? "group" : "direct",
|
|
499
514
|
ConversationLabel: isGroup ? chatId : from,
|
|
500
515
|
SenderId: from,
|
|
@@ -515,7 +530,7 @@ async function handleInboundMessage(data, accountId, account, ctx) {
|
|
|
515
530
|
});
|
|
516
531
|
void runtime.channel.session.recordSessionMetaFromInbound({
|
|
517
532
|
storePath,
|
|
518
|
-
sessionKey: ctxPayload.SessionKey ??
|
|
533
|
+
sessionKey: ctxPayload.SessionKey ?? routeSessionKey,
|
|
519
534
|
ctx: ctxPayload,
|
|
520
535
|
}).catch(() => { });
|
|
521
536
|
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/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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emotion-machine/claw-messenger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "iMessage, RCS & SMS channel plugin for OpenClaw — no phone or Mac Mini required",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"build": "tsc",
|
|
34
34
|
"dev": "tsc --watch",
|
|
35
35
|
"clean": "rm -rf dist",
|
|
36
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
36
37
|
"prepare": "test -d dist || npm run build",
|
|
37
38
|
"prepublishOnly": "npm run clean && npm run build"
|
|
38
39
|
},
|