kugelaudio 0.1.11 → 0.1.13

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 CHANGED
@@ -135,8 +135,35 @@ function createWavBlob(audio, sampleRate) {
135
135
  return new Blob([wavBuffer], { type: "audio/wav" });
136
136
  }
137
137
 
138
+ // src/websocket.ts
139
+ var _cachedWs = null;
140
+ function getWebSocket() {
141
+ if (_cachedWs) return _cachedWs;
142
+ if (typeof globalThis !== "undefined" && typeof globalThis.WebSocket !== "undefined") {
143
+ _cachedWs = globalThis.WebSocket;
144
+ return _cachedWs;
145
+ }
146
+ try {
147
+ const _require = typeof require !== "undefined" ? require : Function('return typeof require !== "undefined" ? require : undefined')();
148
+ if (_require) {
149
+ const ws = _require("ws");
150
+ _cachedWs = ws.default || ws;
151
+ return _cachedWs;
152
+ }
153
+ } catch {
154
+ }
155
+ throw new Error(
156
+ 'WebSocket not available. In Node.js, install the "ws" package: npm install ws'
157
+ );
158
+ }
159
+
138
160
  // src/client.ts
139
161
  var DEFAULT_API_URL = "https://api.kugelaudio.com";
162
+ function createWs(url) {
163
+ const WS = getWebSocket();
164
+ return new WS(url);
165
+ }
166
+ var WS_OPEN = 1;
140
167
  var ModelsResource = class {
141
168
  constructor(client) {
142
169
  this.client = client;
@@ -212,6 +239,7 @@ var VoicesResource = class {
212
239
  var TTSResource = class {
213
240
  constructor(client) {
214
241
  this.client = client;
242
+ // Using any for WebSocket to support both browser WebSocket and ws package
215
243
  this.wsConnection = null;
216
244
  this.wsUrl = null;
217
245
  this.pendingRequests = /* @__PURE__ */ new Map();
@@ -241,7 +269,7 @@ var TTSResource = class {
241
269
  * Check if WebSocket connection is established and open.
242
270
  */
243
271
  isConnected() {
244
- return this.wsConnection !== null && this.wsConnection.readyState === WebSocket.OPEN;
272
+ return this.wsConnection !== null && this.wsConnection.readyState === WS_OPEN;
245
273
  }
246
274
  /**
247
275
  * Generate audio from text with streaming via WebSocket.
@@ -299,7 +327,7 @@ var TTSResource = class {
299
327
  */
300
328
  async getConnection() {
301
329
  const url = this.buildWsUrl();
302
- if (this.wsConnection && this.wsUrl === url && this.wsConnection.readyState === WebSocket.OPEN) {
330
+ if (this.wsConnection && this.wsUrl === url && this.wsConnection.readyState === WS_OPEN) {
303
331
  return this.wsConnection;
304
332
  }
305
333
  if (this.wsConnection) {
@@ -310,7 +338,7 @@ var TTSResource = class {
310
338
  this.wsConnection = null;
311
339
  }
312
340
  return new Promise((resolve, reject) => {
313
- const ws = new WebSocket(url);
341
+ const ws = createWs(url);
314
342
  ws.onopen = () => {
315
343
  this.wsConnection = ws;
316
344
  this.wsUrl = url;
@@ -328,7 +356,8 @@ var TTSResource = class {
328
356
  setupMessageHandler(ws) {
329
357
  ws.onmessage = (event) => {
330
358
  try {
331
- const data = JSON.parse(event.data);
359
+ const messageData = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
360
+ const data = JSON.parse(messageData);
332
361
  const [requestId, pending] = [...this.pendingRequests.entries()][0] || [];
333
362
  if (!pending) return;
334
363
  if (data.error) {
@@ -431,7 +460,7 @@ var TTSResource = class {
431
460
  streamWithoutPooling(options, callbacks) {
432
461
  return new Promise((resolve, reject) => {
433
462
  const url = this.buildWsUrl();
434
- const ws = new WebSocket(url);
463
+ const ws = createWs(url);
435
464
  ws.onopen = () => {
436
465
  callbacks.onOpen?.();
437
466
  ws.send(JSON.stringify({
@@ -447,7 +476,8 @@ var TTSResource = class {
447
476
  };
448
477
  ws.onmessage = (event) => {
449
478
  try {
450
- const data = JSON.parse(event.data);
479
+ const messageData = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
480
+ const data = JSON.parse(messageData);
451
481
  if (data.error) {
452
482
  const error = this.parseError(data.error);
453
483
  callbacks.onError?.(error);
@@ -593,12 +623,13 @@ var MultiContextSession = class {
593
623
  authParam = "api_key";
594
624
  }
595
625
  const url = `${wsUrl}/ws/tts/multi?${authParam}=${this.client.apiKey}`;
596
- this.ws = new WebSocket(url);
626
+ this.ws = createWs(url);
597
627
  this.ws.onopen = () => {
598
628
  };
599
629
  this.ws.onmessage = (event) => {
600
630
  try {
601
- const data = JSON.parse(event.data);
631
+ const messageData = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
632
+ const data = JSON.parse(messageData);
602
633
  if (data.error) {
603
634
  this.callbacks.onError?.(
604
635
  new KugelAudioError(data.error),
@@ -662,7 +693,7 @@ var MultiContextSession = class {
662
693
  * Create a new context with optional voice settings.
663
694
  */
664
695
  createContext(contextId, options) {
665
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
696
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
666
697
  throw new KugelAudioError("WebSocket not connected");
667
698
  }
668
699
  const msg = {
@@ -693,7 +724,7 @@ var MultiContextSession = class {
693
724
  * Send text to a specific context.
694
725
  */
695
726
  send(contextId, text, flush = false) {
696
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
727
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
697
728
  throw new KugelAudioError("WebSocket not connected");
698
729
  }
699
730
  if (!this.contexts.has(contextId) && !this.isStarted) {
@@ -709,7 +740,7 @@ var MultiContextSession = class {
709
740
  * Flush a context's buffer.
710
741
  */
711
742
  flush(contextId) {
712
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
743
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
713
744
  this.ws.send(JSON.stringify({
714
745
  flush: true,
715
746
  context_id: contextId
@@ -719,7 +750,7 @@ var MultiContextSession = class {
719
750
  * Close a specific context.
720
751
  */
721
752
  closeContext(contextId) {
722
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
753
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
723
754
  this.ws.send(JSON.stringify({
724
755
  close_context: true,
725
756
  context_id: contextId
@@ -729,7 +760,7 @@ var MultiContextSession = class {
729
760
  * Send keep-alive to reset a context's inactivity timeout.
730
761
  */
731
762
  keepAlive(contextId) {
732
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
763
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
733
764
  this.ws.send(JSON.stringify({
734
765
  text: "",
735
766
  context_id: contextId
@@ -739,7 +770,7 @@ var MultiContextSession = class {
739
770
  * Close the session and all contexts.
740
771
  */
741
772
  close() {
742
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
773
+ if (this.ws && this.ws.readyState === WS_OPEN) {
743
774
  this.ws.send(JSON.stringify({ close_socket: true }));
744
775
  this.ws.close();
745
776
  }
@@ -757,7 +788,7 @@ var MultiContextSession = class {
757
788
  * Check if connected.
758
789
  */
759
790
  get isConnected() {
760
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
791
+ return this.ws !== null && this.ws.readyState === WS_OPEN;
761
792
  }
762
793
  };
763
794
  var KugelAudio = class _KugelAudio {
package/dist/index.mjs CHANGED
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/errors.ts
2
9
  var KugelAudioError = class _KugelAudioError extends Error {
3
10
  constructor(message, statusCode) {
@@ -99,8 +106,35 @@ function createWavBlob(audio, sampleRate) {
99
106
  return new Blob([wavBuffer], { type: "audio/wav" });
100
107
  }
101
108
 
109
+ // src/websocket.ts
110
+ var _cachedWs = null;
111
+ function getWebSocket() {
112
+ if (_cachedWs) return _cachedWs;
113
+ if (typeof globalThis !== "undefined" && typeof globalThis.WebSocket !== "undefined") {
114
+ _cachedWs = globalThis.WebSocket;
115
+ return _cachedWs;
116
+ }
117
+ try {
118
+ const _require = typeof __require !== "undefined" ? __require : Function('return typeof require !== "undefined" ? require : undefined')();
119
+ if (_require) {
120
+ const ws = _require("ws");
121
+ _cachedWs = ws.default || ws;
122
+ return _cachedWs;
123
+ }
124
+ } catch {
125
+ }
126
+ throw new Error(
127
+ 'WebSocket not available. In Node.js, install the "ws" package: npm install ws'
128
+ );
129
+ }
130
+
102
131
  // src/client.ts
103
132
  var DEFAULT_API_URL = "https://api.kugelaudio.com";
133
+ function createWs(url) {
134
+ const WS = getWebSocket();
135
+ return new WS(url);
136
+ }
137
+ var WS_OPEN = 1;
104
138
  var ModelsResource = class {
105
139
  constructor(client) {
106
140
  this.client = client;
@@ -176,6 +210,7 @@ var VoicesResource = class {
176
210
  var TTSResource = class {
177
211
  constructor(client) {
178
212
  this.client = client;
213
+ // Using any for WebSocket to support both browser WebSocket and ws package
179
214
  this.wsConnection = null;
180
215
  this.wsUrl = null;
181
216
  this.pendingRequests = /* @__PURE__ */ new Map();
@@ -205,7 +240,7 @@ var TTSResource = class {
205
240
  * Check if WebSocket connection is established and open.
206
241
  */
207
242
  isConnected() {
208
- return this.wsConnection !== null && this.wsConnection.readyState === WebSocket.OPEN;
243
+ return this.wsConnection !== null && this.wsConnection.readyState === WS_OPEN;
209
244
  }
210
245
  /**
211
246
  * Generate audio from text with streaming via WebSocket.
@@ -263,7 +298,7 @@ var TTSResource = class {
263
298
  */
264
299
  async getConnection() {
265
300
  const url = this.buildWsUrl();
266
- if (this.wsConnection && this.wsUrl === url && this.wsConnection.readyState === WebSocket.OPEN) {
301
+ if (this.wsConnection && this.wsUrl === url && this.wsConnection.readyState === WS_OPEN) {
267
302
  return this.wsConnection;
268
303
  }
269
304
  if (this.wsConnection) {
@@ -274,7 +309,7 @@ var TTSResource = class {
274
309
  this.wsConnection = null;
275
310
  }
276
311
  return new Promise((resolve, reject) => {
277
- const ws = new WebSocket(url);
312
+ const ws = createWs(url);
278
313
  ws.onopen = () => {
279
314
  this.wsConnection = ws;
280
315
  this.wsUrl = url;
@@ -292,7 +327,8 @@ var TTSResource = class {
292
327
  setupMessageHandler(ws) {
293
328
  ws.onmessage = (event) => {
294
329
  try {
295
- const data = JSON.parse(event.data);
330
+ const messageData = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
331
+ const data = JSON.parse(messageData);
296
332
  const [requestId, pending] = [...this.pendingRequests.entries()][0] || [];
297
333
  if (!pending) return;
298
334
  if (data.error) {
@@ -395,7 +431,7 @@ var TTSResource = class {
395
431
  streamWithoutPooling(options, callbacks) {
396
432
  return new Promise((resolve, reject) => {
397
433
  const url = this.buildWsUrl();
398
- const ws = new WebSocket(url);
434
+ const ws = createWs(url);
399
435
  ws.onopen = () => {
400
436
  callbacks.onOpen?.();
401
437
  ws.send(JSON.stringify({
@@ -411,7 +447,8 @@ var TTSResource = class {
411
447
  };
412
448
  ws.onmessage = (event) => {
413
449
  try {
414
- const data = JSON.parse(event.data);
450
+ const messageData = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
451
+ const data = JSON.parse(messageData);
415
452
  if (data.error) {
416
453
  const error = this.parseError(data.error);
417
454
  callbacks.onError?.(error);
@@ -557,12 +594,13 @@ var MultiContextSession = class {
557
594
  authParam = "api_key";
558
595
  }
559
596
  const url = `${wsUrl}/ws/tts/multi?${authParam}=${this.client.apiKey}`;
560
- this.ws = new WebSocket(url);
597
+ this.ws = createWs(url);
561
598
  this.ws.onopen = () => {
562
599
  };
563
600
  this.ws.onmessage = (event) => {
564
601
  try {
565
- const data = JSON.parse(event.data);
602
+ const messageData = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
603
+ const data = JSON.parse(messageData);
566
604
  if (data.error) {
567
605
  this.callbacks.onError?.(
568
606
  new KugelAudioError(data.error),
@@ -626,7 +664,7 @@ var MultiContextSession = class {
626
664
  * Create a new context with optional voice settings.
627
665
  */
628
666
  createContext(contextId, options) {
629
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
667
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
630
668
  throw new KugelAudioError("WebSocket not connected");
631
669
  }
632
670
  const msg = {
@@ -657,7 +695,7 @@ var MultiContextSession = class {
657
695
  * Send text to a specific context.
658
696
  */
659
697
  send(contextId, text, flush = false) {
660
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
698
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
661
699
  throw new KugelAudioError("WebSocket not connected");
662
700
  }
663
701
  if (!this.contexts.has(contextId) && !this.isStarted) {
@@ -673,7 +711,7 @@ var MultiContextSession = class {
673
711
  * Flush a context's buffer.
674
712
  */
675
713
  flush(contextId) {
676
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
714
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
677
715
  this.ws.send(JSON.stringify({
678
716
  flush: true,
679
717
  context_id: contextId
@@ -683,7 +721,7 @@ var MultiContextSession = class {
683
721
  * Close a specific context.
684
722
  */
685
723
  closeContext(contextId) {
686
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
724
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
687
725
  this.ws.send(JSON.stringify({
688
726
  close_context: true,
689
727
  context_id: contextId
@@ -693,7 +731,7 @@ var MultiContextSession = class {
693
731
  * Send keep-alive to reset a context's inactivity timeout.
694
732
  */
695
733
  keepAlive(contextId) {
696
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
734
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
697
735
  this.ws.send(JSON.stringify({
698
736
  text: "",
699
737
  context_id: contextId
@@ -703,7 +741,7 @@ var MultiContextSession = class {
703
741
  * Close the session and all contexts.
704
742
  */
705
743
  close() {
706
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
744
+ if (this.ws && this.ws.readyState === WS_OPEN) {
707
745
  this.ws.send(JSON.stringify({ close_socket: true }));
708
746
  this.ws.close();
709
747
  }
@@ -721,7 +759,7 @@ var MultiContextSession = class {
721
759
  * Check if connected.
722
760
  */
723
761
  get isConnected() {
724
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
762
+ return this.ws !== null && this.ws.readyState === WS_OPEN;
725
763
  }
726
764
  };
727
765
  var KugelAudio = class _KugelAudio {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kugelaudio",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -52,5 +52,9 @@
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=18.0.0"
55
+ },
56
+ "dependencies": {
57
+ "tsx": "^4.21.0",
58
+ "ws": "^8.18.0"
55
59
  }
56
60
  }
package/src/client.ts CHANGED
@@ -19,9 +19,23 @@ import type {
19
19
  Voice
20
20
  } from './types';
21
21
  import { base64ToArrayBuffer } from './utils';
22
+ import { getWebSocket } from './websocket';
22
23
 
23
24
  const DEFAULT_API_URL = 'https://api.kugelaudio.com';
24
25
 
26
+ /**
27
+ * Create a new WebSocket instance.
28
+ * Lazily resolves the constructor to avoid top-level side-effects
29
+ * that break server-side bundlers (Turbopack/Webpack).
30
+ */
31
+ function createWs(url: string): WebSocket {
32
+ const WS = getWebSocket();
33
+ return new WS(url);
34
+ }
35
+
36
+ /** WebSocket OPEN readyState constant. */
37
+ const WS_OPEN = 1;
38
+
25
39
  /**
26
40
  * Models resource for listing TTS models.
27
41
  */
@@ -111,6 +125,7 @@ class VoicesResource {
111
125
  * TTS resource for text-to-speech generation.
112
126
  */
113
127
  class TTSResource {
128
+ // Using any for WebSocket to support both browser WebSocket and ws package
114
129
  private wsConnection: WebSocket | null = null;
115
130
  private wsUrl: string | null = null;
116
131
  private pendingRequests: Map<number, {
@@ -147,7 +162,7 @@ class TTSResource {
147
162
  * Check if WebSocket connection is established and open.
148
163
  */
149
164
  isConnected(): boolean {
150
- return this.wsConnection !== null && this.wsConnection.readyState === WebSocket.OPEN;
165
+ return this.wsConnection !== null && this.wsConnection.readyState === WS_OPEN;
151
166
  }
152
167
 
153
168
  /**
@@ -221,7 +236,7 @@ class TTSResource {
221
236
  if (
222
237
  this.wsConnection &&
223
238
  this.wsUrl === url &&
224
- this.wsConnection.readyState === WebSocket.OPEN
239
+ this.wsConnection.readyState === WS_OPEN
225
240
  ) {
226
241
  return this.wsConnection;
227
242
  }
@@ -238,7 +253,7 @@ class TTSResource {
238
253
 
239
254
  // Create new connection
240
255
  return new Promise((resolve, reject) => {
241
- const ws = new WebSocket(url);
256
+ const ws = createWs(url);
242
257
 
243
258
  ws.onopen = () => {
244
259
  this.wsConnection = ws;
@@ -257,9 +272,15 @@ class TTSResource {
257
272
  * Setup message handler for pooled connection.
258
273
  */
259
274
  private setupMessageHandler(ws: WebSocket): void {
260
- ws.onmessage = (event) => {
275
+ ws.onmessage = (event: { data: unknown }) => {
261
276
  try {
262
- const data = JSON.parse(event.data);
277
+ // Handle both browser (string) and Node.js (Buffer) message formats
278
+ const messageData = typeof event.data === 'string'
279
+ ? event.data
280
+ : event.data instanceof Buffer
281
+ ? event.data.toString()
282
+ : String(event.data);
283
+ const data = JSON.parse(messageData);
263
284
 
264
285
  // Get the current pending request (we process one at a time)
265
286
  const [requestId, pending] = [...this.pendingRequests.entries()][0] || [];
@@ -389,7 +410,7 @@ class TTSResource {
389
410
  ): Promise<void> {
390
411
  return new Promise((resolve, reject) => {
391
412
  const url = this.buildWsUrl();
392
- const ws = new WebSocket(url);
413
+ const ws = createWs(url);
393
414
 
394
415
  ws.onopen = () => {
395
416
  callbacks.onOpen?.();
@@ -406,9 +427,15 @@ class TTSResource {
406
427
  }));
407
428
  };
408
429
 
409
- ws.onmessage = (event) => {
430
+ ws.onmessage = (event: { data: unknown }) => {
410
431
  try {
411
- const data = JSON.parse(event.data);
432
+ // Handle both browser (string) and Node.js (Buffer) message formats
433
+ const messageData = typeof event.data === 'string'
434
+ ? event.data
435
+ : event.data instanceof Buffer
436
+ ? event.data.toString()
437
+ : String(event.data);
438
+ const data = JSON.parse(messageData);
412
439
 
413
440
  if (data.error) {
414
441
  const error = this.parseError(data.error);
@@ -580,15 +607,21 @@ class MultiContextSession {
580
607
  }
581
608
 
582
609
  const url = `${wsUrl}/ws/tts/multi?${authParam}=${this.client.apiKey}`;
583
- this.ws = new WebSocket(url);
610
+ this.ws = createWs(url);
584
611
 
585
612
  this.ws.onopen = () => {
586
613
  // Connection established, ready to create contexts
587
614
  };
588
615
 
589
- this.ws.onmessage = (event) => {
616
+ this.ws.onmessage = (event: { data: unknown }) => {
590
617
  try {
591
- const data = JSON.parse(event.data);
618
+ // Handle both browser (string) and Node.js (Buffer) message formats
619
+ const messageData = typeof event.data === 'string'
620
+ ? event.data
621
+ : event.data instanceof Buffer
622
+ ? event.data.toString()
623
+ : String(event.data);
624
+ const data = JSON.parse(messageData);
592
625
 
593
626
  if (data.error) {
594
627
  this.callbacks.onError?.(
@@ -669,7 +702,7 @@ class MultiContextSession {
669
702
  voiceSettings?: import('./types').ContextVoiceSettings;
670
703
  }
671
704
  ): void {
672
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
705
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
673
706
  throw new KugelAudioError('WebSocket not connected');
674
707
  }
675
708
 
@@ -708,7 +741,7 @@ class MultiContextSession {
708
741
  * Send text to a specific context.
709
742
  */
710
743
  send(contextId: string, text: string, flush = false): void {
711
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
744
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
712
745
  throw new KugelAudioError('WebSocket not connected');
713
746
  }
714
747
 
@@ -728,7 +761,7 @@ class MultiContextSession {
728
761
  * Flush a context's buffer.
729
762
  */
730
763
  flush(contextId: string): void {
731
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
764
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
732
765
 
733
766
  this.ws.send(JSON.stringify({
734
767
  flush: true,
@@ -740,7 +773,7 @@ class MultiContextSession {
740
773
  * Close a specific context.
741
774
  */
742
775
  closeContext(contextId: string): void {
743
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
776
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
744
777
 
745
778
  this.ws.send(JSON.stringify({
746
779
  close_context: true,
@@ -752,7 +785,7 @@ class MultiContextSession {
752
785
  * Send keep-alive to reset a context's inactivity timeout.
753
786
  */
754
787
  keepAlive(contextId: string): void {
755
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
788
+ if (!this.ws || this.ws.readyState !== WS_OPEN) return;
756
789
 
757
790
  this.ws.send(JSON.stringify({
758
791
  text: '',
@@ -764,7 +797,7 @@ class MultiContextSession {
764
797
  * Close the session and all contexts.
765
798
  */
766
799
  close(): void {
767
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
800
+ if (this.ws && this.ws.readyState === WS_OPEN) {
768
801
  this.ws.send(JSON.stringify({ close_socket: true }));
769
802
  this.ws.close();
770
803
  }
@@ -784,7 +817,7 @@ class MultiContextSession {
784
817
  * Check if connected.
785
818
  */
786
819
  get isConnected(): boolean {
787
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
820
+ return this.ws !== null && this.ws.readyState === WS_OPEN;
788
821
  }
789
822
  }
790
823
 
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WebSocket compatibility layer for browser and Node.js environments.
3
+ *
4
+ * IMPORTANT: WebSocket resolution is lazy to avoid top-level side-effects
5
+ * that break server-side bundlers (Turbopack / Webpack) when this module
6
+ * is imported in a Node.js (API route) context.
7
+ */
8
+
9
+ let _cachedWs: typeof WebSocket | null = null;
10
+
11
+ /**
12
+ * Get the WebSocket constructor for the current environment.
13
+ * Uses native WebSocket in browsers, ws package in Node.js.
14
+ * Result is cached after first call.
15
+ */
16
+ export function getWebSocket(): typeof WebSocket {
17
+ if (_cachedWs) return _cachedWs;
18
+
19
+ // Browser environment
20
+ if (typeof globalThis !== 'undefined' && typeof (globalThis as any).WebSocket !== 'undefined') {
21
+ _cachedWs = (globalThis as any).WebSocket;
22
+ return _cachedWs!;
23
+ }
24
+
25
+ // Node.js environment - use ws package via dynamic require
26
+ try {
27
+ // Use Function constructor to hide require from static analysis by bundlers
28
+ // eslint-disable-next-line no-new-func
29
+ const _require = typeof require !== 'undefined'
30
+ ? require
31
+ : Function('return typeof require !== "undefined" ? require : undefined')();
32
+ if (_require) {
33
+ const ws = _require('ws');
34
+ _cachedWs = ws.default || ws;
35
+ return _cachedWs!;
36
+ }
37
+ } catch {
38
+ // Fall through to error
39
+ }
40
+
41
+ throw new Error(
42
+ 'WebSocket not available. In Node.js, install the "ws" package: npm install ws'
43
+ );
44
+ }