@openclaw/voice-call 2026.10.1 → 2026.10.5-beta.1

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.
@@ -2,13 +2,30 @@ import { TtsConfigSchema } from "../api.js";
2
2
  import { t as TWILIO_REGIONS } from "./twilio-region-wBOx-8sz.mjs";
3
3
  import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
4
4
  import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES, resolveRealtimeVoiceAgentConsultTools } from "openclaw/plugin-sdk/realtime-voice";
5
+ import { z } from "zod";
5
6
  import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
6
7
  import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
7
8
  import { canonicalizeMainSessionAlias } from "openclaw/plugin-sdk/session-store-runtime";
8
9
  import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core";
9
10
  import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
10
- import { z } from "zod";
11
11
  import { MAX_TCP_PORT } from "openclaw/plugin-sdk/number-runtime";
12
+ //#region extensions/voice-call/src/call-brief-schema.ts
13
+ const text = (max) => z.string().trim().min(1).max(max);
14
+ const CallBriefSchema = z.object({
15
+ task: text(2e3).optional(),
16
+ context: text(4e3).optional(),
17
+ language: text(100).optional(),
18
+ identity: z.union([text(500), z.object({
19
+ introduction: text(500),
20
+ disclose: z.enum(["volunteer", "when-asked"]).optional()
21
+ }).strict()]).optional(),
22
+ disclosures: z.array(text(500)).max(20).optional(),
23
+ approvals: text(2e3).optional(),
24
+ voicemailMessage: text(1e3).optional(),
25
+ successCriteria: text(1e3).optional(),
26
+ maxDurationSeconds: z.number().int().positive().optional()
27
+ }).strict().refine((brief) => JSON.stringify(brief).length <= 8e3, "Call brief exceeds 8000 characters");
28
+ //#endregion
12
29
  //#region extensions/voice-call/src/allowlist.ts
13
30
  function normalizePhoneNumber(input) {
14
31
  return input?.replace(/\D/g, "") ?? "";
@@ -19,6 +36,51 @@ function isAllowlistedCaller(normalizedFrom, allowFrom) {
19
36
  return (allowFrom ?? []).some((num) => normalizePhoneNumber(num) === normalizedFrom);
20
37
  }
21
38
  //#endregion
39
+ //#region extensions/voice-call/src/errand-config.ts
40
+ const CallReportsConfigSchema = z.object({
41
+ enabled: z.boolean().default(false),
42
+ includeTranscript: z.boolean().default(true),
43
+ summaryModel: z.string().trim().min(1).optional(),
44
+ inboundSessionKey: z.string().trim().min(1).optional()
45
+ }).strict().default({
46
+ enabled: false,
47
+ includeTranscript: true
48
+ });
49
+ const CallLiveConfigSchema = z.object({
50
+ transcript: z.boolean().default(false),
51
+ minIntervalMs: z.number().int().positive().default(5e3)
52
+ }).strict().default({
53
+ transcript: false,
54
+ minIntervalMs: 5e3
55
+ });
56
+ const CallCallbacksConfigSchema = z.object({
57
+ enabled: z.boolean().default(false),
58
+ windowMinutes: z.number().int().positive().default(60),
59
+ greeting: z.string().trim().min(1).optional(),
60
+ brief: CallBriefSchema.optional()
61
+ }).strict().default({
62
+ enabled: false,
63
+ windowMinutes: 60
64
+ }).describe("Accept recent outbound recipients as callbacks only when realtime voice is enabled.");
65
+ const DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS = 3e4;
66
+ const CallVoicemailConfigSchema = z.object({
67
+ detection: z.enum(["off", "twilio"]).default("off"),
68
+ onMachine: z.enum(["leave-message", "hang-up"]).default("leave-message"),
69
+ holdOpeningMaxMs: z.number().int().positive().default(DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS),
70
+ machineDetectionSpeechThresholdMs: z.number().int().min(1e3).max(6e3).default(6e3),
71
+ machineDetectionSpeechEndThresholdMs: z.number().int().min(500).max(5e3).default(1200),
72
+ machineDetectionSilenceTimeoutMs: z.number().int().min(2e3).max(1e4).default(5e3),
73
+ machineDetectionTimeoutMs: z.number().int().min(3e3).max(59e3).multipleOf(1e3).default(3e4)
74
+ }).strict().default({
75
+ detection: "off",
76
+ onMachine: "leave-message",
77
+ holdOpeningMaxMs: DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS,
78
+ machineDetectionSpeechThresholdMs: 6e3,
79
+ machineDetectionSpeechEndThresholdMs: 1200,
80
+ machineDetectionSilenceTimeoutMs: 5e3,
81
+ machineDetectionTimeoutMs: 3e4
82
+ });
83
+ //#endregion
22
84
  //#region extensions/voice-call/src/realtime-call-control.ts
23
85
  /** Stable provider-facing tool name for ending the current phone call. */
24
86
  const REALTIME_VOICE_END_CALL_TOOL_NAME = "openclaw_end_call";
@@ -103,11 +165,7 @@ const VoiceCallServeConfigSchema = z.object({
103
165
  port: z.number().int().positive().default(3334),
104
166
  bind: z.string().default("127.0.0.1"),
105
167
  path: z.string().min(1).default("/voice/webhook")
106
- }).strict().default({
107
- port: 3334,
108
- bind: "127.0.0.1",
109
- path: "/voice/webhook"
110
- });
168
+ }).strict();
111
169
  const VoiceCallTunnelConfigSchema = z.object({
112
170
  provider: z.enum([
113
171
  "none",
@@ -121,10 +179,7 @@ const VoiceCallTunnelConfigSchema = z.object({
121
179
  ngrokDomain: z.string().min(1).optional(),
122
180
  /** Trust loopback forwarding for ngrok URL reconstruction; signatures remain mandatory. */
123
181
  allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
124
- }).strict().default({
125
- provider: "none",
126
- allowNgrokFreeTierLoopbackBypass: false
127
- });
182
+ }).strict();
128
183
  const VoiceCallWebhookSecurityConfigSchema = z.object({
129
184
  /** Only these hosts are accepted from forwarding headers. */
130
185
  allowedHosts: z.array(z.string().min(1)).default([]),
@@ -138,11 +193,7 @@ const VoiceCallWebhookSecurityConfigSchema = z.object({
138
193
  * the remote IP matches one of these addresses.
139
194
  */
140
195
  trustedProxyIPs: z.array(z.string().min(1)).default([])
141
- }).strict().default({
142
- allowedHosts: [],
143
- trustForwardingHeaders: false,
144
- trustedProxyIPs: []
145
- });
196
+ }).strict();
146
197
  const CallModeSchema = z.enum(["notify", "conversation"]);
147
198
  const VoiceCallSessionScopeSchema = z.enum([
148
199
  "per-phone",
@@ -153,10 +204,7 @@ const OutboundConfigSchema = z.object({
153
204
  defaultMode: CallModeSchema.default("notify"),
154
205
  /** Seconds to wait after TTS before auto-hangup in notify mode */
155
206
  notifyHangupDelaySec: z.number().int().nonnegative().default(3)
156
- }).strict().default({
157
- defaultMode: "notify",
158
- notifyHangupDelaySec: 3
159
- });
207
+ }).strict();
160
208
  const RealtimeToolSchema = z.object({
161
209
  type: z.literal("function"),
162
210
  name: z.string().min(1),
@@ -186,13 +234,7 @@ const VoiceCallRealtimeFastContextConfigSchema = z.object({
186
234
  sources: z.array(VoiceCallRealtimeFastContextSourceSchema).min(1).default(["memory", "sessions"]),
187
235
  /** Fall back to the full agent consult when fast context has no answer. */
188
236
  fallbackToConsult: z.boolean().default(false)
189
- }).strict().default({
190
- enabled: false,
191
- timeoutMs: 800,
192
- maxResults: 3,
193
- sources: ["memory", "sessions"],
194
- fallbackToConsult: false
195
- });
237
+ }).strict();
196
238
  const VoiceCallRealtimeAgentContextConfigSchema = z.object({
197
239
  /** Include configured identity and selected profile files alongside the always-on agent context. */
198
240
  enabled: z.boolean().default(false),
@@ -207,17 +249,7 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
207
249
  "IDENTITY.md",
208
250
  "USER.md"
209
251
  ])
210
- }).strict().default({
211
- enabled: false,
212
- maxChars: 6e3,
213
- includeIdentity: true,
214
- includeWorkspaceFiles: true,
215
- files: [
216
- "SOUL.md",
217
- "IDENTITY.md",
218
- "USER.md"
219
- ]
220
- });
252
+ }).strict();
221
253
  const VoiceCallRealtimeConsultThinkingLevelSchema = z.enum([
222
254
  "off",
223
255
  "minimal",
@@ -235,6 +267,8 @@ const VoiceCallRealtimeConfigSchema = z.object({
235
267
  provider: z.string().min(1).optional(),
236
268
  /** Optional override for the local WebSocket route path. */
237
269
  streamPath: z.string().min(1).optional(),
270
+ /** End an active realtime call after this much speech inactivity. */
271
+ idleHangupMs: z.number().int().positive().optional(),
238
272
  /** System instructions passed to the realtime provider. */
239
273
  instructions: z.string().default(DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS),
240
274
  /** Tool policy for the shared OpenClaw agent consult tool. */
@@ -247,9 +281,9 @@ const VoiceCallRealtimeConfigSchema = z.object({
247
281
  consultFastMode: z.boolean().optional(),
248
282
  tools: z.array(RealtimeToolSchema).default([]),
249
283
  /** Low-latency memory/session context for the consult tool. */
250
- fastContext: VoiceCallRealtimeFastContextConfigSchema,
284
+ fastContext: VoiceCallRealtimeFastContextConfigSchema.default(VoiceCallRealtimeFastContextConfigSchema.parse({})),
251
285
  /** Bounded agent persona/context injection for the fast realtime voice path. */
252
- agentContext: VoiceCallRealtimeAgentContextConfigSchema,
286
+ agentContext: VoiceCallRealtimeAgentContextConfigSchema.default(VoiceCallRealtimeAgentContextConfigSchema.parse({})),
253
287
  /** Provider-owned raw config blobs keyed by provider id. */
254
288
  providers: VoiceCallProvidersConfigSchema
255
289
  }).strict().default({
@@ -281,15 +315,7 @@ const VoiceCallStreamingConfigSchema = z.object({
281
315
  maxPendingConnectionsPerIp: z.number().int().positive().default(4),
282
316
  /** Hard cap for all open media stream sockets (pending + active). */
283
317
  maxConnections: z.number().int().positive().default(128)
284
- }).strict().default({
285
- enabled: false,
286
- streamPath: "/voice/stream",
287
- providers: {},
288
- preStartTimeoutMs: 5e3,
289
- maxPendingConnections: 32,
290
- maxPendingConnectionsPerIp: 4,
291
- maxConnections: 128
292
- });
318
+ }).strict();
293
319
  const VoiceCallConfigSchema = z.object({
294
320
  enabled: z.boolean().default(false),
295
321
  provider: z.enum([
@@ -309,7 +335,12 @@ const VoiceCallConfigSchema = z.object({
309
335
  inboundGreeting: z.string().optional(),
310
336
  /** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
311
337
  numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
312
- outbound: OutboundConfigSchema,
338
+ outbound: OutboundConfigSchema.default(OutboundConfigSchema.parse({})),
339
+ reports: CallReportsConfigSchema,
340
+ live: CallLiveConfigSchema,
341
+ callbacks: CallCallbacksConfigSchema,
342
+ voicemail: CallVoicemailConfigSchema,
343
+ /** Maximum call duration in seconds */
313
344
  maxDurationSeconds: z.number().int().positive().default(300),
314
345
  /**
315
346
  * Maximum age of a call in seconds before it is automatically reaped.
@@ -322,12 +353,12 @@ const VoiceCallConfigSchema = z.object({
322
353
  transcriptTimeoutMs: z.number().int().positive().default(18e4),
323
354
  ringTimeoutMs: z.number().int().positive().default(3e4),
324
355
  maxConcurrentCalls: z.number().int().positive().default(1),
325
- serve: VoiceCallServeConfigSchema,
356
+ serve: VoiceCallServeConfigSchema.default(VoiceCallServeConfigSchema.parse({})),
326
357
  /** @deprecated Prefer tunnel config. */
327
358
  tailscale: VoiceCallTailscaleConfigSchema,
328
- tunnel: VoiceCallTunnelConfigSchema,
329
- webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
330
- streaming: VoiceCallStreamingConfigSchema,
359
+ tunnel: VoiceCallTunnelConfigSchema.default(VoiceCallTunnelConfigSchema.parse({})),
360
+ webhookSecurity: VoiceCallWebhookSecurityConfigSchema.default(VoiceCallWebhookSecurityConfigSchema.parse({})),
361
+ streaming: VoiceCallStreamingConfigSchema.default(VoiceCallStreamingConfigSchema.parse({})),
331
362
  realtime: VoiceCallRealtimeConfigSchema,
332
363
  /** Session memory scope for voice conversations. */
333
364
  sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
@@ -386,7 +417,6 @@ function normalizeVoiceCallTtsConfig(defaults, overrides) {
386
417
  }
387
418
  function resolveVoiceCallNumberRouteKey(config, phone) {
388
419
  const routes = config.numbers;
389
- if (!routes) return;
390
420
  if (phone && Object.hasOwn(routes, phone)) return phone;
391
421
  const normalizedPhone = normalizePhoneNumber(phone);
392
422
  if (!normalizedPhone) return;
@@ -409,18 +439,15 @@ function resolveVoiceCallEffectiveConfig(config, phoneOrRouteKey) {
409
439
  config: {
410
440
  ...config,
411
441
  ...route,
412
- tts: normalizeVoiceCallTtsConfig(config.tts, route.tts),
413
- numbers: config.numbers
442
+ tts: normalizeVoiceCallTtsConfig(config.tts, route.tts)
414
443
  }
415
444
  };
416
445
  }
417
446
  function sanitizeVoiceCallProviderConfigs(value) {
418
- if (!value) return {};
419
- return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
447
+ return Object.fromEntries(Object.entries(value ?? {}).filter((entry) => entry[1] !== void 0));
420
448
  }
421
449
  function sanitizeVoiceCallNumberRoutes(value) {
422
- if (!value) return {};
423
- return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
450
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([, route]) => route !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
424
451
  }
425
452
  function resolveTwilioAuthToken(config) {
426
453
  return normalizeResolvedSecretInputString({
@@ -457,6 +484,10 @@ function normalizeVoiceCallConfig(config) {
457
484
  ...defaults.outbound,
458
485
  ...config.outbound
459
486
  },
487
+ reports: CallReportsConfigSchema.parse(config.reports),
488
+ live: CallLiveConfigSchema.parse(config.live),
489
+ callbacks: CallCallbacksConfigSchema.parse(config.callbacks),
490
+ voicemail: CallVoicemailConfigSchema.parse(config.voicemail),
460
491
  serve,
461
492
  tailscale: {
462
493
  ...defaults.tailscale,
@@ -495,14 +526,9 @@ function normalizeVoiceCallConfig(config) {
495
526
  }
496
527
  function resolveVoiceCallSessionKey(params) {
497
528
  const explicit = params.explicitSessionKey?.trim();
498
- if (explicit) return resolveVoiceCallAgentSessionKey({
499
- config: params.config,
500
- sessionKey: explicit,
501
- coreSession: params.coreSession
502
- });
503
- if (params.config.sessionScope === "main") return resolveVoiceCallAgentSessionKey({
529
+ if (explicit || params.config.sessionScope === "main") return resolveVoiceCallAgentSessionKey({
504
530
  config: params.config,
505
- sessionKey: "main",
531
+ sessionKey: explicit || "main",
506
532
  coreSession: params.coreSession
507
533
  });
508
534
  const prefix = `agent:${normalizeAgentId(params.config.agentId)}:voice`;
@@ -554,7 +580,7 @@ function resolveVoiceCallConfig(config) {
554
580
  resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
555
581
  resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
556
582
  resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
557
- return normalizeVoiceCallConfig(resolved);
583
+ return resolved;
558
584
  }
559
585
  function validateProviderConfig(config) {
560
586
  const errors = [];
@@ -590,4 +616,4 @@ function validateProviderConfig(config) {
590
616
  };
591
617
  }
592
618
  //#endregion
593
- export { resolveVoiceCallEffectiveConfig as a, resolveVoiceCallSessionKey as c, REALTIME_VOICE_END_CALL_TOOL_NAME as d, resolveVoiceCallRealtimeTools as f, resolveVoiceCallConfig as i, resolveVoiceCallStreamExposurePaths as l, normalizePhoneNumber as m, normalizeVoiceCallConfig as n, resolveVoiceCallNumberRouteKeyForCall as o, isAllowlistedCaller as p, resolveTwilioAuthToken as r, resolveVoiceCallPublicPathPrefix as s, VoiceCallConfigSchema as t, validateProviderConfig as u };
619
+ export { resolveVoiceCallEffectiveConfig as a, resolveVoiceCallSessionKey as c, REALTIME_VOICE_END_CALL_TOOL_NAME as d, resolveVoiceCallRealtimeTools as f, CallBriefSchema as g, normalizePhoneNumber as h, resolveVoiceCallConfig as i, resolveVoiceCallStreamExposurePaths as l, isAllowlistedCaller as m, normalizeVoiceCallConfig as n, resolveVoiceCallNumberRouteKeyForCall as o, DEFAULT_VOICEMAIL_HOLD_OPENING_MAX_MS as p, resolveTwilioAuthToken as r, resolveVoiceCallPublicPathPrefix as s, VoiceCallConfigSchema as t, validateProviderConfig as u };
@@ -43,12 +43,15 @@ var MockProvider = class {
43
43
  }
44
44
  }
45
45
  normalizeEvent(evt) {
46
- if (!evt.type || !evt.callId) return null;
46
+ if (!evt.type || typeof evt.callId !== "string" || !evt.callId) return null;
47
47
  const base = {
48
48
  id: evt.id ?? crypto.randomUUID(),
49
49
  callId: evt.callId,
50
- providerCallId: evt.providerCallId,
51
- timestamp: evt.timestamp ?? Date.now()
50
+ providerCallId: typeof evt.providerCallId === "string" ? evt.providerCallId : void 0,
51
+ timestamp: evt.timestamp ?? Date.now(),
52
+ direction: evt.direction === "inbound" || evt.direction === "outbound" ? evt.direction : void 0,
53
+ from: typeof evt.from === "string" ? evt.from : void 0,
54
+ to: typeof evt.to === "string" ? evt.to : void 0
52
55
  };
53
56
  switch (evt.type) {
54
57
  case "call.initiated":
@@ -58,6 +61,11 @@ var MockProvider = class {
58
61
  ...base,
59
62
  type: evt.type
60
63
  };
64
+ case "call.amd": return evt.answeredBy ? {
65
+ ...base,
66
+ type: evt.type,
67
+ answeredBy: evt.answeredBy
68
+ } : null;
61
69
  case "call.speaking": return {
62
70
  ...base,
63
71
  type: evt.type,
@@ -111,6 +119,7 @@ var MockProvider = class {
111
119
  }
112
120
  async hangupCall(_input) {}
113
121
  async playTts(_input) {}
122
+ async playMessageAndHangup(_input) {}
114
123
  async sendDtmf(_input) {}
115
124
  async startListening(_input) {}
116
125
  async stopListening(_input) {}
@@ -1,6 +1,6 @@
1
- import { s as getHeader } from "./runtime-entry-CbSKxSKk.mjs";
2
- import { t as escapeXml } from "./voice-mapping-B_iAUkER.mjs";
3
- import { i as verifyPlivoWebhook, n as readProviderCallStatus, r as reconstructWebhookUrl, t as guardedJsonApiRequest } from "./guarded-json-api-x55yqHvW.mjs";
1
+ import { c as getHeader } from "./runtime-entry-STjFmi_B.mjs";
2
+ import { t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
3
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
4
4
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
5
  import crypto from "node:crypto";
6
6
  //#region extensions/voice-call/src/providers/plivo.ts
@@ -40,27 +40,9 @@ var PlivoProvider = class PlivoProvider {
40
40
  this.pendingListenByCallId = /* @__PURE__ */ new Map();
41
41
  if (!config.authId) throw new Error("Plivo Auth ID is required");
42
42
  if (!config.authToken) throw new Error("Plivo Auth Token is required");
43
- this.authId = config.authId;
44
43
  this.authToken = config.authToken;
45
- this.baseUrl = `https://api.plivo.com/v1/Account/${this.authId}`;
46
- this.apiHost = new URL(this.baseUrl).hostname;
47
44
  this.options = options;
48
- }
49
- async apiRequest(params) {
50
- const { method, endpoint, body, allowNotFound } = params;
51
- return await guardedJsonApiRequest({
52
- url: `${this.baseUrl}${endpoint}`,
53
- method,
54
- headers: {
55
- Authorization: `Basic ${Buffer.from(`${this.authId}:${this.authToken}`).toString("base64")}`,
56
- "Content-Type": "application/json"
57
- },
58
- body,
59
- allowNotFound,
60
- allowedHostnames: [this.apiHost],
61
- auditContext: "voice-call.plivo.api",
62
- errorPrefix: "Plivo API error"
63
- });
45
+ this.api = createCarrierApi("Plivo", `https://api.plivo.com/v1/Account/${config.authId}`, `Basic ${Buffer.from(`${config.authId}:${config.authToken}`).toString("base64")}`);
64
46
  }
65
47
  verifyWebhook(ctx) {
66
48
  const result = verifyPlivoWebhook(ctx, this.authToken, {
@@ -91,27 +73,22 @@ var PlivoProvider = class PlivoProvider {
91
73
  const callId = normalizeOptionalString(ctx.query?.callId);
92
74
  let event = null;
93
75
  let providerResponseBody;
94
- if (flow === "xml-speak") {
95
- const pending = callId ? this.pendingSpeakByCallId.get(callId) : void 0;
96
- if (callId) this.pendingSpeakByCallId.delete(callId);
97
- const actionUrl = pending?.listenAfterPlayback && callId ? this.buildActionUrl(ctx, callId) : null;
98
- providerResponseBody = pending ? actionUrl ? PlivoProvider.xmlGetInputSpeech({
99
- text: pending.text,
100
- language: pending.locale,
101
- actionUrl
102
- }) : PlivoProvider.xmlSpeak(pending.text, pending.locale) : PlivoProvider.xmlKeepAlive();
103
- } else if (flow === "xml-listen") {
104
- const pending = callId ? this.pendingListenByCallId.get(callId) : void 0;
105
- if (callId) this.pendingListenByCallId.delete(callId);
106
- const actionUrl = this.buildActionUrl(ctx, callId);
76
+ if (flow === "xml-speak" || flow === "xml-listen") {
77
+ const speaks = flow === "xml-speak";
78
+ const pendingSpeak = speaks && callId ? this.pendingSpeakByCallId.get(callId) : void 0;
79
+ const pendingListen = !speaks && callId ? this.pendingListenByCallId.get(callId) : void 0;
80
+ const language = speaks ? pendingSpeak?.locale : pendingListen?.language;
81
+ if (callId) (speaks ? this.pendingSpeakByCallId : this.pendingListenByCallId).delete(callId);
82
+ const actionUrl = !speaks || pendingSpeak?.listenAfterPlayback && callId ? this.buildActionUrl(ctx, callId) : null;
107
83
  providerResponseBody = actionUrl && callId ? PlivoProvider.xmlGetInputSpeech({
108
- actionUrl,
109
- language: pending?.language
110
- }) : PlivoProvider.xmlKeepAlive();
84
+ text: pendingSpeak?.text,
85
+ language,
86
+ actionUrl
87
+ }) : pendingSpeak ? PlivoProvider.xmlKeepAlive(` <Speak language="${escapeXml(language || "en-US")}">${escapeXml(pendingSpeak.text)}</Speak>\n`) : PlivoProvider.xmlKeepAlive();
111
88
  } else {
112
89
  const dedupeKey = options?.verifiedRequestKey ?? createPlivoRequestDedupeKey(ctx);
113
90
  event = this.normalizeEvent(parsed, callId, dedupeKey);
114
- providerResponseBody = flow === "answer" || flow === "getinput" ? PlivoProvider.xmlKeepAlive() : PlivoProvider.xmlEmpty();
91
+ providerResponseBody = flow === "answer" || flow === "getinput" ? PlivoProvider.xmlKeepAlive() : "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>";
115
92
  }
116
93
  return {
117
94
  events: event ? [event] : [],
@@ -185,18 +162,14 @@ var PlivoProvider = class PlivoProvider {
185
162
  const hangupUrl = new URL(webhookUrl);
186
163
  hangupUrl.searchParams.set("flow", "hangup");
187
164
  this.callIdToWebhookUrl.set(input.callId, input.webhookUrl);
188
- const result = await this.apiRequest({
189
- method: "POST",
190
- endpoint: "/Call/",
191
- body: {
192
- from: PlivoProvider.normalizeNumber(input.from),
193
- to: PlivoProvider.normalizeNumber(input.to),
194
- answer_url: answerUrl.toString(),
195
- answer_method: "POST",
196
- hangup_url: hangupUrl.toString(),
197
- hangup_method: "POST",
198
- hangup_on_ring: this.options.ringTimeoutSec ?? 30
199
- }
165
+ const result = await this.api.request("/Call/", {
166
+ from: PlivoProvider.normalizeNumber(input.from),
167
+ to: PlivoProvider.normalizeNumber(input.to),
168
+ answer_url: answerUrl.toString(),
169
+ answer_method: "POST",
170
+ hangup_url: hangupUrl.toString(),
171
+ hangup_method: "POST",
172
+ hangup_on_ring: this.options.ringTimeoutSec ?? 30
200
173
  });
201
174
  const requestUuid = Array.isArray(result.request_uuid) ? result.request_uuid[0] : result.request_uuid;
202
175
  if (!requestUuid) throw new Error("Plivo call create returned no request_uuid");
@@ -207,14 +180,12 @@ var PlivoProvider = class PlivoProvider {
207
180
  }
208
181
  async hangupCall(input) {
209
182
  const callUuid = this.requestUuidToCallUuid.get(input.providerCallId);
210
- await this.apiRequest({
183
+ await this.api.request(`/Call/${callUuid || input.providerCallId}/`, void 0, {
211
184
  method: "DELETE",
212
- endpoint: `/Call/${callUuid || input.providerCallId}/`,
213
185
  allowNotFound: true
214
186
  });
215
- if (!callUuid) await this.apiRequest({
187
+ if (!callUuid) await this.api.request(`/Request/${input.providerCallId}/`, void 0, {
216
188
  method: "DELETE",
217
- endpoint: `/Request/${input.providerCallId}/`,
218
189
  allowNotFound: true
219
190
  });
220
191
  this.releaseCallState({
@@ -239,14 +210,10 @@ var PlivoProvider = class PlivoProvider {
239
210
  transferUrl.searchParams.set("provider", "plivo");
240
211
  transferUrl.searchParams.set("flow", params.flow);
241
212
  transferUrl.searchParams.set("callId", params.callId);
242
- await this.apiRequest({
243
- method: "POST",
244
- endpoint: `/Call/${params.callUuid}/`,
245
- body: {
246
- legs: "aleg",
247
- aleg_url: transferUrl.toString(),
248
- aleg_method: "POST"
249
- }
213
+ await this.api.request(`/Call/${params.callUuid}/`, {
214
+ legs: "aleg",
215
+ aleg_url: transferUrl.toString(),
216
+ aleg_method: "POST"
250
217
  });
251
218
  }
252
219
  async playTts(input) {
@@ -281,15 +248,7 @@ var PlivoProvider = class PlivoProvider {
281
248
  "machine",
282
249
  "hangup"
283
250
  ]);
284
- return readProviderCallStatus(() => guardedJsonApiRequest({
285
- url: `${this.baseUrl}/Call/${input.providerCallId}/`,
286
- method: "GET",
287
- headers: { Authorization: `Basic ${Buffer.from(`${this.authId}:${this.authToken}`).toString("base64")}` },
288
- allowNotFound: true,
289
- allowedHostnames: [this.apiHost],
290
- auditContext: "plivo-get-call-status",
291
- errorPrefix: "Plivo get call status error"
292
- }), (data) => {
251
+ return this.api.getCallStatus(`/Call/${input.providerCallId}/`, (data) => {
293
252
  const status = data.call_status ?? "unknown";
294
253
  return {
295
254
  status,
@@ -302,31 +261,18 @@ var PlivoProvider = class PlivoProvider {
302
261
  if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("sip:")) return trimmed;
303
262
  return trimmed.replace(/[^\d+]/g, "");
304
263
  }
305
- static xmlEmpty() {
306
- return `<?xml version="1.0" encoding="UTF-8"?><Response></Response>`;
307
- }
308
- static xmlKeepAlive() {
309
- return `<?xml version="1.0" encoding="UTF-8"?>
310
- <Response>
311
- <Wait length="300" />
312
- </Response>`;
313
- }
314
- static xmlSpeak(text, locale) {
264
+ static xmlKeepAlive(content = "") {
315
265
  return `<?xml version="1.0" encoding="UTF-8"?>
316
266
  <Response>
317
- <Speak language="${escapeXml(locale || "en-US")}">${escapeXml(text)}</Speak>
318
- <Wait length="300" />
267
+ ${content} <Wait length="300" />
319
268
  </Response>`;
320
269
  }
321
270
  static xmlGetInputSpeech(params) {
322
271
  const language = params.language || "en-US";
323
272
  const prompt = params.text === void 0 ? "" : ` <Speak language="${escapeXml(language)}">${escapeXml(params.text)}</Speak>\n`;
324
- return `<?xml version="1.0" encoding="UTF-8"?>
325
- <Response>
326
- <GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="2" redirect="false">
273
+ return PlivoProvider.xmlKeepAlive(` <GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="2" redirect="false">
327
274
  ${prompt} </GetInput>
328
- <Wait length="300" />
329
- </Response>`;
275
+ `);
330
276
  }
331
277
  buildActionUrl(ctx, callId) {
332
278
  const base = this.baseWebhookUrlFromCtx(ctx);