openclaw-liveavatar 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.
Files changed (122) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +42 -0
  3. package/.next/app-path-routes-manifest.json +6 -0
  4. package/.next/build-manifest.json +33 -0
  5. package/.next/cache/.previewinfo +1 -0
  6. package/.next/cache/.rscinfo +1 -0
  7. package/.next/cache/.tsbuildinfo +1 -0
  8. package/.next/cache/chrome-devtools-workspace-uuid +1 -0
  9. package/.next/cache/next-devtools-config.json +1 -0
  10. package/.next/cache/webpack/client-production/0.pack +0 -0
  11. package/.next/cache/webpack/client-production/1.pack +0 -0
  12. package/.next/cache/webpack/client-production/2.pack +0 -0
  13. package/.next/cache/webpack/client-production/3.pack +0 -0
  14. package/.next/cache/webpack/client-production/4.pack +0 -0
  15. package/.next/cache/webpack/client-production/index.pack +0 -0
  16. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  17. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  18. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  19. package/.next/cache/webpack/server-production/0.pack +0 -0
  20. package/.next/cache/webpack/server-production/index.pack +0 -0
  21. package/.next/diagnostics/build-diagnostics.json +6 -0
  22. package/.next/diagnostics/framework.json +1 -0
  23. package/.next/export-marker.json +6 -0
  24. package/.next/images-manifest.json +58 -0
  25. package/.next/next-minimal-server.js.nft.json +1 -0
  26. package/.next/next-server.js.nft.json +1 -0
  27. package/.next/package.json +1 -0
  28. package/.next/prerender-manifest.json +61 -0
  29. package/.next/react-loadable-manifest.json +1 -0
  30. package/.next/required-server-files.json +320 -0
  31. package/.next/routes-manifest.json +53 -0
  32. package/.next/server/app/_not-found/page.js +5 -0
  33. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  34. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  35. package/.next/server/app/_not-found.html +4 -0
  36. package/.next/server/app/_not-found.meta +8 -0
  37. package/.next/server/app/_not-found.rsc +15 -0
  38. package/.next/server/app/api/get-avatars/route.js +1 -0
  39. package/.next/server/app/api/get-avatars/route.js.nft.json +1 -0
  40. package/.next/server/app/api/get-avatars/route_client-reference-manifest.js +1 -0
  41. package/.next/server/app/api/start-session/route.js +1 -0
  42. package/.next/server/app/api/start-session/route.js.nft.json +1 -0
  43. package/.next/server/app/api/start-session/route_client-reference-manifest.js +1 -0
  44. package/.next/server/app/index.html +4 -0
  45. package/.next/server/app/index.meta +7 -0
  46. package/.next/server/app/index.rsc +16 -0
  47. package/.next/server/app/page.js +9 -0
  48. package/.next/server/app/page.js.nft.json +1 -0
  49. package/.next/server/app/page_client-reference-manifest.js +1 -0
  50. package/.next/server/app-paths-manifest.json +6 -0
  51. package/.next/server/chunks/361.js +9 -0
  52. package/.next/server/chunks/611.js +6 -0
  53. package/.next/server/chunks/873.js +22 -0
  54. package/.next/server/functions-config-manifest.json +4 -0
  55. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  56. package/.next/server/middleware-build-manifest.js +1 -0
  57. package/.next/server/middleware-manifest.json +6 -0
  58. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  59. package/.next/server/next-font-manifest.js +1 -0
  60. package/.next/server/next-font-manifest.json +1 -0
  61. package/.next/server/pages/404.html +4 -0
  62. package/.next/server/pages/500.html +1 -0
  63. package/.next/server/pages/_app.js +1 -0
  64. package/.next/server/pages/_app.js.nft.json +1 -0
  65. package/.next/server/pages/_document.js +1 -0
  66. package/.next/server/pages/_document.js.nft.json +1 -0
  67. package/.next/server/pages/_error.js +19 -0
  68. package/.next/server/pages/_error.js.nft.json +1 -0
  69. package/.next/server/pages-manifest.json +6 -0
  70. package/.next/server/server-reference-manifest.js +1 -0
  71. package/.next/server/server-reference-manifest.json +1 -0
  72. package/.next/server/webpack-runtime.js +1 -0
  73. package/.next/static/chunks/144d3bae-37bcc55d23f188ee.js +1 -0
  74. package/.next/static/chunks/255-35bf8c00c5dde345.js +1 -0
  75. package/.next/static/chunks/336-a66237a0a1db954a.js +1 -0
  76. package/.next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
  77. package/.next/static/chunks/app/_not-found/page-dfc6e5d8e6c6203c.js +1 -0
  78. package/.next/static/chunks/app/api/get-avatars/route-8017e1cff542d5d0.js +1 -0
  79. package/.next/static/chunks/app/api/start-session/route-8017e1cff542d5d0.js +1 -0
  80. package/.next/static/chunks/app/layout-ff675313cc8f8fcf.js +1 -0
  81. package/.next/static/chunks/app/page-9e4b703722bef650.js +1 -0
  82. package/.next/static/chunks/framework-de98b93a850cfc71.js +1 -0
  83. package/.next/static/chunks/main-1a0dcce460eb61ce.js +1 -0
  84. package/.next/static/chunks/main-app-e7f1007edc7ad7e1.js +1 -0
  85. package/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  86. package/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  87. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  88. package/.next/static/chunks/webpack-4a462cecab786e93.js +1 -0
  89. package/.next/static/css/bfd73afa11897439.css +3 -0
  90. package/.next/static/v_GdCj8lVweDVhmIhhEcM/_buildManifest.js +1 -0
  91. package/.next/static/v_GdCj8lVweDVhmIhhEcM/_ssgManifest.js +1 -0
  92. package/.next/trace +2 -0
  93. package/.next/types/app/api/get-avatars/route.ts +347 -0
  94. package/.next/types/app/api/start-session/route.ts +347 -0
  95. package/.next/types/app/layout.ts +84 -0
  96. package/.next/types/app/page.ts +84 -0
  97. package/.next/types/cache-life.d.ts +141 -0
  98. package/.next/types/package.json +1 -0
  99. package/.next/types/routes.d.ts +74 -0
  100. package/.next/types/validator.ts +88 -0
  101. package/README.md +241 -0
  102. package/app/api/config.ts +18 -0
  103. package/app/api/get-avatars/route.ts +117 -0
  104. package/app/api/start-session/route.ts +95 -0
  105. package/app/globals.css +3 -0
  106. package/app/layout.tsx +37 -0
  107. package/app/page.tsx +9 -0
  108. package/bin/cli.js +100 -0
  109. package/package.json +66 -0
  110. package/src/components/LiveAvatarSession.tsx +825 -0
  111. package/src/components/OpenClawDemo.tsx +399 -0
  112. package/src/gateway/client.ts +522 -0
  113. package/src/gateway/types.ts +83 -0
  114. package/src/liveavatar/context.tsx +750 -0
  115. package/src/liveavatar/index.ts +6 -0
  116. package/src/liveavatar/types.ts +10 -0
  117. package/src/liveavatar/useAvatarActions.ts +41 -0
  118. package/src/liveavatar/useChatHistory.ts +7 -0
  119. package/src/liveavatar/useSession.ts +37 -0
  120. package/src/liveavatar/useTextChat.ts +32 -0
  121. package/src/liveavatar/useVoiceChat.ts +70 -0
  122. package/tsconfig.json +40 -0
@@ -0,0 +1,522 @@
1
+ // OpenClaw Gateway WebSocket Client
2
+ // Connects to the local OpenClaw Gateway to send messages to the agent
3
+
4
+ import {
5
+ GatewayMessage,
6
+ GatewayResponse,
7
+ GatewayEvent,
8
+ GatewayConnectionState,
9
+ AgentResponse,
10
+ AgentEvent,
11
+ } from "./types";
12
+
13
+ type MessageHandler = (message: GatewayMessage) => void;
14
+ type ConnectionStateHandler = (state: GatewayConnectionState) => void;
15
+
16
+ export class OpenClawGatewayClient {
17
+ private ws: WebSocket | null = null;
18
+ private url: string;
19
+ private token: string;
20
+ private reconnectAttempts = 0;
21
+ private maxReconnectAttempts = 5;
22
+ private reconnectDelay = 1000;
23
+ private messageId = 0;
24
+ private pendingRequests = new Map<
25
+ string,
26
+ { resolve: (value: unknown) => void; reject: (error: Error) => void }
27
+ >();
28
+ private connectionState: GatewayConnectionState = "disconnected";
29
+ private conversationId: string | null = null;
30
+ private sessionKey: string | null = null;
31
+
32
+ // Event handlers
33
+ private onMessageHandlers: MessageHandler[] = [];
34
+ private onConnectionStateHandlers: ConnectionStateHandler[] = [];
35
+
36
+ constructor(url: string, token?: string) {
37
+ this.url = url;
38
+ this.token = token || "";
39
+ }
40
+
41
+ get state(): GatewayConnectionState {
42
+ return this.connectionState;
43
+ }
44
+
45
+ private setConnectionState(state: GatewayConnectionState) {
46
+ this.connectionState = state;
47
+ this.onConnectionStateHandlers.forEach((handler) => handler(state));
48
+ }
49
+
50
+ async connect(): Promise<void> {
51
+ return new Promise((resolve, reject) => {
52
+ try {
53
+ this.setConnectionState("connecting");
54
+
55
+ // Build URL with token if provided
56
+ let wsUrl = this.url;
57
+ if (this.token) {
58
+ const separator = wsUrl.includes("?") ? "&" : "?";
59
+ wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(this.token)}`;
60
+ }
61
+
62
+ this.ws = new WebSocket(wsUrl);
63
+
64
+ this.ws.onopen = () => {
65
+ console.log("[Gateway] WebSocket connected");
66
+ this.reconnectAttempts = 0;
67
+ this.performHandshake()
68
+ .then(() => {
69
+ this.setConnectionState("connected");
70
+ resolve();
71
+ })
72
+ .catch((err) => {
73
+ this.setConnectionState("error");
74
+ reject(err);
75
+ });
76
+ };
77
+
78
+ this.ws.onclose = (event) => {
79
+ console.log("[Gateway] WebSocket closed:", event.code, event.reason);
80
+ this.setConnectionState("disconnected");
81
+ this.handleReconnect();
82
+ };
83
+
84
+ this.ws.onerror = (error) => {
85
+ console.error("[Gateway] WebSocket error:", error);
86
+ this.setConnectionState("error");
87
+ reject(new Error("WebSocket connection failed"));
88
+ };
89
+
90
+ this.ws.onmessage = (event) => {
91
+ this.handleMessage(event.data);
92
+ };
93
+ } catch (error) {
94
+ this.setConnectionState("error");
95
+ reject(error);
96
+ }
97
+ });
98
+ }
99
+
100
+ private async performHandshake(): Promise<void> {
101
+ // Send connect request matching OpenClaw protocol schema
102
+ // Protocol version 3 is required by OpenClaw Gateway 2026.1.30
103
+ const connectRequest: Record<string, unknown> = {
104
+ minProtocol: 3,
105
+ maxProtocol: 3,
106
+ client: {
107
+ id: "webchat" as const,
108
+ version: "0.1.0",
109
+ platform: "web",
110
+ mode: "webchat" as const,
111
+ displayName: "OpenClaw LiveAvatar",
112
+ },
113
+ };
114
+
115
+ // Add token auth if provided - this allows skipping device identity
116
+ if (this.token) {
117
+ connectRequest.auth = { token: this.token };
118
+ }
119
+
120
+ const response = (await this.sendRequest("connect", connectRequest)) as {
121
+ type?: string;
122
+ snapshot?: {
123
+ sessionDefaults?: {
124
+ mainSessionKey?: string;
125
+ defaultAgentId?: string;
126
+ };
127
+ };
128
+ };
129
+
130
+ // Capture session key for agent event routing
131
+ if (response.snapshot?.sessionDefaults?.mainSessionKey) {
132
+ this.sessionKey = response.snapshot.sessionDefaults.mainSessionKey;
133
+ console.log("[Gateway] Session key captured:", this.sessionKey);
134
+ }
135
+
136
+ console.log("[Gateway] Handshake complete:", response);
137
+ }
138
+
139
+ private handleMessage(data: string) {
140
+ try {
141
+ const message = JSON.parse(data) as GatewayMessage;
142
+
143
+ // Notify all message handlers
144
+ this.onMessageHandlers.forEach((handler) => handler(message));
145
+
146
+ if (message.type === "res") {
147
+ // Handle response to a pending request
148
+ const pending = this.pendingRequests.get(message.id);
149
+ if (pending) {
150
+ this.pendingRequests.delete(message.id);
151
+ if (message.ok) {
152
+ pending.resolve(message.payload);
153
+ } else {
154
+ pending.reject(
155
+ new Error(message.error?.message || "Request failed")
156
+ );
157
+ }
158
+ }
159
+ } else if (message.type === "event") {
160
+ // Handle events
161
+ this.handleEvent(message);
162
+ }
163
+ } catch (error) {
164
+ console.error("[Gateway] Failed to parse message:", error);
165
+ }
166
+ }
167
+
168
+ private handleEvent(event: GatewayEvent) {
169
+ // Log all events with full payload for debugging
170
+ console.log("[Gateway] Event:", event.event, JSON.stringify(event.payload));
171
+
172
+ // Handle agent events - these are streamed events with runId, stream, data
173
+ if (event.event === "agent") {
174
+ const payload = event.payload as AgentEvent;
175
+ console.log("[Gateway] Agent event received:", payload.runId, payload.stream, payload.data);
176
+ this.onAgentEventHandlers.forEach((handler) => handler(payload));
177
+ }
178
+ }
179
+
180
+ // Agent event handlers (for streaming events)
181
+ private onAgentEventHandlers: ((event: AgentEvent) => void)[] = [];
182
+
183
+ onAgentEvent(handler: (event: AgentEvent) => void) {
184
+ this.onAgentEventHandlers.push(handler);
185
+ }
186
+
187
+ offAgentEvent(handler: (event: AgentEvent) => void) {
188
+ const index = this.onAgentEventHandlers.indexOf(handler);
189
+ if (index > -1) {
190
+ this.onAgentEventHandlers.splice(index, 1);
191
+ }
192
+ }
193
+
194
+ private handleReconnect() {
195
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
196
+ console.log("[Gateway] Max reconnect attempts reached");
197
+ return;
198
+ }
199
+
200
+ this.reconnectAttempts++;
201
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
202
+ console.log(
203
+ `[Gateway] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
204
+ );
205
+
206
+ setTimeout(() => {
207
+ this.connect().catch((err) => {
208
+ console.error("[Gateway] Reconnect failed:", err);
209
+ });
210
+ }, delay);
211
+ }
212
+
213
+ private async sendRequest(
214
+ method: string,
215
+ params?: Record<string, unknown>
216
+ ): Promise<unknown> {
217
+ return new Promise((resolve, reject) => {
218
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
219
+ reject(new Error("WebSocket not connected"));
220
+ return;
221
+ }
222
+
223
+ const id = `${++this.messageId}`;
224
+ const request = {
225
+ type: "req" as const,
226
+ id,
227
+ method,
228
+ params,
229
+ };
230
+
231
+ this.pendingRequests.set(id, { resolve, reject });
232
+
233
+ // Set timeout for request
234
+ setTimeout(() => {
235
+ if (this.pendingRequests.has(id)) {
236
+ this.pendingRequests.delete(id);
237
+ reject(new Error("Request timeout"));
238
+ }
239
+ }, 30000);
240
+
241
+ this.ws.send(JSON.stringify(request));
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Send a message to the OpenClaw agent and get a response
247
+ * Wraps user message with instructions to return structured response with TTS summary
248
+ */
249
+ async sendToAgent(text: string): Promise<AgentResponse> {
250
+ // Generate a unique idempotency key for this request
251
+ const idempotencyKey = `liveavatar-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
252
+
253
+ // Wrap user message with instructions for structured response
254
+ const wrappedMessage = `${text}
255
+
256
+ [RESPONSE FORMAT: Start with a 3-5 sentence spoken summary wrapped in [TTS]...[/TTS] tags that captures the key points of your response, then provide your full detailed response. The TTS summary should be informative and conversational, giving the user the gist while they read the full text. Example:
257
+ [TTS]Here's what I found. The main issue is X, which can be solved by Y. I'd recommend starting with Z approach because it's the most straightforward.[/TTS]
258
+ Full detailed response here with all the specifics...]`;
259
+
260
+ const params: Record<string, unknown> = {
261
+ message: wrappedMessage,
262
+ idempotencyKey,
263
+ };
264
+
265
+ // Include session key for agent event routing
266
+ if (this.sessionKey) {
267
+ params.sessionKey = this.sessionKey;
268
+ }
269
+
270
+ // Include conversation ID for context continuity
271
+ if (this.conversationId) {
272
+ params.conversationId = this.conversationId;
273
+ }
274
+
275
+ // Create promise to collect streaming events BEFORE sending request
276
+ // This prevents race condition where events arrive before handler is registered
277
+ let runId: string | null = null;
278
+ let collectedText = "";
279
+ let completed = false;
280
+ let resolveResponse: (value: AgentResponse) => void;
281
+ let rejectResponse: (error: Error) => void;
282
+
283
+ // Buffer events that arrive before we have runId
284
+ const bufferedEvents: AgentEvent[] = [];
285
+
286
+ const responsePromise = new Promise<AgentResponse>((resolve, reject) => {
287
+ resolveResponse = resolve;
288
+ rejectResponse = reject;
289
+ });
290
+
291
+ const timeout = setTimeout(() => {
292
+ this.offAgentEvent(handler);
293
+ rejectResponse(new Error("Agent response timeout"));
294
+ }, 60000);
295
+
296
+ // Track processed event sequences to avoid duplicates
297
+ const processedSeqs = new Set<number>();
298
+
299
+ const processEvent = (event: AgentEvent) => {
300
+ if (completed) return;
301
+
302
+ // Deduplicate events by sequence number
303
+ if (event.seq !== undefined && processedSeqs.has(event.seq)) {
304
+ return; // Skip duplicate event
305
+ }
306
+ if (event.seq !== undefined) {
307
+ processedSeqs.add(event.seq);
308
+ }
309
+
310
+ console.log("[Gateway] Agent event:", event.stream, event.seq, event.data?.delta?.substring(0, 20));
311
+
312
+ // Collect text from assistant stream - use delta (incremental) not text (cumulative)
313
+ if (event.stream === "assistant") {
314
+ // Only use delta for incremental text
315
+ if (event.data?.delta) {
316
+ collectedText += event.data.delta;
317
+ }
318
+ }
319
+
320
+ // Check for lifecycle end
321
+ if (event.stream === "lifecycle" && event.data?.phase === "end") {
322
+ completed = true;
323
+ clearTimeout(timeout);
324
+ this.offAgentEvent(handler);
325
+ resolveResponse({
326
+ runId: runId!,
327
+ status: "completed",
328
+ text: collectedText || undefined,
329
+ });
330
+ }
331
+
332
+ // Check for lifecycle error
333
+ if (event.stream === "lifecycle" && event.data?.phase === "error") {
334
+ completed = true;
335
+ clearTimeout(timeout);
336
+ this.offAgentEvent(handler);
337
+ resolveResponse({
338
+ runId: runId!,
339
+ status: "failed",
340
+ text: event.data?.error || "Agent encountered an error",
341
+ });
342
+ }
343
+ };
344
+
345
+ const handler = (event: AgentEvent) => {
346
+ // If we don't have runId yet, buffer all events
347
+ if (!runId) {
348
+ bufferedEvents.push(event);
349
+ return;
350
+ }
351
+
352
+ // Only handle events for this run
353
+ if (event.runId !== runId) {
354
+ return;
355
+ }
356
+
357
+ processEvent(event);
358
+ };
359
+
360
+ // Register handler BEFORE sending request
361
+ this.onAgentEvent(handler);
362
+
363
+ try {
364
+ const response = (await this.sendRequest("agent", params)) as {
365
+ runId: string;
366
+ status: string;
367
+ conversationId?: string;
368
+ };
369
+
370
+ console.log("[Gateway] Agent request accepted:", response);
371
+
372
+ // Now set runId
373
+ runId = response.runId;
374
+
375
+ // Store conversation ID for future messages
376
+ if (response.conversationId) {
377
+ this.conversationId = response.conversationId;
378
+ }
379
+
380
+ // Process any buffered events that match this runId
381
+ for (const event of bufferedEvents) {
382
+ if (event.runId === runId) {
383
+ processEvent(event);
384
+ }
385
+ }
386
+ } catch (error) {
387
+ clearTimeout(timeout);
388
+ this.offAgentEvent(handler);
389
+ throw error;
390
+ }
391
+
392
+ return responsePromise;
393
+ }
394
+
395
+ /**
396
+ * Get current connection status
397
+ */
398
+ async getStatus(): Promise<unknown> {
399
+ return this.sendRequest("status");
400
+ }
401
+
402
+ /**
403
+ * Parse structured response to extract TTS summary and full message
404
+ * Expected format:
405
+ * [TTS]Short summary for speech[/TTS]
406
+ * Full detailed response...
407
+ */
408
+ parseResponse(text: string): { tts: string; full: string } {
409
+ // Look for [TTS]...[/TTS] block
410
+ const ttsMatch = text.match(/\[TTS\]([\s\S]*?)\[\/TTS\]/i);
411
+
412
+ if (ttsMatch) {
413
+ const ttsSummary = ttsMatch[1].trim();
414
+ // Remove the entire TTS block from display message
415
+ const fullMessage = text.replace(/\[TTS\][\s\S]*?\[\/TTS\]\n?/i, "").trim();
416
+ console.log("[Gateway] Parsed TTS summary:", ttsSummary.length, "chars");
417
+ return { tts: ttsSummary, full: fullMessage || text };
418
+ }
419
+
420
+ // Fallback: extract first 3-5 sentences as TTS
421
+ console.log("[Gateway] No [TTS] block found, extracting first sentences");
422
+ const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
423
+
424
+ let tts = "";
425
+ const maxSentences = 5;
426
+ const maxChars = 400; // Allow longer TTS for more informative summary
427
+
428
+ for (let i = 0; i < Math.min(maxSentences, sentences.length); i++) {
429
+ const sentence = sentences[i].trim();
430
+ if (tts.length + sentence.length > maxChars && tts.length > 0) break;
431
+ tts += (tts ? " " : "") + sentence;
432
+ }
433
+
434
+ return {
435
+ tts: tts || this.truncateToSentences(text, 300),
436
+ full: text
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Truncate text to complete sentences within a max length
442
+ */
443
+ private truncateToSentences(text: string, maxLength: number): string {
444
+ if (text.length <= maxLength) return text;
445
+
446
+ const truncated = text.substring(0, maxLength);
447
+ const lastSentence = Math.max(
448
+ truncated.lastIndexOf(". "),
449
+ truncated.lastIndexOf("! "),
450
+ truncated.lastIndexOf("? "),
451
+ truncated.lastIndexOf(".\n"),
452
+ truncated.lastIndexOf("!\n"),
453
+ truncated.lastIndexOf("?\n")
454
+ );
455
+
456
+ if (lastSentence > maxLength * 0.5) {
457
+ return truncated.substring(0, lastSentence + 1).trim();
458
+ }
459
+
460
+ // Fall back to cutting at last space
461
+ const lastSpace = truncated.lastIndexOf(" ");
462
+ if (lastSpace > maxLength * 0.7) {
463
+ return truncated.substring(0, lastSpace).trim() + "...";
464
+ }
465
+
466
+ return truncated.trim() + "...";
467
+ }
468
+
469
+ disconnect() {
470
+ if (this.ws) {
471
+ this.ws.close();
472
+ this.ws = null;
473
+ }
474
+ this.setConnectionState("disconnected");
475
+ }
476
+
477
+ // Event subscription methods
478
+ onMessage(handler: MessageHandler) {
479
+ this.onMessageHandlers.push(handler);
480
+ }
481
+
482
+ offMessage(handler: MessageHandler) {
483
+ const index = this.onMessageHandlers.indexOf(handler);
484
+ if (index > -1) {
485
+ this.onMessageHandlers.splice(index, 1);
486
+ }
487
+ }
488
+
489
+ onConnectionState(handler: ConnectionStateHandler) {
490
+ this.onConnectionStateHandlers.push(handler);
491
+ }
492
+
493
+ offConnectionState(handler: ConnectionStateHandler) {
494
+ const index = this.onConnectionStateHandlers.indexOf(handler);
495
+ if (index > -1) {
496
+ this.onConnectionStateHandlers.splice(index, 1);
497
+ }
498
+ }
499
+
500
+ }
501
+
502
+ // Singleton instance for the app
503
+ let gatewayClient: OpenClawGatewayClient | null = null;
504
+
505
+ export function getGatewayClient(): OpenClawGatewayClient {
506
+ if (!gatewayClient) {
507
+ // Get config from environment or use defaults
508
+ const url =
509
+ typeof window !== "undefined"
510
+ ? (window as unknown as { __OPENCLAW_GATEWAY_URL?: string })
511
+ .__OPENCLAW_GATEWAY_URL || "ws://127.0.0.1:18789"
512
+ : "ws://127.0.0.1:18789";
513
+ const token =
514
+ typeof window !== "undefined"
515
+ ? (window as unknown as { __OPENCLAW_GATEWAY_TOKEN?: string })
516
+ .__OPENCLAW_GATEWAY_TOKEN || ""
517
+ : "";
518
+
519
+ gatewayClient = new OpenClawGatewayClient(url, token);
520
+ }
521
+ return gatewayClient;
522
+ }
@@ -0,0 +1,83 @@
1
+ // OpenClaw Gateway WebSocket Protocol Types
2
+
3
+ export interface GatewayRequest {
4
+ type: "req";
5
+ id: string;
6
+ method: string;
7
+ params?: Record<string, unknown>;
8
+ }
9
+
10
+ export interface GatewayResponse {
11
+ type: "res";
12
+ id: string;
13
+ ok: boolean;
14
+ payload?: unknown;
15
+ error?: {
16
+ code: string;
17
+ message: string;
18
+ };
19
+ }
20
+
21
+ export interface GatewayEvent {
22
+ type: "event";
23
+ event: string;
24
+ payload?: unknown;
25
+ seq?: number;
26
+ stateVersion?: number;
27
+ }
28
+
29
+ export type GatewayMessage = GatewayRequest | GatewayResponse | GatewayEvent;
30
+
31
+ // Connection handshake
32
+ export interface ConnectRequest {
33
+ minProtocol: number;
34
+ maxProtocol: number;
35
+ client: {
36
+ name: string;
37
+ version: string;
38
+ };
39
+ role: "operator" | "node";
40
+ scopes?: string[];
41
+ caps?: string[];
42
+ }
43
+
44
+ export interface ConnectResponse {
45
+ protocol: number;
46
+ policy?: Record<string, unknown>;
47
+ }
48
+
49
+ // Agent request/response
50
+ export interface AgentRequest {
51
+ message: string;
52
+ idempotencyKey: string;
53
+ conversationId?: string;
54
+ }
55
+
56
+ // Agent event from gateway (matches OpenClaw protocol)
57
+ export interface AgentEvent {
58
+ runId: string;
59
+ seq: number;
60
+ stream: string;
61
+ ts: number;
62
+ sessionKey?: string;
63
+ data?: {
64
+ phase?: string;
65
+ text?: string;
66
+ delta?: string;
67
+ error?: string;
68
+ [key: string]: unknown;
69
+ };
70
+ }
71
+
72
+ // Simplified response returned by sendToAgent
73
+ export interface AgentResponse {
74
+ runId: string;
75
+ status: "completed" | "failed" | "cancelled";
76
+ text?: string;
77
+ }
78
+
79
+ export type GatewayConnectionState =
80
+ | "disconnected"
81
+ | "connecting"
82
+ | "connected"
83
+ | "error";