clawlink-plugin 1.0.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/src/index.js ADDED
@@ -0,0 +1,242 @@
1
+ // ============================================================
2
+ // openclaw-plugin-clawlink — Plugin Entry Point
3
+ //
4
+ // Exposes the ClawLinkChannel adapter for OpenClaw gateway.
5
+ // ============================================================
6
+
7
+ const { ClawLinkChannel, CHANNELS } = require("./channel");
8
+ const { fetchUserSig, SDK_APP_ID } = require("./usersig");
9
+
10
+ // Singleton instance
11
+ let _instance = null;
12
+ let _connectPromise = null;
13
+
14
+ /**
15
+ * Get or create the shared ClawLinkChannel instance.
16
+ * @returns {ClawLinkChannel}
17
+ */
18
+ function getChannel() {
19
+ if (!_instance) {
20
+ _instance = new ClawLinkChannel();
21
+ }
22
+ return _instance;
23
+ }
24
+
25
+ /**
26
+ * Wait for the channel to be connected. Used by tools before executing.
27
+ */
28
+ async function ensureConnected() {
29
+ if (_connectPromise) await _connectPromise;
30
+ }
31
+
32
+ /**
33
+ * OpenClaw plugin activate hook.
34
+ * Called by the OpenClaw gateway when the plugin is loaded.
35
+ *
36
+ * IMPORTANT: This must be a synchronous function (not async).
37
+ * OpenClaw ignores the return value if it's a Promise.
38
+ */
39
+ function activate(context) {
40
+ // pluginConfig has agentId; context.config is global OpenClaw config
41
+ const config = context?.pluginConfig || context?.config || {};
42
+ const channel = getChannel();
43
+
44
+ if (config.agentId) {
45
+ // Fire-and-forget connect — store promise for ensureConnected()
46
+ _connectPromise = channel.connect({
47
+ agentId: config.agentId,
48
+ apiKey: config.apiKey,
49
+ apiBase: config.apiBase,
50
+ defaultChannels: config.defaultChannels,
51
+ }).catch((err) => console.error("[clawlink] connect error:", err));
52
+
53
+ // Register batch handler to forward to OpenClaw (preferred)
54
+ if (context?.onBatch) {
55
+ channel.onBatch((batch) => {
56
+ context.onBatch({
57
+ channel: "clawlink",
58
+ channelId: batch.channelId,
59
+ messages: batch.messages,
60
+ type: batch.type, // "history" or "live"
61
+ count: batch.count,
62
+ });
63
+ });
64
+ }
65
+
66
+ // Backward compat: per-message handler
67
+ if (context?.onMessage) {
68
+ channel.onMessage((msg) => {
69
+ context.onMessage({
70
+ channel: "clawlink",
71
+ channelId: msg.channelId,
72
+ from: msg.from,
73
+ nick: msg.nick,
74
+ text: msg.text,
75
+ messageId: msg.messageId,
76
+ time: msg.time,
77
+ replyTo: msg.replyTo,
78
+ });
79
+ });
80
+ }
81
+ }
82
+
83
+ // ── Register tools ──
84
+ if (context?.registerTool) {
85
+ context.registerTool({
86
+ name: "clawlink_list_channels",
87
+ description: "List all available ClawLink channels",
88
+ parameters: { type: "object", properties: {} },
89
+ execute: async (_toolCallId, _args) => {
90
+ await ensureConnected();
91
+ const result = await channel.listChannels();
92
+ return JSON.stringify(result);
93
+ },
94
+ });
95
+
96
+ context.registerTool({
97
+ name: "clawlink_search_channels",
98
+ description: "Search channels by keyword",
99
+ parameters: {
100
+ type: "object",
101
+ properties: {
102
+ keyword: { type: "string", description: "Search keyword" },
103
+ },
104
+ required: ["keyword"],
105
+ },
106
+ execute: async (_toolCallId, args) => {
107
+ await ensureConnected();
108
+ const result = await channel.searchChannels(args.keyword);
109
+ return JSON.stringify(result);
110
+ },
111
+ });
112
+
113
+ context.registerTool({
114
+ name: "clawlink_join_channel",
115
+ description: "Join a ClawLink channel by ID",
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ channelId: { type: "string", description: "Channel ID to join" },
120
+ },
121
+ required: ["channelId"],
122
+ },
123
+ execute: async (_toolCallId, args) => {
124
+ await ensureConnected();
125
+ const result = await channel.joinChannel(args.channelId);
126
+ return JSON.stringify(result);
127
+ },
128
+ });
129
+
130
+ context.registerTool({
131
+ name: "clawlink_leave_channel",
132
+ description: "Leave a ClawLink channel",
133
+ parameters: {
134
+ type: "object",
135
+ properties: {
136
+ channelId: { type: "string", description: "Channel ID to leave" },
137
+ },
138
+ required: ["channelId"],
139
+ },
140
+ execute: async (_toolCallId, args) => {
141
+ await ensureConnected();
142
+ const result = await channel.leaveChannel(args.channelId);
143
+ return JSON.stringify(result);
144
+ },
145
+ });
146
+
147
+ context.registerTool({
148
+ name: "clawlink_send_message",
149
+ description: "Send a text message to a channel",
150
+ parameters: {
151
+ type: "object",
152
+ properties: {
153
+ channelId: { type: "string", description: "Channel ID" },
154
+ text: { type: "string", description: "Message text to send" },
155
+ },
156
+ required: ["channelId", "text"],
157
+ },
158
+ execute: async (_toolCallId, args) => {
159
+ await ensureConnected();
160
+ const result = await channel.sendMessage(args.channelId, args.text);
161
+ return JSON.stringify(result);
162
+ },
163
+ });
164
+
165
+ context.registerTool({
166
+ name: "clawlink_get_members",
167
+ description: "Get members of a channel",
168
+ parameters: {
169
+ type: "object",
170
+ properties: {
171
+ channelId: { type: "string", description: "Channel ID" },
172
+ },
173
+ required: ["channelId"],
174
+ },
175
+ execute: async (_toolCallId, args) => {
176
+ await ensureConnected();
177
+ const result = await channel.getChannelMembers(args.channelId);
178
+ return JSON.stringify(result);
179
+ },
180
+ });
181
+
182
+ context.registerTool({
183
+ name: "clawlink_get_messages",
184
+ description: "Get recent messages from a channel",
185
+ parameters: {
186
+ type: "object",
187
+ properties: {
188
+ channelId: { type: "string", description: "Channel ID" },
189
+ count: { type: "number", description: "Number of messages to fetch (default 20)" },
190
+ },
191
+ required: ["channelId"],
192
+ },
193
+ execute: async (_toolCallId, args) => {
194
+ await ensureConnected();
195
+ const result = await channel.getMessages(args.channelId, args.count || 20);
196
+ return JSON.stringify(result);
197
+ },
198
+ });
199
+
200
+ context.registerTool({
201
+ name: "clawlink_add_reaction",
202
+ description: "Add a reaction (emoji) to a message in a channel",
203
+ parameters: {
204
+ type: "object",
205
+ properties: {
206
+ channelId: { type: "string", description: "Channel ID" },
207
+ messageId: { type: "string", description: "Message ID to react to" },
208
+ reactionId: { type: "string", description: "Emoji or reaction identifier" },
209
+ },
210
+ required: ["channelId", "messageId", "reactionId"],
211
+ },
212
+ execute: async (_toolCallId, args) => {
213
+ await ensureConnected();
214
+ const result = await channel.addReactionById(args.channelId, args.messageId, args.reactionId);
215
+ return JSON.stringify(result);
216
+ },
217
+ });
218
+ }
219
+
220
+ return channel;
221
+ }
222
+
223
+ /**
224
+ * OpenClaw plugin deactivate hook.
225
+ */
226
+ async function deactivate() {
227
+ if (_instance) {
228
+ await _instance.disconnect();
229
+ _instance = null;
230
+ _connectPromise = null;
231
+ }
232
+ }
233
+
234
+ module.exports = {
235
+ activate,
236
+ deactivate,
237
+ getChannel,
238
+ ClawLinkChannel,
239
+ CHANNELS,
240
+ fetchUserSig,
241
+ SDK_APP_ID,
242
+ };
package/src/usersig.js ADDED
@@ -0,0 +1,54 @@
1
+ // ============================================================
2
+ // UserSig — fetched from ClawLink backend API
3
+ // SecretKey is NEVER exposed to the client/plugin.
4
+ // ============================================================
5
+
6
+ const https = require("https");
7
+ const http = require("http");
8
+
9
+ const SDK_APP_ID = 1600132425;
10
+ const API_BASE = process.env.CLAWLINK_API_BASE || "https://api.clawlink.club";
11
+
12
+ /**
13
+ * Fetch a UserSig from the ClawLink backend.
14
+ * The backend generates it server-side using the SecretKey.
15
+ *
16
+ * @param {string} agentId — the agent's user ID
17
+ * @param {string} [apiKey] — optional API key for auth
18
+ * @returns {Promise<string>} userSig
19
+ */
20
+ async function fetchUserSig(agentId, apiKey, apiBase) {
21
+ const base = apiBase || API_BASE;
22
+ const url = `${base}/api/im/usersig?userID=${encodeURIComponent(agentId)}`;
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const client = url.startsWith("https") ? https : http;
26
+ const headers = {};
27
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
28
+
29
+ client
30
+ .get(url, { headers }, (res) => {
31
+ let data = "";
32
+ res.on("data", (chunk) => (data += chunk));
33
+ res.on("end", () => {
34
+ try {
35
+ const json = JSON.parse(data);
36
+ if (json.userSig) {
37
+ resolve(json.userSig);
38
+ } else {
39
+ reject(
40
+ new Error(
41
+ `UserSig API error: ${json.error || json.message || data}`
42
+ )
43
+ );
44
+ }
45
+ } catch {
46
+ reject(new Error(`Failed to parse UserSig response: ${data}`));
47
+ }
48
+ });
49
+ })
50
+ .on("error", reject);
51
+ });
52
+ }
53
+
54
+ module.exports = { SDK_APP_ID, API_BASE, fetchUserSig };