@estuary-ai/sdk 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1164 @@
1
+ 'use strict';
2
+
3
+ var socket_ioClient = require('socket.io-client');
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+ var __esm = (fn, res) => function __init() {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ };
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, { get: all[name], enumerable: true });
21
+ };
22
+ var __copyProps = (to, from, except, desc) => {
23
+ if (from && typeof from === "object" || typeof from === "function") {
24
+ for (let key of __getOwnPropNames(from))
25
+ if (!__hasOwnProp.call(to, key) && key !== except)
26
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
27
+ }
28
+ return to;
29
+ };
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+
32
+ // src/errors.ts
33
+ exports.ErrorCode = void 0; exports.EstuaryError = void 0;
34
+ var init_errors = __esm({
35
+ "src/errors.ts"() {
36
+ exports.ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
37
+ ErrorCode2["CONNECTION_FAILED"] = "CONNECTION_FAILED";
38
+ ErrorCode2["AUTH_FAILED"] = "AUTH_FAILED";
39
+ ErrorCode2["CONNECTION_TIMEOUT"] = "CONNECTION_TIMEOUT";
40
+ ErrorCode2["QUOTA_EXCEEDED"] = "QUOTA_EXCEEDED";
41
+ ErrorCode2["VOICE_NOT_SUPPORTED"] = "VOICE_NOT_SUPPORTED";
42
+ ErrorCode2["VOICE_ALREADY_ACTIVE"] = "VOICE_ALREADY_ACTIVE";
43
+ ErrorCode2["VOICE_NOT_ACTIVE"] = "VOICE_NOT_ACTIVE";
44
+ ErrorCode2["LIVEKIT_UNAVAILABLE"] = "LIVEKIT_UNAVAILABLE";
45
+ ErrorCode2["MICROPHONE_DENIED"] = "MICROPHONE_DENIED";
46
+ ErrorCode2["NOT_CONNECTED"] = "NOT_CONNECTED";
47
+ ErrorCode2["REST_ERROR"] = "REST_ERROR";
48
+ ErrorCode2["UNKNOWN"] = "UNKNOWN";
49
+ return ErrorCode2;
50
+ })(exports.ErrorCode || {});
51
+ exports.EstuaryError = class extends Error {
52
+ code;
53
+ details;
54
+ constructor(code, message, details) {
55
+ super(message);
56
+ this.name = "EstuaryError";
57
+ this.code = code;
58
+ this.details = details;
59
+ }
60
+ };
61
+ }
62
+ });
63
+
64
+ // src/voice/websocket-voice.ts
65
+ var websocket_voice_exports = {};
66
+ __export(websocket_voice_exports, {
67
+ WebSocketVoiceManager: () => WebSocketVoiceManager
68
+ });
69
+ function resample(input, fromRate, toRate) {
70
+ const ratio = fromRate / toRate;
71
+ const outputLength = Math.round(input.length / ratio);
72
+ const output = new Float32Array(outputLength);
73
+ for (let i = 0; i < outputLength; i++) {
74
+ const srcIndex = i * ratio;
75
+ const low = Math.floor(srcIndex);
76
+ const high = Math.min(low + 1, input.length - 1);
77
+ const frac = srcIndex - low;
78
+ output[i] = input[low] * (1 - frac) + input[high] * frac;
79
+ }
80
+ return output;
81
+ }
82
+ function float32ToInt16(float32) {
83
+ const int16 = new Int16Array(float32.length);
84
+ for (let i = 0; i < float32.length; i++) {
85
+ const clamped = Math.max(-1, Math.min(1, float32[i]));
86
+ int16[i] = clamped < 0 ? clamped * 32768 : clamped * 32767;
87
+ }
88
+ return int16;
89
+ }
90
+ function uint8ArrayToBase64(bytes) {
91
+ if (typeof btoa === "function") {
92
+ let binary = "";
93
+ for (let i = 0; i < bytes.length; i++) {
94
+ binary += String.fromCharCode(bytes[i]);
95
+ }
96
+ return btoa(binary);
97
+ }
98
+ return Buffer.from(bytes).toString("base64");
99
+ }
100
+ var WebSocketVoiceManager;
101
+ var init_websocket_voice = __esm({
102
+ "src/voice/websocket-voice.ts"() {
103
+ init_errors();
104
+ WebSocketVoiceManager = class {
105
+ socketManager;
106
+ sampleRate;
107
+ logger;
108
+ audioContext = null;
109
+ mediaStream = null;
110
+ scriptProcessor = null;
111
+ sourceNode = null;
112
+ _isMuted = false;
113
+ _isActive = false;
114
+ constructor(socketManager, sampleRate, logger) {
115
+ this.socketManager = socketManager;
116
+ this.sampleRate = sampleRate;
117
+ this.logger = logger;
118
+ }
119
+ get isMuted() {
120
+ return this._isMuted;
121
+ }
122
+ get isActive() {
123
+ return this._isActive;
124
+ }
125
+ async start() {
126
+ if (this._isActive) {
127
+ throw new exports.EstuaryError("VOICE_ALREADY_ACTIVE" /* VOICE_ALREADY_ACTIVE */, "Voice is already active");
128
+ }
129
+ if (typeof AudioContext === "undefined" && typeof globalThis.webkitAudioContext === "undefined") {
130
+ throw new exports.EstuaryError("VOICE_NOT_SUPPORTED" /* VOICE_NOT_SUPPORTED */, "AudioContext is not available in this environment");
131
+ }
132
+ let stream;
133
+ try {
134
+ stream = await navigator.mediaDevices.getUserMedia({
135
+ audio: { sampleRate: this.sampleRate, channelCount: 1 }
136
+ });
137
+ } catch (err) {
138
+ throw new exports.EstuaryError(
139
+ "MICROPHONE_DENIED" /* MICROPHONE_DENIED */,
140
+ "Microphone access denied",
141
+ err
142
+ );
143
+ }
144
+ this.mediaStream = stream;
145
+ const AudioCtx = globalThis.AudioContext || globalThis.webkitAudioContext;
146
+ this.audioContext = new AudioCtx({ sampleRate: this.sampleRate });
147
+ this.sourceNode = this.audioContext.createMediaStreamSource(stream);
148
+ this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
149
+ const nativeRate = this.audioContext.sampleRate;
150
+ const targetRate = this.sampleRate;
151
+ this.scriptProcessor.onaudioprocess = (event) => {
152
+ if (this._isMuted) return;
153
+ const inputData = event.inputBuffer.getChannelData(0);
154
+ let pcmFloat;
155
+ if (nativeRate !== targetRate) {
156
+ pcmFloat = resample(inputData, nativeRate, targetRate);
157
+ } else {
158
+ pcmFloat = inputData;
159
+ }
160
+ const pcm16 = float32ToInt16(pcmFloat);
161
+ const base64 = uint8ArrayToBase64(new Uint8Array(pcm16.buffer));
162
+ try {
163
+ this.socketManager.emitEvent("stream_audio", { audio: base64 });
164
+ } catch {
165
+ }
166
+ };
167
+ this.sourceNode.connect(this.scriptProcessor);
168
+ this.scriptProcessor.connect(this.audioContext.destination);
169
+ this._isActive = true;
170
+ this.socketManager.emitEvent("start_voice");
171
+ this.logger.debug("WebSocket voice started");
172
+ }
173
+ async stop() {
174
+ if (!this._isActive) return;
175
+ try {
176
+ this.socketManager.emitEvent("stop_voice");
177
+ } catch {
178
+ }
179
+ this.cleanup();
180
+ this._isActive = false;
181
+ this._isMuted = false;
182
+ this.logger.debug("WebSocket voice stopped");
183
+ }
184
+ toggleMute() {
185
+ if (!this._isActive || !this.mediaStream) return;
186
+ this._isMuted = !this._isMuted;
187
+ for (const track of this.mediaStream.getAudioTracks()) {
188
+ track.enabled = !this._isMuted;
189
+ }
190
+ this.logger.debug("Mute toggled:", this._isMuted);
191
+ }
192
+ dispose() {
193
+ this.cleanup();
194
+ this._isActive = false;
195
+ this._isMuted = false;
196
+ }
197
+ cleanup() {
198
+ if (this.scriptProcessor) {
199
+ this.scriptProcessor.onaudioprocess = null;
200
+ this.scriptProcessor.disconnect();
201
+ this.scriptProcessor = null;
202
+ }
203
+ if (this.sourceNode) {
204
+ this.sourceNode.disconnect();
205
+ this.sourceNode = null;
206
+ }
207
+ if (this.mediaStream) {
208
+ for (const track of this.mediaStream.getTracks()) {
209
+ track.stop();
210
+ }
211
+ this.mediaStream = null;
212
+ }
213
+ if (this.audioContext) {
214
+ this.audioContext.close().catch(() => {
215
+ });
216
+ this.audioContext = null;
217
+ }
218
+ }
219
+ };
220
+ }
221
+ });
222
+
223
+ // src/voice/livekit-voice.ts
224
+ var livekit_voice_exports = {};
225
+ __export(livekit_voice_exports, {
226
+ LiveKitVoiceManager: () => LiveKitVoiceManager
227
+ });
228
+ var LiveKitVoiceManager;
229
+ var init_livekit_voice = __esm({
230
+ "src/voice/livekit-voice.ts"() {
231
+ init_errors();
232
+ LiveKitVoiceManager = class {
233
+ socketManager;
234
+ logger;
235
+ room = null;
236
+ // livekit-client Room (dynamically imported)
237
+ _isMuted = false;
238
+ _isActive = false;
239
+ constructor(socketManager, logger) {
240
+ this.socketManager = socketManager;
241
+ this.logger = logger;
242
+ }
243
+ get isMuted() {
244
+ return this._isMuted;
245
+ }
246
+ get isActive() {
247
+ return this._isActive;
248
+ }
249
+ async start() {
250
+ if (this._isActive) {
251
+ throw new exports.EstuaryError("VOICE_ALREADY_ACTIVE" /* VOICE_ALREADY_ACTIVE */, "Voice is already active");
252
+ }
253
+ let Room;
254
+ let RoomEvent;
255
+ let Track;
256
+ try {
257
+ const lk = await import('livekit-client');
258
+ Room = lk.Room;
259
+ RoomEvent = lk.RoomEvent;
260
+ Track = lk.Track;
261
+ } catch {
262
+ throw new exports.EstuaryError(
263
+ "LIVEKIT_UNAVAILABLE" /* LIVEKIT_UNAVAILABLE */,
264
+ "livekit-client package is not installed"
265
+ );
266
+ }
267
+ const tokenData = await this.requestToken();
268
+ this.room = new Room({
269
+ adaptiveStream: true,
270
+ dynacast: true,
271
+ audioCaptureDefaults: {
272
+ echoCancellation: true,
273
+ noiseSuppression: true,
274
+ autoGainControl: true
275
+ }
276
+ });
277
+ this.room.on(RoomEvent.TrackSubscribed, (track, _publication, participant) => {
278
+ if (track.kind === Track.Kind.Audio) {
279
+ this.logger.debug("Bot audio track subscribed from", participant.identity);
280
+ const audioElement = track.attach();
281
+ audioElement.autoplay = true;
282
+ audioElement.style.display = "none";
283
+ if (typeof document !== "undefined") {
284
+ document.body.appendChild(audioElement);
285
+ }
286
+ audioElement.play().catch(() => {
287
+ });
288
+ }
289
+ });
290
+ this.room.on(RoomEvent.TrackUnsubscribed, (track) => {
291
+ if (track.kind === Track.Kind.Audio) {
292
+ track.detach().forEach((el) => el.remove());
293
+ }
294
+ });
295
+ this.room.on(RoomEvent.Disconnected, () => {
296
+ this.logger.debug("LiveKit room disconnected");
297
+ this._isActive = false;
298
+ });
299
+ try {
300
+ await this.room.connect(tokenData.url, tokenData.token);
301
+ this.logger.debug("Connected to LiveKit room:", tokenData.room);
302
+ } catch (err) {
303
+ this.room = null;
304
+ throw new exports.EstuaryError(
305
+ "CONNECTION_FAILED" /* CONNECTION_FAILED */,
306
+ "Failed to connect to LiveKit room",
307
+ err
308
+ );
309
+ }
310
+ try {
311
+ await this.room.localParticipant.setMicrophoneEnabled(true);
312
+ this.logger.debug("Microphone enabled");
313
+ } catch (err) {
314
+ this.room.disconnect();
315
+ this.room = null;
316
+ throw new exports.EstuaryError(
317
+ "MICROPHONE_DENIED" /* MICROPHONE_DENIED */,
318
+ "Failed to enable microphone",
319
+ err
320
+ );
321
+ }
322
+ this.socketManager.emitEvent("livekit_join", { room: tokenData.room });
323
+ this._isActive = true;
324
+ this.logger.debug("LiveKit voice started");
325
+ }
326
+ async stop() {
327
+ if (!this._isActive) return;
328
+ try {
329
+ this.socketManager.emitEvent("livekit_leave");
330
+ } catch {
331
+ }
332
+ if (this.room) {
333
+ for (const [, publication] of this.room.localParticipant.trackPublications) {
334
+ if (publication.track) {
335
+ publication.track.stop();
336
+ }
337
+ }
338
+ this.room.disconnect();
339
+ this.room = null;
340
+ }
341
+ this._isActive = false;
342
+ this._isMuted = false;
343
+ this.logger.debug("LiveKit voice stopped");
344
+ }
345
+ toggleMute() {
346
+ if (!this._isActive || !this.room) return;
347
+ this._isMuted = !this._isMuted;
348
+ this.room.localParticipant.setMicrophoneEnabled(!this._isMuted);
349
+ this.logger.debug("Mute toggled:", this._isMuted);
350
+ }
351
+ dispose() {
352
+ if (this.room) {
353
+ this.room.disconnect();
354
+ this.room = null;
355
+ }
356
+ this._isActive = false;
357
+ this._isMuted = false;
358
+ }
359
+ requestToken() {
360
+ return new Promise((resolve, reject) => {
361
+ const timeout = setTimeout(() => {
362
+ this.socketManager.onLiveKitToken(() => {
363
+ });
364
+ reject(new exports.EstuaryError(
365
+ "CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */,
366
+ "Timed out waiting for LiveKit token"
367
+ ));
368
+ }, 1e4);
369
+ this.socketManager.onLiveKitToken((data) => {
370
+ clearTimeout(timeout);
371
+ resolve(data);
372
+ });
373
+ this.socketManager.emitEvent("livekit_token");
374
+ });
375
+ }
376
+ };
377
+ }
378
+ });
379
+
380
+ // src/utils/event-emitter.ts
381
+ var TypedEventEmitter = class {
382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+ listeners = /* @__PURE__ */ new Map();
384
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
+ onceListeners = /* @__PURE__ */ new Map();
386
+ on(event, listener) {
387
+ if (!this.listeners.has(event)) {
388
+ this.listeners.set(event, /* @__PURE__ */ new Set());
389
+ }
390
+ this.listeners.get(event).add(listener);
391
+ return this;
392
+ }
393
+ off(event, listener) {
394
+ this.listeners.get(event)?.delete(listener);
395
+ this.onceListeners.get(event)?.delete(listener);
396
+ return this;
397
+ }
398
+ once(event, listener) {
399
+ if (!this.onceListeners.has(event)) {
400
+ this.onceListeners.set(event, /* @__PURE__ */ new Set());
401
+ }
402
+ this.onceListeners.get(event).add(listener);
403
+ this.on(event, listener);
404
+ return this;
405
+ }
406
+ emit(event, ...args) {
407
+ const listeners = this.listeners.get(event);
408
+ if (!listeners || listeners.size === 0) return false;
409
+ for (const listener of listeners) {
410
+ try {
411
+ listener(...args);
412
+ } catch {
413
+ }
414
+ }
415
+ const onceSet = this.onceListeners.get(event);
416
+ if (onceSet) {
417
+ for (const listener of onceSet) {
418
+ this.listeners.get(event)?.delete(listener);
419
+ }
420
+ onceSet.clear();
421
+ }
422
+ return true;
423
+ }
424
+ removeAllListeners(event) {
425
+ if (event) {
426
+ this.listeners.delete(event);
427
+ this.onceListeners.delete(event);
428
+ } else {
429
+ this.listeners.clear();
430
+ this.onceListeners.clear();
431
+ }
432
+ return this;
433
+ }
434
+ listenerCount(event) {
435
+ return this.listeners.get(event)?.size ?? 0;
436
+ }
437
+ };
438
+
439
+ // src/connection/socket-manager.ts
440
+ init_errors();
441
+
442
+ // src/types.ts
443
+ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
444
+ ConnectionState2["Disconnected"] = "disconnected";
445
+ ConnectionState2["Connecting"] = "connecting";
446
+ ConnectionState2["Connected"] = "connected";
447
+ ConnectionState2["Reconnecting"] = "reconnecting";
448
+ ConnectionState2["Error"] = "error";
449
+ return ConnectionState2;
450
+ })(ConnectionState || {});
451
+ function toSessionInfo(wire) {
452
+ return {
453
+ sessionId: wire.session_id,
454
+ conversationId: wire.conversation_id,
455
+ characterId: wire.character_id,
456
+ playerId: wire.player_id
457
+ };
458
+ }
459
+ function toBotResponse(wire) {
460
+ return {
461
+ text: wire.text,
462
+ isFinal: wire.is_final,
463
+ partial: wire.partial,
464
+ messageId: wire.message_id,
465
+ chunkIndex: wire.chunk_index,
466
+ isInterjection: wire.is_interjection
467
+ };
468
+ }
469
+ function toBotVoice(wire) {
470
+ return {
471
+ audio: wire.audio,
472
+ messageId: wire.message_id,
473
+ chunkIndex: wire.chunk_index,
474
+ isFinal: wire.is_final
475
+ };
476
+ }
477
+ function toSttResponse(wire) {
478
+ return {
479
+ text: wire.text,
480
+ isFinal: wire.is_final
481
+ };
482
+ }
483
+ function toInterruptData(wire) {
484
+ return {
485
+ messageId: wire.message_id,
486
+ reason: wire.reason,
487
+ interruptedAt: wire.interrupted_at
488
+ };
489
+ }
490
+ function toQuotaExceededData(wire) {
491
+ return {
492
+ message: wire.message,
493
+ current: wire.current,
494
+ limit: wire.limit,
495
+ remaining: wire.remaining,
496
+ tier: wire.tier
497
+ };
498
+ }
499
+ function toLiveKitTokenResponse(wire) {
500
+ return {
501
+ token: wire.token,
502
+ url: wire.url,
503
+ room: wire.room
504
+ };
505
+ }
506
+ function toCameraCaptureRequest(wire) {
507
+ return {
508
+ requestId: wire.request_id,
509
+ text: wire.text
510
+ };
511
+ }
512
+ function toMemoryUpdatedEvent(wire) {
513
+ return {
514
+ agentId: wire.agent_id,
515
+ playerId: wire.player_id,
516
+ memoriesExtracted: wire.memories_extracted,
517
+ factsExtracted: wire.facts_extracted,
518
+ conversationId: wire.conversation_id,
519
+ newMemories: wire.new_memories ?? [],
520
+ timestamp: wire.timestamp
521
+ };
522
+ }
523
+
524
+ // src/connection/socket-manager.ts
525
+ var SocketManager = class extends TypedEventEmitter {
526
+ socket = null;
527
+ config;
528
+ logger;
529
+ connectionState = "disconnected" /* Disconnected */;
530
+ reconnectAttempt = 0;
531
+ reconnectTimer = null;
532
+ sessionInfo = null;
533
+ constructor(config, logger) {
534
+ super();
535
+ this.config = config;
536
+ this.logger = logger;
537
+ }
538
+ get state() {
539
+ return this.connectionState;
540
+ }
541
+ get session() {
542
+ return this.sessionInfo;
543
+ }
544
+ get isConnected() {
545
+ return this.connectionState === "connected" /* Connected */;
546
+ }
547
+ get rawSocket() {
548
+ return this.socket;
549
+ }
550
+ connect() {
551
+ return new Promise((resolve, reject) => {
552
+ if (this.socket?.connected) {
553
+ if (this.sessionInfo) {
554
+ resolve(this.sessionInfo);
555
+ return;
556
+ }
557
+ }
558
+ this.setConnectionState("connecting" /* Connecting */);
559
+ this.reconnectAttempt = 0;
560
+ const url = `${this.config.serverUrl}/sdk`;
561
+ this.logger.debug("Connecting to", url);
562
+ this.socket = socket_ioClient.io(url, {
563
+ transports: ["websocket"],
564
+ timeout: 1e4,
565
+ reconnection: false,
566
+ // We handle reconnection ourselves
567
+ path: "/socket.io/"
568
+ });
569
+ let settled = false;
570
+ const onConnect = () => {
571
+ this.logger.debug("Socket connected, authenticating...");
572
+ this.socket.emit("authenticate", {
573
+ api_key: this.config.apiKey,
574
+ character_id: this.config.characterId,
575
+ player_id: this.config.playerId,
576
+ audio_sample_rate: this.config.audioSampleRate ?? 16e3,
577
+ realtime_memory: this.config.realtimeMemory ?? false
578
+ });
579
+ };
580
+ const onSessionInfo = (data) => {
581
+ this.sessionInfo = toSessionInfo(data);
582
+ this.setConnectionState("connected" /* Connected */);
583
+ this.reconnectAttempt = 0;
584
+ this.logger.info("Connected, session:", this.sessionInfo.sessionId);
585
+ this.emit("connected", this.sessionInfo);
586
+ if (!settled) {
587
+ settled = true;
588
+ resolve(this.sessionInfo);
589
+ }
590
+ };
591
+ const onAuthError = (data) => {
592
+ this.logger.error("Auth error:", data.error);
593
+ this.setConnectionState("error" /* Error */);
594
+ this.emit("authError", data.error);
595
+ if (!settled) {
596
+ settled = true;
597
+ reject(new exports.EstuaryError("AUTH_FAILED" /* AUTH_FAILED */, data.error));
598
+ }
599
+ };
600
+ const onConnectError = (err) => {
601
+ this.logger.error("Connection error:", err.message);
602
+ if (!settled) {
603
+ settled = true;
604
+ this.setConnectionState("error" /* Error */);
605
+ reject(new exports.EstuaryError("CONNECTION_FAILED" /* CONNECTION_FAILED */, err.message));
606
+ } else {
607
+ this.handleDisconnect("connect_error");
608
+ }
609
+ };
610
+ const onDisconnect = (reason) => {
611
+ this.logger.info("Disconnected:", reason);
612
+ this.sessionInfo = null;
613
+ if (!settled) {
614
+ settled = true;
615
+ this.setConnectionState("disconnected" /* Disconnected */);
616
+ reject(new exports.EstuaryError("CONNECTION_FAILED" /* CONNECTION_FAILED */, `Disconnected: ${reason}`));
617
+ } else {
618
+ this.handleDisconnect(reason);
619
+ }
620
+ };
621
+ this.socket.on("connect", onConnect);
622
+ this.socket.on("session_info", onSessionInfo);
623
+ this.socket.on("auth_error", onAuthError);
624
+ this.socket.on("connect_error", onConnectError);
625
+ this.socket.on("disconnect", onDisconnect);
626
+ this.bindServerEvents();
627
+ });
628
+ }
629
+ disconnect() {
630
+ this.clearReconnectTimer();
631
+ this.sessionInfo = null;
632
+ if (this.socket) {
633
+ this.socket.removeAllListeners();
634
+ this.socket.disconnect();
635
+ this.socket = null;
636
+ }
637
+ this.setConnectionState("disconnected" /* Disconnected */);
638
+ this.emit("disconnected", "manual");
639
+ }
640
+ emitEvent(event, data) {
641
+ if (!this.socket?.connected) {
642
+ throw new exports.EstuaryError("NOT_CONNECTED" /* NOT_CONNECTED */, "Not connected to server");
643
+ }
644
+ this.socket.emit(event, data);
645
+ }
646
+ bindServerEvents() {
647
+ if (!this.socket) return;
648
+ this.socket.on("bot_response", (data) => {
649
+ this.emit("botResponse", toBotResponse(data));
650
+ });
651
+ this.socket.on("bot_voice", (data) => {
652
+ this.emit("botVoice", toBotVoice(data));
653
+ });
654
+ this.socket.on("stt_response", (data) => {
655
+ this.emit("sttResponse", toSttResponse(data));
656
+ });
657
+ this.socket.on("interrupt", (data) => {
658
+ this.emit("interrupt", toInterruptData(data));
659
+ });
660
+ this.socket.on("quota_exceeded", (data) => {
661
+ this.emit("quotaExceeded", toQuotaExceededData(data));
662
+ });
663
+ this.socket.on("camera_capture", (data) => {
664
+ this.emit("cameraCaptureRequest", toCameraCaptureRequest(data));
665
+ });
666
+ this.socket.on("error", (data) => {
667
+ this.emit("error", new exports.EstuaryError("UNKNOWN" /* UNKNOWN */, data.message));
668
+ });
669
+ this.socket.on("livekit_token", (data) => {
670
+ this._livekitTokenCallback?.(toLiveKitTokenResponse(data));
671
+ });
672
+ this.socket.on("livekit_ready", (data) => {
673
+ this.emit("livekitConnected", data.room);
674
+ });
675
+ this.socket.on("memory_updated", (data) => {
676
+ this.emit("memoryUpdated", toMemoryUpdatedEvent(data));
677
+ });
678
+ }
679
+ /** Register a callback for livekit_token events (used by LiveKitVoiceManager) */
680
+ onLiveKitToken(callback) {
681
+ this._livekitTokenCallback = callback;
682
+ }
683
+ handleDisconnect(reason) {
684
+ this.sessionInfo = null;
685
+ this.setConnectionState("disconnected" /* Disconnected */);
686
+ this.emit("disconnected", reason);
687
+ const autoReconnect = this.config.autoReconnect ?? true;
688
+ const maxAttempts = this.config.maxReconnectAttempts ?? 5;
689
+ if (autoReconnect && this.reconnectAttempt < maxAttempts) {
690
+ this.attemptReconnect();
691
+ }
692
+ }
693
+ attemptReconnect() {
694
+ const maxAttempts = this.config.maxReconnectAttempts ?? 5;
695
+ const delay = this.config.reconnectDelayMs ?? 2e3;
696
+ if (this.reconnectAttempt >= maxAttempts) {
697
+ this.logger.warn("Max reconnect attempts reached");
698
+ this.setConnectionState("error" /* Error */);
699
+ this.emit("error", new exports.EstuaryError(
700
+ "CONNECTION_FAILED" /* CONNECTION_FAILED */,
701
+ `Failed to reconnect after ${maxAttempts} attempts`
702
+ ));
703
+ return;
704
+ }
705
+ this.reconnectAttempt++;
706
+ this.setConnectionState("reconnecting" /* Reconnecting */);
707
+ this.emit("reconnecting", this.reconnectAttempt);
708
+ this.logger.info(`Reconnecting (attempt ${this.reconnectAttempt}/${maxAttempts})...`);
709
+ this.clearReconnectTimer();
710
+ this.reconnectTimer = setTimeout(() => {
711
+ this.connect().catch((err) => {
712
+ this.logger.error("Reconnect failed:", err.message);
713
+ });
714
+ }, delay * this.reconnectAttempt);
715
+ }
716
+ clearReconnectTimer() {
717
+ if (this.reconnectTimer) {
718
+ clearTimeout(this.reconnectTimer);
719
+ this.reconnectTimer = null;
720
+ }
721
+ }
722
+ setConnectionState(state) {
723
+ if (this.connectionState !== state) {
724
+ this.connectionState = state;
725
+ this.emit("connectionStateChanged", state);
726
+ }
727
+ }
728
+ };
729
+
730
+ // src/voice/voice-manager.ts
731
+ function createVoiceManager(transport, socketManager, sampleRate, logger) {
732
+ if (transport === "websocket") {
733
+ const { WebSocketVoiceManager: WebSocketVoiceManager2 } = (init_websocket_voice(), __toCommonJS(websocket_voice_exports));
734
+ return new WebSocketVoiceManager2(socketManager, sampleRate, logger);
735
+ }
736
+ if (transport === "livekit") {
737
+ try {
738
+ __require.resolve("livekit-client");
739
+ const { LiveKitVoiceManager: LiveKitVoiceManager2 } = (init_livekit_voice(), __toCommonJS(livekit_voice_exports));
740
+ return new LiveKitVoiceManager2(socketManager, logger);
741
+ } catch {
742
+ logger.warn("livekit-client not installed, falling back to WebSocket voice");
743
+ const { WebSocketVoiceManager: WebSocketVoiceManager2 } = (init_websocket_voice(), __toCommonJS(websocket_voice_exports));
744
+ return new WebSocketVoiceManager2(socketManager, sampleRate, logger);
745
+ }
746
+ }
747
+ if (transport === "auto") {
748
+ try {
749
+ __require.resolve("livekit-client");
750
+ const { LiveKitVoiceManager: LiveKitVoiceManager2 } = (init_livekit_voice(), __toCommonJS(livekit_voice_exports));
751
+ return new LiveKitVoiceManager2(socketManager, logger);
752
+ } catch {
753
+ const { WebSocketVoiceManager: WebSocketVoiceManager2 } = (init_websocket_voice(), __toCommonJS(websocket_voice_exports));
754
+ return new WebSocketVoiceManager2(socketManager, sampleRate, logger);
755
+ }
756
+ }
757
+ return null;
758
+ }
759
+
760
+ // src/rest/rest-client.ts
761
+ init_errors();
762
+ var RestClient = class {
763
+ baseUrl;
764
+ apiKey;
765
+ constructor(baseUrl, apiKey) {
766
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
767
+ this.apiKey = apiKey;
768
+ }
769
+ async get(path, params) {
770
+ const url = this.buildUrl(path, params);
771
+ return this.request(url, { method: "GET" });
772
+ }
773
+ async post(path, body) {
774
+ const url = this.buildUrl(path);
775
+ const init = { method: "POST" };
776
+ if (body !== void 0) {
777
+ init.headers = { "Content-Type": "application/json" };
778
+ init.body = JSON.stringify(body);
779
+ }
780
+ return this.request(url, init);
781
+ }
782
+ async delete(path, params) {
783
+ const url = this.buildUrl(path, params);
784
+ return this.request(url, { method: "DELETE" });
785
+ }
786
+ dispose() {
787
+ }
788
+ buildUrl(path, params) {
789
+ const url = new URL(path, this.baseUrl);
790
+ if (params) {
791
+ for (const [key, value] of Object.entries(params)) {
792
+ if (value !== void 0) {
793
+ url.searchParams.set(key, String(value));
794
+ }
795
+ }
796
+ }
797
+ return url.toString();
798
+ }
799
+ async request(url, init) {
800
+ const headers = new Headers(init.headers);
801
+ headers.set("X-API-Key", this.apiKey);
802
+ const response = await fetch(url, { ...init, headers });
803
+ if (!response.ok) {
804
+ let detail;
805
+ try {
806
+ detail = await response.json();
807
+ } catch {
808
+ detail = await response.text().catch(() => null);
809
+ }
810
+ throw new exports.EstuaryError(
811
+ "REST_ERROR" /* REST_ERROR */,
812
+ `HTTP ${response.status}: ${response.statusText}`,
813
+ detail
814
+ );
815
+ }
816
+ return response.json();
817
+ }
818
+ };
819
+
820
+ // src/rest/memory-client.ts
821
+ var MemoryClient = class {
822
+ rest;
823
+ basePath;
824
+ constructor(rest, agentId, playerId) {
825
+ this.rest = rest;
826
+ this.basePath = `/api/agents/${agentId}/players/${playerId}/memories`;
827
+ }
828
+ async getMemories(options) {
829
+ return this.rest.get(this.basePath, options);
830
+ }
831
+ async getTimeline(options) {
832
+ return this.rest.get(`${this.basePath}/timeline`, options);
833
+ }
834
+ async getStats() {
835
+ return this.rest.get(`${this.basePath}/stats`);
836
+ }
837
+ async getCoreFacts() {
838
+ return this.rest.get(`${this.basePath}/core-facts`);
839
+ }
840
+ async getGraph(options) {
841
+ const params = {};
842
+ if (options) {
843
+ if (options.includeEntities !== void 0) params.include_entities = options.includeEntities;
844
+ if (options.includeCharacterMemories !== void 0) params.include_character_memories = options.includeCharacterMemories;
845
+ }
846
+ return this.rest.get(`${this.basePath}/graph`, params);
847
+ }
848
+ async search(query, limit) {
849
+ return this.rest.get(`${this.basePath}/search`, { query, limit });
850
+ }
851
+ async deleteAll(confirm) {
852
+ return this.rest.delete(this.basePath, { confirm });
853
+ }
854
+ dispose() {
855
+ }
856
+ };
857
+
858
+ // src/audio/audio-player.ts
859
+ var AudioPlayer = class {
860
+ sampleRate;
861
+ onEvent;
862
+ audioContext = null;
863
+ queue = [];
864
+ currentSource = null;
865
+ currentMessageId = null;
866
+ isPlaying = false;
867
+ constructor(sampleRate, onEvent) {
868
+ this.sampleRate = sampleRate;
869
+ this.onEvent = onEvent;
870
+ }
871
+ enqueue(voice) {
872
+ const ctx = this.getAudioContext();
873
+ if (!ctx) return;
874
+ const pcm16 = base64ToInt16Array(voice.audio);
875
+ const float32 = int16ToFloat32(pcm16);
876
+ const buffer = ctx.createBuffer(1, float32.length, this.sampleRate);
877
+ buffer.getChannelData(0).set(float32);
878
+ this.queue.push({ buffer, messageId: voice.messageId });
879
+ if (!this.isPlaying) {
880
+ this.playNext();
881
+ }
882
+ }
883
+ clear() {
884
+ this.queue.length = 0;
885
+ if (this.currentSource) {
886
+ try {
887
+ this.currentSource.onended = null;
888
+ this.currentSource.stop();
889
+ } catch {
890
+ }
891
+ this.currentSource = null;
892
+ }
893
+ this.isPlaying = false;
894
+ this.currentMessageId = null;
895
+ }
896
+ dispose() {
897
+ this.clear();
898
+ if (this.audioContext) {
899
+ this.audioContext.close().catch(() => {
900
+ });
901
+ this.audioContext = null;
902
+ }
903
+ }
904
+ getAudioContext() {
905
+ if (this.audioContext) return this.audioContext;
906
+ if (typeof AudioContext === "undefined" && typeof globalThis.webkitAudioContext === "undefined") {
907
+ return null;
908
+ }
909
+ const AudioCtx = globalThis.AudioContext || globalThis.webkitAudioContext;
910
+ this.audioContext = new AudioCtx({ sampleRate: this.sampleRate });
911
+ return this.audioContext;
912
+ }
913
+ playNext() {
914
+ const ctx = this.getAudioContext();
915
+ if (!ctx || this.queue.length === 0) {
916
+ if (this.isPlaying && this.currentMessageId) {
917
+ this.onEvent({ type: "complete", messageId: this.currentMessageId });
918
+ }
919
+ this.isPlaying = false;
920
+ this.currentMessageId = null;
921
+ return;
922
+ }
923
+ const { buffer, messageId } = this.queue.shift();
924
+ if (messageId !== this.currentMessageId) {
925
+ if (this.currentMessageId) {
926
+ this.onEvent({ type: "complete", messageId: this.currentMessageId });
927
+ }
928
+ this.currentMessageId = messageId;
929
+ this.onEvent({ type: "started", messageId });
930
+ }
931
+ this.isPlaying = true;
932
+ const source = ctx.createBufferSource();
933
+ source.buffer = buffer;
934
+ source.connect(ctx.destination);
935
+ this.currentSource = source;
936
+ source.onended = () => {
937
+ this.currentSource = null;
938
+ this.playNext();
939
+ };
940
+ source.start();
941
+ }
942
+ };
943
+ function base64ToInt16Array(base64) {
944
+ let bytes;
945
+ if (typeof atob === "function") {
946
+ const binary = atob(base64);
947
+ bytes = new Uint8Array(binary.length);
948
+ for (let i = 0; i < binary.length; i++) {
949
+ bytes[i] = binary.charCodeAt(i);
950
+ }
951
+ } else {
952
+ bytes = new Uint8Array(Buffer.from(base64, "base64"));
953
+ }
954
+ return new Int16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2);
955
+ }
956
+ function int16ToFloat32(int16) {
957
+ const float32 = new Float32Array(int16.length);
958
+ for (let i = 0; i < int16.length; i++) {
959
+ float32[i] = int16[i] / (int16[i] < 0 ? 32768 : 32767);
960
+ }
961
+ return float32;
962
+ }
963
+
964
+ // src/utils/logger.ts
965
+ var Logger = class {
966
+ enabled;
967
+ constructor(debug = false) {
968
+ this.enabled = debug;
969
+ }
970
+ setDebug(enabled) {
971
+ this.enabled = enabled;
972
+ }
973
+ debug(...args) {
974
+ if (this.enabled) console.debug("[Estuary]", ...args);
975
+ }
976
+ info(...args) {
977
+ if (this.enabled) console.info("[Estuary]", ...args);
978
+ }
979
+ warn(...args) {
980
+ console.warn("[Estuary]", ...args);
981
+ }
982
+ error(...args) {
983
+ console.error("[Estuary]", ...args);
984
+ }
985
+ };
986
+
987
+ // src/client.ts
988
+ init_errors();
989
+ var DEFAULT_SAMPLE_RATE = 16e3;
990
+ var EstuaryClient = class extends TypedEventEmitter {
991
+ config;
992
+ logger;
993
+ socketManager;
994
+ voiceManager = null;
995
+ audioPlayer = null;
996
+ _memory;
997
+ _sessionInfo = null;
998
+ constructor(config) {
999
+ super();
1000
+ this.config = config;
1001
+ this.logger = new Logger(config.debug ?? false);
1002
+ this.socketManager = new SocketManager(config, this.logger);
1003
+ this.forwardSocketEvents();
1004
+ const restClient = new RestClient(config.serverUrl, config.apiKey);
1005
+ this._memory = new MemoryClient(restClient, config.characterId, config.playerId);
1006
+ }
1007
+ /** Memory API client for querying memories, graphs, and facts */
1008
+ get memory() {
1009
+ return this._memory;
1010
+ }
1011
+ /** Current session info (null if not connected) */
1012
+ get session() {
1013
+ return this._sessionInfo;
1014
+ }
1015
+ /** Current connection state */
1016
+ get connectionState() {
1017
+ return this.socketManager.state;
1018
+ }
1019
+ /** Whether the client is connected and authenticated */
1020
+ get isConnected() {
1021
+ return this.socketManager.isConnected;
1022
+ }
1023
+ /** Connect to the Estuary server and authenticate */
1024
+ async connect() {
1025
+ this.logger.info("Connecting...");
1026
+ const session = await this.socketManager.connect();
1027
+ this._sessionInfo = session;
1028
+ return session;
1029
+ }
1030
+ /** Disconnect from the server */
1031
+ disconnect() {
1032
+ this.logger.info("Disconnecting...");
1033
+ this.stopVoice();
1034
+ this.audioPlayer?.dispose();
1035
+ this.audioPlayer = null;
1036
+ this.socketManager.disconnect();
1037
+ this._sessionInfo = null;
1038
+ }
1039
+ /** Send a text message to the character */
1040
+ sendText(text, textOnly = false) {
1041
+ this.ensureConnected();
1042
+ this.socketManager.emitEvent("text", { text, textOnly });
1043
+ }
1044
+ /** Interrupt the current bot response */
1045
+ interrupt(messageId) {
1046
+ this.ensureConnected();
1047
+ this.socketManager.emitEvent("client_interrupt", { message_id: messageId });
1048
+ this.audioPlayer?.clear();
1049
+ }
1050
+ /** Send a camera image for vision processing */
1051
+ sendCameraImage(imageBase64, mimeType, requestId, text) {
1052
+ this.ensureConnected();
1053
+ this.socketManager.emitEvent("camera_image", {
1054
+ image: imageBase64,
1055
+ mime_type: mimeType,
1056
+ request_id: requestId,
1057
+ text
1058
+ });
1059
+ }
1060
+ /** Update session preferences */
1061
+ updatePreferences(preferences) {
1062
+ this.ensureConnected();
1063
+ this.socketManager.emitEvent("update_preferences", preferences);
1064
+ }
1065
+ /** Notify server that audio playback completed for a message */
1066
+ notifyAudioPlaybackComplete(messageId) {
1067
+ this.ensureConnected();
1068
+ this.socketManager.emitEvent("audio_playback_complete", { message_id: messageId });
1069
+ }
1070
+ // ─── Voice ───────────────────────────────────────────────────
1071
+ /** Start voice input (requests microphone permission) */
1072
+ async startVoice() {
1073
+ this.ensureConnected();
1074
+ if (this.voiceManager?.isActive) {
1075
+ throw new exports.EstuaryError("VOICE_ALREADY_ACTIVE" /* VOICE_ALREADY_ACTIVE */, "Voice is already active");
1076
+ }
1077
+ const transport = this.config.voiceTransport ?? "auto";
1078
+ const sampleRate = this.config.audioSampleRate ?? DEFAULT_SAMPLE_RATE;
1079
+ this.voiceManager = createVoiceManager(transport, this.socketManager, sampleRate, this.logger);
1080
+ if (!this.voiceManager) {
1081
+ throw new exports.EstuaryError("VOICE_NOT_SUPPORTED" /* VOICE_NOT_SUPPORTED */, "No voice transport available");
1082
+ }
1083
+ if (!this.audioPlayer && typeof AudioContext !== "undefined") {
1084
+ this.audioPlayer = new AudioPlayer(sampleRate, (event) => {
1085
+ if (event.type === "started") {
1086
+ this.emit("audioPlaybackStarted", event.messageId);
1087
+ } else if (event.type === "complete") {
1088
+ this.emit("audioPlaybackComplete", event.messageId);
1089
+ this.notifyAudioPlaybackComplete(event.messageId);
1090
+ }
1091
+ });
1092
+ }
1093
+ await this.voiceManager.start();
1094
+ this.emit("voiceStarted");
1095
+ }
1096
+ /** Stop voice input */
1097
+ stopVoice() {
1098
+ if (this.voiceManager?.isActive) {
1099
+ this.voiceManager.stop();
1100
+ this.voiceManager.dispose();
1101
+ this.voiceManager = null;
1102
+ this.emit("voiceStopped");
1103
+ }
1104
+ }
1105
+ /** Toggle microphone mute */
1106
+ toggleMute() {
1107
+ if (!this.voiceManager?.isActive) {
1108
+ throw new exports.EstuaryError("VOICE_NOT_ACTIVE" /* VOICE_NOT_ACTIVE */, "Voice is not active");
1109
+ }
1110
+ this.voiceManager.toggleMute();
1111
+ }
1112
+ /** Whether the microphone is muted */
1113
+ get isMuted() {
1114
+ return this.voiceManager?.isMuted ?? false;
1115
+ }
1116
+ /** Whether voice is currently active */
1117
+ get isVoiceActive() {
1118
+ return this.voiceManager?.isActive ?? false;
1119
+ }
1120
+ // ─── Internal ────────────────────────────────────────────────
1121
+ ensureConnected() {
1122
+ if (!this.socketManager.isConnected) {
1123
+ throw new exports.EstuaryError("NOT_CONNECTED" /* NOT_CONNECTED */, "Not connected to server. Call connect() first.");
1124
+ }
1125
+ }
1126
+ forwardSocketEvents() {
1127
+ this.socketManager.on("connected", (session) => {
1128
+ this._sessionInfo = session;
1129
+ this.emit("connected", session);
1130
+ });
1131
+ this.socketManager.on("disconnected", (reason) => {
1132
+ this._sessionInfo = null;
1133
+ this.emit("disconnected", reason);
1134
+ });
1135
+ this.socketManager.on("reconnecting", (attempt) => this.emit("reconnecting", attempt));
1136
+ this.socketManager.on("connectionStateChanged", (state) => this.emit("connectionStateChanged", state));
1137
+ this.socketManager.on("botResponse", (response) => this.emit("botResponse", response));
1138
+ this.socketManager.on("botVoice", (voice) => this.handleBotVoice(voice));
1139
+ this.socketManager.on("sttResponse", (response) => this.emit("sttResponse", response));
1140
+ this.socketManager.on("interrupt", (data) => {
1141
+ this.audioPlayer?.clear();
1142
+ this.emit("interrupt", data);
1143
+ });
1144
+ this.socketManager.on("error", (error) => this.emit("error", error));
1145
+ this.socketManager.on("authError", (error) => this.emit("authError", error));
1146
+ this.socketManager.on("quotaExceeded", (data) => this.emit("quotaExceeded", data));
1147
+ this.socketManager.on("cameraCaptureRequest", (request) => this.emit("cameraCaptureRequest", request));
1148
+ this.socketManager.on("livekitConnected", (room) => this.emit("livekitConnected", room));
1149
+ this.socketManager.on("livekitDisconnected", () => this.emit("livekitDisconnected"));
1150
+ this.socketManager.on("memoryUpdated", (event) => this.emit("memoryUpdated", event));
1151
+ }
1152
+ handleBotVoice(voice) {
1153
+ this.emit("botVoice", voice);
1154
+ this.audioPlayer?.enqueue(voice);
1155
+ }
1156
+ };
1157
+
1158
+ // src/index.ts
1159
+ init_errors();
1160
+
1161
+ exports.ConnectionState = ConnectionState;
1162
+ exports.EstuaryClient = EstuaryClient;
1163
+ //# sourceMappingURL=index.js.map
1164
+ //# sourceMappingURL=index.js.map