@emotion-machine/claw-messenger 0.1.2 → 0.1.5
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 +2 -2
- package/dist/channel.d.ts +5 -0
- package/dist/channel.js +28 -0
- package/dist/index.js +27 -1
- package/dist/ws/client.d.ts +4 -0
- package/dist/ws/client.js +28 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ openclaw plugins install @emotion-machine/claw-messenger
|
|
|
10
10
|
|
|
11
11
|
## Configuration
|
|
12
12
|
|
|
13
|
-
After installing, add to your
|
|
13
|
+
After installing, add to your config under `channels`:
|
|
14
14
|
|
|
15
15
|
```json5
|
|
16
16
|
{
|
|
@@ -61,4 +61,4 @@ The plugin registers two tools your agent can call:
|
|
|
61
61
|
|
|
62
62
|
## License
|
|
63
63
|
|
|
64
|
-
UNLICENSED
|
|
64
|
+
UNLICENSED
|
package/dist/channel.d.ts
CHANGED
|
@@ -14,5 +14,10 @@ export declare function getConnectionStatus(): {
|
|
|
14
14
|
preferredService: string;
|
|
15
15
|
accountId: string;
|
|
16
16
|
};
|
|
17
|
+
export declare function createGroup(to: string[], text: string): Promise<{
|
|
18
|
+
ok: boolean;
|
|
19
|
+
messageId: string;
|
|
20
|
+
chatId: string;
|
|
21
|
+
}>;
|
|
17
22
|
export declare const clawMessengerPlugin: ChannelPlugin<ResolvedAccount>;
|
|
18
23
|
export {};
|
package/dist/channel.js
CHANGED
|
@@ -41,6 +41,19 @@ export function getConnectionStatus() {
|
|
|
41
41
|
accountId: account.accountId,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
export async function createGroup(to, text) {
|
|
45
|
+
const runtime = getRuntime();
|
|
46
|
+
const cfg = runtime.config.loadConfig();
|
|
47
|
+
const account = resolveAccount(cfg);
|
|
48
|
+
const ws = wsClients.get(account.accountId);
|
|
49
|
+
if (!ws)
|
|
50
|
+
throw new Error("Not connected to claw-messenger");
|
|
51
|
+
if (!Array.isArray(to) || to.length < 2) {
|
|
52
|
+
throw new Error("Group requires at least 2 recipients");
|
|
53
|
+
}
|
|
54
|
+
const result = await sendToNewGroup(ws, to, text, account.preferredService);
|
|
55
|
+
return { ok: true, ...result };
|
|
56
|
+
}
|
|
44
57
|
// -- Channel Plugin --
|
|
45
58
|
const meta = {
|
|
46
59
|
label: "Claw Messenger",
|
|
@@ -143,8 +156,10 @@ export const clawMessengerPlugin = {
|
|
|
143
156
|
messageToolHints: () => [
|
|
144
157
|
"You can react to messages using iMessage tapbacks via the react action. Available: love (❤️), like (👍), dislike (👎), laugh (😂), emphasize (‼️), question (❓).",
|
|
145
158
|
"Use reactions naturally — the way a real person would in iMessage.",
|
|
159
|
+
"If you encounter connection issues, the server is WebSocket-only — do NOT test with HTTP. Full API docs and troubleshooting: https://clawmessenger.com/docs",
|
|
146
160
|
],
|
|
147
161
|
},
|
|
162
|
+
// Legacy API (OpenClaw <2026.3.22): kept for backward compatibility
|
|
148
163
|
actions: {
|
|
149
164
|
listActions: () => ["send", "react"],
|
|
150
165
|
handleAction: async ({ action, params, cfg }) => {
|
|
@@ -373,6 +388,19 @@ export const clawMessengerPlugin = {
|
|
|
373
388
|
},
|
|
374
389
|
},
|
|
375
390
|
};
|
|
391
|
+
// OpenClaw 2026.3.22+ message-action-discovery API.
|
|
392
|
+
// Added as a runtime property because the ChannelPlugin type from older SDK
|
|
393
|
+
// versions doesn't include it. Newer OpenClaw calls this; older versions ignore it.
|
|
394
|
+
clawMessengerPlugin.describeMessageTool = (cfg) => {
|
|
395
|
+
const account = resolveAccount(cfg);
|
|
396
|
+
const ws = wsClients.get(account.accountId);
|
|
397
|
+
const connected = Boolean(ws?.connected);
|
|
398
|
+
return {
|
|
399
|
+
actions: connected ? ["send", "react"] : [],
|
|
400
|
+
capabilities: [],
|
|
401
|
+
schema: null,
|
|
402
|
+
};
|
|
403
|
+
};
|
|
376
404
|
// -- Inbound message handler --
|
|
377
405
|
async function handleInboundMessage(data, accountId, account, ctx) {
|
|
378
406
|
const from = data.from;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { clawMessengerPlugin, getConnectionStatus } from "./channel.js";
|
|
3
|
+
import { clawMessengerPlugin, getConnectionStatus, createGroup } from "./channel.js";
|
|
4
4
|
import { setRuntime, getRuntime } from "./runtime.js";
|
|
5
5
|
const VALID_SERVICES = ["iMessage", "RCS", "SMS"];
|
|
6
6
|
const plugin = {
|
|
@@ -56,6 +56,32 @@ const plugin = {
|
|
|
56
56
|
};
|
|
57
57
|
},
|
|
58
58
|
});
|
|
59
|
+
api.registerTool({
|
|
60
|
+
name: "claw_messenger_create_group",
|
|
61
|
+
label: "Create iMessage/RCS/SMS Group",
|
|
62
|
+
description: "Create a new group chat and send the first message. Pass an array of E.164 phone numbers (at least 2) and the initial message text. Returns the chatId to use for future messages to this group.",
|
|
63
|
+
parameters: Type.Object({
|
|
64
|
+
to: Type.Array(Type.String(), {
|
|
65
|
+
description: "Array of E.164 phone numbers (e.g. [\"+12125551234\", \"+14155559876\"])",
|
|
66
|
+
minItems: 2,
|
|
67
|
+
}),
|
|
68
|
+
text: Type.String({ description: "The initial message to send to the group" }),
|
|
69
|
+
}),
|
|
70
|
+
async execute(_toolCallId, params) {
|
|
71
|
+
const { to, text } = params;
|
|
72
|
+
try {
|
|
73
|
+
const result = await createGroup(to, text);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: err.message }) }],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
59
85
|
// -- Auto-Reply Commands --
|
|
60
86
|
api.registerCommand({
|
|
61
87
|
name: "cm-status",
|
package/dist/ws/client.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket client with reconnection and request/response correlation.
|
|
3
3
|
* Connects to claw-messenger server with API key auth.
|
|
4
|
+
*
|
|
5
|
+
* Uses the `ws` npm package instead of the native WebSocket to avoid
|
|
6
|
+
* Node 22+ HTTP/2 compatibility issues (native WebSocket silently fails
|
|
7
|
+
* against HTTP/2 servers like Render.com).
|
|
4
8
|
*/
|
|
5
9
|
type MessageHandler = (message: Record<string, unknown>) => void;
|
|
6
10
|
export interface WsClientOptions {
|
package/dist/ws/client.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket client with reconnection and request/response correlation.
|
|
3
3
|
* Connects to claw-messenger server with API key auth.
|
|
4
|
+
*
|
|
5
|
+
* Uses the `ws` npm package instead of the native WebSocket to avoid
|
|
6
|
+
* Node 22+ HTTP/2 compatibility issues (native WebSocket silently fails
|
|
7
|
+
* against HTTP/2 servers like Render.com).
|
|
4
8
|
*/
|
|
9
|
+
import WebSocket from "ws";
|
|
5
10
|
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
11
|
+
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
6
12
|
const PING_INTERVAL_MS = 30_000;
|
|
7
13
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
8
14
|
const PONG_TIMEOUT_MS = 10_000;
|
|
15
|
+
/** Close codes that indicate the client should NOT reconnect. */
|
|
16
|
+
const NO_RECONNECT_CODES = new Set([
|
|
17
|
+
1008, // Policy violation — connection limit reached (evicted)
|
|
18
|
+
4003, // Subscription inactive (cancelled/expired)
|
|
19
|
+
]);
|
|
9
20
|
export class WsClient {
|
|
10
21
|
ws = null;
|
|
11
22
|
opts;
|
|
@@ -87,7 +98,9 @@ export class WsClient {
|
|
|
87
98
|
const ws = new WebSocket(url.toString());
|
|
88
99
|
this.ws = ws;
|
|
89
100
|
ws.onopen = () => {
|
|
90
|
-
|
|
101
|
+
// Don't reset reconnectAttempt here — reset on first successful
|
|
102
|
+
// message exchange instead, so eviction loops can't keep the
|
|
103
|
+
// counter at 0 and bypass backoff.
|
|
91
104
|
this.lastPongAt = Date.now();
|
|
92
105
|
this.opts.log?.("Connected");
|
|
93
106
|
// Reject stale pending requests from previous connection
|
|
@@ -108,15 +121,27 @@ export class WsClient {
|
|
|
108
121
|
this.opts.log?.(`Disconnected (code=${event.code})`);
|
|
109
122
|
this._clearTimers();
|
|
110
123
|
this.opts.onDisconnect?.();
|
|
111
|
-
if (
|
|
112
|
-
|
|
124
|
+
if (this.stopped)
|
|
125
|
+
return;
|
|
126
|
+
if (NO_RECONNECT_CODES.has(event.code)) {
|
|
127
|
+
this.opts.log?.(`Not reconnecting (code=${event.code}: ${event.reason})`);
|
|
128
|
+
this.stopped = true;
|
|
129
|
+
return;
|
|
113
130
|
}
|
|
131
|
+
if (this.reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
132
|
+
this.opts.log?.(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached — giving up`);
|
|
133
|
+
this.stopped = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this._scheduleReconnect();
|
|
114
137
|
};
|
|
115
138
|
ws.onerror = (event) => {
|
|
116
139
|
this.opts.log?.(`WebSocket error`);
|
|
117
140
|
};
|
|
118
141
|
}
|
|
119
142
|
_handleMessage(data) {
|
|
143
|
+
// Connection is stable — reset backoff counter
|
|
144
|
+
this.reconnectAttempt = 0;
|
|
120
145
|
// Check if this is a response to a pending request
|
|
121
146
|
const id = data.id;
|
|
122
147
|
if (id && this.pendingRequests.has(id)) {
|
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.5",
|
|
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",
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@sinclair/typebox": "^0.34.48",
|
|
53
|
+
"ws": "^8.18.0",
|
|
53
54
|
"zod": "^4.3.6"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^20.14.0",
|
|
58
|
+
"@types/ws": "^8.5.0",
|
|
57
59
|
"typescript": "^5.5.0"
|
|
58
60
|
},
|
|
59
61
|
"engines": {
|