/** * OpenClaw Gateway WebSocket client. * Connects to the local OpenClaw gateway (ws://127.0.0.1:18789) * and provides a simple RPC interface. */ import WebSocket from 'ws'; interface RpcFrame { id: string; method: string; params?: unknown; } interface RpcResponse { id: string; result?: unknown; error?: { message: string }; } export class OpenClawClient { private ws: WebSocket | null = null; private pending = new Map void; reject: (e: Error) => void }>(); private readonly gatewayUrl: string; private readonly token: string; private msgCounter = 0; private connected = false; constructor(gatewayUrl: string, token: string) { this.gatewayUrl = gatewayUrl; this.token = token; } connect(): Promise { return new Promise((resolve, reject) => { this.ws = new WebSocket(this.gatewayUrl, { headers: { Authorization: `Bearer ${this.token}` }, }); this.ws.once('open', () => { this.connected = true; resolve(); }); this.ws.once('error', (err) => { if (!this.connected) reject(err); }); this.ws.on('message', (raw) => { try { const frame: RpcResponse = JSON.parse(raw.toString()); const pending = this.pending.get(frame.id); if (!pending) return; this.pending.delete(frame.id); if (frame.error) { pending.reject(new Error(frame.error.message)); } else { pending.resolve(frame.result); } } catch { // ignore malformed frames } }); this.ws.on('close', () => { this.connected = false; // Reject all pending RPCs for (const [, p] of this.pending) { p.reject(new Error('OpenClaw gateway disconnected')); } this.pending.clear(); }); }); } isConnected(): boolean { return this.connected && this.ws?.readyState === WebSocket.OPEN; } rpc(method: string, params?: unknown): Promise { return new Promise((resolve, reject) => { if (!this.isConnected()) { reject(new Error('Not connected to OpenClaw gateway')); return; } const id = String(++this.msgCounter); const frame: RpcFrame = { id, method, params }; this.pending.set(id, { resolve, reject }); this.ws!.send(JSON.stringify(frame)); // Timeout after 30s setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`RPC timeout: ${method}`)); } }, 30_000); }); } close(): void { this.ws?.close(); } }