@emotion-machine/claw-messenger 0.1.0
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 +68 -0
- package/dist/channel.d.ts +22 -0
- package/dist/channel.js +505 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +17 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +109 -0
- package/dist/outbound/send.d.ts +15 -0
- package/dist/outbound/send.js +70 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +10 -0
- package/dist/ws/client.d.ts +48 -0
- package/dist/ws/client.js +208 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +63 -0
- package/src/types/openclaw.d.ts +311 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { clawMessengerPlugin, getConnectionStatus } from "./channel.js";
|
|
4
|
+
import { setRuntime, getRuntime } from "./runtime.js";
|
|
5
|
+
const VALID_SERVICES = ["iMessage", "RCS", "SMS"];
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "claw-messenger",
|
|
8
|
+
name: "Claw Messenger",
|
|
9
|
+
description: "iMessage, RCS & SMS without a Mac or Linq account — via shared claw-messenger proxy",
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api) {
|
|
12
|
+
setRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: clawMessengerPlugin });
|
|
14
|
+
// -- Agent Tools --
|
|
15
|
+
api.registerTool({
|
|
16
|
+
name: "claw_messenger_status",
|
|
17
|
+
label: "Claw Messenger Status",
|
|
18
|
+
description: "Check claw-messenger connection status and account info",
|
|
19
|
+
parameters: Type.Object({}),
|
|
20
|
+
async execute() {
|
|
21
|
+
const status = getConnectionStatus();
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
api.registerTool({
|
|
28
|
+
name: "claw_messenger_switch_service",
|
|
29
|
+
label: "Switch Messaging Service",
|
|
30
|
+
description: "Switch the preferred messaging service for future outbound messages (iMessage, RCS, or SMS)",
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
service: Type.Union([Type.Literal("iMessage"), Type.Literal("RCS"), Type.Literal("SMS")], { description: "The messaging service to switch to" }),
|
|
33
|
+
}),
|
|
34
|
+
async execute(_toolCallId, params) {
|
|
35
|
+
const { service } = params;
|
|
36
|
+
if (!VALID_SERVICES.includes(service)) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Invalid service "${service}". Must be one of: ${VALID_SERVICES.join(", ")}` }) }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
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"] ?? {}),
|
|
49
|
+
preferredService: service,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
await runtime.config.writeConfigFile(updated);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, preferredService: service }) }],
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
// -- Auto-Reply Commands --
|
|
60
|
+
api.registerCommand({
|
|
61
|
+
name: "cm-status",
|
|
62
|
+
label: "Claw Messenger Status",
|
|
63
|
+
description: "Show claw-messenger connection state, server URL, and preferred service",
|
|
64
|
+
acceptsArgs: false,
|
|
65
|
+
async handler() {
|
|
66
|
+
const s = getConnectionStatus();
|
|
67
|
+
return {
|
|
68
|
+
text: [
|
|
69
|
+
`Connected: ${s.connected ? "yes" : "no"}`,
|
|
70
|
+
`Server: ${s.serverUrl}`,
|
|
71
|
+
`Preferred Service: ${s.preferredService}`,
|
|
72
|
+
`Account: ${s.accountId}`,
|
|
73
|
+
].join("\n"),
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
api.registerCommand({
|
|
78
|
+
name: "cm-switch",
|
|
79
|
+
label: "Switch Messaging Service",
|
|
80
|
+
description: "Switch the preferred messaging service (iMessage, RCS, SMS)",
|
|
81
|
+
acceptsArgs: true,
|
|
82
|
+
async handler(ctx) {
|
|
83
|
+
const arg = ctx.args?.trim();
|
|
84
|
+
if (!arg) {
|
|
85
|
+
return { text: `Usage: /cm-switch <service>\nValid services: ${VALID_SERVICES.join(", ")}` };
|
|
86
|
+
}
|
|
87
|
+
const match = VALID_SERVICES.find((s) => s.toLowerCase() === arg.toLowerCase());
|
|
88
|
+
if (!match) {
|
|
89
|
+
return { text: `Invalid service "${arg}". Must be one of: ${VALID_SERVICES.join(", ")}` };
|
|
90
|
+
}
|
|
91
|
+
const runtime = getRuntime();
|
|
92
|
+
const cfg = runtime.config.loadConfig();
|
|
93
|
+
const updated = {
|
|
94
|
+
...cfg,
|
|
95
|
+
channels: {
|
|
96
|
+
...cfg.channels,
|
|
97
|
+
"claw-messenger": {
|
|
98
|
+
...(cfg.channels?.["claw-messenger"] ?? {}),
|
|
99
|
+
preferredService: match,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
await runtime.config.writeConfigFile(updated);
|
|
104
|
+
return { text: `Preferred service switched to ${match}` };
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
export default plugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send messages via WebSocket to claw-messenger -> Linq -> iMessage.
|
|
3
|
+
* Replaces direct Linq API calls from channel-linq/src/outbound/send.ts.
|
|
4
|
+
*/
|
|
5
|
+
import type { WsClient } from "../ws/client.js";
|
|
6
|
+
export interface SendResult {
|
|
7
|
+
messageId: string;
|
|
8
|
+
chatId: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function sendText(ws: WsClient, to: string, text: string, service?: string): Promise<SendResult>;
|
|
11
|
+
export declare function sendMedia(ws: WsClient, to: string, mediaUrl: string, text?: string, service?: string): Promise<SendResult>;
|
|
12
|
+
export declare function sendMessage(ws: WsClient, to: string, parts: Array<Record<string, unknown>>, service?: string): Promise<SendResult>;
|
|
13
|
+
export declare function sendToGroup(ws: WsClient, chatId: string, text: string, service?: string): Promise<SendResult>;
|
|
14
|
+
export declare function sendGroupMedia(ws: WsClient, chatId: string, mediaUrl: string, text?: string, service?: string): Promise<SendResult>;
|
|
15
|
+
export declare function sendToNewGroup(ws: WsClient, to: string[], text: string, service?: string): Promise<SendResult>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send messages via WebSocket to claw-messenger -> Linq -> iMessage.
|
|
3
|
+
* Replaces direct Linq API calls from channel-linq/src/outbound/send.ts.
|
|
4
|
+
*/
|
|
5
|
+
export async function sendText(ws, to, text, service) {
|
|
6
|
+
return sendMessage(ws, to, [{ type: "text", value: text }], service);
|
|
7
|
+
}
|
|
8
|
+
export async function sendMedia(ws, to, mediaUrl, text, service) {
|
|
9
|
+
const parts = [];
|
|
10
|
+
if (text)
|
|
11
|
+
parts.push({ type: "text", value: text });
|
|
12
|
+
parts.push({ type: "media", url: mediaUrl });
|
|
13
|
+
return sendMessage(ws, to, parts, service);
|
|
14
|
+
}
|
|
15
|
+
export async function sendMessage(ws, to, parts, service) {
|
|
16
|
+
const resp = await ws.request({
|
|
17
|
+
type: "send",
|
|
18
|
+
to,
|
|
19
|
+
parts,
|
|
20
|
+
...(service ? { service } : {}),
|
|
21
|
+
});
|
|
22
|
+
if (resp.ok) {
|
|
23
|
+
return {
|
|
24
|
+
messageId: resp.messageId ?? "",
|
|
25
|
+
chatId: "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
throw new Error(resp.error ?? "Send failed");
|
|
29
|
+
}
|
|
30
|
+
// -- Group send functions --
|
|
31
|
+
export async function sendToGroup(ws, chatId, text, service) {
|
|
32
|
+
return sendGroupMessage(ws, chatId, [{ type: "text", value: text }], service);
|
|
33
|
+
}
|
|
34
|
+
export async function sendGroupMedia(ws, chatId, mediaUrl, text, service) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
if (text)
|
|
37
|
+
parts.push({ type: "text", value: text });
|
|
38
|
+
parts.push({ type: "media", url: mediaUrl });
|
|
39
|
+
return sendGroupMessage(ws, chatId, parts, service);
|
|
40
|
+
}
|
|
41
|
+
async function sendGroupMessage(ws, chatId, parts, service) {
|
|
42
|
+
const resp = await ws.request({
|
|
43
|
+
type: "send",
|
|
44
|
+
chatId,
|
|
45
|
+
parts,
|
|
46
|
+
...(service ? { service } : {}),
|
|
47
|
+
});
|
|
48
|
+
if (resp.ok) {
|
|
49
|
+
return {
|
|
50
|
+
messageId: resp.messageId ?? "",
|
|
51
|
+
chatId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
throw new Error(resp.error ?? "Group send failed");
|
|
55
|
+
}
|
|
56
|
+
export async function sendToNewGroup(ws, to, text, service) {
|
|
57
|
+
const resp = await ws.request({
|
|
58
|
+
type: "send",
|
|
59
|
+
to,
|
|
60
|
+
parts: [{ type: "text", value: text }],
|
|
61
|
+
...(service ? { service } : {}),
|
|
62
|
+
});
|
|
63
|
+
if (resp.ok) {
|
|
64
|
+
return {
|
|
65
|
+
messageId: resp.messageId ?? "",
|
|
66
|
+
chatId: resp.chatId ?? "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
throw new Error(resp.error ?? "Group creation failed");
|
|
70
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client with reconnection and request/response correlation.
|
|
3
|
+
* Connects to claw-messenger server with API key auth.
|
|
4
|
+
*/
|
|
5
|
+
type MessageHandler = (message: Record<string, unknown>) => void;
|
|
6
|
+
export interface WsClientOptions {
|
|
7
|
+
serverUrl: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
onMessage: MessageHandler;
|
|
10
|
+
onConnect?: () => void;
|
|
11
|
+
onDisconnect?: () => void;
|
|
12
|
+
log?: (msg: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class WsClient {
|
|
15
|
+
private ws;
|
|
16
|
+
private opts;
|
|
17
|
+
private reconnectAttempt;
|
|
18
|
+
private reconnectTimer;
|
|
19
|
+
private pingTimer;
|
|
20
|
+
private pongTimeoutTimer;
|
|
21
|
+
private pendingRequests;
|
|
22
|
+
private stopped;
|
|
23
|
+
private correlationCounter;
|
|
24
|
+
private lastPongAt;
|
|
25
|
+
constructor(opts: WsClientOptions);
|
|
26
|
+
connect(): void;
|
|
27
|
+
stop(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Send a message and wait for a correlated response (e.g., send -> send.result).
|
|
30
|
+
*/
|
|
31
|
+
request(message: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
32
|
+
/**
|
|
33
|
+
* Send a message without waiting for a response.
|
|
34
|
+
*/
|
|
35
|
+
send(message: Record<string, unknown>): void;
|
|
36
|
+
get connected(): boolean;
|
|
37
|
+
private _nextId;
|
|
38
|
+
private _connect;
|
|
39
|
+
private _handleMessage;
|
|
40
|
+
private _send;
|
|
41
|
+
private _startPing;
|
|
42
|
+
private _schedulePongTimeout;
|
|
43
|
+
private _clearPongTimeout;
|
|
44
|
+
private _rejectPendingRequests;
|
|
45
|
+
private _scheduleReconnect;
|
|
46
|
+
private _clearTimers;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client with reconnection and request/response correlation.
|
|
3
|
+
* Connects to claw-messenger server with API key auth.
|
|
4
|
+
*/
|
|
5
|
+
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
6
|
+
const PING_INTERVAL_MS = 30_000;
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
const PONG_TIMEOUT_MS = 10_000;
|
|
9
|
+
export class WsClient {
|
|
10
|
+
ws = null;
|
|
11
|
+
opts;
|
|
12
|
+
reconnectAttempt = 0;
|
|
13
|
+
reconnectTimer = null;
|
|
14
|
+
pingTimer = null;
|
|
15
|
+
pongTimeoutTimer = null;
|
|
16
|
+
pendingRequests = new Map();
|
|
17
|
+
stopped = false;
|
|
18
|
+
correlationCounter = 0;
|
|
19
|
+
lastPongAt = 0;
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
}
|
|
23
|
+
connect() {
|
|
24
|
+
this.stopped = false;
|
|
25
|
+
this._connect();
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
this.stopped = true;
|
|
29
|
+
this._clearTimers();
|
|
30
|
+
if (this.ws) {
|
|
31
|
+
try {
|
|
32
|
+
this.ws.close(1000, "Client stopping");
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
this.ws = null;
|
|
36
|
+
}
|
|
37
|
+
// Reject all pending requests
|
|
38
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
39
|
+
pending.reject(new Error("Client stopped"));
|
|
40
|
+
clearTimeout(pending.timer);
|
|
41
|
+
}
|
|
42
|
+
this.pendingRequests.clear();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Send a message and wait for a correlated response (e.g., send -> send.result).
|
|
46
|
+
*/
|
|
47
|
+
async request(message) {
|
|
48
|
+
const id = message.id ?? this._nextId();
|
|
49
|
+
message.id = id;
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
this.pendingRequests.delete(id);
|
|
53
|
+
reject(new Error(`Request ${id} timed out`));
|
|
54
|
+
}, REQUEST_TIMEOUT_MS);
|
|
55
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
56
|
+
this._send(message);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Send a message without waiting for a response.
|
|
61
|
+
*/
|
|
62
|
+
send(message) {
|
|
63
|
+
this._send(message);
|
|
64
|
+
}
|
|
65
|
+
get connected() {
|
|
66
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
67
|
+
}
|
|
68
|
+
// -- Internal --
|
|
69
|
+
_nextId() {
|
|
70
|
+
return `corr-${++this.correlationCounter}-${Date.now()}`;
|
|
71
|
+
}
|
|
72
|
+
_connect() {
|
|
73
|
+
if (this.stopped)
|
|
74
|
+
return;
|
|
75
|
+
const url = new URL(this.opts.serverUrl);
|
|
76
|
+
// Ensure ws/wss scheme
|
|
77
|
+
if (url.protocol === "https:")
|
|
78
|
+
url.protocol = "wss:";
|
|
79
|
+
if (url.protocol === "http:")
|
|
80
|
+
url.protocol = "ws:";
|
|
81
|
+
// Add /ws path if not already there
|
|
82
|
+
if (!url.pathname.endsWith("/ws")) {
|
|
83
|
+
url.pathname = url.pathname.replace(/\/$/, "") + "/ws";
|
|
84
|
+
}
|
|
85
|
+
url.searchParams.set("key", this.opts.apiKey);
|
|
86
|
+
this.opts.log?.(`Connecting to ${url.origin}${url.pathname}...`);
|
|
87
|
+
const ws = new WebSocket(url.toString());
|
|
88
|
+
this.ws = ws;
|
|
89
|
+
ws.onopen = () => {
|
|
90
|
+
this.reconnectAttempt = 0;
|
|
91
|
+
this.lastPongAt = Date.now();
|
|
92
|
+
this.opts.log?.("Connected");
|
|
93
|
+
// Reject stale pending requests from previous connection
|
|
94
|
+
this._rejectPendingRequests("Connection reset");
|
|
95
|
+
this.opts.onConnect?.();
|
|
96
|
+
this._startPing();
|
|
97
|
+
};
|
|
98
|
+
ws.onmessage = (event) => {
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(String(event.data));
|
|
101
|
+
this._handleMessage(data);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.opts.log?.(`Failed to parse message: ${err}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
ws.onclose = (event) => {
|
|
108
|
+
this.opts.log?.(`Disconnected (code=${event.code})`);
|
|
109
|
+
this._clearTimers();
|
|
110
|
+
this.opts.onDisconnect?.();
|
|
111
|
+
if (!this.stopped) {
|
|
112
|
+
this._scheduleReconnect();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
ws.onerror = (event) => {
|
|
116
|
+
this.opts.log?.(`WebSocket error`);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
_handleMessage(data) {
|
|
120
|
+
// Check if this is a response to a pending request
|
|
121
|
+
const id = data.id;
|
|
122
|
+
if (id && this.pendingRequests.has(id)) {
|
|
123
|
+
const pending = this.pendingRequests.get(id);
|
|
124
|
+
this.pendingRequests.delete(id);
|
|
125
|
+
clearTimeout(pending.timer);
|
|
126
|
+
pending.resolve(data);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Server-initiated ping — respond with pong
|
|
130
|
+
if (data.type === "ping") {
|
|
131
|
+
try {
|
|
132
|
+
this._send({ type: "pong" });
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
this.lastPongAt = Date.now();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Pong response to our ping
|
|
139
|
+
if (data.type === "pong") {
|
|
140
|
+
this.lastPongAt = Date.now();
|
|
141
|
+
this._clearPongTimeout();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Pass to handler
|
|
145
|
+
this.opts.onMessage(data);
|
|
146
|
+
}
|
|
147
|
+
_send(message) {
|
|
148
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
149
|
+
throw new Error("WebSocket not connected");
|
|
150
|
+
}
|
|
151
|
+
this.ws.send(JSON.stringify(message));
|
|
152
|
+
}
|
|
153
|
+
_startPing() {
|
|
154
|
+
this._clearTimers();
|
|
155
|
+
this.pingTimer = setInterval(() => {
|
|
156
|
+
try {
|
|
157
|
+
this._send({ type: "ping" });
|
|
158
|
+
this._schedulePongTimeout();
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
}, PING_INTERVAL_MS);
|
|
162
|
+
}
|
|
163
|
+
_schedulePongTimeout() {
|
|
164
|
+
this._clearPongTimeout();
|
|
165
|
+
this.pongTimeoutTimer = setTimeout(() => {
|
|
166
|
+
this.opts.log?.("Pong timeout — forcing reconnect");
|
|
167
|
+
// Force-close to trigger reconnect via onclose
|
|
168
|
+
if (this.ws) {
|
|
169
|
+
try {
|
|
170
|
+
this.ws.close(4000, "Pong timeout");
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
}
|
|
174
|
+
}, PONG_TIMEOUT_MS);
|
|
175
|
+
}
|
|
176
|
+
_clearPongTimeout() {
|
|
177
|
+
if (this.pongTimeoutTimer) {
|
|
178
|
+
clearTimeout(this.pongTimeoutTimer);
|
|
179
|
+
this.pongTimeoutTimer = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
_rejectPendingRequests(reason) {
|
|
183
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
184
|
+
clearTimeout(pending.timer);
|
|
185
|
+
pending.reject(new Error(reason));
|
|
186
|
+
}
|
|
187
|
+
this.pendingRequests.clear();
|
|
188
|
+
}
|
|
189
|
+
_scheduleReconnect() {
|
|
190
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), MAX_RECONNECT_DELAY_MS);
|
|
191
|
+
this.opts.log?.(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
|
|
192
|
+
this.reconnectTimer = setTimeout(() => {
|
|
193
|
+
this.reconnectAttempt++;
|
|
194
|
+
this._connect();
|
|
195
|
+
}, delay);
|
|
196
|
+
}
|
|
197
|
+
_clearTimers() {
|
|
198
|
+
if (this.pingTimer) {
|
|
199
|
+
clearInterval(this.pingTimer);
|
|
200
|
+
this.pingTimer = null;
|
|
201
|
+
}
|
|
202
|
+
if (this.reconnectTimer) {
|
|
203
|
+
clearTimeout(this.reconnectTimer);
|
|
204
|
+
this.reconnectTimer = null;
|
|
205
|
+
}
|
|
206
|
+
this._clearPongTimeout();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "claw-messenger",
|
|
3
|
+
"description": "Integrate Claw Messenger to send and receive messages across multiple platforms including iMessage, RCS, and SMS.",
|
|
4
|
+
"channels": ["claw-messenger"],
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"apiKey": { "type": "string" },
|
|
10
|
+
"serverUrl": { "type": "string" },
|
|
11
|
+
"preferredService": { "type": "string", "enum": ["iMessage", "RCS", "SMS"] },
|
|
12
|
+
"dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist"] },
|
|
13
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } }
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"uiHints": {
|
|
17
|
+
"apiKey": { "label": "API Key", "sensitive": true, "placeholder": "cm_live_..." },
|
|
18
|
+
"serverUrl": { "label": "Server URL", "placeholder": "ws://claw-messenger.onrender.com" },
|
|
19
|
+
"preferredService": { "label": "Preferred Service" },
|
|
20
|
+
"dmPolicy": { "label": "DM Policy" },
|
|
21
|
+
"allowFrom": { "label": "Allow List" }
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emotion-machine/claw-messenger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "iMessage, RCS & SMS channel plugin for OpenClaw — no phone or Mac Mini required",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"homepage": "https://clawmessenger.com",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"openclaw",
|
|
11
|
+
"openclaw-plugin",
|
|
12
|
+
"imessage",
|
|
13
|
+
"rcs",
|
|
14
|
+
"sms",
|
|
15
|
+
"emotion-machine",
|
|
16
|
+
"claw-messenger"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"src/types/",
|
|
21
|
+
"openclaw.plugin.json",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"dev": "tsc --watch",
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"prepare": "test -d dist || npm run build",
|
|
29
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"openclaw": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./dist/index.js"
|
|
34
|
+
],
|
|
35
|
+
"channel": {
|
|
36
|
+
"id": "claw-messenger",
|
|
37
|
+
"label": "Claw Messenger",
|
|
38
|
+
"selectionLabel": "Claw Messenger",
|
|
39
|
+
"blurb": "iMessage, RCS & SMS support for OpenClaw - no phone or Mac Mini required",
|
|
40
|
+
"order": 51,
|
|
41
|
+
"aliases": [
|
|
42
|
+
"claw-messenger"
|
|
43
|
+
],
|
|
44
|
+
"systemImage": "message.fill"
|
|
45
|
+
},
|
|
46
|
+
"install": {
|
|
47
|
+
"npmSpec": "@emotion-machine/claw-messenger",
|
|
48
|
+
"defaultChoice": "npm"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@sinclair/typebox": "^0.34.48",
|
|
53
|
+
"zod": "^4.3.6"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.14.0",
|
|
57
|
+
"typescript": "^5.5.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18"
|
|
61
|
+
},
|
|
62
|
+
"license": "UNLICENSED"
|
|
63
|
+
}
|