@anganyai/voice-sdk 0.0.2 → 0.0.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.
package/dist/index.js CHANGED
@@ -3459,8 +3459,18 @@ var AuthManager = class extends EventEmitter {
3459
3459
  }
3460
3460
  /**
3461
3461
  * Get ephemeral credentials for WebRTC connections
3462
+ *
3463
+ * Returns cached credentials if available and valid, otherwise fetches new ones.
3464
+ * This supports both:
3465
+ * - Externally-provided credentials (via setEphemeralCredentials)
3466
+ * - SDK-managed credentials (fetched using access token)
3462
3467
  */
3463
3468
  async getEphemeralCredentials() {
3469
+ const cached = this.credentialManager.getCachedEphemeralCredentials();
3470
+ if (cached) {
3471
+ this.logger.debug("Using cached ephemeral credentials");
3472
+ return cached;
3473
+ }
3464
3474
  const accessToken = await this.getAccessToken();
3465
3475
  if (!accessToken) {
3466
3476
  throw new AuthenticationError("No access token available for ephemeral credentials");
@@ -16599,7 +16609,9 @@ var SipManager = class extends EventEmitter {
16599
16609
  this.remoteStream = event.streams[0];
16600
16610
  this.logger.info("Remote audio stream received via ontrack fallback");
16601
16611
  if (isReactNative()) {
16602
- this.logger.info("React Native: Remote audio from ontrack will be played automatically");
16612
+ this.logger.info(
16613
+ "React Native: Remote audio from ontrack will be played automatically"
16614
+ );
16603
16615
  } else {
16604
16616
  this.createAudioElement();
16605
16617
  if (this.audioElement && this.remoteStream) {
@@ -16644,7 +16656,9 @@ var SipManager = class extends EventEmitter {
16644
16656
  */
16645
16657
  createAudioElement() {
16646
16658
  if (isReactNative()) {
16647
- this.logger.debug("React Native detected - skipping audio element creation (handled by WebRTC)");
16659
+ this.logger.debug(
16660
+ "React Native detected - skipping audio element creation (handled by WebRTC)"
16661
+ );
16648
16662
  return;
16649
16663
  }
16650
16664
  if (!isBrowser()) {
@@ -16921,7 +16935,7 @@ var TranscriptionService = class extends EventEmitter {
16921
16935
  * Connect to SSE endpoint with token refresh support
16922
16936
  */
16923
16937
  async connectToSSE(accessToken, isRetry = false) {
16924
- const sseUrl = `${this.apiUrl}/api/v1/events?event_types=transcription`;
16938
+ const sseUrl = `${this.apiUrl}/api/v1/events?event_types=transcription,call_event`;
16925
16939
  this.logger.debug("Connecting to SSE endpoint", { url: sseUrl, isRetry });
16926
16940
  const response = await fetch(sseUrl, {
16927
16941
  method: "GET",
@@ -17051,13 +17065,36 @@ var TranscriptionService = class extends EventEmitter {
17051
17065
  }
17052
17066
  }
17053
17067
  handleMessage(data) {
17054
- this.logger.debug("Handling SSE message", { data });
17055
- if (data.type === "transcription" && data.text) {
17056
- if (data.call_id && !this.currentCallId) {
17057
- this.currentCallId = data.call_id;
17058
- this.emit("callId", data.call_id);
17059
- this.logger.info("Captured call ID from transcription", { callId: data.call_id });
17068
+ this.logger.debug("Handling SSE message", { type: data.type, data });
17069
+ if (data.call_id && !this.currentCallId) {
17070
+ this.currentCallId = data.call_id;
17071
+ this.emit("callId", data.call_id);
17072
+ this.logger.info("Captured call ID", { callId: data.call_id, messageType: data.type });
17073
+ }
17074
+ if (data.type === "connection" || data.type === "welcome" || data.type === "connected") {
17075
+ this.logger.info("Received connection event", {
17076
+ type: data.type,
17077
+ status: data.status
17078
+ });
17079
+ if (data.type === "connected" || data.status === "connected") {
17080
+ this.emit("connected");
17060
17081
  }
17082
+ } else if (data.type === "call_started") {
17083
+ this.logger.info("Received call_started event", {
17084
+ callId: data.call_id,
17085
+ organizationId: data.organization_id
17086
+ });
17087
+ if (data.call_id) {
17088
+ this.emit("callStarted", data.call_id);
17089
+ }
17090
+ } else if (data.type === "call_ended") {
17091
+ this.logger.info("Received call_ended event", {
17092
+ callId: data.call_id
17093
+ });
17094
+ if (data.call_id) {
17095
+ this.emit("callEnded", data.call_id);
17096
+ }
17097
+ } else if (data.type === "transcription" && data.text) {
17061
17098
  const event = {
17062
17099
  speaker: data.direction === "agent" ? "agent" : "user",
17063
17100
  text: data.text.trim(),
@@ -17065,6 +17102,9 @@ var TranscriptionService = class extends EventEmitter {
17065
17102
  isFinal: true,
17066
17103
  // Assume final for now
17067
17104
  callId: data.call_id,
17105
+ humanTurnId: data.human_turn_id ?? void 0,
17106
+ agentTurnId: data.agent_turn_id ?? void 0,
17107
+ speakerId: data.speaker_id ?? void 0,
17068
17108
  metadata: data
17069
17109
  };
17070
17110
  this.logger.debug("Emitting transcription event", { event });
@@ -17136,7 +17176,8 @@ var ApiService = class {
17136
17176
  this.logger.debug("Sending voice text", {
17137
17177
  callId: options.callId,
17138
17178
  textLength: options.text.length,
17139
- interrupt: options.interrupt,
17179
+ interruptsConversation: options.interruptsConversation,
17180
+ queueWhenSpeaking: options.queueWhenSpeaking,
17140
17181
  muteAgent: options.muteAgent
17141
17182
  });
17142
17183
  try {
@@ -17150,10 +17191,10 @@ var ApiService = class {
17150
17191
  },
17151
17192
  body: JSON.stringify({
17152
17193
  text: options.text,
17153
- interrupts_conversation: options.interrupt !== false,
17154
- // Default true
17155
- queue_when_speaking: false,
17156
- mute_agent: options.muteAgent
17194
+ interrupts_conversation: options.interruptsConversation ?? true,
17195
+ queue_when_speaking: options.queueWhenSpeaking ?? false,
17196
+ mute_agent: options.muteAgent ?? null,
17197
+ voice_settings: options.voiceSettings ?? null
17157
17198
  })
17158
17199
  }
17159
17200
  );
@@ -17239,11 +17280,103 @@ var ApiService = class {
17239
17280
  throw new NetworkError("Failed to set agent mute status", { cause: error });
17240
17281
  }
17241
17282
  }
17283
+ /**
17284
+ * Mute a call (POST /calls/{call_id}/mute)
17285
+ */
17286
+ async muteCall(callId, accessToken) {
17287
+ this.logger.debug("Muting call", { callId });
17288
+ try {
17289
+ const response = await fetch(`${this.apiUrl}/api/v1/conversations/calls/${callId}/mute`, {
17290
+ method: "POST",
17291
+ headers: {
17292
+ Authorization: `Bearer ${accessToken}`
17293
+ }
17294
+ });
17295
+ if (response.status === 401) {
17296
+ throw new AuthenticationError("Authentication failed");
17297
+ }
17298
+ if (!response.ok) {
17299
+ const errorText = await response.text();
17300
+ throw new NetworkError(
17301
+ `Failed to mute call: ${response.status} ${response.statusText} - ${errorText}`
17302
+ );
17303
+ }
17304
+ this.logger.info("Call muted successfully", { callId });
17305
+ } catch (error) {
17306
+ if (error instanceof AuthenticationError || error instanceof NetworkError) {
17307
+ throw error;
17308
+ }
17309
+ this.logger.error("Failed to mute call", { error });
17310
+ throw new NetworkError("Failed to mute call", { cause: error });
17311
+ }
17312
+ }
17313
+ /**
17314
+ * Unmute a call (DELETE /calls/{call_id}/mute)
17315
+ */
17316
+ async unmuteCall(callId, accessToken) {
17317
+ this.logger.debug("Unmuting call", { callId });
17318
+ try {
17319
+ const response = await fetch(`${this.apiUrl}/api/v1/conversations/calls/${callId}/mute`, {
17320
+ method: "DELETE",
17321
+ headers: {
17322
+ Authorization: `Bearer ${accessToken}`
17323
+ }
17324
+ });
17325
+ if (response.status === 401) {
17326
+ throw new AuthenticationError("Authentication failed");
17327
+ }
17328
+ if (!response.ok) {
17329
+ const errorText = await response.text();
17330
+ throw new NetworkError(
17331
+ `Failed to unmute call: ${response.status} ${response.statusText} - ${errorText}`
17332
+ );
17333
+ }
17334
+ this.logger.info("Call unmuted successfully", { callId });
17335
+ } catch (error) {
17336
+ if (error instanceof AuthenticationError || error instanceof NetworkError) {
17337
+ throw error;
17338
+ }
17339
+ this.logger.error("Failed to unmute call", { error });
17340
+ throw new NetworkError("Failed to unmute call", { cause: error });
17341
+ }
17342
+ }
17343
+ /**
17344
+ * Get call mute status (GET /calls/{call_id}/mute)
17345
+ */
17346
+ async getCallMuteStatus(callId, accessToken) {
17347
+ this.logger.debug("Getting call mute status", { callId });
17348
+ try {
17349
+ const response = await fetch(`${this.apiUrl}/api/v1/conversations/calls/${callId}/mute`, {
17350
+ method: "GET",
17351
+ headers: {
17352
+ Authorization: `Bearer ${accessToken}`
17353
+ }
17354
+ });
17355
+ if (response.status === 401) {
17356
+ throw new AuthenticationError("Authentication failed");
17357
+ }
17358
+ if (!response.ok) {
17359
+ const errorText = await response.text();
17360
+ throw new NetworkError(
17361
+ `Failed to get call mute status: ${response.status} ${response.statusText} - ${errorText}`
17362
+ );
17363
+ }
17364
+ const data = await response.json();
17365
+ this.logger.debug("Call mute status retrieved", { callId, muted: data.muted });
17366
+ return data;
17367
+ } catch (error) {
17368
+ if (error instanceof AuthenticationError || error instanceof NetworkError) {
17369
+ throw error;
17370
+ }
17371
+ this.logger.error("Failed to get call mute status", { error });
17372
+ throw new NetworkError("Failed to get call mute status", { cause: error });
17373
+ }
17374
+ }
17242
17375
  };
17243
17376
 
17244
17377
  // src/conversation/Conversation.ts
17245
17378
  var Conversation = class extends EventEmitter {
17246
- constructor(id, options, authManager, apiUrl) {
17379
+ constructor(id, options, authManager, urls) {
17247
17380
  super();
17248
17381
  this.logger = getLogger(["angany", "sdk", "conversation"]);
17249
17382
  this.state = "idle";
@@ -17252,12 +17385,17 @@ var Conversation = class extends EventEmitter {
17252
17385
  this.id = id;
17253
17386
  this.options = options;
17254
17387
  this.authManager = authManager;
17255
- this.apiUrl = apiUrl;
17388
+ this.urls = urls;
17389
+ const sseUrl = urls.sseUrl || urls.apiUrl;
17256
17390
  this.sipManager = new SipManager();
17257
- this.transcriptionService = new TranscriptionService(apiUrl);
17258
- this.apiService = new ApiService(apiUrl);
17391
+ this.transcriptionService = new TranscriptionService(sseUrl);
17392
+ this.apiService = new ApiService(urls.apiUrl);
17259
17393
  this.logger = this.logger.with({ conversationId: id, resource: options.resource });
17260
- this.logger.debug("Conversation created");
17394
+ this.logger.debug("Conversation created", {
17395
+ apiUrl: urls.apiUrl,
17396
+ sseUrl,
17397
+ sipUrl: urls.sipUrl || "will be derived"
17398
+ });
17261
17399
  }
17262
17400
  /**
17263
17401
  * Initialize and start the conversation
@@ -17354,8 +17492,12 @@ var Conversation = class extends EventEmitter {
17354
17492
  if (this.ephemeralCredentials.sip.realm) {
17355
17493
  sipConfig.realm = this.ephemeralCredentials.sip.realm;
17356
17494
  }
17357
- if (this.ephemeralCredentials.sip.websocketUrl) {
17495
+ if (this.urls.sipUrl) {
17496
+ sipConfig.websocketUrl = this.urls.sipUrl;
17497
+ this.logger.debug("Using configured SIP URL", { url: sipConfig.websocketUrl });
17498
+ } else if (this.ephemeralCredentials.sip.websocketUrl) {
17358
17499
  sipConfig.websocketUrl = this.ephemeralCredentials.sip.websocketUrl;
17500
+ this.logger.debug("Using platform-provided WebSocket URL", { url: sipConfig.websocketUrl });
17359
17501
  } else if (this.ephemeralCredentials.sip.uris && this.ephemeralCredentials.sip.uris.length > 0) {
17360
17502
  const wssUris = this.ephemeralCredentials.sip.uris.filter(
17361
17503
  (uri) => uri.includes("transport=wss")
@@ -17366,7 +17508,7 @@ var Conversation = class extends EventEmitter {
17366
17508
  const wsUri = publicUri || wssUris[0];
17367
17509
  if (wsUri) {
17368
17510
  this.logger.debug("Selected SIP URI", { uri: wsUri, isPublic: !!publicUri });
17369
- const apiDomain = new URL(this.apiUrl).hostname;
17511
+ const apiDomain = new URL(this.urls.apiUrl).hostname;
17370
17512
  sipConfig.websocketUrl = `wss://${apiDomain}/api/webrtc/`;
17371
17513
  this.logger.debug("Derived WebSocket URL from API domain", {
17372
17514
  apiDomain,
@@ -17374,15 +17516,18 @@ var Conversation = class extends EventEmitter {
17374
17516
  });
17375
17517
  }
17376
17518
  }
17377
- if (sipConfig.websocketUrl) {
17519
+ if (sipConfig.websocketUrl && !this.urls.sipUrl) {
17378
17520
  this.logger.debug("Original WebSocket URL", { url: sipConfig.websocketUrl });
17379
17521
  const privateIpPattern = /wss?:\/\/(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/;
17380
17522
  if (privateIpPattern.test(sipConfig.websocketUrl)) {
17381
17523
  this.logger.warn("Detected private IP in WebSocket URL, replacing with API domain");
17382
- const apiDomain = new URL(this.apiUrl).hostname;
17524
+ const apiDomain = new URL(this.urls.apiUrl).hostname;
17383
17525
  const originalUrl = sipConfig.websocketUrl;
17384
17526
  const correctedUrl = `wss://${apiDomain}/api/webrtc/`;
17385
- this.logger.debug("Extracted API domain", { domain: apiDomain, apiUrl: this.apiUrl });
17527
+ this.logger.debug("Extracted API domain", {
17528
+ domain: apiDomain,
17529
+ apiUrl: this.urls.apiUrl
17530
+ });
17386
17531
  sipConfig.websocketUrl = correctedUrl;
17387
17532
  this.logger.info("Corrected WebSocket URL", {
17388
17533
  original: originalUrl,
@@ -17454,11 +17599,11 @@ var Conversation = class extends EventEmitter {
17454
17599
  const sendVoiceOptions = {
17455
17600
  text,
17456
17601
  callId: this.callId,
17457
- muteAgent: this.agentMuted
17602
+ interruptsConversation: options?.interruptsConversation,
17603
+ queueWhenSpeaking: options?.queueWhenSpeaking,
17604
+ muteAgent: options?.muteAgent,
17605
+ voiceSettings: options?.voiceSettings
17458
17606
  };
17459
- if (options?.interrupt !== void 0) {
17460
- sendVoiceOptions.interrupt = options.interrupt;
17461
- }
17462
17607
  try {
17463
17608
  await this.apiService.sendVoice(sendVoiceOptions, this.accessToken);
17464
17609
  } catch (error) {
@@ -17545,6 +17690,118 @@ var Conversation = class extends EventEmitter {
17545
17690
  isAgentMuted() {
17546
17691
  return this.agentMuted;
17547
17692
  }
17693
+ /**
17694
+ * Get the current call ID (available after connection)
17695
+ */
17696
+ getCallId() {
17697
+ return this.callId;
17698
+ }
17699
+ /**
17700
+ * Mute the call via API (POST /calls/{call_id}/mute)
17701
+ */
17702
+ async muteCall() {
17703
+ this.logger.debug("Muting call via API");
17704
+ if (!this.callId) {
17705
+ throw new ConversationError("No call ID available - call may not be connected yet");
17706
+ }
17707
+ if (!this.accessToken) {
17708
+ throw new AuthenticationError("No access token available");
17709
+ }
17710
+ try {
17711
+ await this.apiService.muteCall(this.callId, this.accessToken);
17712
+ this.logger.info("Call muted via API", { callId: this.callId });
17713
+ } catch (error) {
17714
+ if (error instanceof AuthenticationError) {
17715
+ this.logger.debug("API call failed with auth error, attempting token refresh");
17716
+ try {
17717
+ const freshToken = await this.authManager.getAccessToken();
17718
+ if (freshToken && freshToken !== this.accessToken) {
17719
+ this.accessToken = freshToken;
17720
+ this.logger.debug("Token refreshed, retrying mute call operation");
17721
+ await this.apiService.muteCall(this.callId, this.accessToken);
17722
+ return;
17723
+ }
17724
+ throw error;
17725
+ } catch (refreshError) {
17726
+ this.logger.error("Failed to refresh token for mute call operation", {
17727
+ error: refreshError
17728
+ });
17729
+ throw error;
17730
+ }
17731
+ }
17732
+ throw error;
17733
+ }
17734
+ }
17735
+ /**
17736
+ * Unmute the call via API (DELETE /calls/{call_id}/mute)
17737
+ */
17738
+ async unmuteCall() {
17739
+ this.logger.debug("Unmuting call via API");
17740
+ if (!this.callId) {
17741
+ throw new ConversationError("No call ID available - call may not be connected yet");
17742
+ }
17743
+ if (!this.accessToken) {
17744
+ throw new AuthenticationError("No access token available");
17745
+ }
17746
+ try {
17747
+ await this.apiService.unmuteCall(this.callId, this.accessToken);
17748
+ this.logger.info("Call unmuted via API", { callId: this.callId });
17749
+ } catch (error) {
17750
+ if (error instanceof AuthenticationError) {
17751
+ this.logger.debug("API call failed with auth error, attempting token refresh");
17752
+ try {
17753
+ const freshToken = await this.authManager.getAccessToken();
17754
+ if (freshToken && freshToken !== this.accessToken) {
17755
+ this.accessToken = freshToken;
17756
+ this.logger.debug("Token refreshed, retrying unmute call operation");
17757
+ await this.apiService.unmuteCall(this.callId, this.accessToken);
17758
+ return;
17759
+ }
17760
+ throw error;
17761
+ } catch (refreshError) {
17762
+ this.logger.error("Failed to refresh token for unmute call operation", {
17763
+ error: refreshError
17764
+ });
17765
+ throw error;
17766
+ }
17767
+ }
17768
+ throw error;
17769
+ }
17770
+ }
17771
+ /**
17772
+ * Get call mute status via API (GET /calls/{call_id}/mute)
17773
+ */
17774
+ async getCallMuteStatus() {
17775
+ this.logger.debug("Getting call mute status via API");
17776
+ if (!this.callId) {
17777
+ throw new ConversationError("No call ID available - call may not be connected yet");
17778
+ }
17779
+ if (!this.accessToken) {
17780
+ throw new AuthenticationError("No access token available");
17781
+ }
17782
+ try {
17783
+ return await this.apiService.getCallMuteStatus(this.callId, this.accessToken);
17784
+ } catch (error) {
17785
+ if (error instanceof AuthenticationError) {
17786
+ this.logger.debug("API call failed with auth error, attempting token refresh");
17787
+ try {
17788
+ const freshToken = await this.authManager.getAccessToken();
17789
+ if (freshToken && freshToken !== this.accessToken) {
17790
+ this.accessToken = freshToken;
17791
+ this.logger.debug("Token refreshed, retrying get call mute status operation");
17792
+ return await this.apiService.getCallMuteStatus(this.callId, this.accessToken);
17793
+ }
17794
+ throw error;
17795
+ } catch (refreshError) {
17796
+ this.logger.error("Failed to refresh token for get call mute status operation", {
17797
+ error: refreshError
17798
+ });
17799
+ throw error;
17800
+ }
17801
+ }
17802
+ throw error;
17803
+ }
17804
+ }
17548
17805
  /**
17549
17806
  * Get conversation status
17550
17807
  */
@@ -17700,12 +17957,24 @@ var Conversation = class extends EventEmitter {
17700
17957
  speaker: event.speaker,
17701
17958
  text: event.text,
17702
17959
  timestamp: event.timestamp,
17703
- isFinal: event.isFinal
17960
+ isFinal: event.isFinal,
17961
+ humanTurnId: event.humanTurnId,
17962
+ agentTurnId: event.agentTurnId,
17963
+ speakerId: event.speakerId
17704
17964
  });
17705
17965
  });
17706
17966
  this.transcriptionService.on("callId", (callId) => {
17707
17967
  this.logger.info("Received call ID", { callId });
17708
17968
  this.callId = callId;
17969
+ this.emit("callId", callId);
17970
+ });
17971
+ this.transcriptionService.on("callStarted", (callId) => {
17972
+ this.logger.info("Call started", { callId });
17973
+ this.emit("callStarted", callId);
17974
+ });
17975
+ this.transcriptionService.on("callEnded", (callId) => {
17976
+ this.logger.info("Call ended", { callId });
17977
+ this.emit("callEnded", callId);
17709
17978
  });
17710
17979
  this.transcriptionService.on("error", (error) => {
17711
17980
  this.logger.error("Transcription error", { error });
@@ -17763,8 +18032,12 @@ var AnganyVoice = class extends EventEmitter {
17763
18032
  this.logger = getLogger(["angany", "sdk", "core"]);
17764
18033
  this.conversations = /* @__PURE__ */ new Map();
17765
18034
  this.config = config;
17766
- this.auth = new AuthManager(config.apiUrl, config.apiUrl);
17767
- this.logger.debug("AnganyVoice initialized", { apiUrl: config.apiUrl });
18035
+ this.auth = new AuthManager(config.apiUrl, config.issuer || config.apiUrl);
18036
+ this.logger.debug("AnganyVoice initialized", {
18037
+ apiUrl: config.apiUrl,
18038
+ sseUrl: config.sseUrl || config.apiUrl,
18039
+ sipUrl: config.sipUrl || "derived from apiUrl"
18040
+ });
17768
18041
  }
17769
18042
  /**
17770
18043
  * Get the current configuration
@@ -17799,7 +18072,11 @@ var AnganyVoice = class extends EventEmitter {
17799
18072
  ...options
17800
18073
  },
17801
18074
  this.auth,
17802
- this.config.apiUrl
18075
+ {
18076
+ apiUrl: this.config.apiUrl,
18077
+ sseUrl: this.config.sseUrl,
18078
+ sipUrl: this.config.sipUrl
18079
+ }
17803
18080
  );
17804
18081
  conversation.on("ended", () => {
17805
18082
  this.conversations.delete(conversationId);