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