xingp14-clawlink 0.1.2
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/bin/clawlink-cli.js +193 -0
- package/dist/bin/clawlink-cli.js +193 -0
- package/dist/index.js +231 -0
- package/dist/openclaw.plugin.json +61 -0
- package/dist/skills/clawlink/SKILL.md +177 -0
- package/openclaw.plugin.json +61 -0
- package/package.json +38 -0
- package/skills/clawlink/SKILL.md +177 -0
- package/src/index.js +231 -0
- package/src/index.ts +253 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// ClawLink Plugin for OpenClaw
|
|
2
|
+
// Enables OpenClaw agents to connect to ClawLink hub
|
|
3
|
+
|
|
4
|
+
import type { ChannelPlugin, ChannelPluginContext } from '@openclaw/sdk';
|
|
5
|
+
|
|
6
|
+
export interface ClawLinkConfig {
|
|
7
|
+
hubUrl: string;
|
|
8
|
+
agentId: string;
|
|
9
|
+
token: string;
|
|
10
|
+
autoJoin?: string[];
|
|
11
|
+
topics?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface OutboundMessage {
|
|
15
|
+
type: string;
|
|
16
|
+
topic?: string;
|
|
17
|
+
content?: string;
|
|
18
|
+
key?: string;
|
|
19
|
+
value?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ClawLinkChannel implements ChannelPlugin {
|
|
23
|
+
name = 'clawlink';
|
|
24
|
+
private ws: WebSocket | null = null;
|
|
25
|
+
private config: ClawLinkConfig | null = null;
|
|
26
|
+
private ctx: ChannelPluginContext | null = null;
|
|
27
|
+
private reconnectTimer: number | null = null;
|
|
28
|
+
private pingTimer: number | null = null;
|
|
29
|
+
private pendingMessages: Map<string, (result: any) => void> = new Map();
|
|
30
|
+
private messageHandlers: ((msg: any) => void)[] = [];
|
|
31
|
+
private topics: Set<string> = new Set();
|
|
32
|
+
private agentId: string = '';
|
|
33
|
+
|
|
34
|
+
async initialize(ctx: ChannelPluginContext): Promise<void> {
|
|
35
|
+
this.ctx = ctx;
|
|
36
|
+
const config = ctx.config as ClawLinkConfig;
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.agentId = config.agentId;
|
|
39
|
+
|
|
40
|
+
if (config.autoJoin) {
|
|
41
|
+
for (const topic of config.autoJoin) {
|
|
42
|
+
this.topics.add(topic);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (config.topics) {
|
|
46
|
+
for (const topic of config.topics) {
|
|
47
|
+
this.topics.add(topic);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.connect();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private connect(): void {
|
|
55
|
+
if (!this.config || !this.ctx) return;
|
|
56
|
+
|
|
57
|
+
const url = `${this.config.hubUrl}?agentId=${encodeURIComponent(this.config.agentId)}&token=${encodeURIComponent(this.config.token)}`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
this.ws = new WebSocket(url);
|
|
61
|
+
|
|
62
|
+
this.ws.onopen = () => {
|
|
63
|
+
this.ctx?.logger.info(`[ClawLink] Connected to hub: ${this.config?.hubUrl}`);
|
|
64
|
+
|
|
65
|
+
// Auto-join topics
|
|
66
|
+
for (const topic of this.topics) {
|
|
67
|
+
this.send({ type: 'join', topic });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Start ping interval
|
|
71
|
+
this.pingTimer = setInterval(() => {
|
|
72
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
73
|
+
this.send({ type: 'ping' });
|
|
74
|
+
}
|
|
75
|
+
}, 25000);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.ws.onmessage = (event) => {
|
|
79
|
+
try {
|
|
80
|
+
const msg = JSON.parse(event.data);
|
|
81
|
+
this.handleMessage(msg);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
this.ctx?.logger.error('[ClawLink] Failed to parse message:', e);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.ws.onclose = (event) => {
|
|
88
|
+
this.ctx?.logger.warn(`[ClawLink] Disconnected (code: ${event.code})`);
|
|
89
|
+
this.scheduleReconnect();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.ws.onerror = (error) => {
|
|
93
|
+
this.ctx?.logger.error('[ClawLink] WebSocket error:', error);
|
|
94
|
+
};
|
|
95
|
+
} catch (e) {
|
|
96
|
+
this.ctx?.logger.error('[ClawLink] Failed to connect:', e);
|
|
97
|
+
this.scheduleReconnect();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private scheduleReconnect(): void {
|
|
102
|
+
if (this.reconnectTimer) return;
|
|
103
|
+
|
|
104
|
+
this.reconnectTimer = setTimeout(() => {
|
|
105
|
+
this.reconnectTimer = null;
|
|
106
|
+
this.ctx?.logger.info('[ClawLink] Attempting to reconnect...');
|
|
107
|
+
this.connect();
|
|
108
|
+
}, 5000);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private send(msg: OutboundMessage): void {
|
|
112
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
113
|
+
this.ws.send(JSON.stringify(msg));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private handleMessage(msg: any): void {
|
|
118
|
+
switch (msg.type) {
|
|
119
|
+
case 'welcome':
|
|
120
|
+
this.ctx?.logger.info(`[ClawLink] Authenticated as ${msg.agentId}`);
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case 'message':
|
|
124
|
+
// Handle incoming message - dispatch to OpenClaw
|
|
125
|
+
if (msg.from !== this.agentId) {
|
|
126
|
+
const message = {
|
|
127
|
+
channel: 'clawlink',
|
|
128
|
+
id: msg.id,
|
|
129
|
+
from: msg.from,
|
|
130
|
+
text: msg.content,
|
|
131
|
+
topic: msg.topic,
|
|
132
|
+
timestamp: msg.timestamp,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Dispatch to OpenClaw
|
|
136
|
+
this.ctx?.dispatch(message);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'join':
|
|
141
|
+
this.ctx?.logger.debug(`[ClawLink] Agent ${msg.agent} joined ${msg.topic}`);
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'leave':
|
|
145
|
+
this.ctx?.logger.debug(`[ClawLink] Agent ${msg.agent} left ${msg.topic}`);
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'history':
|
|
149
|
+
this.ctx?.logger.debug(`[ClawLink] Received ${msg.messages.length} historical messages for ${msg.topic}`);
|
|
150
|
+
// Process history if needed
|
|
151
|
+
for (const historicalMsg of msg.messages) {
|
|
152
|
+
if (historicalMsg.from !== this.agentId) {
|
|
153
|
+
this.ctx?.dispatch({
|
|
154
|
+
channel: 'clawlink',
|
|
155
|
+
id: historicalMsg.id,
|
|
156
|
+
from: historicalMsg.from,
|
|
157
|
+
text: historicalMsg.content,
|
|
158
|
+
topic: historicalMsg.topic,
|
|
159
|
+
timestamp: historicalMsg.timestamp,
|
|
160
|
+
isHistorical: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'pong':
|
|
167
|
+
// Keepalive response, nothing to do
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'memory_update':
|
|
171
|
+
this.ctx?.logger.debug(`[ClawLink] Memory updated: ${msg.key} by ${msg.from}`);
|
|
172
|
+
// Could trigger a context update here
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'memory_value':
|
|
176
|
+
// Response to memory_read
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'topics_list':
|
|
180
|
+
case 'topic_members':
|
|
181
|
+
// Response to queries
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'error':
|
|
185
|
+
this.ctx?.logger.error(`[ClawLink] Server error: ${msg.code} - ${msg.message}`);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Notify handlers
|
|
190
|
+
for (const handler of this.messageHandlers) {
|
|
191
|
+
try {
|
|
192
|
+
handler(msg);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
this.ctx?.logger.error('[ClawLink] Handler error:', e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Public API for sending messages from OpenClaw
|
|
200
|
+
async sendMessage(topic: string, content: string): Promise<void> {
|
|
201
|
+
this.send({ type: 'message', topic, content });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async joinTopic(topic: string): Promise<void> {
|
|
205
|
+
this.topics.add(topic);
|
|
206
|
+
this.send({ type: 'join', topic });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async leaveTopic(topic: string): Promise<void> {
|
|
210
|
+
this.topics.delete(topic);
|
|
211
|
+
this.send({ type: 'leave', topic });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async writeMemory(key: string, value: any): Promise<void> {
|
|
215
|
+
this.send({ type: 'memory_write', key, value });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async readMemory(key: string): Promise<any> {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
const mid = Date.now().toString();
|
|
221
|
+
this.pendingMessages.set(mid, resolve);
|
|
222
|
+
this.send({ type: 'memory_read', key });
|
|
223
|
+
|
|
224
|
+
// Timeout after 5 seconds
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
if (this.pendingMessages.has(mid)) {
|
|
227
|
+
this.pendingMessages.delete(mid);
|
|
228
|
+
resolve(null);
|
|
229
|
+
}
|
|
230
|
+
}, 5000);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onMessage(handler: (msg: any) => void): void {
|
|
235
|
+
this.messageHandlers.push(handler);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async shutdown(): Promise<void> {
|
|
239
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
240
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
241
|
+
|
|
242
|
+
// Leave all topics
|
|
243
|
+
for (const topic of this.topics) {
|
|
244
|
+
this.send({ type: 'leave', topic });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.ws) {
|
|
248
|
+
this.ws.close(1000, 'Agent shutting down');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default new ClawLinkChannel();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts"],"errors":true,"version":"6.0.2"}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|