@hypercli.com/sdk 2026.4.13-3 → 2026.4.13-4

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 (97) hide show
  1. package/dist/agent.d.ts +350 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +620 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/agents.d.ts +433 -0
  6. package/dist/agents.d.ts.map +1 -0
  7. package/dist/agents.js +1140 -0
  8. package/dist/agents.js.map +1 -0
  9. package/dist/billing.d.ts +72 -0
  10. package/dist/billing.d.ts.map +1 -0
  11. package/dist/billing.js +93 -0
  12. package/dist/billing.js.map +1 -0
  13. package/dist/browser.d.ts +25 -0
  14. package/dist/browser.d.ts.map +1 -0
  15. package/dist/browser.js +33 -0
  16. package/dist/browser.js.map +1 -0
  17. package/dist/client.d.ts +67 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +100 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/config.d.ts +46 -0
  22. package/dist/config.d.ts.map +1 -0
  23. package/dist/config.js +235 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/errors.d.ts +9 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +15 -0
  28. package/dist/errors.js.map +1 -0
  29. package/dist/files.d.ts +60 -0
  30. package/dist/files.d.ts.map +1 -0
  31. package/dist/files.js +128 -0
  32. package/dist/files.js.map +1 -0
  33. package/dist/gateway.d.ts +2 -0
  34. package/dist/gateway.d.ts.map +1 -0
  35. package/dist/gateway.js +2 -0
  36. package/dist/gateway.js.map +1 -0
  37. package/dist/http.d.ts +42 -0
  38. package/dist/http.d.ts.map +1 -0
  39. package/dist/http.js +238 -0
  40. package/dist/http.js.map +1 -0
  41. package/dist/index.d.ts +26 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +42 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/instances.d.ts +94 -0
  46. package/dist/instances.d.ts.map +1 -0
  47. package/dist/instances.js +197 -0
  48. package/dist/instances.js.map +1 -0
  49. package/dist/job/base.d.ts +80 -0
  50. package/dist/job/base.d.ts.map +1 -0
  51. package/dist/job/base.js +200 -0
  52. package/dist/job/base.js.map +1 -0
  53. package/dist/job/comfyui.d.ts +75 -0
  54. package/dist/job/comfyui.d.ts.map +1 -0
  55. package/dist/job/comfyui.js +454 -0
  56. package/dist/job/comfyui.js.map +1 -0
  57. package/dist/job/gradio.d.ts +48 -0
  58. package/dist/job/gradio.d.ts.map +1 -0
  59. package/dist/job/gradio.js +90 -0
  60. package/dist/job/gradio.js.map +1 -0
  61. package/dist/jobs.d.ts +153 -0
  62. package/dist/jobs.d.ts.map +1 -0
  63. package/dist/jobs.js +352 -0
  64. package/dist/jobs.js.map +1 -0
  65. package/dist/keys.d.ts +83 -0
  66. package/dist/keys.d.ts.map +1 -0
  67. package/dist/keys.js +69 -0
  68. package/dist/keys.js.map +1 -0
  69. package/dist/logs.d.ts +54 -0
  70. package/dist/logs.d.ts.map +1 -0
  71. package/dist/logs.js +256 -0
  72. package/dist/logs.js.map +1 -0
  73. package/dist/models.d.ts +15 -0
  74. package/dist/models.d.ts.map +1 -0
  75. package/dist/models.js +19 -0
  76. package/dist/models.js.map +1 -0
  77. package/dist/openclaw/gateway.d.ts +269 -0
  78. package/dist/openclaw/gateway.d.ts.map +1 -0
  79. package/dist/openclaw/gateway.js +2021 -0
  80. package/dist/openclaw/gateway.js.map +1 -0
  81. package/dist/renders.d.ts +187 -0
  82. package/dist/renders.d.ts.map +1 -0
  83. package/dist/renders.js +360 -0
  84. package/dist/renders.js.map +1 -0
  85. package/dist/user.d.ts +49 -0
  86. package/dist/user.d.ts.map +1 -0
  87. package/dist/user.js +57 -0
  88. package/dist/user.js.map +1 -0
  89. package/dist/voice.d.ts +31 -0
  90. package/dist/voice.d.ts.map +1 -0
  91. package/dist/voice.js +36 -0
  92. package/dist/voice.js.map +1 -0
  93. package/dist/x402.d.ts +79 -0
  94. package/dist/x402.d.ts.map +1 -0
  95. package/dist/x402.js +295 -0
  96. package/dist/x402.js.map +1 -0
  97. package/package.json +2 -1
@@ -0,0 +1,2021 @@
1
+ /**
2
+ * OpenClaw Gateway WebSocket Client
3
+ *
4
+ * Connects to an agent's OpenClaw gateway over WebSocket for real-time
5
+ * configuration, chat, session management, and file operations.
6
+ *
7
+ * Protocol: OpenClaw Gateway v3
8
+ */
9
+ import { getPublicKeyAsync, signAsync, utils as edUtils } from "@noble/ed25519";
10
+ import NodeWebSocket from "ws";
11
+ class GatewayRequestError extends Error {
12
+ gatewayCode;
13
+ details;
14
+ constructor(error) {
15
+ super(error.message);
16
+ this.name = "GatewayRequestError";
17
+ this.gatewayCode = error.code;
18
+ this.details = error.details;
19
+ }
20
+ }
21
+ const PROTOCOL_VERSION = 3;
22
+ const DEFAULT_TIMEOUT = 15_000;
23
+ const CHAT_TIMEOUT = 120_000;
24
+ const RECONNECT_CLOSE_CODE = 4008;
25
+ const DEFAULT_CLIENT_ID = "cli";
26
+ const DEFAULT_CLIENT_MODE = "cli";
27
+ const DEFAULT_CLIENT_VERSION = "@hypercli/sdk";
28
+ const DEFAULT_CAPS = ["tool-events"];
29
+ const CONNECT_TIMER_MS = 750;
30
+ const INITIAL_BACKOFF_MS = 800;
31
+ const MAX_BACKOFF_MS = 15_000;
32
+ const BACKOFF_MULTIPLIER = 1.7;
33
+ const OPERATOR_ROLE = "operator";
34
+ const OPERATOR_SCOPES = ["operator.admin", "operator.approvals", "operator.pairing"];
35
+ const STORAGE_KEY = "openclaw.device.auth.v1";
36
+ const CONNECT_ERROR_PAIRING_REQUIRED = "PAIRING_REQUIRED";
37
+ const CONNECT_ERROR_DEVICE_TOKEN_MISMATCH = "AUTH_DEVICE_TOKEN_MISMATCH";
38
+ const VALID_CLIENT_IDS = new Set([
39
+ "webchat-ui",
40
+ "openclaw-control-ui",
41
+ "webchat",
42
+ "cli",
43
+ "gateway-client",
44
+ "openclaw-macos",
45
+ "openclaw-ios",
46
+ "openclaw-android",
47
+ "node-host",
48
+ "test",
49
+ "fingerprint",
50
+ "openclaw-probe",
51
+ ]);
52
+ const VALID_CLIENT_MODES = new Set([
53
+ "webchat",
54
+ "cli",
55
+ "ui",
56
+ "backend",
57
+ "node",
58
+ "probe",
59
+ "test",
60
+ ]);
61
+ let memoryDeviceAuthStore = null;
62
+ function makeId() {
63
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
64
+ return crypto.randomUUID();
65
+ }
66
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
67
+ const random = Math.random() * 16 | 0;
68
+ return (char === "x" ? random : (random & 0x3) | 0x8).toString(16);
69
+ });
70
+ }
71
+ function asRecord(value) {
72
+ return value && typeof value === "object" && !Array.isArray(value)
73
+ ? value
74
+ : null;
75
+ }
76
+ function asContentItems(value) {
77
+ if (!Array.isArray(value)) {
78
+ return [];
79
+ }
80
+ return value
81
+ .map((item) => asRecord(item))
82
+ .filter((item) => item !== null);
83
+ }
84
+ function isBrowserChatAttachment(attachment) {
85
+ return typeof attachment.dataUrl === "string";
86
+ }
87
+ function parseAttachmentDataUrl(dataUrl) {
88
+ const match = /^data:([^;]+);base64,(.+)$/i.exec(dataUrl.trim());
89
+ if (!match)
90
+ return null;
91
+ return { mimeType: match[1], content: match[2] };
92
+ }
93
+ export function normalizeChatAttachments(attachments) {
94
+ if (!attachments || attachments.length === 0)
95
+ return undefined;
96
+ return attachments.map((attachment) => {
97
+ if (!isBrowserChatAttachment(attachment)) {
98
+ return attachment;
99
+ }
100
+ const parsed = parseAttachmentDataUrl(attachment.dataUrl);
101
+ if (!parsed) {
102
+ throw new Error(`Invalid chat attachment dataUrl for mime type ${attachment.mimeType}`);
103
+ }
104
+ return {
105
+ type: "image",
106
+ mimeType: parsed.mimeType,
107
+ content: parsed.content,
108
+ ...(attachment.fileName ? { fileName: attachment.fileName } : {}),
109
+ };
110
+ });
111
+ }
112
+ function normalizeToolArgs(value) {
113
+ if (typeof value !== "string") {
114
+ return value;
115
+ }
116
+ const trimmed = value.trim();
117
+ if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
118
+ return value;
119
+ }
120
+ try {
121
+ return JSON.parse(trimmed);
122
+ }
123
+ catch {
124
+ return value;
125
+ }
126
+ }
127
+ function stringifyToolResult(value) {
128
+ if (value == null) {
129
+ return undefined;
130
+ }
131
+ if (typeof value === "string") {
132
+ return value;
133
+ }
134
+ try {
135
+ return JSON.stringify(value, null, 2);
136
+ }
137
+ catch {
138
+ return String(value);
139
+ }
140
+ }
141
+ function gatewayToolCallId(record) {
142
+ const direct = (typeof record.id === "string" && record.id.trim()) ||
143
+ (typeof record.toolCallId === "string" && record.toolCallId.trim()) ||
144
+ (typeof record.tool_call_id === "string" && record.tool_call_id.trim());
145
+ return direct || undefined;
146
+ }
147
+ function gatewayToolName(record) {
148
+ const direct = (typeof record.name === "string" && record.name.trim()) ||
149
+ (typeof record.toolName === "string" && record.toolName.trim()) ||
150
+ (typeof record.tool_name === "string" && record.tool_name.trim());
151
+ return direct || undefined;
152
+ }
153
+ function mergeGatewayToolResult(toolCalls, result) {
154
+ const next = [...toolCalls];
155
+ let index = -1;
156
+ for (let cursor = next.length - 1; cursor >= 0; cursor -= 1) {
157
+ const entry = next[cursor];
158
+ if (result.id && entry.id && entry.id === result.id) {
159
+ index = cursor;
160
+ break;
161
+ }
162
+ if (result.name && entry.name === result.name && entry.result == null) {
163
+ index = cursor;
164
+ break;
165
+ }
166
+ }
167
+ if (index >= 0) {
168
+ const current = next[index];
169
+ next[index] = {
170
+ ...current,
171
+ ...(result.id ? { id: result.id } : {}),
172
+ ...(result.result !== undefined ? { result: result.result } : {}),
173
+ };
174
+ return next;
175
+ }
176
+ next.push(result);
177
+ return next;
178
+ }
179
+ export function extractGatewayChatThinking(message) {
180
+ const record = asRecord(message);
181
+ if (!record) {
182
+ return "";
183
+ }
184
+ const parts = asContentItems(record.content)
185
+ .map((item) => {
186
+ if (item.type !== "thinking" || typeof item.thinking !== "string") {
187
+ return null;
188
+ }
189
+ return item.thinking.trim();
190
+ })
191
+ .filter((value) => Boolean(value));
192
+ return parts.join("\n");
193
+ }
194
+ export function extractGatewayChatMediaUrls(message) {
195
+ const record = asRecord(message);
196
+ if (!record) {
197
+ return [];
198
+ }
199
+ const mediaUrls = [];
200
+ for (const item of asContentItems(record.content)) {
201
+ if (item.type !== "image") {
202
+ continue;
203
+ }
204
+ const source = asRecord(item.source);
205
+ if (!source) {
206
+ continue;
207
+ }
208
+ if (source.type === "url" && typeof source.url === "string" && source.url.trim()) {
209
+ mediaUrls.push(source.url);
210
+ continue;
211
+ }
212
+ if (source.type === "base64" && typeof source.data === "string" && source.data.trim()) {
213
+ const mimeType = typeof source.media_type === "string" && source.media_type.trim()
214
+ ? source.media_type.trim()
215
+ : "image/png";
216
+ mediaUrls.push(`data:${mimeType};base64,${source.data}`);
217
+ }
218
+ }
219
+ if (typeof record.mediaUrl === "string" && record.mediaUrl.trim()) {
220
+ mediaUrls.push(record.mediaUrl);
221
+ }
222
+ if (Array.isArray(record.mediaUrls)) {
223
+ for (const entry of record.mediaUrls) {
224
+ if (typeof entry === "string" && entry.trim()) {
225
+ mediaUrls.push(entry);
226
+ }
227
+ }
228
+ }
229
+ return mediaUrls;
230
+ }
231
+ export function extractGatewayChatToolCalls(message) {
232
+ const record = asRecord(message);
233
+ if (!record) {
234
+ return [];
235
+ }
236
+ let toolCalls = [];
237
+ for (const item of asContentItems(record.content)) {
238
+ const kind = typeof item.type === "string" ? item.type.trim().toLowerCase() : "";
239
+ const name = gatewayToolName(item);
240
+ const id = gatewayToolCallId(item);
241
+ if (kind === "toolcall" ||
242
+ kind === "tool_call" ||
243
+ kind === "tooluse" ||
244
+ kind === "tool_use" ||
245
+ (name && (item.arguments !== undefined || item.args !== undefined))) {
246
+ toolCalls.push({
247
+ ...(id ? { id } : {}),
248
+ name: name ?? "tool",
249
+ args: normalizeToolArgs(item.arguments ?? item.args),
250
+ });
251
+ continue;
252
+ }
253
+ if (kind === "toolresult" || kind === "tool_result") {
254
+ toolCalls = mergeGatewayToolResult(toolCalls, {
255
+ ...(id ? { id } : {}),
256
+ name: name ?? "tool",
257
+ result: stringifyToolResult(item.text ?? item.content ?? item.result),
258
+ });
259
+ }
260
+ }
261
+ if (Array.isArray(record.tool_calls)) {
262
+ for (const item of record.tool_calls) {
263
+ const tool = asRecord(item);
264
+ if (!tool)
265
+ continue;
266
+ const name = gatewayToolName(tool);
267
+ if (!name)
268
+ continue;
269
+ toolCalls.push({
270
+ ...(gatewayToolCallId(tool) ? { id: gatewayToolCallId(tool) } : {}),
271
+ name,
272
+ args: normalizeToolArgs(tool.arguments ?? tool.args),
273
+ });
274
+ }
275
+ }
276
+ const topLevelToolName = gatewayToolName(record);
277
+ const topLevelResult = stringifyToolResult(record.result ?? record.content ?? record.text ?? record.partialResult);
278
+ const role = typeof record.role === "string" ? record.role.trim().toLowerCase() : "";
279
+ if (topLevelToolName &&
280
+ topLevelResult &&
281
+ (role === "toolresult" || role === "tool_result" || record.toolCallId || record.tool_call_id)) {
282
+ toolCalls = mergeGatewayToolResult(toolCalls, {
283
+ ...(gatewayToolCallId(record) ? { id: gatewayToolCallId(record) } : {}),
284
+ name: topLevelToolName,
285
+ result: topLevelResult,
286
+ });
287
+ }
288
+ return toolCalls;
289
+ }
290
+ export function normalizeGatewayChatMessage(message) {
291
+ const record = asRecord(message);
292
+ if (!record) {
293
+ return null;
294
+ }
295
+ const text = extractMessageText(record) ?? "";
296
+ const thinking = extractGatewayChatThinking(record);
297
+ const toolCalls = extractGatewayChatToolCalls(record);
298
+ const mediaUrls = extractGatewayChatMediaUrls(record);
299
+ const timestamp = typeof record.timestamp === "number" ? record.timestamp : undefined;
300
+ const role = typeof record.role === "string" && record.role.trim() ? record.role : "assistant";
301
+ if (!text && !thinking && toolCalls.length === 0 && mediaUrls.length === 0) {
302
+ return null;
303
+ }
304
+ return {
305
+ role,
306
+ text,
307
+ thinking,
308
+ toolCalls,
309
+ mediaUrls,
310
+ ...(timestamp !== undefined ? { timestamp } : {}),
311
+ };
312
+ }
313
+ function extractMessageText(message) {
314
+ if (typeof message === "string") {
315
+ return message;
316
+ }
317
+ const record = asRecord(message);
318
+ if (!record) {
319
+ return null;
320
+ }
321
+ const content = record.content;
322
+ if (typeof content === "string") {
323
+ return content;
324
+ }
325
+ if (Array.isArray(content)) {
326
+ const parts = content
327
+ .map((entry) => {
328
+ const item = asRecord(entry);
329
+ if (!item || item.type !== "text" || typeof item.text !== "string" || !item.text) {
330
+ return null;
331
+ }
332
+ return item.text;
333
+ })
334
+ .filter((value) => typeof value === "string");
335
+ if (parts.length > 0) {
336
+ return parts.join("\n");
337
+ }
338
+ }
339
+ return typeof record.text === "string" ? record.text : null;
340
+ }
341
+ function extractMessageRunId(message) {
342
+ const record = asRecord(message);
343
+ if (!record) {
344
+ return null;
345
+ }
346
+ const directRunId = typeof record.runId === "string" ? record.runId.trim() : "";
347
+ if (directRunId) {
348
+ return directRunId;
349
+ }
350
+ const agentRunId = typeof record.agentRunId === "string" ? record.agentRunId.trim() : "";
351
+ if (agentRunId) {
352
+ return agentRunId;
353
+ }
354
+ const meta = asRecord(record.meta);
355
+ const metaRunId = typeof meta?.runId === "string" ? meta.runId.trim() : "";
356
+ return metaRunId || null;
357
+ }
358
+ function latestHistoryAssistantText(messages, acceptedRunIds) {
359
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
360
+ const message = asRecord(messages[index]);
361
+ if (!message) {
362
+ continue;
363
+ }
364
+ const role = typeof message.role === "string" ? message.role.trim().toLowerCase() : "";
365
+ if (role !== "assistant") {
366
+ continue;
367
+ }
368
+ const messageRunId = extractMessageRunId(message);
369
+ if (messageRunId && acceptedRunIds.size > 0 && !acceptedRunIds.has(messageRunId)) {
370
+ continue;
371
+ }
372
+ const text = extractMessageText(message)?.trim();
373
+ if (text) {
374
+ return text;
375
+ }
376
+ }
377
+ return null;
378
+ }
379
+ function streamDelta(previousText, nextText) {
380
+ if (!nextText) {
381
+ return { delta: "", nextText: previousText };
382
+ }
383
+ if (previousText && nextText.startsWith(previousText)) {
384
+ return { delta: nextText.slice(previousText.length), nextText };
385
+ }
386
+ if (nextText === previousText) {
387
+ return { delta: "", nextText: previousText };
388
+ }
389
+ return { delta: nextText, nextText };
390
+ }
391
+ function parseAgentSessionKey(sessionKey) {
392
+ const normalized = (sessionKey ?? "").trim().toLowerCase();
393
+ if (!normalized) {
394
+ return null;
395
+ }
396
+ const parts = normalized.split(":").filter(Boolean);
397
+ if (parts.length < 3 || parts[0] !== "agent") {
398
+ return null;
399
+ }
400
+ const agentId = parts[1]?.trim();
401
+ const rest = parts.slice(2).join(":").trim();
402
+ if (!agentId || !rest) {
403
+ return null;
404
+ }
405
+ return { agentId, rest };
406
+ }
407
+ function sameSessionKey(left, right) {
408
+ const normalizedLeft = (left ?? "").trim().toLowerCase();
409
+ const normalizedRight = (right ?? "").trim().toLowerCase();
410
+ if (!normalizedLeft || !normalizedRight) {
411
+ return false;
412
+ }
413
+ if (normalizedLeft === normalizedRight) {
414
+ return true;
415
+ }
416
+ const parsedLeft = parseAgentSessionKey(normalizedLeft);
417
+ const parsedRight = parseAgentSessionKey(normalizedRight);
418
+ if (parsedLeft && parsedRight) {
419
+ return parsedLeft.agentId === parsedRight.agentId && parsedLeft.rest === parsedRight.rest;
420
+ }
421
+ if (parsedLeft) {
422
+ return parsedLeft.rest === normalizedRight;
423
+ }
424
+ if (parsedRight) {
425
+ return normalizedLeft === parsedRight.rest;
426
+ }
427
+ return false;
428
+ }
429
+ function splitConfigPath(path) {
430
+ return path
431
+ .split(".")
432
+ .map((part) => part.trim())
433
+ .filter(Boolean)
434
+ .flatMap((part) => part.endsWith("[]") && part !== "[]"
435
+ ? [part.slice(0, -2), "[]"]
436
+ : [part]);
437
+ }
438
+ export function normalizeOpenClawConfigSchemaNode(value) {
439
+ const schema = asRecord(value) ?? {};
440
+ const oneOf = Array.isArray(schema.oneOf) ? schema.oneOf : [];
441
+ const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : [];
442
+ const union = [...oneOf, ...anyOf];
443
+ if (union.length === 0)
444
+ return schema;
445
+ const primary = union.find((entry) => {
446
+ const obj = asRecord(entry);
447
+ if (!obj)
448
+ return false;
449
+ const t = obj.type;
450
+ if (typeof t === "string")
451
+ return t !== "null";
452
+ if (Array.isArray(t))
453
+ return t.some((v) => v !== "null");
454
+ return true;
455
+ });
456
+ return asRecord(primary) ?? schema;
457
+ }
458
+ export function describeOpenClawConfigNode(value) {
459
+ const schema = normalizeOpenClawConfigSchemaNode(value);
460
+ const rawType = schema.type;
461
+ const type = Array.isArray(rawType)
462
+ ? rawType.find((entry) => entry !== "null")
463
+ : (typeof rawType === "string" ? rawType : undefined);
464
+ const rawProperties = asRecord(schema.properties) ?? {};
465
+ const properties = {};
466
+ for (const [key, child] of Object.entries(rawProperties)) {
467
+ const childSchema = asRecord(child);
468
+ if (childSchema) {
469
+ properties[key] = childSchema;
470
+ }
471
+ }
472
+ const additionalPropertySchema = asRecord(schema.additionalProperties);
473
+ const additionalProperties = schema.additionalProperties === true || Boolean(additionalPropertySchema);
474
+ return {
475
+ schema,
476
+ type,
477
+ properties,
478
+ additionalProperties,
479
+ additionalPropertySchema,
480
+ isDynamicMap: (type === "object" || Object.keys(properties).length > 0 || additionalProperties) &&
481
+ additionalProperties,
482
+ };
483
+ }
484
+ export function createOpenClawConfigValue(value) {
485
+ const descriptor = describeOpenClawConfigNode(value);
486
+ switch (descriptor.type) {
487
+ case "object":
488
+ return {};
489
+ case "array":
490
+ return [];
491
+ case "boolean":
492
+ return false;
493
+ case "number":
494
+ case "integer":
495
+ return 0;
496
+ default:
497
+ return "";
498
+ }
499
+ }
500
+ function resolveSchemaRef(ref, root) {
501
+ // Handle JSON Pointer style refs: #/$defs/Name, #/definitions/Name
502
+ if (!ref.startsWith('#/'))
503
+ return null;
504
+ const parts = ref.slice(2).split('/');
505
+ let cursor = root;
506
+ for (const part of parts) {
507
+ cursor = asRecord(cursor)?.[part];
508
+ if (cursor === undefined || cursor === null)
509
+ return null;
510
+ }
511
+ return asRecord(cursor);
512
+ }
513
+ function mergeAllOfSchemas(schemas) {
514
+ const merged = {};
515
+ for (const schema of schemas) {
516
+ for (const [key, value] of Object.entries(schema)) {
517
+ if (key === 'properties') {
518
+ merged.properties = { ...(asRecord(merged.properties) ?? {}), ...(asRecord(value) ?? {}) };
519
+ }
520
+ else if (key === 'required' && Array.isArray(value)) {
521
+ merged.required = [...(Array.isArray(merged.required) ? merged.required : []), ...value];
522
+ }
523
+ else if (!(key in merged)) {
524
+ merged[key] = value;
525
+ }
526
+ }
527
+ }
528
+ return merged;
529
+ }
530
+ function resolveSchemaNode(node, root, visited) {
531
+ // Resolve $ref
532
+ const ref = typeof node.$ref === 'string' ? node.$ref : null;
533
+ if (ref) {
534
+ if (visited.has(ref))
535
+ return node; // circular reference guard
536
+ visited.add(ref);
537
+ const resolved = resolveSchemaRef(ref, root);
538
+ if (resolved) {
539
+ // Merge any sibling properties (e.g. title, description) with the resolved ref
540
+ const { $ref: _, ...siblings } = node;
541
+ const resolvedNode = resolveSchemaNode(resolved, root, visited);
542
+ return Object.keys(siblings).length > 0
543
+ ? { ...resolvedNode, ...siblings, ...(resolvedNode.properties && siblings.properties ? { properties: { ...resolvedNode.properties, ...siblings.properties } } : {}) }
544
+ : resolvedNode;
545
+ }
546
+ }
547
+ // Resolve allOf
548
+ const allOf = Array.isArray(node.allOf) ? node.allOf : null;
549
+ if (allOf) {
550
+ const resolved = allOf
551
+ .map((entry) => asRecord(entry))
552
+ .filter((entry) => entry !== null)
553
+ .map((entry) => resolveSchemaNode(entry, root, new Set(visited)));
554
+ const { allOf: _, ...rest } = node;
555
+ const merged = mergeAllOfSchemas([...resolved, rest]);
556
+ return merged;
557
+ }
558
+ // Recursively resolve properties
559
+ const props = asRecord(node.properties);
560
+ if (props) {
561
+ const resolvedProps = {};
562
+ for (const [key, child] of Object.entries(props)) {
563
+ const childSchema = asRecord(child);
564
+ if (childSchema) {
565
+ resolvedProps[key] = resolveSchemaNode(childSchema, root, new Set(visited));
566
+ }
567
+ else {
568
+ resolvedProps[key] = child;
569
+ }
570
+ }
571
+ return { ...node, properties: resolvedProps };
572
+ }
573
+ // Resolve additionalProperties if it's a schema object
574
+ const additionalProps = asRecord(node.additionalProperties);
575
+ if (additionalProps) {
576
+ return { ...node, additionalProperties: resolveSchemaNode(additionalProps, root, new Set(visited)) };
577
+ }
578
+ // Resolve items (for array schemas)
579
+ const items = asRecord(node.items);
580
+ if (items) {
581
+ return { ...node, items: resolveSchemaNode(items, root, new Set(visited)) };
582
+ }
583
+ return node;
584
+ }
585
+ function resolveSchemaRefs(schema) {
586
+ return resolveSchemaNode(schema, schema, new Set());
587
+ }
588
+ export function normalizeOpenClawConfigSchema(value) {
589
+ const raw = asRecord(value);
590
+ if (!raw)
591
+ return null;
592
+ const wrappedSchema = asRecord(raw.schema);
593
+ const uiHints = asRecord(raw.uiHints) ?? {};
594
+ const resolvedSchema = wrappedSchema
595
+ ? resolveSchemaRefs(wrappedSchema)
596
+ : resolveSchemaRefs(raw);
597
+ const normalized = {
598
+ schema: resolvedSchema,
599
+ uiHints: uiHints,
600
+ };
601
+ if (typeof raw.version === "string") {
602
+ normalized.version = raw.version;
603
+ }
604
+ if (typeof raw.generatedAt === "string") {
605
+ normalized.generatedAt = raw.generatedAt;
606
+ }
607
+ return normalized;
608
+ }
609
+ export function resolveOpenClawConfigUiHint(source, path) {
610
+ if (!path.trim())
611
+ return null;
612
+ const sourceRecord = asRecord(source);
613
+ const uiHints = sourceRecord && asRecord(sourceRecord.uiHints)
614
+ ? sourceRecord.uiHints
615
+ : sourceRecord;
616
+ if (!uiHints || typeof uiHints !== "object")
617
+ return null;
618
+ const targetParts = splitConfigPath(path);
619
+ let best = null;
620
+ for (const [hintPath, hint] of Object.entries(uiHints)) {
621
+ const hintParts = splitConfigPath(hintPath);
622
+ if (hintParts.length !== targetParts.length)
623
+ continue;
624
+ let wildcardCount = 0;
625
+ let matches = true;
626
+ for (let index = 0; index < hintParts.length; index += 1) {
627
+ const hintPart = hintParts[index];
628
+ const targetPart = targetParts[index];
629
+ if (hintPart === targetPart)
630
+ continue;
631
+ if (hintPart === "*" || hintPart === "[]") {
632
+ wildcardCount += 1;
633
+ continue;
634
+ }
635
+ matches = false;
636
+ break;
637
+ }
638
+ if (!matches)
639
+ continue;
640
+ if (!best || wildcardCount < best.wildcardCount) {
641
+ best = { path: hintPath, hint, wildcardCount };
642
+ }
643
+ }
644
+ return best ? { path: best.path, hint: best.hint } : null;
645
+ }
646
+ function normalizeClientId(value) {
647
+ const normalized = value?.trim().toLowerCase();
648
+ return normalized && VALID_CLIENT_IDS.has(normalized) ? normalized : DEFAULT_CLIENT_ID;
649
+ }
650
+ function normalizeClientMode(value) {
651
+ const normalized = value?.trim().toLowerCase();
652
+ return normalized && VALID_CLIENT_MODES.has(normalized) ? normalized : DEFAULT_CLIENT_MODE;
653
+ }
654
+ function getNavigatorLike() {
655
+ const maybeNavigator = globalThis.navigator;
656
+ return maybeNavigator ?? null;
657
+ }
658
+ function resolvePlatform(value) {
659
+ const normalized = value?.trim();
660
+ if (normalized)
661
+ return normalized;
662
+ const browserNavigator = getNavigatorLike();
663
+ if (browserNavigator?.platform)
664
+ return browserNavigator.platform;
665
+ if (typeof process !== "undefined" && process.platform)
666
+ return process.platform;
667
+ return "web";
668
+ }
669
+ function inferBrowserName(userAgent) {
670
+ const ua = userAgent ?? "";
671
+ if (!ua)
672
+ return null;
673
+ if (/Firefox\//i.test(ua))
674
+ return "Firefox";
675
+ if (/Edg\//i.test(ua))
676
+ return "Edge";
677
+ if (/Chrome\//i.test(ua) && !/Edg\//i.test(ua))
678
+ return "Chrome";
679
+ if (/Safari\//i.test(ua) && !/Chrome\//i.test(ua))
680
+ return "Safari";
681
+ return null;
682
+ }
683
+ function inferPlatformName(platform) {
684
+ const normalized = platform.trim().toLowerCase();
685
+ if (!normalized)
686
+ return null;
687
+ if (normalized.includes("mac"))
688
+ return "macOS";
689
+ if (normalized.includes("win"))
690
+ return "Windows";
691
+ if (normalized.includes("linux"))
692
+ return "Linux";
693
+ if (normalized.includes("iphone") || normalized.includes("ipad") || normalized.includes("ios")) {
694
+ return "iOS";
695
+ }
696
+ if (normalized.includes("android"))
697
+ return "Android";
698
+ return platform.trim();
699
+ }
700
+ function resolveBrowserHost() {
701
+ const browserWindow = globalThis.window;
702
+ const hostname = browserWindow?.location?.hostname?.trim();
703
+ return hostname || null;
704
+ }
705
+ function resolveClientDisplayName(value, platform) {
706
+ const provided = value?.trim();
707
+ if (provided)
708
+ return provided;
709
+ const browserName = inferBrowserName(resolveUserAgent());
710
+ const platformName = inferPlatformName(platform);
711
+ const host = resolveBrowserHost();
712
+ const details = [browserName, platformName ? `on ${platformName}` : null]
713
+ .filter(Boolean)
714
+ .join(" ");
715
+ if (details && host) {
716
+ return `Hyper Agent Web (${details}, ${host})`;
717
+ }
718
+ if (details) {
719
+ return `Hyper Agent Web (${details})`;
720
+ }
721
+ if (host) {
722
+ return `Hyper Agent Web (${host})`;
723
+ }
724
+ return "Hyper Agent Web";
725
+ }
726
+ function normalizeScopes(scopes) {
727
+ if (!Array.isArray(scopes)) {
728
+ return [];
729
+ }
730
+ const unique = new Set();
731
+ for (const scope of scopes) {
732
+ const normalized = scope.trim();
733
+ if (normalized)
734
+ unique.add(normalized);
735
+ }
736
+ return [...unique].sort();
737
+ }
738
+ function getStorage() {
739
+ const storage = globalThis.localStorage;
740
+ return storage ?? null;
741
+ }
742
+ function readDeviceAuthStore() {
743
+ const storage = getStorage();
744
+ if (!storage) {
745
+ return memoryDeviceAuthStore;
746
+ }
747
+ try {
748
+ const raw = storage.getItem(STORAGE_KEY);
749
+ if (!raw)
750
+ return null;
751
+ const parsed = JSON.parse(raw);
752
+ return parsed?.version === 1 && typeof parsed === "object" && !Array.isArray(parsed)
753
+ ? parsed
754
+ : null;
755
+ }
756
+ catch {
757
+ return null;
758
+ }
759
+ }
760
+ function writeDeviceAuthStore(store) {
761
+ const normalized = {
762
+ version: 1,
763
+ ...(store.deviceId ? { deviceId: store.deviceId } : {}),
764
+ ...(store.publicKey ? { publicKey: store.publicKey } : {}),
765
+ ...(store.privateKey ? { privateKey: store.privateKey } : {}),
766
+ ...(typeof store.createdAtMs === "number" ? { createdAtMs: store.createdAtMs } : {}),
767
+ ...(store.tokens ? { tokens: store.tokens } : {}),
768
+ ...(store.pendingPairings ? { pendingPairings: store.pendingPairings } : {}),
769
+ };
770
+ const storage = getStorage();
771
+ if (!storage) {
772
+ memoryDeviceAuthStore = normalized;
773
+ return;
774
+ }
775
+ try {
776
+ storage.setItem(STORAGE_KEY, JSON.stringify(normalized));
777
+ }
778
+ catch {
779
+ memoryDeviceAuthStore = normalized;
780
+ }
781
+ }
782
+ function storageScopeKey(scope, role) {
783
+ return `${scope.trim()}|${role.trim()}`;
784
+ }
785
+ function loadStoredDeviceToken(deviceId, scope, role) {
786
+ const store = readDeviceAuthStore();
787
+ if (!store || store.deviceId !== deviceId || !store.tokens)
788
+ return null;
789
+ const entry = store.tokens[storageScopeKey(scope, role)];
790
+ return entry && typeof entry.token === "string" ? entry : null;
791
+ }
792
+ function storeStoredDeviceToken(params) {
793
+ const role = params.role.trim();
794
+ const key = storageScopeKey(params.scope, role);
795
+ const existing = readDeviceAuthStore();
796
+ const next = {
797
+ version: 1,
798
+ ...(existing?.deviceId ? { deviceId: existing.deviceId } : {}),
799
+ ...(existing?.publicKey ? { publicKey: existing.publicKey } : {}),
800
+ ...(existing?.privateKey ? { privateKey: existing.privateKey } : {}),
801
+ ...(typeof existing?.createdAtMs === "number" ? { createdAtMs: existing.createdAtMs } : {}),
802
+ ...(existing?.pendingPairings ? { pendingPairings: existing.pendingPairings } : {}),
803
+ tokens: {
804
+ ...(existing?.tokens ?? {}),
805
+ [key]: {
806
+ token: params.token,
807
+ role,
808
+ scopes: normalizeScopes(params.scopes),
809
+ updatedAtMs: Date.now(),
810
+ ...(params.gatewayUrl ? { gatewayUrl: params.gatewayUrl } : {}),
811
+ },
812
+ },
813
+ };
814
+ if (!next.deviceId) {
815
+ next.deviceId = params.deviceId;
816
+ }
817
+ writeDeviceAuthStore(next);
818
+ return next.tokens?.[key];
819
+ }
820
+ function clearStoredDeviceToken(deviceId, scope, role) {
821
+ const store = readDeviceAuthStore();
822
+ if (!store || store.deviceId !== deviceId || !store.tokens)
823
+ return;
824
+ const key = storageScopeKey(scope, role);
825
+ if (!store.tokens[key])
826
+ return;
827
+ const nextTokens = { ...store.tokens };
828
+ delete nextTokens[key];
829
+ writeDeviceAuthStore({
830
+ version: 1,
831
+ ...(store.deviceId ? { deviceId: store.deviceId } : {}),
832
+ ...(store.publicKey ? { publicKey: store.publicKey } : {}),
833
+ ...(store.privateKey ? { privateKey: store.privateKey } : {}),
834
+ ...(typeof store.createdAtMs === "number" ? { createdAtMs: store.createdAtMs } : {}),
835
+ ...(store.pendingPairings ? { pendingPairings: store.pendingPairings } : {}),
836
+ tokens: nextTokens,
837
+ });
838
+ }
839
+ function pairingStoreKey(scope, role) {
840
+ return storageScopeKey(scope, role);
841
+ }
842
+ function loadPendingPairing(scope, role) {
843
+ const store = readDeviceAuthStore();
844
+ const key = pairingStoreKey(scope, role);
845
+ return store?.pendingPairings?.[key] ?? null;
846
+ }
847
+ function storePendingPairing(pairing, scope) {
848
+ const existing = readDeviceAuthStore();
849
+ const key = pairingStoreKey(scope, pairing.role);
850
+ writeDeviceAuthStore({
851
+ version: 1,
852
+ ...(existing?.deviceId ? { deviceId: existing.deviceId } : {}),
853
+ ...(existing?.publicKey ? { publicKey: existing.publicKey } : {}),
854
+ ...(existing?.privateKey ? { privateKey: existing.privateKey } : {}),
855
+ ...(typeof existing?.createdAtMs === "number" ? { createdAtMs: existing.createdAtMs } : {}),
856
+ ...(existing?.tokens ? { tokens: existing.tokens } : {}),
857
+ pendingPairings: {
858
+ ...(existing?.pendingPairings ?? {}),
859
+ [key]: pairing,
860
+ },
861
+ });
862
+ return pairing;
863
+ }
864
+ function clearPendingPairing(scope, role) {
865
+ const store = readDeviceAuthStore();
866
+ if (!store?.pendingPairings)
867
+ return;
868
+ const key = pairingStoreKey(scope, role);
869
+ if (!store.pendingPairings[key])
870
+ return;
871
+ const nextPendingPairings = { ...store.pendingPairings };
872
+ delete nextPendingPairings[key];
873
+ writeDeviceAuthStore({
874
+ version: 1,
875
+ ...(store.deviceId ? { deviceId: store.deviceId } : {}),
876
+ ...(store.publicKey ? { publicKey: store.publicKey } : {}),
877
+ ...(store.privateKey ? { privateKey: store.privateKey } : {}),
878
+ ...(typeof store.createdAtMs === "number" ? { createdAtMs: store.createdAtMs } : {}),
879
+ ...(store.tokens ? { tokens: store.tokens } : {}),
880
+ ...(Object.keys(nextPendingPairings).length > 0 ? { pendingPairings: nextPendingPairings } : {}),
881
+ });
882
+ }
883
+ function bytesToHex(bytes) {
884
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
885
+ }
886
+ function bytesToBase64Url(bytes) {
887
+ if (typeof Buffer !== "undefined") {
888
+ return Buffer.from(bytes)
889
+ .toString("base64")
890
+ .replace(/\+/g, "-")
891
+ .replace(/\//g, "_")
892
+ .replace(/=+$/g, "");
893
+ }
894
+ const encoder = globalThis;
895
+ if (typeof encoder.btoa !== "function") {
896
+ throw new Error("base64 encoder unavailable");
897
+ }
898
+ let binary = "";
899
+ for (const byte of bytes) {
900
+ binary += String.fromCharCode(byte);
901
+ }
902
+ return encoder
903
+ .btoa(binary)
904
+ .replace(/\+/g, "-")
905
+ .replace(/\//g, "_")
906
+ .replace(/=+$/g, "");
907
+ }
908
+ function base64UrlToBytes(value) {
909
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
910
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
911
+ if (typeof Buffer !== "undefined") {
912
+ return new Uint8Array(Buffer.from(padded, "base64"));
913
+ }
914
+ const decoder = globalThis;
915
+ if (typeof decoder.atob !== "function") {
916
+ throw new Error("base64 decoder unavailable");
917
+ }
918
+ const binary = decoder.atob(padded);
919
+ const output = new Uint8Array(binary.length);
920
+ for (let index = 0; index < binary.length; index += 1) {
921
+ output[index] = binary.charCodeAt(index);
922
+ }
923
+ return output;
924
+ }
925
+ async function sha256Hex(bytes) {
926
+ if (!globalThis.crypto?.subtle) {
927
+ throw new Error("crypto.subtle is required for device auth");
928
+ }
929
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
930
+ return bytesToHex(new Uint8Array(digest));
931
+ }
932
+ async function loadOrCreateDeviceIdentity() {
933
+ const store = readDeviceAuthStore();
934
+ if (store?.version === 1 &&
935
+ typeof store.deviceId === "string" &&
936
+ typeof store.publicKey === "string" &&
937
+ typeof store.privateKey === "string") {
938
+ const derivedId = await sha256Hex(base64UrlToBytes(store.publicKey));
939
+ if (derivedId !== store.deviceId) {
940
+ writeDeviceAuthStore({
941
+ ...store,
942
+ version: 1,
943
+ deviceId: derivedId,
944
+ });
945
+ return {
946
+ deviceId: derivedId,
947
+ publicKey: store.publicKey,
948
+ privateKey: store.privateKey,
949
+ createdAtMs: store.createdAtMs,
950
+ };
951
+ }
952
+ return {
953
+ deviceId: store.deviceId,
954
+ publicKey: store.publicKey,
955
+ privateKey: store.privateKey,
956
+ createdAtMs: store.createdAtMs,
957
+ };
958
+ }
959
+ const privateKeyBytes = edUtils.randomSecretKey();
960
+ const publicKeyBytes = await getPublicKeyAsync(privateKeyBytes);
961
+ const deviceId = await sha256Hex(publicKeyBytes);
962
+ const identity = {
963
+ deviceId,
964
+ publicKey: bytesToBase64Url(publicKeyBytes),
965
+ privateKey: bytesToBase64Url(privateKeyBytes),
966
+ createdAtMs: Date.now(),
967
+ };
968
+ writeDeviceAuthStore({
969
+ version: 1,
970
+ ...identity,
971
+ ...(store?.tokens ? { tokens: store.tokens } : {}),
972
+ });
973
+ return identity;
974
+ }
975
+ async function signDevicePayload(privateKey, payload) {
976
+ const signature = await signAsync(new TextEncoder().encode(payload), base64UrlToBytes(privateKey));
977
+ return bytesToBase64Url(signature);
978
+ }
979
+ function buildDeviceAuthPayload(params) {
980
+ return [
981
+ "v2",
982
+ params.deviceId,
983
+ params.clientId,
984
+ params.clientMode,
985
+ params.role,
986
+ params.scopes.join(","),
987
+ String(params.signedAtMs),
988
+ params.token ?? "",
989
+ params.nonce,
990
+ ].join("|");
991
+ }
992
+ function toCloseError(error) {
993
+ if (error instanceof GatewayRequestError) {
994
+ return {
995
+ code: error.gatewayCode,
996
+ message: error.message,
997
+ details: error.details,
998
+ };
999
+ }
1000
+ if (error instanceof Error) {
1001
+ return {
1002
+ code: "UNAVAILABLE",
1003
+ message: error.message,
1004
+ };
1005
+ }
1006
+ return null;
1007
+ }
1008
+ function readConnectErrorCode(error) {
1009
+ if (!(error instanceof GatewayRequestError))
1010
+ return null;
1011
+ const details = error.details;
1012
+ if (!details || typeof details !== "object" || Array.isArray(details))
1013
+ return null;
1014
+ const code = details.code;
1015
+ return typeof code === "string" && code.trim() ? code.trim() : null;
1016
+ }
1017
+ function readConnectPairingRequestId(error) {
1018
+ if (!(error instanceof GatewayRequestError))
1019
+ return null;
1020
+ const details = error.details;
1021
+ if (!details || typeof details !== "object" || Array.isArray(details))
1022
+ return null;
1023
+ const requestId = details.requestId;
1024
+ return typeof requestId === "string" && requestId.trim() ? requestId.trim() : null;
1025
+ }
1026
+ function isSocketOpen(ws) {
1027
+ return Boolean(ws && ws.readyState === 1);
1028
+ }
1029
+ function resolveUserAgent() {
1030
+ return getNavigatorLike()?.userAgent;
1031
+ }
1032
+ function resolveLocale() {
1033
+ return getNavigatorLike()?.language;
1034
+ }
1035
+ export class GatewayClient {
1036
+ url;
1037
+ token;
1038
+ gatewayToken;
1039
+ deploymentId;
1040
+ apiKey;
1041
+ apiBase;
1042
+ autoApprovePairing;
1043
+ clientId;
1044
+ clientMode;
1045
+ clientDisplayName;
1046
+ clientVersion;
1047
+ clientPlatform;
1048
+ clientInstanceId;
1049
+ caps;
1050
+ origin;
1051
+ defaultTimeout;
1052
+ ws = null;
1053
+ pending = new Map();
1054
+ eventHandlers = new Set();
1055
+ connected = false;
1056
+ closed = false;
1057
+ reconnectTimer = null;
1058
+ connectTimer = null;
1059
+ backoffMs = INITIAL_BACKOFF_MS;
1060
+ connectNonce = null;
1061
+ connectSent = false;
1062
+ pendingConnectError = null;
1063
+ pairingState = null;
1064
+ autoApproveAttemptedRequestIds = new Set();
1065
+ deviceTokenMismatchRetried = false;
1066
+ lastSeq = null;
1067
+ connectPromise = null;
1068
+ resolveConnectPromise = null;
1069
+ rejectConnectPromise = null;
1070
+ _version = null;
1071
+ _protocol = null;
1072
+ onDisconnect = null;
1073
+ constructor(options) {
1074
+ this.url = options.url;
1075
+ this.token = options.token?.trim() || undefined;
1076
+ this.gatewayToken = options.gatewayToken?.trim() || undefined;
1077
+ this.deploymentId = options.deploymentId?.trim() || undefined;
1078
+ this.apiKey = options.apiKey?.trim() || undefined;
1079
+ this.apiBase = options.apiBase?.trim().replace(/\/$/, "") || undefined;
1080
+ this.autoApprovePairing = options.autoApprovePairing === true;
1081
+ this.clientId = normalizeClientId(options.clientId);
1082
+ this.clientMode = normalizeClientMode(options.clientMode);
1083
+ this.clientVersion = options.clientVersion?.trim() || DEFAULT_CLIENT_VERSION;
1084
+ this.clientPlatform = resolvePlatform(options.platform);
1085
+ this.clientDisplayName = resolveClientDisplayName(options.clientDisplayName, this.clientPlatform);
1086
+ this.clientInstanceId = options.instanceId?.trim() || makeId();
1087
+ this.caps = Array.isArray(options.caps)
1088
+ ? options.caps.map((cap) => cap.trim()).filter(Boolean)
1089
+ : [...DEFAULT_CAPS];
1090
+ // Non-browser SDK clients should not send Origin by default. OpenClaw
1091
+ // treats any Origin header as browser-originated and applies browser
1092
+ // origin checks to the connection.
1093
+ this.origin = typeof options.origin === "string" && options.origin.trim()
1094
+ ? options.origin.trim()
1095
+ : undefined;
1096
+ this.defaultTimeout = options.timeout ?? DEFAULT_TIMEOUT;
1097
+ this.onHello = options.onHello;
1098
+ this.onClose = options.onClose;
1099
+ this.onGap = options.onGap;
1100
+ this.onPairing = options.onPairing;
1101
+ this.pairingState = loadPendingPairing(this.storageScope(), OPERATOR_ROLE);
1102
+ }
1103
+ onHello;
1104
+ onClose;
1105
+ onGap;
1106
+ onPairing;
1107
+ get version() {
1108
+ return this._version;
1109
+ }
1110
+ get protocol() {
1111
+ return this._protocol;
1112
+ }
1113
+ get isConnected() {
1114
+ return this.connected;
1115
+ }
1116
+ get pendingPairing() {
1117
+ return this.pairingState;
1118
+ }
1119
+ /** Update the gateway token for subsequent connect attempts. */
1120
+ setGatewayToken(token) {
1121
+ this.gatewayToken = token.trim() || undefined;
1122
+ }
1123
+ storageScope() {
1124
+ return this.deploymentId || this.url;
1125
+ }
1126
+ /** Subscribe to server-sent events */
1127
+ onEvent(handler) {
1128
+ this.eventHandlers.add(handler);
1129
+ return () => this.eventHandlers.delete(handler);
1130
+ }
1131
+ /** Connect and keep reconnecting until stopped */
1132
+ connect() {
1133
+ return this.start();
1134
+ }
1135
+ start() {
1136
+ this.closed = false;
1137
+ if (this.connected) {
1138
+ return Promise.resolve();
1139
+ }
1140
+ if (!this.connectPromise) {
1141
+ this.connectPromise = new Promise((resolve, reject) => {
1142
+ this.resolveConnectPromise = resolve;
1143
+ this.rejectConnectPromise = reject;
1144
+ });
1145
+ }
1146
+ this.openSocket();
1147
+ return this.connectPromise;
1148
+ }
1149
+ /** Close permanently and stop reconnecting */
1150
+ close() {
1151
+ this.stop();
1152
+ }
1153
+ stop() {
1154
+ this.closed = true;
1155
+ this.connected = false;
1156
+ this.connectSent = false;
1157
+ this.connectNonce = null;
1158
+ this.pendingConnectError = null;
1159
+ if (this.reconnectTimer) {
1160
+ clearTimeout(this.reconnectTimer);
1161
+ this.reconnectTimer = null;
1162
+ }
1163
+ if (this.connectTimer) {
1164
+ clearTimeout(this.connectTimer);
1165
+ this.connectTimer = null;
1166
+ }
1167
+ const ws = this.ws;
1168
+ this.ws = null;
1169
+ if (ws) {
1170
+ ws.close();
1171
+ }
1172
+ this.flushPending(new Error("gateway client stopped"));
1173
+ if (this.rejectConnectPromise) {
1174
+ this.rejectConnectPromise(new Error("gateway client stopped"));
1175
+ }
1176
+ this.connectPromise = null;
1177
+ this.resolveConnectPromise = null;
1178
+ this.rejectConnectPromise = null;
1179
+ }
1180
+ updatePairingState(pairing) {
1181
+ this.pairingState = pairing;
1182
+ if (pairing) {
1183
+ storePendingPairing(pairing, this.storageScope());
1184
+ }
1185
+ else {
1186
+ clearPendingPairing(this.storageScope(), OPERATOR_ROLE);
1187
+ }
1188
+ this.onPairing?.(pairing);
1189
+ }
1190
+ canAutoApprovePairing() {
1191
+ return Boolean(this.autoApprovePairing &&
1192
+ this.deploymentId &&
1193
+ this.apiKey &&
1194
+ this.apiBase &&
1195
+ typeof fetch === "function");
1196
+ }
1197
+ async approvePairingRequest(requestId) {
1198
+ if (!this.canAutoApprovePairing()) {
1199
+ throw new Error("autoApprovePairing requires deploymentId, apiKey, apiBase, and fetch()");
1200
+ }
1201
+ const command = `openclaw devices approve ${JSON.stringify(requestId)} --json`;
1202
+ const response = await fetch(`${this.apiBase}/deployments/${encodeURIComponent(this.deploymentId)}/exec`, {
1203
+ method: "POST",
1204
+ headers: {
1205
+ Authorization: `Bearer ${this.apiKey}`,
1206
+ "Content-Type": "application/json",
1207
+ },
1208
+ body: JSON.stringify({
1209
+ command,
1210
+ timeout: 30,
1211
+ }),
1212
+ });
1213
+ if (!response.ok) {
1214
+ const errorText = await response.text();
1215
+ throw new Error(`Pairing approval failed: ${response.status} ${errorText}`);
1216
+ }
1217
+ const payload = await response.json();
1218
+ if ((payload.exitCode ?? payload.exit_code ?? 1) !== 0) {
1219
+ throw new Error(payload.stderr?.trim() || payload.stdout?.trim() || "pairing approval command failed");
1220
+ }
1221
+ }
1222
+ openSocket() {
1223
+ if (this.closed || this.ws)
1224
+ return;
1225
+ const useBrowserSocket = "localStorage" in globalThis && typeof WebSocket !== "undefined";
1226
+ if (!useBrowserSocket && typeof NodeWebSocket === "undefined") {
1227
+ throw new Error("WebSocket is not available in this environment");
1228
+ }
1229
+ if (this.reconnectTimer) {
1230
+ clearTimeout(this.reconnectTimer);
1231
+ this.reconnectTimer = null;
1232
+ }
1233
+ const wsUrl = this.token
1234
+ ? `${this.url}${this.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.token)}`
1235
+ : this.url;
1236
+ const ws = useBrowserSocket
1237
+ ? new WebSocket(wsUrl)
1238
+ : new NodeWebSocket(wsUrl, this.origin ? { headers: { Origin: this.origin } } : undefined);
1239
+ this.ws = ws;
1240
+ if (useBrowserSocket) {
1241
+ ws.onopen = () => {
1242
+ this.queueConnect();
1243
+ };
1244
+ ws.onmessage = (event) => {
1245
+ this.handleMessage(String(event.data ?? ""));
1246
+ };
1247
+ ws.onerror = () => {
1248
+ // Close handling covers retries and surfaced errors.
1249
+ };
1250
+ ws.onclose = (event) => {
1251
+ this.handleClose(ws, event.code ?? 1006, String(event.reason ?? ""));
1252
+ };
1253
+ return;
1254
+ }
1255
+ const nodeWs = ws;
1256
+ nodeWs.on("open", () => {
1257
+ this.queueConnect();
1258
+ });
1259
+ nodeWs.on("message", (data) => {
1260
+ this.handleMessage(typeof data === "string" ? data : data.toString());
1261
+ });
1262
+ nodeWs.on("error", () => {
1263
+ // Close handling covers retries and surfaced errors.
1264
+ });
1265
+ nodeWs.on("close", (code, reason) => {
1266
+ this.handleClose(ws, code ?? 1006, reason?.toString() ?? "");
1267
+ });
1268
+ }
1269
+ queueConnect() {
1270
+ this.connectNonce = null;
1271
+ this.connectSent = false;
1272
+ if (this.connectTimer) {
1273
+ clearTimeout(this.connectTimer);
1274
+ }
1275
+ this.connectTimer = setTimeout(() => {
1276
+ if (!this.ws || !isSocketOpen(this.ws) || this.closed || this.connectSent) {
1277
+ return;
1278
+ }
1279
+ if (!this.connectNonce) {
1280
+ this.pendingConnectError = {
1281
+ code: "CONNECT_CHALLENGE_TIMEOUT",
1282
+ message: "gateway connect challenge timeout",
1283
+ };
1284
+ this.ws.close(RECONNECT_CLOSE_CODE, "connect challenge timeout");
1285
+ }
1286
+ }, CONNECT_TIMER_MS);
1287
+ }
1288
+ scheduleReconnect() {
1289
+ if (this.closed || this.reconnectTimer) {
1290
+ return;
1291
+ }
1292
+ const delay = this.backoffMs;
1293
+ this.backoffMs = Math.min(this.backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
1294
+ this.reconnectTimer = setTimeout(() => {
1295
+ this.reconnectTimer = null;
1296
+ this.openSocket();
1297
+ }, delay);
1298
+ }
1299
+ flushPending(error) {
1300
+ for (const [, pending] of this.pending) {
1301
+ clearTimeout(pending.timer);
1302
+ pending.reject(error);
1303
+ }
1304
+ this.pending.clear();
1305
+ }
1306
+ handleClose(ws, code, reason) {
1307
+ if (this.ws !== ws) {
1308
+ return;
1309
+ }
1310
+ this.ws = null;
1311
+ this.connected = false;
1312
+ this.connectSent = false;
1313
+ this.connectNonce = null;
1314
+ if (this.connectTimer) {
1315
+ clearTimeout(this.connectTimer);
1316
+ this.connectTimer = null;
1317
+ }
1318
+ const error = this.pendingConnectError;
1319
+ this.pendingConnectError = null;
1320
+ this.flushPending(new Error(`gateway closed (${code}): ${reason || "no reason"}`));
1321
+ this.onClose?.({ code, reason, error });
1322
+ if (!this.closed) {
1323
+ this.onDisconnect?.();
1324
+ this.scheduleReconnect();
1325
+ }
1326
+ }
1327
+ async sendConnect() {
1328
+ if (this.connectSent || !this.ws || !isSocketOpen(this.ws)) {
1329
+ return;
1330
+ }
1331
+ const nonce = this.connectNonce?.trim() ?? "";
1332
+ if (!nonce) {
1333
+ this.pendingConnectError = {
1334
+ code: "DEVICE_AUTH_NONCE_REQUIRED",
1335
+ message: "gateway connect challenge missing nonce",
1336
+ };
1337
+ this.ws.close(RECONNECT_CLOSE_CODE, "connect challenge missing nonce");
1338
+ return;
1339
+ }
1340
+ this.connectSent = true;
1341
+ if (this.connectTimer) {
1342
+ clearTimeout(this.connectTimer);
1343
+ this.connectTimer = null;
1344
+ }
1345
+ let identity = null;
1346
+ try {
1347
+ identity = await loadOrCreateDeviceIdentity();
1348
+ const storedDeviceToken = loadStoredDeviceToken(identity.deviceId, this.storageScope(), OPERATOR_ROLE)?.token;
1349
+ const authToken = storedDeviceToken ?? this.gatewayToken;
1350
+ const signedAtMs = Date.now();
1351
+ const payload = buildDeviceAuthPayload({
1352
+ deviceId: identity.deviceId,
1353
+ clientId: this.clientId,
1354
+ clientMode: this.clientMode,
1355
+ role: OPERATOR_ROLE,
1356
+ scopes: OPERATOR_SCOPES,
1357
+ signedAtMs,
1358
+ token: authToken ?? null,
1359
+ nonce,
1360
+ });
1361
+ const signature = await signDevicePayload(identity.privateKey, payload);
1362
+ const params = {
1363
+ minProtocol: PROTOCOL_VERSION,
1364
+ maxProtocol: PROTOCOL_VERSION,
1365
+ client: {
1366
+ id: this.clientId,
1367
+ ...(this.clientDisplayName ? { displayName: this.clientDisplayName } : {}),
1368
+ version: this.clientVersion,
1369
+ platform: this.clientPlatform,
1370
+ mode: this.clientMode,
1371
+ ...(this.clientInstanceId ? { instanceId: this.clientInstanceId } : {}),
1372
+ },
1373
+ role: OPERATOR_ROLE,
1374
+ scopes: [...OPERATOR_SCOPES],
1375
+ device: {
1376
+ id: identity.deviceId,
1377
+ publicKey: identity.publicKey,
1378
+ signature,
1379
+ signedAt: signedAtMs,
1380
+ nonce,
1381
+ },
1382
+ caps: this.caps,
1383
+ ...(authToken
1384
+ ? {
1385
+ auth: {
1386
+ ...(authToken ? { token: authToken } : {}),
1387
+ },
1388
+ }
1389
+ : {}),
1390
+ ...(resolveUserAgent() ? { userAgent: resolveUserAgent() } : {}),
1391
+ ...(resolveLocale() ? { locale: resolveLocale() } : {}),
1392
+ };
1393
+ const hello = await this.sendRawRequest("connect", params, this.defaultTimeout, true);
1394
+ if (hello?.auth?.deviceToken) {
1395
+ storeStoredDeviceToken({
1396
+ deviceId: identity.deviceId,
1397
+ scope: this.storageScope(),
1398
+ gatewayUrl: this.url,
1399
+ role: hello.auth.role ?? OPERATOR_ROLE,
1400
+ token: hello.auth.deviceToken,
1401
+ scopes: hello.auth.scopes ?? [],
1402
+ });
1403
+ }
1404
+ this._version = hello?.server?.version ?? hello?.version ?? null;
1405
+ this._protocol = hello?.protocol ?? null;
1406
+ this.connected = true;
1407
+ this.pendingConnectError = null;
1408
+ this.backoffMs = INITIAL_BACKOFF_MS;
1409
+ this.deviceTokenMismatchRetried = false;
1410
+ this.updatePairingState(null);
1411
+ if (this.resolveConnectPromise) {
1412
+ this.resolveConnectPromise();
1413
+ }
1414
+ this.connectPromise = null;
1415
+ this.resolveConnectPromise = null;
1416
+ this.rejectConnectPromise = null;
1417
+ this.onHello?.(hello);
1418
+ }
1419
+ catch (error) {
1420
+ this.pendingConnectError = toCloseError(error);
1421
+ const detailCode = readConnectErrorCode(error);
1422
+ const requestId = readConnectPairingRequestId(error);
1423
+ // Stale device token after agent restart — clear it and retry on the
1424
+ // same socket instead of forcing a full reconnect cycle. Guard with a
1425
+ // one-shot flag to prevent infinite recursion if the retry also fails.
1426
+ if (identity && detailCode === CONNECT_ERROR_DEVICE_TOKEN_MISMATCH && !this.deviceTokenMismatchRetried) {
1427
+ this.deviceTokenMismatchRetried = true;
1428
+ clearStoredDeviceToken(identity.deviceId, this.storageScope(), OPERATOR_ROLE);
1429
+ this.connectSent = false;
1430
+ this.pendingConnectError = null;
1431
+ await this.sendConnect();
1432
+ return;
1433
+ }
1434
+ if (identity && detailCode === CONNECT_ERROR_PAIRING_REQUIRED) {
1435
+ clearStoredDeviceToken(identity.deviceId, this.storageScope(), OPERATOR_ROLE);
1436
+ }
1437
+ if (detailCode === CONNECT_ERROR_PAIRING_REQUIRED && requestId) {
1438
+ if (this.canAutoApprovePairing() && !this.autoApproveAttemptedRequestIds.has(requestId)) {
1439
+ // Auto-approve silently — don't emit intermediate pairing states
1440
+ // that would cause UI flicker in the dashboard.
1441
+ this.autoApproveAttemptedRequestIds.add(requestId);
1442
+ try {
1443
+ await this.approvePairingRequest(requestId);
1444
+ this.pendingConnectError = {
1445
+ code: "PAIRING_APPROVED",
1446
+ message: "Pairing approved, reconnecting",
1447
+ };
1448
+ }
1449
+ catch (approvalError) {
1450
+ this.pendingConnectError = toCloseError(approvalError);
1451
+ this.updatePairingState({
1452
+ requestId,
1453
+ role: OPERATOR_ROLE,
1454
+ gatewayUrl: this.url,
1455
+ ...(identity ? { deviceId: identity.deviceId } : {}),
1456
+ status: "failed",
1457
+ updatedAtMs: Date.now(),
1458
+ error: approvalError instanceof Error ? approvalError.message : String(approvalError),
1459
+ });
1460
+ }
1461
+ }
1462
+ else {
1463
+ // No auto-approve — surface pairing state so the UI can prompt.
1464
+ this.updatePairingState({
1465
+ requestId,
1466
+ role: OPERATOR_ROLE,
1467
+ gatewayUrl: this.url,
1468
+ ...(identity ? { deviceId: identity.deviceId } : {}),
1469
+ status: "pending",
1470
+ updatedAtMs: Date.now(),
1471
+ });
1472
+ }
1473
+ }
1474
+ if (this.ws) {
1475
+ this.ws.close(RECONNECT_CLOSE_CODE, "connect failed");
1476
+ }
1477
+ }
1478
+ }
1479
+ handleMessage(raw) {
1480
+ let message;
1481
+ try {
1482
+ message = JSON.parse(raw);
1483
+ }
1484
+ catch {
1485
+ return;
1486
+ }
1487
+ if (message.type === "event") {
1488
+ const gatewayEvent = message;
1489
+ if (gatewayEvent.event === "connect.challenge") {
1490
+ const nonce = gatewayEvent.payload && typeof gatewayEvent.payload.nonce === "string"
1491
+ ? gatewayEvent.payload.nonce.trim()
1492
+ : "";
1493
+ if (!nonce) {
1494
+ this.pendingConnectError = {
1495
+ code: "DEVICE_AUTH_NONCE_REQUIRED",
1496
+ message: "gateway connect challenge missing nonce",
1497
+ };
1498
+ this.ws?.close(RECONNECT_CLOSE_CODE, "connect challenge missing nonce");
1499
+ return;
1500
+ }
1501
+ this.connectNonce = nonce;
1502
+ void this.sendConnect();
1503
+ return;
1504
+ }
1505
+ if (typeof gatewayEvent.seq === "number") {
1506
+ if (this.lastSeq !== null && gatewayEvent.seq > this.lastSeq + 1) {
1507
+ this.onGap?.({ expected: this.lastSeq + 1, received: gatewayEvent.seq });
1508
+ }
1509
+ this.lastSeq = gatewayEvent.seq;
1510
+ }
1511
+ for (const handler of this.eventHandlers) {
1512
+ try {
1513
+ handler(gatewayEvent);
1514
+ }
1515
+ catch {
1516
+ // Event handlers are isolated from the socket lifecycle.
1517
+ }
1518
+ }
1519
+ return;
1520
+ }
1521
+ if (message.type !== "res") {
1522
+ return;
1523
+ }
1524
+ const pending = this.pending.get(message.id);
1525
+ if (!pending) {
1526
+ return;
1527
+ }
1528
+ this.pending.delete(message.id);
1529
+ clearTimeout(pending.timer);
1530
+ if (message.ok) {
1531
+ pending.resolve(message.payload);
1532
+ return;
1533
+ }
1534
+ pending.reject(new GatewayRequestError({
1535
+ code: message.error?.code ?? "UNAVAILABLE",
1536
+ message: message.error?.message ?? "gateway request failed",
1537
+ details: message.error?.details,
1538
+ }));
1539
+ }
1540
+ sendRawRequest(method, params = {}, timeout = this.defaultTimeout, allowBeforeHello = false) {
1541
+ if (!this.ws || !isSocketOpen(this.ws)) {
1542
+ return Promise.reject(new Error("gateway not connected"));
1543
+ }
1544
+ if (!allowBeforeHello && !this.connected) {
1545
+ return Promise.reject(new Error("gateway not connected"));
1546
+ }
1547
+ const id = makeId();
1548
+ const request = { type: "req", id, method, params };
1549
+ return new Promise((resolve, reject) => {
1550
+ const timer = setTimeout(() => {
1551
+ this.pending.delete(id);
1552
+ reject(new Error(`RPC timeout: ${method}`));
1553
+ }, timeout);
1554
+ this.pending.set(id, { resolve, reject, timer });
1555
+ this.ws?.send(JSON.stringify(request));
1556
+ });
1557
+ }
1558
+ // ---------------------------------------------------------------------------
1559
+ // RPC
1560
+ // ---------------------------------------------------------------------------
1561
+ rpc(method, params = {}, timeout) {
1562
+ return this.sendRawRequest(method, params, timeout ?? this.defaultTimeout);
1563
+ }
1564
+ request(method, params = {}, timeout) {
1565
+ return this.rpc(method, params, timeout);
1566
+ }
1567
+ // ---------------------------------------------------------------------------
1568
+ // Config
1569
+ // ---------------------------------------------------------------------------
1570
+ async configGet() {
1571
+ const res = await this.rpc("config.get");
1572
+ if (res?.parsed)
1573
+ return res.parsed;
1574
+ if (res?.raw) {
1575
+ try {
1576
+ return JSON.parse(res.raw);
1577
+ }
1578
+ catch {
1579
+ // Fall through to the raw payload.
1580
+ }
1581
+ }
1582
+ return res?.config ?? res ?? {};
1583
+ }
1584
+ async configSchema() {
1585
+ const res = await this.rpc("config.schema");
1586
+ return normalizeOpenClawConfigSchema(res) ?? { schema: {}, uiHints: {} };
1587
+ }
1588
+ async configPatch(patch) {
1589
+ const { hash, baseHash } = await this.rpc("config.get");
1590
+ await this.rpc("config.patch", {
1591
+ raw: JSON.stringify(patch),
1592
+ baseHash: hash ?? baseHash ?? "",
1593
+ });
1594
+ }
1595
+ async configApply(config) {
1596
+ const { hash, baseHash } = await this.rpc("config.get");
1597
+ await this.rpc("config.apply", {
1598
+ raw: JSON.stringify(config),
1599
+ baseHash: hash ?? baseHash ?? "",
1600
+ });
1601
+ }
1602
+ async configSet(config) {
1603
+ const { hash, baseHash } = await this.rpc("config.get");
1604
+ await this.rpc("config.set", {
1605
+ raw: JSON.stringify(config),
1606
+ baseHash: hash ?? baseHash ?? "",
1607
+ });
1608
+ }
1609
+ async modelsList() {
1610
+ const res = await this.rpc("models.list");
1611
+ return res?.models ?? res ?? [];
1612
+ }
1613
+ async waitReady(timeoutMs = 300_000, options = {}) {
1614
+ const retryIntervalMs = options.retryIntervalMs ?? 5_000;
1615
+ const probe = options.probe ?? "config";
1616
+ const deadline = Date.now() + timeoutMs;
1617
+ let lastError = null;
1618
+ while (!this.closed) {
1619
+ try {
1620
+ if (!this.connected) {
1621
+ await this.connect();
1622
+ }
1623
+ if (probe === "status") {
1624
+ return await this.status();
1625
+ }
1626
+ return await this.configGet();
1627
+ }
1628
+ catch (error) {
1629
+ lastError = error;
1630
+ this.close();
1631
+ if (Date.now() >= deadline) {
1632
+ break;
1633
+ }
1634
+ await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
1635
+ }
1636
+ }
1637
+ const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
1638
+ throw new Error(`Gateway readiness probe timed out after ${timeoutMs}ms${detail}`);
1639
+ }
1640
+ async channelsStatus(probe = false, timeoutMs) {
1641
+ const params = { probe };
1642
+ if (timeoutMs !== undefined)
1643
+ params.timeoutMs = timeoutMs;
1644
+ return await this.rpc("channels.status", params);
1645
+ }
1646
+ async channelsLogout(channel, accountId) {
1647
+ const params = { channel };
1648
+ if (accountId)
1649
+ params.accountId = accountId;
1650
+ return await this.rpc("channels.logout", params);
1651
+ }
1652
+ async webLoginStart(options = {}) {
1653
+ const params = {};
1654
+ if (options.force)
1655
+ params.force = true;
1656
+ if (options.timeoutMs !== undefined)
1657
+ params.timeoutMs = options.timeoutMs;
1658
+ if (options.verbose)
1659
+ params.verbose = true;
1660
+ if (options.accountId)
1661
+ params.accountId = options.accountId;
1662
+ return await this.rpc("web.login.start", params, 30_000);
1663
+ }
1664
+ async webLoginWait(options = {}) {
1665
+ const params = {};
1666
+ if (options.timeoutMs !== undefined)
1667
+ params.timeoutMs = options.timeoutMs;
1668
+ if (options.accountId)
1669
+ params.accountId = options.accountId;
1670
+ return await this.rpc("web.login.wait", params, CHAT_TIMEOUT);
1671
+ }
1672
+ // ---------------------------------------------------------------------------
1673
+ // Sessions
1674
+ // ---------------------------------------------------------------------------
1675
+ async sessionsList() {
1676
+ const res = await this.rpc("sessions.list");
1677
+ return res?.sessions ?? res ?? [];
1678
+ }
1679
+ async sessionsPreview(sessionKey, limit = 20) {
1680
+ const res = await this.rpc("sessions.preview", { keys: [sessionKey], limit });
1681
+ return res?.previews?.[0]?.items ?? [];
1682
+ }
1683
+ async sessionsPatch(patch) {
1684
+ return await this.rpc("sessions.patch", patch);
1685
+ }
1686
+ async chatHistory(sessionKey, limit = 50) {
1687
+ const params = { limit };
1688
+ if (sessionKey)
1689
+ params.sessionKey = sessionKey;
1690
+ const res = await this.rpc("chat.history", params);
1691
+ return res?.messages ?? res ?? [];
1692
+ }
1693
+ async chatAbort(sessionKey) {
1694
+ const params = {};
1695
+ if (sessionKey)
1696
+ params.sessionKey = sessionKey;
1697
+ await this.rpc("chat.abort", params);
1698
+ }
1699
+ async sendChat(message, sessionKey = "main", agentId, attachments) {
1700
+ const normalizedAttachments = normalizeChatAttachments(attachments);
1701
+ const params = {
1702
+ message,
1703
+ deliver: false,
1704
+ sessionKey,
1705
+ idempotencyKey: makeId(),
1706
+ };
1707
+ if (agentId)
1708
+ params.agentId = agentId;
1709
+ if (normalizedAttachments)
1710
+ params.attachments = normalizedAttachments;
1711
+ return this.rpc("chat.send", params, CHAT_TIMEOUT);
1712
+ }
1713
+ async sessionsReset(sessionKey, reason) {
1714
+ const params = { key: sessionKey };
1715
+ if (reason)
1716
+ params.reason = reason;
1717
+ await this.rpc("sessions.reset", params);
1718
+ }
1719
+ // ---------------------------------------------------------------------------
1720
+ // Chat (streaming via events)
1721
+ // ---------------------------------------------------------------------------
1722
+ async *chatSend(message, sessionKey, attachments) {
1723
+ if (!this.connected || !this.ws) {
1724
+ throw new Error("Not connected");
1725
+ }
1726
+ const idempotencyKey = makeId();
1727
+ const acceptedRunIds = new Set([idempotencyKey]);
1728
+ const queuedEvents = [];
1729
+ let resolveWait = null;
1730
+ let streamedDisplayText = false;
1731
+ let lastLegacyText = "";
1732
+ let lastThinkingText = "";
1733
+ const seenToolCallIds = new Set();
1734
+ const seenToolResultIds = new Set();
1735
+ const handler = (evt) => {
1736
+ if (evt.event === "chat" || evt.event?.startsWith("chat.")) {
1737
+ queuedEvents.push(evt);
1738
+ const waiter = resolveWait;
1739
+ resolveWait = null;
1740
+ waiter?.();
1741
+ }
1742
+ };
1743
+ this.eventHandlers.add(handler);
1744
+ try {
1745
+ const normalizedAttachments = normalizeChatAttachments(attachments);
1746
+ const params = {
1747
+ message,
1748
+ deliver: false,
1749
+ sessionKey,
1750
+ idempotencyKey,
1751
+ };
1752
+ if (normalizedAttachments)
1753
+ params.attachments = normalizedAttachments;
1754
+ const ack = await this.rpc("chat.send", params, CHAT_TIMEOUT);
1755
+ const serverRunId = typeof ack?.runId === "string" ? ack.runId.trim() : "";
1756
+ if (serverRunId) {
1757
+ acceptedRunIds.add(serverRunId);
1758
+ }
1759
+ let deadline = Date.now() + CHAT_TIMEOUT;
1760
+ while (Date.now() < deadline) {
1761
+ if (queuedEvents.length === 0) {
1762
+ const remainingMs = Math.max(100, Math.min(1000, deadline - Date.now()));
1763
+ await new Promise((resolve) => {
1764
+ const timer = setTimeout(() => {
1765
+ if (resolveWait === release) {
1766
+ resolveWait = null;
1767
+ }
1768
+ resolve();
1769
+ }, remainingMs);
1770
+ const release = () => {
1771
+ clearTimeout(timer);
1772
+ resolve();
1773
+ };
1774
+ resolveWait = release;
1775
+ });
1776
+ continue;
1777
+ }
1778
+ const evt = queuedEvents.shift();
1779
+ const payload = asRecord(evt.payload) ?? {};
1780
+ const payloadRunId = typeof payload.runId === "string" ? payload.runId.trim() : "";
1781
+ const payloadSessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey.trim() : "";
1782
+ if (payloadRunId && !acceptedRunIds.has(payloadRunId)) {
1783
+ continue;
1784
+ }
1785
+ if (payloadSessionKey && !sameSessionKey(payloadSessionKey, sessionKey)) {
1786
+ continue;
1787
+ }
1788
+ deadline = Date.now() + CHAT_TIMEOUT;
1789
+ if (evt.event === "chat.content") {
1790
+ const text = typeof payload.text === "string" ? payload.text : "";
1791
+ if (text) {
1792
+ streamedDisplayText = true;
1793
+ yield { type: "content", text };
1794
+ }
1795
+ continue;
1796
+ }
1797
+ if (evt.event === "agent" && String(payload.stream || "").toLowerCase() === "tool") {
1798
+ const toolPayload = asRecord(payload.data) ?? {};
1799
+ const phase = typeof toolPayload.phase === "string" ? toolPayload.phase.toLowerCase() : "";
1800
+ if (phase === "start") {
1801
+ const toolCallId = typeof toolPayload.toolCallId === "string" && toolPayload.toolCallId.trim()
1802
+ ? toolPayload.toolCallId.trim()
1803
+ : `${typeof toolPayload.name === "string" ? toolPayload.name : "tool"}:${JSON.stringify(toolPayload.args ?? null)}`;
1804
+ seenToolCallIds.add(toolCallId);
1805
+ yield {
1806
+ type: "tool_call",
1807
+ data: {
1808
+ ...(toolPayload.toolCallId ? { toolCallId: toolPayload.toolCallId } : {}),
1809
+ name: toolPayload.name,
1810
+ args: toolPayload.args,
1811
+ },
1812
+ };
1813
+ }
1814
+ else if (phase === "result") {
1815
+ const toolCallId = typeof toolPayload.toolCallId === "string" && toolPayload.toolCallId.trim()
1816
+ ? toolPayload.toolCallId.trim()
1817
+ : `${typeof toolPayload.name === "string" ? toolPayload.name : "tool"}:${JSON.stringify(toolPayload.args ?? null)}`;
1818
+ seenToolResultIds.add(toolCallId);
1819
+ yield {
1820
+ type: "tool_result",
1821
+ data: {
1822
+ ...(toolPayload.toolCallId ? { toolCallId: toolPayload.toolCallId } : {}),
1823
+ name: toolPayload.name,
1824
+ result: toolPayload.result,
1825
+ isError: toolPayload.isError,
1826
+ },
1827
+ };
1828
+ }
1829
+ continue;
1830
+ }
1831
+ if (evt.event === "chat.thinking") {
1832
+ const text = typeof payload.text === "string" ? payload.text : "";
1833
+ if (text) {
1834
+ lastThinkingText += text;
1835
+ }
1836
+ yield { type: "thinking", text };
1837
+ continue;
1838
+ }
1839
+ if (evt.event === "chat.tool_call") {
1840
+ const toolCallId = typeof payload.toolCallId === "string" && payload.toolCallId.trim()
1841
+ ? payload.toolCallId.trim()
1842
+ : `${typeof payload.name === "string" ? payload.name : "tool"}:${JSON.stringify(payload.args ?? payload.arguments ?? null)}`;
1843
+ seenToolCallIds.add(toolCallId);
1844
+ yield { type: "tool_call", data: payload };
1845
+ continue;
1846
+ }
1847
+ if (evt.event === "chat.tool_result") {
1848
+ const toolCallId = typeof payload.toolCallId === "string" && payload.toolCallId.trim()
1849
+ ? payload.toolCallId.trim()
1850
+ : `${typeof payload.name === "string" ? payload.name : "tool"}:${JSON.stringify(payload.args ?? payload.arguments ?? null)}`;
1851
+ seenToolResultIds.add(toolCallId);
1852
+ yield { type: "tool_result", data: payload };
1853
+ continue;
1854
+ }
1855
+ if (evt.event === "chat.done") {
1856
+ yield { type: "done", data: payload };
1857
+ return;
1858
+ }
1859
+ if (evt.event === "chat.error") {
1860
+ yield {
1861
+ type: "error",
1862
+ text: typeof payload.message === "string" ? payload.message : "Unknown error",
1863
+ data: payload,
1864
+ };
1865
+ return;
1866
+ }
1867
+ if (evt.event !== "chat") {
1868
+ continue;
1869
+ }
1870
+ const state = typeof payload.state === "string" ? payload.state.trim().toLowerCase() : "";
1871
+ const currentText = extractMessageText(payload.message) ?? "";
1872
+ const normalizedMessage = normalizeGatewayChatMessage(payload.message);
1873
+ if (state === "delta") {
1874
+ const streamed = streamDelta(lastLegacyText, currentText);
1875
+ lastLegacyText = streamed.nextText;
1876
+ if (streamed.delta) {
1877
+ streamedDisplayText = true;
1878
+ yield { type: "content", text: streamed.delta, data: payload };
1879
+ }
1880
+ continue;
1881
+ }
1882
+ if (state === "final") {
1883
+ const thinkingDelta = normalizedMessage?.thinking
1884
+ ? streamDelta(lastThinkingText, normalizedMessage.thinking)
1885
+ : { delta: "", nextText: lastThinkingText };
1886
+ lastThinkingText = thinkingDelta.nextText;
1887
+ if (thinkingDelta.delta) {
1888
+ yield { type: "thinking", text: thinkingDelta.delta, data: payload };
1889
+ }
1890
+ for (const toolCall of normalizedMessage?.toolCalls ?? []) {
1891
+ const toolCallKey = toolCall.id?.trim() ||
1892
+ `${toolCall.name}:${JSON.stringify(toolCall.args ?? null)}`;
1893
+ if (toolCall.args !== undefined && !seenToolCallIds.has(toolCallKey)) {
1894
+ seenToolCallIds.add(toolCallKey);
1895
+ yield {
1896
+ type: "tool_call",
1897
+ data: {
1898
+ ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
1899
+ name: toolCall.name,
1900
+ args: toolCall.args,
1901
+ },
1902
+ };
1903
+ }
1904
+ if (toolCall.result !== undefined && !seenToolResultIds.has(toolCallKey)) {
1905
+ seenToolResultIds.add(toolCallKey);
1906
+ yield {
1907
+ type: "tool_result",
1908
+ data: {
1909
+ ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
1910
+ name: toolCall.name,
1911
+ result: toolCall.result,
1912
+ },
1913
+ };
1914
+ }
1915
+ }
1916
+ if (currentText) {
1917
+ const streamed = streamDelta(lastLegacyText, currentText);
1918
+ lastLegacyText = streamed.nextText;
1919
+ if (streamed.delta) {
1920
+ streamedDisplayText = true;
1921
+ yield { type: "content", text: streamed.delta, data: payload };
1922
+ }
1923
+ yield { type: "done", data: payload };
1924
+ return;
1925
+ }
1926
+ if (streamedDisplayText || lastLegacyText) {
1927
+ yield { type: "done", data: payload };
1928
+ return;
1929
+ }
1930
+ if (normalizedMessage?.thinking || (normalizedMessage?.toolCalls.length ?? 0) > 0) {
1931
+ yield { type: "done", data: payload };
1932
+ return;
1933
+ }
1934
+ const historyText = latestHistoryAssistantText(await this.chatHistory(sessionKey, 20), acceptedRunIds);
1935
+ if (historyText) {
1936
+ yield { type: "content", text: historyText, data: payload };
1937
+ }
1938
+ yield { type: "done", data: payload };
1939
+ return;
1940
+ }
1941
+ if (state === "error" || state === "aborted") {
1942
+ if (currentText) {
1943
+ const streamed = streamDelta(lastLegacyText, currentText);
1944
+ lastLegacyText = streamed.nextText;
1945
+ if (streamed.delta) {
1946
+ streamedDisplayText = true;
1947
+ yield { type: "content", text: streamed.delta, data: payload };
1948
+ }
1949
+ }
1950
+ yield {
1951
+ type: "error",
1952
+ text: typeof payload.errorMessage === "string" ? payload.errorMessage : state,
1953
+ data: payload,
1954
+ };
1955
+ return;
1956
+ }
1957
+ }
1958
+ throw new Error("Streaming chat.send timed out");
1959
+ }
1960
+ finally {
1961
+ this.eventHandlers.delete(handler);
1962
+ }
1963
+ }
1964
+ // ---------------------------------------------------------------------------
1965
+ // Files (agent workspace files)
1966
+ // ---------------------------------------------------------------------------
1967
+ async filesList(agentId = "main") {
1968
+ const res = await this.rpc("agents.files.list", { agentId });
1969
+ return res?.files ?? [];
1970
+ }
1971
+ async fileGet(agentId, name) {
1972
+ const res = await this.rpc("agents.files.get", { agentId, name });
1973
+ return res?.content ?? "";
1974
+ }
1975
+ async fileSet(agentId, name, content) {
1976
+ await this.rpc("agents.files.set", { agentId, name, content });
1977
+ }
1978
+ // ---------------------------------------------------------------------------
1979
+ // Agents
1980
+ // ---------------------------------------------------------------------------
1981
+ async agentsList() {
1982
+ const res = await this.rpc("agents.list");
1983
+ const agents = res?.agents ?? res ?? [];
1984
+ return Array.isArray(agents)
1985
+ ? agents.map((agent) => ({ ...agent, id: agent?.agentId ?? agent?.id }))
1986
+ : [];
1987
+ }
1988
+ async agentGet(agentId = "main") {
1989
+ const res = await this.rpc("agents.get", { agentId });
1990
+ return res?.agent ?? res ?? {};
1991
+ }
1992
+ // ---------------------------------------------------------------------------
1993
+ // Cron
1994
+ // ---------------------------------------------------------------------------
1995
+ async cronList() {
1996
+ const res = await this.rpc("cron.list");
1997
+ return res?.jobs ?? res ?? [];
1998
+ }
1999
+ async cronAdd(job) {
2000
+ return this.rpc("cron.add", { job });
2001
+ }
2002
+ async cronRemove(jobId) {
2003
+ await this.rpc("cron.remove", { jobId });
2004
+ }
2005
+ async cronRun(jobId) {
2006
+ return this.rpc("cron.run", { jobId });
2007
+ }
2008
+ async execApprove(execId) {
2009
+ await this.rpc("exec.approve", { execId });
2010
+ }
2011
+ async execDeny(execId) {
2012
+ await this.rpc("exec.deny", { execId });
2013
+ }
2014
+ // ---------------------------------------------------------------------------
2015
+ // Status
2016
+ // ---------------------------------------------------------------------------
2017
+ async status() {
2018
+ return this.rpc("status");
2019
+ }
2020
+ }
2021
+ //# sourceMappingURL=gateway.js.map