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/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
+ }