getpatter 0.6.2 → 0.6.3

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
@@ -1901,6 +1901,160 @@ var init_elevenlabs_convai = __esm({
1901
1901
  }
1902
1902
  });
1903
1903
 
1904
+ // src/providers/plivo-adapter.ts
1905
+ async function dropPlivoVoicemail(callUuid, voicemailMessage, authId, authToken) {
1906
+ if (!callUuid || !voicemailMessage || !authId || !authToken) return;
1907
+ const auth2 = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
1908
+ const base = `${PLIVO_API_BASE}/Account/${encodeURIComponent(authId)}/Call/${encodeURIComponent(callUuid)}`;
1909
+ try {
1910
+ const speak = await fetch(`${base}/Speak/`, {
1911
+ method: "POST",
1912
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: auth2 },
1913
+ body: new URLSearchParams({ text: voicemailMessage }).toString(),
1914
+ signal: AbortSignal.timeout(1e4)
1915
+ });
1916
+ if (!speak.ok) {
1917
+ getLogger().warn(
1918
+ `Plivo voicemail Speak failed (${speak.status}): ${(await speak.text()).slice(0, 200)}`
1919
+ );
1920
+ return;
1921
+ }
1922
+ await new Promise(
1923
+ (r) => setTimeout(r, Math.min(3e4, voicemailMessage.length * 60))
1924
+ );
1925
+ await fetch(`${base}/`, { method: "DELETE", headers: { Authorization: auth2 } });
1926
+ getLogger().info(`Voicemail dropped for ${callUuid}`);
1927
+ } catch (e) {
1928
+ getLogger().warn(`Could not drop voicemail: ${String(e)}`);
1929
+ }
1930
+ }
1931
+ function xmlEscapePlivo(s) {
1932
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1933
+ }
1934
+ var PLIVO_API_BASE, PlivoAdapter;
1935
+ var init_plivo_adapter = __esm({
1936
+ "src/providers/plivo-adapter.ts"() {
1937
+ "use strict";
1938
+ init_cjs_shims();
1939
+ init_logger();
1940
+ PLIVO_API_BASE = "https://api.plivo.com/v1";
1941
+ PlivoAdapter = class {
1942
+ authId;
1943
+ baseUrl;
1944
+ authHeader;
1945
+ constructor(authId, authToken) {
1946
+ if (!authId) throw new Error("PlivoAdapter: authId is required");
1947
+ if (!authToken) throw new Error("PlivoAdapter: authToken is required");
1948
+ this.authId = authId;
1949
+ this.baseUrl = `${PLIVO_API_BASE}/Account/${encodeURIComponent(authId)}`;
1950
+ this.authHeader = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
1951
+ }
1952
+ async request(method, path6, jsonBody) {
1953
+ const headers = { Authorization: this.authHeader };
1954
+ if (jsonBody !== void 0) headers["Content-Type"] = "application/json";
1955
+ const response = await fetch(`${this.baseUrl}${path6}`, {
1956
+ method,
1957
+ headers,
1958
+ body: jsonBody !== void 0 ? JSON.stringify(jsonBody) : void 0,
1959
+ signal: AbortSignal.timeout(3e4)
1960
+ });
1961
+ const text = await response.text();
1962
+ if (!response.ok && response.status !== 404) {
1963
+ throw new Error(`Plivo ${method} ${path6} failed: ${response.status} ${text}`);
1964
+ }
1965
+ let data = {};
1966
+ if (text) {
1967
+ try {
1968
+ data = JSON.parse(text);
1969
+ } catch {
1970
+ }
1971
+ }
1972
+ return { status: response.status, data };
1973
+ }
1974
+ /** Search and rent an available Plivo number for the given ISO country. */
1975
+ async provisionNumber(countryIso) {
1976
+ const { data } = await this.request(
1977
+ "GET",
1978
+ `/PhoneNumber/?country_iso=${encodeURIComponent(countryIso)}&limit=1`
1979
+ );
1980
+ const number4 = data.objects?.[0]?.number;
1981
+ if (!number4) throw new Error(`PlivoAdapter: no numbers available for ${countryIso}`);
1982
+ await this.request("POST", `/PhoneNumber/${encodeURIComponent(number4)}/`);
1983
+ return number4;
1984
+ }
1985
+ /**
1986
+ * Point the inbound answer flow for ``number`` at ``answerUrl`` by creating
1987
+ * (or reusing) a Plivo Application and linking the number to it. Most
1988
+ * production deployments pre-configure this in the Plivo console; this
1989
+ * mirrors Twilio's ``configureNumber`` auto-setup convenience.
1990
+ */
1991
+ async configureNumber(number4, answerUrl) {
1992
+ const { data } = await this.request("POST", "/Application/", {
1993
+ app_name: "patter-inbound",
1994
+ answer_url: answerUrl,
1995
+ answer_method: "POST"
1996
+ });
1997
+ if (!data.app_id) {
1998
+ getLogger().warn("Plivo Application create returned no app_id");
1999
+ return;
2000
+ }
2001
+ await this.request("POST", `/Number/${encodeURIComponent(number4)}/`, { app_id: data.app_id });
2002
+ }
2003
+ /**
2004
+ * Place an outbound Plivo call routed through ``answerUrl``. Returns Plivo's
2005
+ * ``request_uuid``. The WSS URL travels inside the answer XML, not as a dial
2006
+ * parameter — mirroring the Python adapter.
2007
+ */
2008
+ async initiateCall(opts) {
2009
+ const payload = {
2010
+ from: opts.from,
2011
+ to: opts.to,
2012
+ answer_url: opts.answerUrl,
2013
+ answer_method: "POST"
2014
+ };
2015
+ if (opts.ringTimeout != null) payload.ring_timeout = Math.max(1, Math.floor(opts.ringTimeout));
2016
+ if (opts.machineDetection) {
2017
+ payload.machine_detection = "true";
2018
+ payload.machine_detection_time = 5e3;
2019
+ if (opts.machineDetectionUrl) {
2020
+ payload.machine_detection_url = opts.machineDetectionUrl;
2021
+ payload.machine_detection_method = "POST";
2022
+ }
2023
+ }
2024
+ const { data } = await this.request("POST", "/Call/", payload);
2025
+ return { requestUuid: data.request_uuid ?? "" };
2026
+ }
2027
+ /** Hang up an active Plivo call by CallUUID. 204 and 404 are both success. */
2028
+ async endCall(callUuid) {
2029
+ if (!callUuid) throw new Error("PlivoAdapter: callUuid is required");
2030
+ try {
2031
+ await this.request("DELETE", `/Call/${encodeURIComponent(callUuid)}/`);
2032
+ } catch (err) {
2033
+ getLogger().warn(`[PlivoAdapter] endCall failed for ${callUuid}: ${String(err)}`);
2034
+ throw err;
2035
+ }
2036
+ }
2037
+ /**
2038
+ * Build the Plivo answer XML. Unlike Twilio (``url=`` attribute), Plivo's
2039
+ * ``<Stream>`` takes the WSS URL as its **text content**. ``bidirectional``
2040
+ * enables two-way audio; ``keepCallAlive`` keeps the leg up for the lifetime
2041
+ * of the WebSocket. ``extraHeaders`` (comma-separated ``key=value``) is
2042
+ * delivered back on the WS ``start`` frame as a caller/callee fallback.
2043
+ *
2044
+ * Mirrors the Python adapter's ``generate_stream_xml``.
2045
+ */
2046
+ static generateStreamXml(streamUrl, contentType = "audio/x-mulaw;rate=8000", extraHeaders) {
2047
+ let attrs = `bidirectional="true" keepCallAlive="true" contentType="${xmlEscapePlivo(contentType)}"`;
2048
+ if (extraHeaders) {
2049
+ const joined = Object.entries(extraHeaders).map(([k, v]) => `${k}=${v}`).join(",");
2050
+ attrs += ` extraHeaders="${xmlEscapePlivo(joined)}"`;
2051
+ }
2052
+ return `<Response><Stream ${attrs}>${xmlEscapePlivo(streamUrl)}</Stream></Response>`;
2053
+ }
2054
+ };
2055
+ }
2056
+ });
2057
+
1904
2058
  // src/provider-factory.ts
1905
2059
  async function createSTT(agent) {
1906
2060
  return agent.stt ?? null;
@@ -1915,6 +2069,178 @@ var init_provider_factory = __esm({
1915
2069
  }
1916
2070
  });
1917
2071
 
2072
+ // src/telephony/plivo.ts
2073
+ function classifyPlivoAmd(result) {
2074
+ const r = (result || "").trim().toLowerCase();
2075
+ if (r === "human" || r === "person") return "human";
2076
+ if (r.startsWith("machine") || r === "answering_machine" || r === "amd" || r === "true") {
2077
+ return "machine";
2078
+ }
2079
+ if (r === "fax") return "fax";
2080
+ return "unknown";
2081
+ }
2082
+ function validatePlivoSignature(url2, nonce, signature, authToken, params, method = "POST") {
2083
+ if (!signature || !nonce || !authToken) return false;
2084
+ let base = url2;
2085
+ if (method === "POST" && params && Object.keys(params).length > 0) {
2086
+ const keys = Object.keys(params).sort();
2087
+ base += keys.map((k) => `${k}${params[k]}`).join("");
2088
+ }
2089
+ const signed = `${base}.${nonce}`;
2090
+ const expected = import_node_crypto.default.createHmac("sha256", authToken).update(signed).digest("base64");
2091
+ const expBuf = Buffer.from(expected);
2092
+ for (const rawSig of signature.split(",")) {
2093
+ const trimmed = rawSig.trim();
2094
+ if (!trimmed) continue;
2095
+ try {
2096
+ const sigBuf = Buffer.from(trimmed);
2097
+ if (sigBuf.length === expBuf.length && import_node_crypto.default.timingSafeEqual(sigBuf, expBuf)) {
2098
+ return true;
2099
+ }
2100
+ } catch {
2101
+ continue;
2102
+ }
2103
+ }
2104
+ return false;
2105
+ }
2106
+ var import_node_crypto, Carrier, PLIVO_DTMF_ALLOWED, PlivoBridge;
2107
+ var init_plivo = __esm({
2108
+ "src/telephony/plivo.ts"() {
2109
+ "use strict";
2110
+ init_cjs_shims();
2111
+ import_node_crypto = __toESM(require("crypto"));
2112
+ init_provider_factory();
2113
+ init_logger();
2114
+ Carrier = class {
2115
+ kind = "plivo";
2116
+ authId;
2117
+ authToken;
2118
+ constructor(opts = {}) {
2119
+ const authId = opts.authId ?? process.env.PLIVO_AUTH_ID;
2120
+ const authToken = opts.authToken ?? process.env.PLIVO_AUTH_TOKEN;
2121
+ if (!authId) {
2122
+ throw new Error(
2123
+ "Plivo carrier requires authId. Pass { authId: 'MA...' } or set PLIVO_AUTH_ID in the environment."
2124
+ );
2125
+ }
2126
+ if (!authToken) {
2127
+ throw new Error(
2128
+ "Plivo carrier requires authToken. Pass { authToken: '...' } or set PLIVO_AUTH_TOKEN in the environment."
2129
+ );
2130
+ }
2131
+ this.authId = authId;
2132
+ this.authToken = authToken;
2133
+ }
2134
+ };
2135
+ PLIVO_DTMF_ALLOWED = new Set("0123456789*#ABCDabcdwW");
2136
+ PlivoBridge = class {
2137
+ constructor(config2) {
2138
+ this.config = config2;
2139
+ const authId = config2.plivoAuthId ?? "";
2140
+ const authToken = config2.plivoAuthToken ?? "";
2141
+ this.authHeader = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
2142
+ this.apiBase = `https://api.plivo.com/v1/Account/${encodeURIComponent(authId)}`;
2143
+ }
2144
+ config;
2145
+ label = "Plivo";
2146
+ telephonyProvider = "plivo";
2147
+ inputWireFormat = "ulaw_8000";
2148
+ authHeader;
2149
+ apiBase;
2150
+ sendAudio(ws, audioBase64, _streamSid) {
2151
+ ws.send(
2152
+ JSON.stringify({
2153
+ event: "playAudio",
2154
+ media: { contentType: "audio/x-mulaw", sampleRate: 8e3, payload: audioBase64 }
2155
+ })
2156
+ );
2157
+ }
2158
+ sendMark(ws, markName, streamSid) {
2159
+ ws.send(JSON.stringify({ event: "checkpoint", streamId: streamSid, name: markName }));
2160
+ }
2161
+ sendClear(ws, streamSid) {
2162
+ ws.send(JSON.stringify({ event: "clearAudio", streamId: streamSid }));
2163
+ }
2164
+ async transferCall(callId, toNumber) {
2165
+ if (!/^\+[1-9]\d{6,14}$/.test(toNumber)) {
2166
+ getLogger().warn(`PlivoBridge.transferCall rejected: invalid target ${JSON.stringify(toNumber)}`);
2167
+ return;
2168
+ }
2169
+ if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
2170
+ if (!this.config.webhookUrl) {
2171
+ getLogger().warn("PlivoBridge.transferCall skipped: no webhookUrl for aleg_url");
2172
+ return;
2173
+ }
2174
+ const alegUrl = `https://${this.config.webhookUrl}/webhooks/plivo/transfer?to=${encodeURIComponent(toNumber)}`;
2175
+ await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/`, {
2176
+ method: "POST",
2177
+ headers: { "Content-Type": "application/json", Authorization: this.authHeader },
2178
+ body: JSON.stringify({ legs: "aleg", aleg_url: alegUrl, aleg_method: "GET" })
2179
+ });
2180
+ getLogger().info(`Call transferred to ${toNumber}`);
2181
+ }
2182
+ async sendDtmf(ws, _callId, digits, _delayMs) {
2183
+ const filtered = Array.from(digits ?? "").filter((d) => PLIVO_DTMF_ALLOWED.has(d)).join("");
2184
+ if (!filtered) {
2185
+ getLogger().warn(`PlivoBridge.sendDtmf: no valid digits in ${JSON.stringify(digits)}`);
2186
+ return;
2187
+ }
2188
+ ws.send(JSON.stringify({ event: "sendDTMF", dtmf: filtered }));
2189
+ }
2190
+ async startRecording(callId) {
2191
+ if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
2192
+ try {
2193
+ const resp = await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/Record/`, {
2194
+ method: "POST",
2195
+ headers: { Authorization: this.authHeader }
2196
+ });
2197
+ if (!resp.ok) {
2198
+ getLogger().warn(`Plivo record start failed (${resp.status}): ${(await resp.text()).slice(0, 200)}`);
2199
+ } else {
2200
+ getLogger().info("Plivo recording started");
2201
+ }
2202
+ } catch (e) {
2203
+ getLogger().warn(`Plivo record start error: ${String(e)}`);
2204
+ }
2205
+ }
2206
+ async endCall(callId, _ws) {
2207
+ if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
2208
+ try {
2209
+ const resp = await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/`, {
2210
+ method: "DELETE",
2211
+ headers: { Authorization: this.authHeader }
2212
+ });
2213
+ if (!resp.ok && resp.status !== 404) {
2214
+ getLogger().warn(`Plivo hangup returned ${resp.status}`);
2215
+ }
2216
+ } catch {
2217
+ }
2218
+ }
2219
+ createStt(agent) {
2220
+ return createSTT(agent);
2221
+ }
2222
+ async queryTelephonyCost(metricsAcc, callId) {
2223
+ if (!this.config.plivoAuthId || !this.config.plivoAuthToken || !callId) return;
2224
+ try {
2225
+ const resp = await fetch(`${this.apiBase}/Call/${encodeURIComponent(callId)}/`, {
2226
+ headers: { Authorization: this.authHeader },
2227
+ signal: AbortSignal.timeout(5e3)
2228
+ });
2229
+ if (resp.ok) {
2230
+ const data = await resp.json();
2231
+ if (data.total_amount != null) {
2232
+ metricsAcc.setActualTelephonyCost(Math.abs(parseFloat(data.total_amount)));
2233
+ getLogger().info(`Plivo actual cost: $${data.total_amount}`);
2234
+ }
2235
+ }
2236
+ } catch (err) {
2237
+ getLogger().debug(`queryTelephonyCost(plivo) failed: ${err?.message ?? err}`);
2238
+ }
2239
+ }
2240
+ };
2241
+ }
2242
+ });
2243
+
1918
2244
  // src/pricing.ts
1919
2245
  function resolveProviderRates(providerConfig, model) {
1920
2246
  if (!providerConfig) return { unit: "" };
@@ -2054,7 +2380,7 @@ function calculateLlmCost(provider2, model, inputTokens, outputTokens, cacheRead
2054
2380
  function calculateTelephonyCost(provider2, durationSeconds, pricing) {
2055
2381
  const config2 = pricing[provider2];
2056
2382
  if (!config2 || config2.unit !== "minute") return 0;
2057
- const minutes = provider2 === "twilio" ? Math.ceil(durationSeconds / 60) : durationSeconds / 60;
2383
+ const minutes = config2.roundUp ? Math.ceil(durationSeconds / 60) : durationSeconds / 60;
2058
2384
  return minutes * (config2.price ?? 0);
2059
2385
  }
2060
2386
  var PRICING_VERSION, PRICING_LAST_UPDATED, PricingUnit, DEFAULT_PRICING, llmPricing;
@@ -2283,7 +2609,7 @@ var init_pricing = __esm({
2283
2609
  // twilio default = US inbound local (the 99% case for voice agents receiving
2284
2610
  // calls on a local number). For US toll-free inbound ($0.022/min) or US
2285
2611
  // outbound local ($0.0140/min), override via Patter({ pricing: { twilio: {...} } }).
2286
- twilio: { unit: PricingUnit.MINUTE, price: 85e-4 },
2612
+ twilio: { unit: PricingUnit.MINUTE, price: 85e-4, roundUp: true },
2287
2613
  // Telnyx — direction-aware rates as of 2026-05-11.
2288
2614
  // Sources:
2289
2615
  // https://telnyx.com/pricing/elastic-sip
@@ -2301,7 +2627,17 @@ var init_pricing = __esm({
2301
2627
  // price: 0.0035 } } })`` to bill all inbound at the lower rate.
2302
2628
  telnyx: { unit: PricingUnit.MINUTE, price: 7e-3 },
2303
2629
  telnyx_inbound: { unit: PricingUnit.MINUTE, price: 35e-4 },
2304
- telnyx_outbound: { unit: PricingUnit.MINUTE, price: 7e-3 }
2630
+ telnyx_outbound: { unit: PricingUnit.MINUTE, price: 7e-3 },
2631
+ // Plivo — official US pay-as-you-go voice rates (per minute; Plivo rounds
2632
+ // partial minutes up like Twilio). Source: https://www.plivo.com/voice/pricing/
2633
+ // US local inbound: $0.0055/min
2634
+ // US local outbound: $0.0115/min
2635
+ // US toll-free inbound: $0.0180/min (override via new Patter({ pricing }))
2636
+ // The flat ``plivo`` key defaults to inbound local; the billed amount is
2637
+ // also reconciled post-call from the Plivo CDR (``total_amount``).
2638
+ plivo: { unit: PricingUnit.MINUTE, price: 55e-4, roundUp: true },
2639
+ plivo_inbound: { unit: PricingUnit.MINUTE, price: 55e-4, roundUp: true },
2640
+ plivo_outbound: { unit: PricingUnit.MINUTE, price: 0.0115, roundUp: true }
2305
2641
  };
2306
2642
  llmPricing = {
2307
2643
  anthropic: {
@@ -2985,10 +3321,10 @@ function timingSafeCompare(a, b) {
2985
3321
  const aBuf = Buffer.from(a);
2986
3322
  const bBuf = Buffer.from(b);
2987
3323
  if (aBuf.length !== bBuf.length) {
2988
- import_node_crypto.default.timingSafeEqual(aBuf, aBuf);
3324
+ import_node_crypto2.default.timingSafeEqual(aBuf, aBuf);
2989
3325
  return false;
2990
3326
  }
2991
- return import_node_crypto.default.timingSafeEqual(aBuf, bBuf);
3327
+ return import_node_crypto2.default.timingSafeEqual(aBuf, bBuf);
2992
3328
  }
2993
3329
  function makeAuthMiddleware(token = "") {
2994
3330
  return (req, res, next) => {
@@ -3010,12 +3346,12 @@ function makeAuthMiddleware(token = "") {
3010
3346
  res.status(401).json({ error: "Unauthorized" });
3011
3347
  };
3012
3348
  }
3013
- var import_node_crypto;
3349
+ var import_node_crypto2;
3014
3350
  var init_auth = __esm({
3015
3351
  "src/dashboard/auth.ts"() {
3016
3352
  "use strict";
3017
3353
  init_cjs_shims();
3018
- import_node_crypto = __toESM(require("crypto"));
3354
+ import_node_crypto2 = __toESM(require("crypto"));
3019
3355
  }
3020
3356
  });
3021
3357
 
@@ -3320,12 +3656,12 @@ function isRemoteUrl(onMessage) {
3320
3656
  function isWebSocketUrl(url2) {
3321
3657
  return url2.startsWith("ws://") || url2.startsWith("wss://");
3322
3658
  }
3323
- var import_node_crypto2, MAX_RESPONSE_BYTES, RemoteMessageHandler;
3659
+ var import_node_crypto3, MAX_RESPONSE_BYTES, RemoteMessageHandler;
3324
3660
  var init_remote_message = __esm({
3325
3661
  "src/remote-message.ts"() {
3326
3662
  "use strict";
3327
3663
  init_cjs_shims();
3328
- import_node_crypto2 = __toESM(require("crypto"));
3664
+ import_node_crypto3 = __toESM(require("crypto"));
3329
3665
  init_logger();
3330
3666
  init_server();
3331
3667
  MAX_RESPONSE_BYTES = 64 * 1024;
@@ -3346,7 +3682,7 @@ var init_remote_message = __esm({
3346
3682
  if (!this.webhookSecret) {
3347
3683
  throw new Error("Cannot sign without a webhookSecret");
3348
3684
  }
3349
- return import_node_crypto2.default.createHmac("sha256", this.webhookSecret).update(body).digest("hex");
3685
+ return import_node_crypto3.default.createHmac("sha256", this.webhookSecret).update(body).digest("hex");
3350
3686
  }
3351
3687
  /**
3352
3688
  * Release resources held by this handler.
@@ -22119,7 +22455,7 @@ var init_transport = __esm({
22119
22455
 
22120
22456
  // node_modules/pkce-challenge/dist/index.node.js
22121
22457
  async function getRandomValues(size) {
22122
- return (await crypto3).getRandomValues(new Uint8Array(size));
22458
+ return (await crypto4).getRandomValues(new Uint8Array(size));
22123
22459
  }
22124
22460
  async function random(size) {
22125
22461
  const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
@@ -22139,7 +22475,7 @@ async function generateVerifier(length) {
22139
22475
  return await random(length);
22140
22476
  }
22141
22477
  async function generateChallenge(code_verifier) {
22142
- const buffer = await (await crypto3).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
22478
+ const buffer = await (await crypto4).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
22143
22479
  return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
22144
22480
  }
22145
22481
  async function pkceChallenge(length) {
@@ -22155,12 +22491,12 @@ async function pkceChallenge(length) {
22155
22491
  code_challenge: challenge
22156
22492
  };
22157
22493
  }
22158
- var crypto3;
22494
+ var crypto4;
22159
22495
  var init_index_node = __esm({
22160
22496
  "node_modules/pkce-challenge/dist/index.node.js"() {
22161
22497
  "use strict";
22162
22498
  init_cjs_shims();
22163
- crypto3 = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
22499
+ crypto4 = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
22164
22500
  globalThis.crypto ?? // Node.js >18
22165
22501
  import("crypto").then((m) => m.webcrypto);
22166
22502
  }
@@ -26754,6 +27090,35 @@ function maskPhoneNumber(number4) {
26754
27090
  function isValidE164(number4) {
26755
27091
  return /^\+[1-9]\d{6,14}$/.test(number4);
26756
27092
  }
27093
+ function augmentWithBuiltinHandoffTools(userTools, callbacks) {
27094
+ const out = [...userTools ?? []];
27095
+ if (callbacks.transferCall) {
27096
+ const transferCall = callbacks.transferCall;
27097
+ out.push({
27098
+ ...TRANSFER_CALL_TOOL,
27099
+ handler: async (args) => {
27100
+ const number4 = typeof args.number === "string" ? args.number : "";
27101
+ if (!isValidE164(number4)) {
27102
+ return JSON.stringify({ error: "Invalid phone number format", status: "rejected" });
27103
+ }
27104
+ await transferCall(number4);
27105
+ return JSON.stringify({ status: "transferring", to: number4 });
27106
+ }
27107
+ });
27108
+ }
27109
+ if (callbacks.endCall) {
27110
+ const endCall = callbacks.endCall;
27111
+ out.push({
27112
+ ...END_CALL_TOOL,
27113
+ handler: async (args) => {
27114
+ const reason = typeof args.reason === "string" ? args.reason : "conversation_complete";
27115
+ await endCall(reason);
27116
+ return JSON.stringify({ status: "ending", reason });
27117
+ }
27118
+ });
27119
+ }
27120
+ return out;
27121
+ }
26757
27122
  async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
26758
27123
  try {
26759
27124
  const projResp = await fetch("https://api.deepgram.com/v1/projects", {
@@ -27397,8 +27762,8 @@ var init_stream_handler = __esm({
27397
27762
  this.ttsByteCarry = null;
27398
27763
  }
27399
27764
  /**
27400
- * Start call recording when configured. Currently Twilio-only — bridges may
27401
- * expose ``startRecording`` for parity when we add other carriers.
27765
+ * Start call recording when configured. Bridges expose
27766
+ * ``startRecording`` for carrier parity (Twilio and Telnyx supported).
27402
27767
  */
27403
27768
  async startRecordingIfRequested(callId) {
27404
27769
  const { recording, config: config2 } = this.deps;
@@ -27644,7 +28009,7 @@ var init_stream_handler = __esm({
27644
28009
  this.metricsAcc.addSttAudioBytes(pcm16k.length);
27645
28010
  }
27646
28011
  } else if (this.adapter) {
27647
- if (this.adapter instanceof ElevenLabsConvAIAdapter && this.deps.bridge.telephonyProvider === "twilio" && this.adapter.inputAudioFormat !== "ulaw_8000") {
28012
+ if (this.adapter instanceof ElevenLabsConvAIAdapter && this.deps.bridge.inputWireFormat === "ulaw_8000" && this.adapter.inputAudioFormat !== "ulaw_8000") {
27648
28013
  const pcm8k = mulawToPcm16(audioBuffer);
27649
28014
  const pcm16k = this.inboundResampler.process(pcm8k);
27650
28015
  this.adapter.sendAudio(pcm16k);
@@ -27805,6 +28170,7 @@ var init_stream_handler = __esm({
27805
28170
  const carrier = this.deps.bridge.telephonyProvider;
27806
28171
  if (carrier === "twilio") return fmt === "ulaw_8000";
27807
28172
  if (carrier === "telnyx") return fmt === "pcm_16000";
28173
+ if (carrier === "plivo") return fmt === "ulaw_8000";
27808
28174
  return false;
27809
28175
  }
27810
28176
  /**
@@ -27921,12 +28287,9 @@ var init_stream_handler = __esm({
27921
28287
  }
27922
28288
  }
27923
28289
  if (this.deps.agent.echoCancellation) {
27924
- const carrier = this.deps.bridge.telephonyProvider;
27925
- if (carrier === "twilio" || carrier === "telnyx") {
27926
- getLogger().warn(
27927
- `echoCancellation: true on ${carrier} (PSTN). Server-side NLMS cannot model PSTN's ~250\u20131500 ms round-trip echo with a 32 ms filter window \u2014 it will silently no-op. Best practice: keep echoCancellation: false; rely on the carrier + caller device's built-in echo suppression and Patter's self-hearing guard. Enable AEC only for browser/native deployments where the SDK owns the audio path end-to-end.`
27928
- );
27929
- }
28290
+ getLogger().warn(
28291
+ `echoCancellation: true on ${this.deps.bridge.telephonyProvider} (PSTN). Server-side NLMS cannot model PSTN's ~250\u20131500 ms round-trip echo with a 32 ms filter window \u2014 it will silently no-op. Best practice: keep echoCancellation: false; rely on the carrier + caller device's built-in echo suppression and Patter's self-hearing guard. Enable AEC only for browser/native deployments where the SDK owns the audio path end-to-end.`
28292
+ );
27930
28293
  try {
27931
28294
  const { NlmsEchoCanceller: NlmsEchoCanceller2 } = await Promise.resolve().then(() => (init_aec(), aec_exports));
27932
28295
  this.aec = new NlmsEchoCanceller2({ sampleRate: 16e3 });
@@ -28059,13 +28422,20 @@ var init_stream_handler = __esm({
28059
28422
  );
28060
28423
  }
28061
28424
  const providerModel = this.deps.agent.llm?.model ?? "";
28425
+ const augmentedTools = augmentWithBuiltinHandoffTools(
28426
+ this.deps.agent.tools,
28427
+ {
28428
+ transferCall: (number4) => this.deps.bridge.transferCall(this.callId, number4),
28429
+ endCall: () => this.deps.bridge.endCall(this.callId, this.ws)
28430
+ }
28431
+ );
28062
28432
  this.llmLoop = new LLMLoop(
28063
28433
  "",
28064
28434
  // apiKey unused when llmProvider is supplied
28065
28435
  providerModel,
28066
28436
  // propagate so calculateLlmCost can match the price row
28067
28437
  resolvedPrompt,
28068
- this.deps.agent.tools,
28438
+ augmentedTools,
28069
28439
  this.deps.agent.llm,
28070
28440
  this.deps.agent.disablePhonePreamble ?? false
28071
28441
  );
@@ -28076,11 +28446,18 @@ var init_stream_handler = __esm({
28076
28446
  } else if (!this.deps.onMessage && this.deps.config.openaiKey) {
28077
28447
  let llmModel = this.deps.agent.model || "gpt-4o-mini";
28078
28448
  if (llmModel.includes("realtime")) llmModel = "gpt-4o-mini";
28449
+ const augmentedTools = augmentWithBuiltinHandoffTools(
28450
+ this.deps.agent.tools,
28451
+ {
28452
+ transferCall: (number4) => this.deps.bridge.transferCall(this.callId, number4),
28453
+ endCall: () => this.deps.bridge.endCall(this.callId, this.ws)
28454
+ }
28455
+ );
28079
28456
  this.llmLoop = new LLMLoop(
28080
28457
  this.deps.config.openaiKey,
28081
28458
  llmModel,
28082
28459
  resolvedPrompt,
28083
- this.deps.agent.tools,
28460
+ augmentedTools,
28084
28461
  void 0,
28085
28462
  this.deps.agent.disablePhonePreamble ?? false
28086
28463
  );
@@ -29178,7 +29555,7 @@ function redactPhone(raw) {
29178
29555
  const mode = redactMode();
29179
29556
  if (mode === "full") return raw;
29180
29557
  if (mode === "hash_only") {
29181
- return "sha256:" + crypto4.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
29558
+ return "sha256:" + crypto5.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
29182
29559
  }
29183
29560
  return maskPhoneNumber(raw);
29184
29561
  }
@@ -29189,7 +29566,7 @@ function utcIso(tsSeconds) {
29189
29566
  async function atomicWriteJson(filePath, payload) {
29190
29567
  const dir = path4.dirname(filePath);
29191
29568
  await import_node_fs2.promises.mkdir(dir, { recursive: true });
29192
- const tmp = path4.join(dir, `.tmp.${process.pid}.${crypto4.randomBytes(4).toString("hex")}.json`);
29569
+ const tmp = path4.join(dir, `.tmp.${process.pid}.${crypto5.randomBytes(4).toString("hex")}.json`);
29193
29570
  try {
29194
29571
  const handle = await import_node_fs2.promises.open(tmp, "w");
29195
29572
  try {
@@ -29229,12 +29606,12 @@ function rmTree(target) {
29229
29606
  } catch {
29230
29607
  }
29231
29608
  }
29232
- var crypto4, fs4, import_node_fs2, os, path4, SCHEMA_VERSION, DEFAULT_RETENTION_DAYS, CallLogger;
29609
+ var crypto5, fs4, import_node_fs2, os, path4, SCHEMA_VERSION, DEFAULT_RETENTION_DAYS, CallLogger;
29233
29610
  var init_call_log = __esm({
29234
29611
  "src/services/call-log.ts"() {
29235
29612
  "use strict";
29236
29613
  init_cjs_shims();
29237
- crypto4 = __toESM(require("crypto"));
29614
+ crypto5 = __toESM(require("crypto"));
29238
29615
  fs4 = __toESM(require("fs"));
29239
29616
  import_node_fs2 = require("fs");
29240
29617
  os = __toESM(require("os"));
@@ -29306,7 +29683,7 @@ var init_call_log = __esm({
29306
29683
  } catch (err) {
29307
29684
  getLogger().warn(`call_log write failed (${sanitizeLogValue(callId)}): ${sanitizeLogValue(String(err))}`);
29308
29685
  }
29309
- if (crypto4.randomBytes(1)[0] < 5) {
29686
+ if (crypto5.randomBytes(1)[0] < 5) {
29310
29687
  this.sweepOldDays();
29311
29688
  }
29312
29689
  }
@@ -29443,6 +29820,19 @@ function classifyTelnyxAmd(result) {
29443
29820
  if (result === "fax") return "fax";
29444
29821
  return "unknown";
29445
29822
  }
29823
+ function twilioStatusToOutcome(callStatus) {
29824
+ const s = (callStatus || "").toLowerCase();
29825
+ if (s === "no-answer") return "no_answer";
29826
+ if (s === "busy") return "busy";
29827
+ return "failed";
29828
+ }
29829
+ function telnyxHangupOutcome(cause) {
29830
+ const c = (cause || "").toLowerCase();
29831
+ if (c === "no_answer" || c === "timeout" || c === "no_user_response") return "no_answer";
29832
+ if (c === "user_busy" || c === "busy") return "busy";
29833
+ if (c === "call_rejected" || c === "rejected" || c === "destination_out_of_order") return "failed";
29834
+ return null;
29835
+ }
29446
29836
  function validateWebhookUrl(url2) {
29447
29837
  const parsed = new URL(url2);
29448
29838
  if (!["http:", "https:"].includes(parsed.protocol)) {
@@ -29500,7 +29890,7 @@ function validateTelnyxSignature(rawBody, signature, timestamp, publicKey, toler
29500
29890
  if (ageMs < 0 || ageMs > toleranceSec * 1e3) return false;
29501
29891
  const payload = `${timestamp}|${rawBody}`;
29502
29892
  const keyBuffer = Buffer.from(publicKey, "base64");
29503
- const keyObject = import_node_crypto3.default.createPublicKey({
29893
+ const keyObject = import_node_crypto4.default.createPublicKey({
29504
29894
  key: keyBuffer,
29505
29895
  format: "der",
29506
29896
  type: "spki"
@@ -29510,7 +29900,7 @@ function validateTelnyxSignature(rawBody, signature, timestamp, publicKey, toler
29510
29900
  if (!trimmed) continue;
29511
29901
  try {
29512
29902
  const sigBuffer = Buffer.from(trimmed, "base64");
29513
- if (import_node_crypto3.default.verify(null, Buffer.from(payload), keyObject, sigBuffer)) {
29903
+ if (import_node_crypto4.default.verify(null, Buffer.from(payload), keyObject, sigBuffer)) {
29514
29904
  return true;
29515
29905
  }
29516
29906
  } catch {
@@ -29527,12 +29917,12 @@ function validateTwilioSid(sid, prefix = "CA") {
29527
29917
  }
29528
29918
  function validateTwilioSignature(url2, params, signature, authToken) {
29529
29919
  const data = url2 + Object.keys(params).sort().reduce((acc, key) => acc + key + (params[key] ?? ""), "");
29530
- const expected = import_node_crypto3.default.createHmac("sha1", authToken).update(data).digest("base64");
29920
+ const expected = import_node_crypto4.default.createHmac("sha1", authToken).update(data).digest("base64");
29531
29921
  try {
29532
29922
  const sigBuf = Buffer.from(signature);
29533
29923
  const expBuf = Buffer.from(expected);
29534
29924
  if (sigBuf.length !== expBuf.length) return false;
29535
- return import_node_crypto3.default.timingSafeEqual(sigBuf, expBuf);
29925
+ return import_node_crypto4.default.timingSafeEqual(sigBuf, expBuf);
29536
29926
  } catch {
29537
29927
  return false;
29538
29928
  }
@@ -29607,18 +29997,20 @@ async function sleep(ms) {
29607
29997
  if (ms <= 0) return;
29608
29998
  await new Promise((resolve2) => setTimeout(resolve2, ms));
29609
29999
  }
29610
- var import_node_crypto3, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
30000
+ var import_node_crypto4, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
29611
30001
  var init_server = __esm({
29612
30002
  "src/server.ts"() {
29613
30003
  "use strict";
29614
30004
  init_cjs_shims();
29615
- import_node_crypto3 = __toESM(require("crypto"));
30005
+ import_node_crypto4 = __toESM(require("crypto"));
29616
30006
  import_express = __toESM(require("express"));
29617
30007
  import_http = require("http");
29618
30008
  import_ws5 = require("ws");
29619
30009
  init_openai_realtime();
29620
30010
  init_openai_realtime_2();
29621
30011
  init_elevenlabs_convai();
30012
+ init_plivo_adapter();
30013
+ init_plivo();
29622
30014
  init_provider_factory();
29623
30015
  init_pricing();
29624
30016
  init_store();
@@ -29661,6 +30053,7 @@ var init_server = __esm({
29661
30053
  config;
29662
30054
  label = "Twilio";
29663
30055
  telephonyProvider = "twilio";
30056
+ inputWireFormat = "ulaw_8000";
29664
30057
  sendAudio(ws, audioBase64, streamSid) {
29665
30058
  ws.send(JSON.stringify({ event: "media", streamSid, media: { payload: audioBase64 } }));
29666
30059
  }
@@ -29748,6 +30141,11 @@ var init_server = __esm({
29748
30141
  config;
29749
30142
  label = "Telnyx";
29750
30143
  telephonyProvider = "telnyx";
30144
+ // ``streaming_start`` negotiates PCMU bidirectional by default — keeping
30145
+ // ``ulaw_8000`` here matches what TwilioBridge does and keeps the stream
30146
+ // handler's input-transcode branch in the right shape. If a deployment
30147
+ // overrides the negotiation to L16, this should flip to ``pcm_16000``.
30148
+ inputWireFormat = "ulaw_8000";
29751
30149
  sendAudio(ws, audioBase64, _streamSid) {
29752
30150
  ws.send(JSON.stringify({ event: "media", media: { payload: audioBase64 } }));
29753
30151
  }
@@ -29769,7 +30167,7 @@ var init_server = __esm({
29769
30167
  });
29770
30168
  getLogger().info(`Telnyx call transferred to ${toNumber}`);
29771
30169
  }
29772
- async sendDtmf(callId, digits, delayMs) {
30170
+ async sendDtmf(_ws, callId, digits, delayMs) {
29773
30171
  if (!digits) {
29774
30172
  getLogger().warn("TelnyxBridge.sendDtmf called with empty digits");
29775
30173
  return;
@@ -29967,6 +30365,99 @@ var init_server = __esm({
29967
30365
  * (tests) work without further setup. See FIX #91.
29968
30366
  */
29969
30367
  recordPrewarmWaste = () => void 0;
30368
+ /**
30369
+ * Per-callId completion deferreds for ``Patter.call({ wait: true })``.
30370
+ * Resolved by the FIRST terminal signal: the Twilio/Telnyx status callback
30371
+ * for no-media outcomes (no-answer / busy / failed), or ``onCallEnd`` for a
30372
+ * connected call (answered / voicemail). The AMD classification is recorded
30373
+ * per callId so the connected-call path can distinguish ``answered`` from
30374
+ * ``voicemail``. This is what lets ``call({ wait: true })`` resolve to a
30375
+ * structured {@link CallResult} without the caller hand-wiring ``onCallEnd``
30376
+ * to a promise. Public so ``client.ts`` can register/await + fail in-flight
30377
+ * waiters on ``disconnect()``. Mirrors Python's ``EmbeddedServer._completions``.
30378
+ */
30379
+ completions = /* @__PURE__ */ new Map();
30380
+ /** AMD classification recorded per callId, used by the connected-call path. */
30381
+ amdClass = /* @__PURE__ */ new Map();
30382
+ // === Outbound completion registry (call({ wait: true })) ===
30383
+ /**
30384
+ * Register (or return) a completion promise for an outbound call.
30385
+ *
30386
+ * Called by ``Patter.call({ wait: true })`` immediately after the carrier
30387
+ * accepts the dial — the promise resolves to a {@link CallResult} once a
30388
+ * terminal signal arrives. Idempotent: returns the existing pending promise
30389
+ * if one is already registered for ``callId``. Mirrors Python's
30390
+ * ``register_completion``.
30391
+ */
30392
+ registerCompletion(callId) {
30393
+ const existing = this.completions.get(callId);
30394
+ if (existing && !existing.done) {
30395
+ return existing.promise;
30396
+ }
30397
+ let resolve2;
30398
+ let reject;
30399
+ const promise = new Promise((res, rej) => {
30400
+ resolve2 = res;
30401
+ reject = rej;
30402
+ });
30403
+ this.completions.set(callId, { promise, resolve: resolve2, reject, done: false });
30404
+ return promise;
30405
+ }
30406
+ /** Drop a registered completion (e.g. on a backstop timeout) without resolving it. */
30407
+ deleteCompletion(callId) {
30408
+ this.completions.delete(callId);
30409
+ this.amdClass.delete(callId);
30410
+ }
30411
+ /**
30412
+ * Resolve a pending completion with a {@link CallResult}.
30413
+ *
30414
+ * No-op when no completion is registered for ``callId`` (the common case —
30415
+ * most calls are placed without ``wait: true``) or it is already done.
30416
+ * Builds the result from the ``onCallEnd`` payload when ``data`` is provided
30417
+ * (connected calls carry transcript + {@link CallMetrics}); no-media
30418
+ * outcomes pass ``data`` undefined and yield an empty transcript / no cost.
30419
+ * Mirrors Python's ``_resolve_completion``.
30420
+ */
30421
+ resolveCompletion(callId, args) {
30422
+ const entry = this.completions.get(callId);
30423
+ if (!entry || entry.done) return;
30424
+ const data = args.data;
30425
+ const metrics = data?.metrics ?? null;
30426
+ const cost = metrics?.cost ?? null;
30427
+ const durationRaw = metrics?.duration_seconds;
30428
+ const duration3 = typeof durationRaw === "number" ? durationRaw : 0;
30429
+ const transcriptRaw = data?.transcript;
30430
+ const transcript = Array.isArray(transcriptRaw) ? transcriptRaw : [];
30431
+ const result = {
30432
+ callId,
30433
+ outcome: args.outcome,
30434
+ status: args.status,
30435
+ durationSeconds: duration3,
30436
+ transcript,
30437
+ cost,
30438
+ metrics
30439
+ };
30440
+ entry.done = true;
30441
+ entry.resolve(result);
30442
+ this.completions.delete(callId);
30443
+ this.amdClass.delete(callId);
30444
+ }
30445
+ /**
30446
+ * Fail every in-flight completion with ``error``. Called by
30447
+ * ``Patter.disconnect()`` so a ``call({ wait: true })`` awaiter does not
30448
+ * hang until its backstop timeout once the server is gone. Mirrors the
30449
+ * Python ``disconnect()`` change that fails in-flight ``wait=True`` awaiters.
30450
+ */
30451
+ failPendingCompletions(error2) {
30452
+ for (const entry of this.completions.values()) {
30453
+ if (!entry.done) {
30454
+ entry.done = true;
30455
+ entry.reject(error2);
30456
+ }
30457
+ }
30458
+ this.completions.clear();
30459
+ this.amdClass.clear();
30460
+ }
29970
30461
  /** Bind HTTP + WebSocket listeners on `port`, mount carrier webhooks and dashboard routes. */
29971
30462
  async start(port = 8e3) {
29972
30463
  const webhookUrlPattern = /^[a-zA-Z0-9][a-zA-Z0-9.\-]+[a-zA-Z0-9]$/;
@@ -30030,8 +30521,10 @@ var init_server = __esm({
30030
30521
  return;
30031
30522
  }
30032
30523
  const body = req.body;
30033
- const callSid = sanitizeLogValue(body["CallSid"] ?? "");
30034
- const callStatus = sanitizeLogValue(body["CallStatus"] ?? "");
30524
+ const rawCallSid = body["CallSid"] ?? "";
30525
+ const rawCallStatus = body["CallStatus"] ?? "";
30526
+ const callSid = sanitizeLogValue(rawCallSid);
30527
+ const callStatus = sanitizeLogValue(rawCallStatus);
30035
30528
  const duration3 = body["CallDuration"] ?? body["Duration"] ?? "";
30036
30529
  getLogger().info(
30037
30530
  `Twilio status ${callStatus} for call ${callSid} (duration=${duration3})`
@@ -30048,6 +30541,10 @@ var init_server = __esm({
30048
30541
  } catch (err) {
30049
30542
  getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
30050
30543
  }
30544
+ this.resolveCompletion(rawCallSid, {
30545
+ outcome: twilioStatusToOutcome(rawCallStatus),
30546
+ status: rawCallStatus
30547
+ });
30051
30548
  }
30052
30549
  res.status(204).send();
30053
30550
  });
@@ -30090,6 +30587,9 @@ var init_server = __esm({
30090
30587
  const answeredBy = body["AnsweredBy"] ?? "";
30091
30588
  const callSid = body["CallSid"] ?? "";
30092
30589
  getLogger().info(`AMD result for ${sanitizeLogValue(callSid)}: ${sanitizeLogValue(answeredBy)}`);
30590
+ if (callSid) {
30591
+ this.amdClass.set(callSid, classifyTwilioAmd(answeredBy));
30592
+ }
30093
30593
  const cb = this.onMachineDetection;
30094
30594
  if (cb && callSid) {
30095
30595
  try {
@@ -30215,6 +30715,9 @@ var init_server = __esm({
30215
30715
  getLogger().info(
30216
30716
  `Telnyx AMD result for ${sanitizeLogValue(amdCallId)}: ${sanitizeLogValue(amdResult)}`
30217
30717
  );
30718
+ if (amdCallId) {
30719
+ this.amdClass.set(amdCallId, classifyTelnyxAmd(amdResult));
30720
+ }
30218
30721
  const cbTx = this.onMachineDetection;
30219
30722
  if (cbTx && amdCallId) {
30220
30723
  try {
@@ -30251,6 +30754,13 @@ var init_server = __esm({
30251
30754
  } catch (err) {
30252
30755
  getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
30253
30756
  }
30757
+ const noMediaOutcome = telnyxHangupOutcome(hangupCause);
30758
+ if (noMediaOutcome !== null) {
30759
+ this.resolveCompletion(hangupCallId, {
30760
+ outcome: noMediaOutcome,
30761
+ status: hangupCause
30762
+ });
30763
+ }
30254
30764
  }
30255
30765
  return res.status(200).send();
30256
30766
  }
@@ -30303,6 +30813,121 @@ var init_server = __esm({
30303
30813
  }
30304
30814
  return res.status(200).send();
30305
30815
  });
30816
+ const validatePlivoRequest = (req, res) => {
30817
+ const authToken = this.config.plivoAuthToken;
30818
+ if (!authToken) {
30819
+ if (this.config.requireSignature !== false) {
30820
+ getLogger().error(
30821
+ "Plivo webhook rejected: plivoAuthToken not configured and requireSignature is not false"
30822
+ );
30823
+ res.status(503).send("Webhook signature required");
30824
+ return false;
30825
+ }
30826
+ return true;
30827
+ }
30828
+ const method = req.method.toUpperCase();
30829
+ const params = method === "POST" && req.body && typeof req.body === "object" ? Object.fromEntries(
30830
+ Object.entries(req.body).map(([k, v]) => [k, String(v)])
30831
+ ) : {};
30832
+ const signature = req.headers["x-plivo-signature-v3"] || "";
30833
+ const nonce = req.headers["x-plivo-signature-v3-nonce"] || "";
30834
+ const url2 = `https://${this.config.webhookUrl}${req.originalUrl}`;
30835
+ if (!validatePlivoSignature(url2, nonce, signature, authToken, params, method)) {
30836
+ getLogger().warn("Plivo webhook rejected: invalid or missing V3 signature");
30837
+ res.status(403).send("Invalid signature");
30838
+ return false;
30839
+ }
30840
+ return true;
30841
+ };
30842
+ app.post("/webhooks/plivo/voice", (req, res) => {
30843
+ if (!validatePlivoRequest(req, res)) return;
30844
+ const body = req.body ?? {};
30845
+ const callUuid = body["CallUUID"] ?? "";
30846
+ const caller = body["From"] ?? "";
30847
+ const callee = body["To"] ?? "";
30848
+ const qs = `?caller=${encodeURIComponent(caller)}&callee=${encodeURIComponent(callee)}`;
30849
+ const streamUrl = `wss://${this.config.webhookUrl}/ws/plivo/stream/${callUuid || "outbound"}${qs}`;
30850
+ const xml = PlivoAdapter.generateStreamXml(streamUrl, "audio/x-mulaw;rate=8000", {
30851
+ "X-PH-caller": caller,
30852
+ "X-PH-callee": callee
30853
+ });
30854
+ res.type("text/xml").send(xml);
30855
+ });
30856
+ app.post("/webhooks/plivo/status", (req, res) => {
30857
+ if (!validatePlivoRequest(req, res)) return;
30858
+ const body = req.body ?? {};
30859
+ const callUuid = body["CallUUID"] ?? "";
30860
+ const callStatus = body["CallStatus"] ?? body["Status"] ?? "";
30861
+ const duration3 = body["Duration"] ?? body["BillDuration"] ?? "";
30862
+ getLogger().info(
30863
+ `Plivo status ${sanitizeLogValue(callStatus)} for call ${sanitizeLogValue(callUuid)} (duration=${duration3})`
30864
+ );
30865
+ if (callUuid && callStatus) {
30866
+ const extra = {};
30867
+ const parsed = parseFloat(duration3);
30868
+ if (!Number.isNaN(parsed)) extra.duration_seconds = parsed;
30869
+ this.metricsStore.updateCallStatus(callUuid, callStatus, extra);
30870
+ }
30871
+ if (callUuid && ["no-answer", "busy", "failed", "timeout", "cancel"].includes(callStatus)) {
30872
+ try {
30873
+ this.recordPrewarmWaste(callUuid);
30874
+ } catch (err) {
30875
+ getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
30876
+ }
30877
+ const outcome = callStatus === "no-answer" || callStatus === "timeout" ? "no_answer" : callStatus === "busy" ? "busy" : "failed";
30878
+ this.resolveCompletion(callUuid, { outcome, status: callStatus });
30879
+ }
30880
+ res.status(200).send();
30881
+ });
30882
+ app.post("/webhooks/plivo/amd", async (req, res) => {
30883
+ if (!validatePlivoRequest(req, res)) return;
30884
+ const body = req.body ?? {};
30885
+ const callUuid = body["CallUUID"] ?? "";
30886
+ const amdRaw = body["Machine"] || body["MachineDetection"] || body["AnsweredBy"] || body["CallStatus"] || "";
30887
+ getLogger().info(`AMD result for ${sanitizeLogValue(callUuid)}: ${sanitizeLogValue(amdRaw)}`);
30888
+ const classification = classifyPlivoAmd(amdRaw);
30889
+ if (callUuid) this.amdClass.set(callUuid, classification);
30890
+ const cb = this.onMachineDetection;
30891
+ if (cb && callUuid) {
30892
+ try {
30893
+ await cb({
30894
+ call_id: callUuid,
30895
+ carrier: "plivo",
30896
+ classification,
30897
+ raw: amdRaw,
30898
+ detected_at: Date.now() / 1e3
30899
+ });
30900
+ } catch (err) {
30901
+ getLogger().warn(`onMachineDetection callback threw: ${sanitizeLogValue(String(err))}`);
30902
+ }
30903
+ }
30904
+ if (classification === "machine" && callUuid) {
30905
+ try {
30906
+ this.recordPrewarmWaste(callUuid);
30907
+ } catch (err) {
30908
+ getLogger().debug(`recordPrewarmWaste threw: ${String(err)}`);
30909
+ }
30910
+ if (this.voicemailMessage && this.config.plivoAuthId && this.config.plivoAuthToken) {
30911
+ await dropPlivoVoicemail(
30912
+ callUuid,
30913
+ this.voicemailMessage,
30914
+ this.config.plivoAuthId,
30915
+ this.config.plivoAuthToken
30916
+ );
30917
+ }
30918
+ }
30919
+ res.status(200).send();
30920
+ });
30921
+ app.all("/webhooks/plivo/transfer", (req, res) => {
30922
+ if (!validatePlivoRequest(req, res)) return;
30923
+ const to = String(req.query.to ?? "");
30924
+ if (!to || !/^\+[1-9]\d{6,14}$/.test(to)) {
30925
+ getLogger().warn(`Plivo transfer XML: invalid target ${JSON.stringify(to)}`);
30926
+ res.type("text/xml").send("<Response><Hangup/></Response>");
30927
+ return;
30928
+ }
30929
+ res.type("text/xml").send(`<Response><Dial><Number>${xmlEscape(to)}</Number></Dial></Response>`);
30930
+ });
30306
30931
  this.server = (0, import_http.createServer)(app);
30307
30932
  this.wss = new import_ws5.WebSocketServer({ noServer: true });
30308
30933
  const MAX_WS_PER_IP = 10;
@@ -30335,9 +30960,11 @@ var init_server = __esm({
30335
30960
  ws.once("close", () => {
30336
30961
  this.activeConnections.delete(ws);
30337
30962
  });
30338
- const isTelnyx = this.config.telephonyProvider === "telnyx";
30339
- if (isTelnyx) {
30963
+ const provider2 = this.config.telephonyProvider;
30964
+ if (provider2 === "telnyx") {
30340
30965
  this.handleTelnyxStream(ws, url2);
30966
+ } else if (provider2 === "plivo") {
30967
+ this.handlePlivoStream(ws, url2);
30341
30968
  } else {
30342
30969
  this.handleTwilioStream(ws, url2);
30343
30970
  }
@@ -30521,6 +31148,12 @@ var init_server = __esm({
30521
31148
  }).catch((err) => getLogger().error(`call_log end error: ${String(err)}`));
30522
31149
  }
30523
31150
  if (userEnd) await userEnd(data);
31151
+ const cid = typeof data.call_id === "string" ? data.call_id : "";
31152
+ if (cid) {
31153
+ const cls = this.amdClass.get(cid);
31154
+ const outcome = cls === "machine" ? "voicemail" : "answered";
31155
+ this.resolveCompletion(cid, { outcome, status: "completed", data });
31156
+ }
30524
31157
  };
30525
31158
  return [wrappedStart, wrappedMetrics, wrappedEnd];
30526
31159
  }
@@ -30627,6 +31260,52 @@ var init_server = __esm({
30627
31260
  });
30628
31261
  }
30629
31262
  // ---------------------------------------------------------------------------
31263
+ // Plivo WebSocket message parser (thin layer)
31264
+ // ---------------------------------------------------------------------------
31265
+ handlePlivoStream(ws, url2) {
31266
+ const caller = url2.searchParams.get("caller") ?? "";
31267
+ const callee = url2.searchParams.get("callee") ?? "";
31268
+ const bridge = new PlivoBridge(this.config);
31269
+ const handler = new StreamHandler(this.buildStreamHandlerDeps(bridge), ws, caller, callee);
31270
+ ws.on("message", async (raw) => {
31271
+ try {
31272
+ let data;
31273
+ try {
31274
+ data = JSON.parse(raw.toString());
31275
+ } catch (e) {
31276
+ getLogger().error("Failed to parse Plivo WS message:", e);
31277
+ return;
31278
+ }
31279
+ const event = data.event ?? "";
31280
+ if (event === "start") {
31281
+ handler.setStreamSid(data.start?.streamId ?? "");
31282
+ const callId = data.start?.callId ?? "";
31283
+ if (callId) this.activeCallIds.set(ws, callId);
31284
+ await handler.handleCallStart(callId);
31285
+ } else if (event === "media") {
31286
+ const payload = data.media?.payload ?? "";
31287
+ if (payload) handler.handleAudio(Buffer.from(payload, "base64"));
31288
+ } else if (event === "playedStream") {
31289
+ const markName = String(data.name ?? "");
31290
+ if (markName) await handler.onMark(markName);
31291
+ } else if (event === "dtmf") {
31292
+ const digit = String(data.dtmf?.digit ?? "").trim();
31293
+ if (digit) await handler.handleDtmf(digit);
31294
+ } else if (event === "playFailed" || event === "error") {
31295
+ getLogger().warn(`Plivo ${event}: ${data.reason ?? "unknown"}`);
31296
+ } else if (event === "stop") {
31297
+ await handler.handleStop();
31298
+ }
31299
+ } catch (err) {
31300
+ getLogger().error("Stream handler error (Plivo):", err);
31301
+ }
31302
+ });
31303
+ ws.on("close", async () => {
31304
+ this.activeCallIds.delete(ws);
31305
+ await handler.handleWsClose();
31306
+ });
31307
+ }
31308
+ // ---------------------------------------------------------------------------
30630
31309
  // Graceful shutdown
30631
31310
  // ---------------------------------------------------------------------------
30632
31311
  /**
@@ -30643,10 +31322,10 @@ var init_server = __esm({
30643
31322
  const httpClosePromise = new Promise((resolve2) => {
30644
31323
  this.server.close(() => resolve2());
30645
31324
  });
30646
- const isTelnyx = this.config.telephonyProvider === "telnyx";
31325
+ const provider2 = this.config.telephonyProvider;
30647
31326
  for (const [ws, callId] of this.activeCallIds) {
30648
31327
  try {
30649
- const bridge = isTelnyx ? new TelnyxBridge(this.config) : new TwilioBridge(this.config);
31328
+ const bridge = provider2 === "telnyx" ? new TelnyxBridge(this.config) : provider2 === "plivo" ? new PlivoBridge(this.config) : new TwilioBridge(this.config);
30650
31329
  await bridge.endCall(callId, ws);
30651
31330
  } catch {
30652
31331
  }
@@ -30795,6 +31474,7 @@ var init_tunnel = __esm({
30795
31474
  var carrier_config_exports = {};
30796
31475
  __export(carrier_config_exports, {
30797
31476
  autoConfigureCarrier: () => autoConfigureCarrier,
31477
+ configurePlivoNumber: () => configurePlivoNumber,
30798
31478
  configureTelnyxNumber: () => configureTelnyxNumber,
30799
31479
  configureTwilioNumber: () => configureTwilioNumber
30800
31480
  });
@@ -30846,9 +31526,38 @@ async function configureTelnyxNumber(apiKey, connectionId, phoneNumber) {
30846
31526
  );
30847
31527
  }
30848
31528
  }
31529
+ async function configurePlivoNumber(authId, authToken, phoneNumber, answerUrl) {
31530
+ const auth2 = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
31531
+ const base = `${PLIVO_API_BASE2}/Account/${encodeURIComponent(authId)}`;
31532
+ const appResp = await fetch(`${base}/Application/`, {
31533
+ method: "POST",
31534
+ headers: { Authorization: auth2, "Content-Type": "application/json" },
31535
+ body: JSON.stringify({
31536
+ app_name: "patter-inbound",
31537
+ answer_url: answerUrl,
31538
+ answer_method: "POST"
31539
+ })
31540
+ });
31541
+ if (!appResp.ok) {
31542
+ throw new Error(`Plivo Application create failed: ${appResp.status} ${await appResp.text()}`);
31543
+ }
31544
+ const appBody = await appResp.json();
31545
+ if (!appBody.app_id) {
31546
+ getLogger().warn("Plivo Application create returned no app_id");
31547
+ return;
31548
+ }
31549
+ const linkResp = await fetch(`${base}/Number/${encodeURIComponent(phoneNumber)}/`, {
31550
+ method: "POST",
31551
+ headers: { Authorization: auth2, "Content-Type": "application/json" },
31552
+ body: JSON.stringify({ app_id: appBody.app_id })
31553
+ });
31554
+ if (!linkResp.ok) {
31555
+ throw new Error(`Plivo Number update failed: ${linkResp.status} ${await linkResp.text()}`);
31556
+ }
31557
+ }
30849
31558
  async function autoConfigureCarrier(params) {
30850
31559
  const log3 = getLogger();
30851
- const provider2 = params.telephonyProvider ?? (params.twilioSid ? "twilio" : "telnyx");
31560
+ const provider2 = params.telephonyProvider ?? (params.twilioSid ? "twilio" : params.plivoAuthId ? "plivo" : "telnyx");
30852
31561
  if (provider2 === "twilio" && params.twilioSid && params.twilioToken) {
30853
31562
  const voiceUrl = `https://${params.webhookHost}/webhooks/twilio/voice`;
30854
31563
  try {
@@ -30867,9 +31576,20 @@ async function autoConfigureCarrier(params) {
30867
31576
  } catch (err) {
30868
31577
  log3.warn("Could not auto-configure Telnyx number: %s", err instanceof Error ? err.message : String(err));
30869
31578
  }
31579
+ return;
31580
+ }
31581
+ if (provider2 === "plivo" && params.plivoAuthId && params.plivoAuthToken) {
31582
+ const answerUrl = `https://${params.webhookHost}/webhooks/plivo/voice`;
31583
+ try {
31584
+ await configurePlivoNumber(params.plivoAuthId, params.plivoAuthToken, params.phoneNumber, answerUrl);
31585
+ log3.info("Plivo answer URL set to %s", answerUrl);
31586
+ } catch (err) {
31587
+ log3.warn("Could not auto-configure Plivo answer URL: %s", err instanceof Error ? err.message : String(err));
31588
+ log3.info("Set the Plivo application answer URL manually to: %s", answerUrl);
31589
+ }
30870
31590
  }
30871
31591
  }
30872
- var TWILIO_API_BASE, TELNYX_API_BASE;
31592
+ var TWILIO_API_BASE, TELNYX_API_BASE, PLIVO_API_BASE2;
30873
31593
  var init_carrier_config = __esm({
30874
31594
  "src/carrier-config.ts"() {
30875
31595
  "use strict";
@@ -30877,6 +31597,7 @@ var init_carrier_config = __esm({
30877
31597
  init_logger();
30878
31598
  TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
30879
31599
  TELNYX_API_BASE = "https://api.telnyx.com/v2";
31600
+ PLIVO_API_BASE2 = "https://api.plivo.com/v1";
30880
31601
  }
30881
31602
  });
30882
31603
 
@@ -32500,6 +33221,8 @@ __export(index_exports, {
32500
33221
  PatterTool: () => PatterTool,
32501
33222
  PcmCarry: () => PcmCarry,
32502
33223
  PipelineHookExecutor: () => PipelineHookExecutor,
33224
+ Plivo: () => Carrier,
33225
+ PlivoAdapter: () => PlivoAdapter,
32503
33226
  PricingUnit: () => PricingUnit,
32504
33227
  ProvisionError: () => ProvisionError,
32505
33228
  RateLimitError: () => RateLimitError,
@@ -32526,7 +33249,7 @@ __export(index_exports, {
32526
33249
  SpeechmaticsTurnDetectionMode: () => TurnDetectionMode,
32527
33250
  StatefulResampler: () => StatefulResampler,
32528
33251
  StaticTunnel: () => Static,
32529
- Telnyx: () => Carrier2,
33252
+ Telnyx: () => Carrier3,
32530
33253
  TelnyxAdapter: () => TelnyxAdapter,
32531
33254
  TelnyxSTT: () => TelnyxSTT,
32532
33255
  TelnyxSTTInputFormat: () => TelnyxSTTInputFormat,
@@ -32537,7 +33260,7 @@ __export(index_exports, {
32537
33260
  TestSession: () => TestSession,
32538
33261
  TfidfLoopDetector: () => TfidfLoopDetector,
32539
33262
  Tool: () => Tool,
32540
- Twilio: () => Carrier,
33263
+ Twilio: () => Carrier2,
32541
33264
  TwilioAdapter: () => TwilioAdapter,
32542
33265
  ULTRAVOX_DEFAULT_API_BASE: () => ULTRAVOX_DEFAULT_API_BASE,
32543
33266
  ULTRAVOX_DEFAULT_SR: () => ULTRAVOX_DEFAULT_SR,
@@ -33314,7 +34037,7 @@ var Patter = class {
33314
34037
  }
33315
34038
  if (!options.carrier) {
33316
34039
  throw new Error(
33317
- "Local mode requires a `carrier` instance. Pass `carrier: new Twilio({...})` or `carrier: new Telnyx({...})`."
34040
+ "Local mode requires a `carrier` instance. Pass `carrier: new Twilio({...})`, `carrier: new Telnyx({...})` or `carrier: new Plivo({...})`."
33318
34041
  );
33319
34042
  }
33320
34043
  const carrier = options.carrier;
@@ -33492,7 +34215,7 @@ var Patter = class {
33492
34215
  throw err;
33493
34216
  }
33494
34217
  const carrier = this.localConfig.carrier;
33495
- const telephonyProvider = carrier.kind === "twilio" ? "twilio" : "telnyx";
34218
+ const telephonyProvider = carrier.kind;
33496
34219
  const wantsCarrierManagement = opts.manageWebhook !== false || wantsCloudflared;
33497
34220
  if (wantsCarrierManagement) {
33498
34221
  const { autoConfigureCarrier: autoConfigureCarrier2 } = await Promise.resolve().then(() => (init_carrier_config(), carrier_config_exports));
@@ -33502,6 +34225,8 @@ var Patter = class {
33502
34225
  twilioToken: carrier.kind === "twilio" ? carrier.authToken : void 0,
33503
34226
  telnyxKey: carrier.kind === "telnyx" ? carrier.apiKey : void 0,
33504
34227
  telnyxConnectionId: carrier.kind === "telnyx" ? carrier.connectionId : void 0,
34228
+ plivoAuthId: carrier.kind === "plivo" ? carrier.authId : void 0,
34229
+ plivoAuthToken: carrier.kind === "plivo" ? carrier.authToken : void 0,
33505
34230
  phoneNumber: this.localConfig.phoneNumber,
33506
34231
  webhookHost: webhookUrl
33507
34232
  });
@@ -33517,6 +34242,8 @@ var Patter = class {
33517
34242
  telnyxKey: carrier.kind === "telnyx" ? carrier.apiKey : void 0,
33518
34243
  telnyxConnectionId: carrier.kind === "telnyx" ? carrier.connectionId : void 0,
33519
34244
  telnyxPublicKey: carrier.kind === "telnyx" ? carrier.publicKey : void 0,
34245
+ plivoAuthId: carrier.kind === "plivo" ? carrier.authId : void 0,
34246
+ plivoAuthToken: carrier.kind === "plivo" ? carrier.authToken : void 0,
33520
34247
  persistRoot: this.localConfig.persistRoot
33521
34248
  },
33522
34249
  opts.agent,
@@ -33928,7 +34655,15 @@ var Patter = class {
33928
34655
  this.prewarmTtlTimers.set(callId, handle);
33929
34656
  });
33930
34657
  }
33931
- /** Place an outbound call via the configured carrier. */
34658
+ /**
34659
+ * Place an outbound call via the configured carrier.
34660
+ *
34661
+ * With `wait: false` (default) this resolves to `void` the instant the
34662
+ * carrier accepts the dial (fire-and-forget). With `wait: true` it blocks
34663
+ * until the call reaches a terminal state and resolves to a
34664
+ * {@link CallResult} — see {@link LocalCallOptions.wait}. Mirrors Python's
34665
+ * `Patter.call(..., wait=False)`.
34666
+ */
33932
34667
  async call(options) {
33933
34668
  if (!options.to) {
33934
34669
  throw new Error("'to' phone number is required");
@@ -33936,7 +34671,13 @@ var Patter = class {
33936
34671
  if (!options.to.startsWith("+")) {
33937
34672
  throw new Error(`'to' must be in E.164 format (e.g., '+1234567890'). Got: '${options.to}'`);
33938
34673
  }
34674
+ if (options.wait && !this.embeddedServer) {
34675
+ throw new PatterConnectionError(
34676
+ "call({ wait: true }) requires an active server to receive the carrier completion webhooks. Call `await phone.serve(...)` first, or use `await using phone = new Patter(...)` (and serve inside the block) which keeps the server up for the duration of the block."
34677
+ );
34678
+ }
33939
34679
  const { phoneNumber, webhookUrl, carrier } = this.localConfig;
34680
+ let callId = "";
33940
34681
  const effectiveRingTimeout = options.ringTimeout === void 0 ? 25 : options.ringTimeout;
33941
34682
  const wantsAmd = options.machineDetection !== false || Boolean(options.voicemailMessage);
33942
34683
  if (this.embeddedServer) {
@@ -33994,11 +34735,74 @@ var Patter = class {
33994
34735
  }
33995
34736
  }
33996
34737
  if (telnyxCallId) {
34738
+ callId = telnyxCallId;
33997
34739
  this.spawnPrewarmFirstMessage(options.agent, telnyxCallId, effectiveRingTimeout, "telnyx");
33998
34740
  if (options.agent.prewarm !== false) {
33999
34741
  this.parkProviderConnections(options.agent, telnyxCallId);
34000
34742
  }
34001
34743
  }
34744
+ return this.maybeAwaitCompletion(options, callId, effectiveRingTimeout);
34745
+ }
34746
+ if (carrier.kind === "plivo") {
34747
+ const auth2 = `Basic ${Buffer.from(`${carrier.authId}:${carrier.authToken}`).toString("base64")}`;
34748
+ const plivoPayload = {
34749
+ from: phoneNumber,
34750
+ to: options.to,
34751
+ answer_url: `https://${webhookUrl}/webhooks/plivo/voice`,
34752
+ answer_method: "POST",
34753
+ // hangup_url is Plivo's StatusCallback analogue — without it the
34754
+ // /webhooks/plivo/status route never fires for outbound calls and
34755
+ // the dashboard misses no-answer / busy / failed.
34756
+ hangup_url: `https://${webhookUrl}/webhooks/plivo/status`,
34757
+ hangup_method: "POST"
34758
+ };
34759
+ if (effectiveRingTimeout !== null && effectiveRingTimeout !== void 0) {
34760
+ plivoPayload.ring_timeout = Math.max(1, Math.floor(effectiveRingTimeout));
34761
+ }
34762
+ if (wantsAmd) {
34763
+ plivoPayload.machine_detection = "true";
34764
+ plivoPayload.machine_detection_time = 5e3;
34765
+ plivoPayload.machine_detection_url = `https://${webhookUrl}/webhooks/plivo/amd`;
34766
+ plivoPayload.machine_detection_method = "POST";
34767
+ }
34768
+ if (options.voicemailMessage && this.embeddedServer) {
34769
+ this.embeddedServer.voicemailMessage = options.voicemailMessage;
34770
+ }
34771
+ const response2 = await fetch(`https://api.plivo.com/v1/Account/${carrier.authId}/Call/`, {
34772
+ method: "POST",
34773
+ headers: { "Content-Type": "application/json", Authorization: auth2 },
34774
+ body: JSON.stringify(plivoPayload)
34775
+ });
34776
+ if (!response2.ok) {
34777
+ throw new ProvisionError(`Failed to initiate Plivo call: ${await response2.text()}`);
34778
+ }
34779
+ let plivoCallId;
34780
+ try {
34781
+ const body = await response2.clone().json();
34782
+ plivoCallId = body.request_uuid;
34783
+ } catch {
34784
+ }
34785
+ if (plivoCallId) {
34786
+ const initiatedPayload = {
34787
+ call_id: plivoCallId,
34788
+ caller: phoneNumber,
34789
+ callee: options.to,
34790
+ direction: "outbound",
34791
+ status: "initiated"
34792
+ };
34793
+ if (this.embeddedServer) {
34794
+ this.embeddedServer.metricsStore.recordCallInitiated(initiatedPayload);
34795
+ }
34796
+ try {
34797
+ const { notifyDashboard: notifyDashboard2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
34798
+ notifyDashboard2(initiatedPayload);
34799
+ } catch {
34800
+ }
34801
+ this.spawnPrewarmFirstMessage(options.agent, plivoCallId, effectiveRingTimeout, "plivo");
34802
+ if (options.agent.prewarm !== false) {
34803
+ this.parkProviderConnections(options.agent, plivoCallId);
34804
+ }
34805
+ }
34002
34806
  return;
34003
34807
  }
34004
34808
  const twilioSid = carrier.accountSid;
@@ -34070,11 +34874,53 @@ var Patter = class {
34070
34874
  }
34071
34875
  }
34072
34876
  if (twilioCallSid) {
34877
+ callId = twilioCallSid;
34073
34878
  this.spawnPrewarmFirstMessage(options.agent, twilioCallSid, effectiveRingTimeout, "twilio");
34074
34879
  if (options.agent.prewarm !== false) {
34075
34880
  this.parkProviderConnections(options.agent, twilioCallSid);
34076
34881
  }
34077
34882
  }
34883
+ return this.maybeAwaitCompletion(options, callId, effectiveRingTimeout);
34884
+ }
34885
+ /**
34886
+ * When `options.wait` is set, register a completion promise keyed by the
34887
+ * carrier-issued `callId` and await it (bounded by a backstop timeout).
34888
+ * Otherwise resolve to `void` immediately (fire-and-forget).
34889
+ *
34890
+ * The registration happens here — after the carrier accepted the dial and
34891
+ * issued the id — so the future correlates to the right call. The race
34892
+ * window between `initiateCall` returning and this registration is
34893
+ * harmless: the callee is still ringing, so no terminal signal can fire
34894
+ * before we register. Mirrors the Python `call(wait=True)` tail block.
34895
+ */
34896
+ async maybeAwaitCompletion(options, callId, ringTimeout) {
34897
+ if (!options.wait) return;
34898
+ const server = this.embeddedServer;
34899
+ if (!server || !callId) {
34900
+ throw new PatterConnectionError(
34901
+ "call({ wait: true }): no active server or carrier call id."
34902
+ );
34903
+ }
34904
+ const completion = server.registerCompletion(callId);
34905
+ const backstopMs = ((ringTimeout ?? 25) + 1800) * 1e3;
34906
+ let timer;
34907
+ const backstop = new Promise((_resolve, reject) => {
34908
+ timer = setTimeout(() => {
34909
+ server.deleteCompletion(callId);
34910
+ reject(
34911
+ new PatterConnectionError(
34912
+ `call({ wait: true }): no terminal signal for call ${callId} within ${(backstopMs / 1e3).toFixed(0)}s`,
34913
+ { code: ErrorCode.TIMEOUT }
34914
+ )
34915
+ );
34916
+ }, backstopMs);
34917
+ timer.unref?.();
34918
+ });
34919
+ try {
34920
+ return await Promise.race([completion, backstop]);
34921
+ } finally {
34922
+ if (timer) clearTimeout(timer);
34923
+ }
34078
34924
  }
34079
34925
  /**
34080
34926
  * Stop the embedded server and any running tunnel. Safe to call multiple
@@ -34115,6 +34961,11 @@ var Patter = class {
34115
34961
  this.tunnelHandle = null;
34116
34962
  }
34117
34963
  if (this.embeddedServer) {
34964
+ this.embeddedServer.failPendingCompletions(
34965
+ new PatterConnectionError(
34966
+ "Patter.disconnect() called while a call({ wait: true }) was still in flight."
34967
+ )
34968
+ );
34118
34969
  await this.embeddedServer.stop();
34119
34970
  this.embeddedServer = null;
34120
34971
  }
@@ -34138,6 +34989,30 @@ var Patter = class {
34138
34989
  this._ready.catch(() => {
34139
34990
  });
34140
34991
  }
34992
+ /**
34993
+ * Explicit-resource-management disposer so callers can write
34994
+ * ``await using phone = new Patter(...)`` and have {@link disconnect} run
34995
+ * automatically when the block exits — on the normal path AND when the
34996
+ * body throws. This guarantees the embedded server, any auto-started
34997
+ * tunnel, and in-flight prewarm/TTS work are torn down so a still-running
34998
+ * TTS WebSocket cannot keep the user billed after the block ends, and any
34999
+ * in-flight ``call({ wait: true })`` awaiter is failed rather than left
35000
+ * hanging. ``disconnect()`` is idempotent, so an explicit ``disconnect()``
35001
+ * inside the block is still safe. Mirrors Python's ``async with Patter(...)``.
35002
+ *
35003
+ * Note: this does NOT start the server (``serve()`` blocks until shutdown,
35004
+ * so it cannot run from a disposer) — call ``serve(...)`` inside the block:
35005
+ *
35006
+ * ```ts
35007
+ * await using phone = new Patter({ carrier: new Twilio(), phoneNumber: "+1555..." });
35008
+ * await phone.serve({ agent }); // inbound, or
35009
+ * const result = await phone.call({ to: "+1555...", agent, wait: true });
35010
+ * // disconnect() has run here — nothing left running.
35011
+ * ```
35012
+ */
35013
+ async [Symbol.asyncDispose]() {
35014
+ await this.disconnect();
35015
+ }
34141
35016
  /**
34142
35017
  * Terminate an active call on the configured carrier.
34143
35018
  *
@@ -34192,6 +35067,17 @@ var Patter = class {
34192
35067
  }
34193
35068
  return;
34194
35069
  }
35070
+ if (carrier.kind === "plivo") {
35071
+ const auth2 = Buffer.from(`${carrier.authId}:${carrier.authToken}`).toString("base64");
35072
+ const res = await fetch(
35073
+ `https://api.plivo.com/v1/Account/${carrier.authId}/Call/${encodeURIComponent(callSid)}/`,
35074
+ { method: "DELETE", headers: { Authorization: `Basic ${auth2}` } }
35075
+ );
35076
+ if (!res.ok && res.status !== 404) {
35077
+ throw new Error(`Plivo hangup failed: ${res.status} ${await res.text()}`);
35078
+ }
35079
+ return;
35080
+ }
34195
35081
  throw new Error(`endCall() requires a configured carrier; got kind=${carrier.kind}`);
34196
35082
  }
34197
35083
  };
@@ -34612,7 +35498,6 @@ init_cjs_shims();
34612
35498
 
34613
35499
  // src/integrations/patter-tool.ts
34614
35500
  init_cjs_shims();
34615
- var import_node_events = require("events");
34616
35501
  var PARAMETERS_SCHEMA = {
34617
35502
  type: "object",
34618
35503
  properties: {
@@ -34639,7 +35524,7 @@ var PARAMETERS_SCHEMA = {
34639
35524
  };
34640
35525
  var DEFAULT_NAME = "make_phone_call";
34641
35526
  var DEFAULT_DESCRIPTION = "Place a real outbound phone call. Returns a JSON object with the full transcript, call status, duration in seconds, and cost. Use this when the user asks you to call someone, schedule appointments by phone, or otherwise reach a human via voice.";
34642
- var PatterTool = class _PatterTool {
35527
+ var PatterTool = class {
34643
35528
  name;
34644
35529
  description;
34645
35530
  phone;
@@ -34647,24 +35532,6 @@ var PatterTool = class _PatterTool {
34647
35532
  maxDurationSec;
34648
35533
  recording;
34649
35534
  started = false;
34650
- /** Resolver for the next `call_initiated` SSE event. Only set inside the
34651
- * dial mutex (`dialQueue`), so two parallel `execute()` calls never share
34652
- * it and never lose a dispatch. */
34653
- pendingDial = null;
34654
- /** Mutex that serializes the dial → call_id capture critical section.
34655
- * Each `execute()` chains a continuation onto this promise so the
34656
- * `pendingDial` slot is owned by exactly one caller at a time. */
34657
- dialQueue = Promise.resolve();
34658
- /** Captured SSE listener so `stop()` can detach it (prevents leaks when
34659
- * the underlying Patter instance outlives this tool). */
34660
- sseListener = null;
34661
- /** Captured Patter metrics store, for cleanup in `stop()`. */
34662
- metricsStoreRef = null;
34663
- /** call_id → pending promise machinery. */
34664
- pending = /* @__PURE__ */ new Map();
34665
- bus = new import_node_events.EventEmitter();
34666
- /** How long to wait for the `call_initiated` SSE before failing the dial. */
34667
- static DIAL_CAPTURE_TIMEOUT_MS = 1e4;
34668
35535
  constructor(opts) {
34669
35536
  if (!opts.phone) {
34670
35537
  throw new Error("PatterTool: `phone` (a Patter instance) is required.");
@@ -34708,7 +35575,15 @@ var PatterTool = class _PatterTool {
34708
35575
  };
34709
35576
  }
34710
35577
  // --- Lifecycle ----------------------------------------------------------
34711
- /** Start the underlying Patter server. Idempotent. */
35578
+ /**
35579
+ * Start the underlying Patter server. Idempotent.
35580
+ *
35581
+ * `execute()` relies on `Patter.call({ wait: true })`, which requires an
35582
+ * active server to receive the carrier completion webhooks — that's what
35583
+ * `serve()` provides here. No `onCallEnd` callback is wired: the SDK's own
35584
+ * per-callId completion registry resolves the result, so the user's
35585
+ * `onCallEnd` slot is left free.
35586
+ */
34712
35587
  async start() {
34713
35588
  if (this.started) return;
34714
35589
  if (!this.agent) {
@@ -34720,52 +35595,31 @@ var PatterTool = class _PatterTool {
34720
35595
  await this.phone.serve({
34721
35596
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
34722
35597
  agent: builtAgent,
34723
- recording: this.recording,
34724
- onCallEnd: this.onCallEndHandler.bind(this)
35598
+ recording: this.recording
34725
35599
  });
34726
- const store = this.phone.metricsStore;
34727
- if (!store) {
34728
- throw new Error(
34729
- "PatterTool.start: phone.metricsStore is null after serve() \u2014 is the dashboard disabled?"
34730
- );
34731
- }
34732
- const listener = (event) => {
34733
- if (event.type === "call_initiated" && this.pendingDial) {
34734
- const callId = event.data.call_id || "";
34735
- if (callId) {
34736
- const dispatch = this.pendingDial;
34737
- this.pendingDial = null;
34738
- dispatch(callId);
34739
- }
34740
- }
34741
- };
34742
- store.on("sse", listener);
34743
- this.sseListener = listener;
34744
- this.metricsStoreRef = store;
34745
35600
  this.started = true;
34746
35601
  }
34747
- /** Stop the underlying Patter server (and reject any pending calls). */
35602
+ /** Best-effort shutdown — tear the Patter server down via `disconnect()`. */
34748
35603
  async stop() {
34749
35604
  if (!this.started) return;
34750
- if (this.metricsStoreRef && this.sseListener) {
34751
- this.metricsStoreRef.off("sse", this.sseListener);
34752
- }
34753
- this.sseListener = null;
34754
- this.metricsStoreRef = null;
34755
- this.pendingDial = null;
34756
- for (const [, p] of this.pending) {
34757
- clearTimeout(p.timer);
34758
- p.reject(new Error("PatterTool: shutdown while call pending"));
34759
- }
34760
- this.pending.clear();
34761
- const stoppable = this.phone;
34762
- if (typeof stoppable.stop === "function") {
34763
- await stoppable.stop();
35605
+ const disconnectable = this.phone;
35606
+ if (typeof disconnectable.disconnect === "function") {
35607
+ try {
35608
+ await disconnectable.disconnect();
35609
+ } catch {
35610
+ }
34764
35611
  }
34765
35612
  this.started = false;
34766
35613
  }
34767
35614
  // --- Execution ----------------------------------------------------------
34768
- /** Place an outbound call and resolve once it ends with the transcript and metrics. */
35615
+ /**
35616
+ * Dial outbound, wait for the call to end, return a structured result.
35617
+ *
35618
+ * Thin wrapper over `Patter.call({ wait: true })`: the SDK now owns the
35619
+ * dial → callId → terminal-signal correlation, so this just bounds the wait
35620
+ * with `max_duration_sec` and maps the {@link CallResult} into the tool's
35621
+ * public envelope. Mirrors Python's `PatterTool.execute`.
35622
+ */
34769
35623
  async execute(args) {
34770
35624
  if (!this.started) await this.start();
34771
35625
  if (!args || typeof args.to !== "string" || !args.to.startsWith("+")) {
@@ -34781,55 +35635,32 @@ var PatterTool = class _PatterTool {
34781
35635
  ...args.goal !== void 0 ? { systemPrompt: args.goal } : {},
34782
35636
  ...args.first_message !== void 0 ? { firstMessage: args.first_message } : {}
34783
35637
  });
34784
- const callId = await this.acquireCallId(args.to, overrideAgent);
34785
- return new Promise((resolve2, reject) => {
34786
- const timer = setTimeout(() => {
34787
- this.pending.delete(callId);
34788
- reject(new Error(`PatterTool.execute: call ${callId} exceeded ${timeoutSec}s timeout`));
35638
+ let timer;
35639
+ const timeout = new Promise((_resolve, reject) => {
35640
+ timer = setTimeout(() => {
35641
+ reject(
35642
+ new Error(
35643
+ `PatterTool.execute: call to ${args.to} exceeded ${timeoutSec}s timeout`
35644
+ )
35645
+ );
34789
35646
  }, timeoutSec * 1e3);
34790
- this.pending.set(callId, {
34791
- resolve: resolve2,
34792
- reject,
34793
- timer,
34794
- startedAt: Date.now() / 1e3
34795
- });
34796
- });
34797
- }
34798
- /** Issue the outbound dial under the mutex and return its assigned call_id. */
34799
- async acquireCallId(to, agent) {
34800
- let release;
34801
- const slot = new Promise((r) => {
34802
- release = r;
35647
+ timer.unref?.();
34803
35648
  });
34804
- const previous = this.dialQueue;
34805
- this.dialQueue = previous.then(() => slot);
34806
- await previous;
34807
- let captureTimer = null;
35649
+ let result;
34808
35650
  try {
34809
- const callIdPromise = new Promise((resolve2, reject) => {
34810
- this.pendingDial = resolve2;
34811
- captureTimer = setTimeout(() => {
34812
- this.pendingDial = null;
34813
- reject(
34814
- new Error(
34815
- `PatterTool.execute: did not observe call_initiated within ${_PatterTool.DIAL_CAPTURE_TIMEOUT_MS}ms`
34816
- )
34817
- );
34818
- }, _PatterTool.DIAL_CAPTURE_TIMEOUT_MS);
34819
- });
34820
- await this.phone.call({
34821
- to,
34822
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34823
- agent
34824
- });
34825
- const callId = await callIdPromise;
34826
- if (captureTimer) clearTimeout(captureTimer);
34827
- return callId;
35651
+ result = await Promise.race([
35652
+ this.phone.call({
35653
+ to: args.to,
35654
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35655
+ agent: overrideAgent,
35656
+ wait: true
35657
+ }),
35658
+ timeout
35659
+ ]);
34828
35660
  } finally {
34829
- if (captureTimer) clearTimeout(captureTimer);
34830
- this.pendingDial = null;
34831
- release();
35661
+ if (timer) clearTimeout(timer);
34832
35662
  }
35663
+ return resultFromCallResult(result);
34833
35664
  }
34834
35665
  /**
34835
35666
  * Hermes-style handler: `(args, kwargs) => Promise<string>` returning a JSON
@@ -34847,32 +35678,32 @@ var PatterTool = class _PatterTool {
34847
35678
  }
34848
35679
  };
34849
35680
  }
34850
- // --- Internal: onCallEnd dispatcher -------------------------------------
34851
- async onCallEndHandler(data) {
34852
- const callId = data.call_id || "";
34853
- if (!callId) return;
34854
- const pending = this.pending.get(callId);
34855
- if (!pending) {
34856
- this.bus.emit("orphan_end", { call_id: callId, data });
34857
- return;
34858
- }
34859
- clearTimeout(pending.timer);
34860
- this.pending.delete(callId);
34861
- const metrics = data.metrics && typeof data.metrics === "object" ? data.metrics : null;
34862
- const cost = metrics && typeof metrics.cost === "object" && metrics.cost && typeof metrics.cost.total === "number" ? metrics.cost.total : void 0;
34863
- const duration3 = typeof metrics?.duration_seconds === "number" ? metrics?.duration_seconds : Math.max(0, Date.now() / 1e3 - pending.startedAt);
34864
- const transcript = Array.isArray(data.transcript) ? data.transcript : [];
34865
- const status = data.status || "completed";
34866
- pending.resolve({
34867
- call_id: callId,
34868
- status,
34869
- duration_seconds: duration3,
34870
- cost_usd: cost,
34871
- transcript,
34872
- metrics
34873
- });
34874
- }
34875
35681
  };
35682
+ function resultFromCallResult(result) {
35683
+ if (!result) {
35684
+ return {
35685
+ call_id: "",
35686
+ status: "completed",
35687
+ outcome: "",
35688
+ duration_seconds: 0,
35689
+ cost_usd: void 0,
35690
+ transcript: [],
35691
+ metrics: null
35692
+ };
35693
+ }
35694
+ const costTotal = result.cost?.total;
35695
+ const costUsd = typeof costTotal === "number" ? costTotal : void 0;
35696
+ const metrics = result.metrics ? result.metrics : null;
35697
+ return {
35698
+ call_id: result.callId || "",
35699
+ status: result.status || "completed",
35700
+ outcome: result.outcome || "",
35701
+ duration_seconds: typeof result.durationSeconds === "number" ? result.durationSeconds : 0,
35702
+ cost_usd: costUsd,
35703
+ transcript: result.transcript ? [...result.transcript] : [],
35704
+ metrics
35705
+ };
35706
+ }
34876
35707
 
34877
35708
  // src/index.ts
34878
35709
  init_test_mode();
@@ -35451,6 +36282,12 @@ var ELEVENLABS_VOICE_ID_BY_NAME = {
35451
36282
  alloy: "EXAVITQu4vr4xnSDxMaL"
35452
36283
  };
35453
36284
  var VOICE_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
36285
+ var CARRIER_NATIVE_FORMAT = {
36286
+ twilio: "ulaw_8000",
36287
+ telnyx: "pcm_16000",
36288
+ // Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
36289
+ plivo: "ulaw_8000"
36290
+ };
35454
36291
  function resolveVoiceId(voice) {
35455
36292
  if (!voice) return voice;
35456
36293
  if (VOICE_ID_PATTERN.test(voice)) return voice;
@@ -35539,11 +36376,8 @@ var ElevenLabsTTS = class _ElevenLabsTTS {
35539
36376
  */
35540
36377
  setTelephonyCarrier(carrier) {
35541
36378
  if (this._outputFormatExplicit) return;
35542
- if (carrier === "twilio") {
35543
- this._outputFormat = ElevenLabsOutputFormat.ULAW_8000;
35544
- } else if (carrier === "telnyx") {
35545
- this._outputFormat = ElevenLabsOutputFormat.PCM_16000;
35546
- }
36379
+ const native = CARRIER_NATIVE_FORMAT[carrier];
36380
+ if (native !== void 0) this._outputFormat = native;
35547
36381
  }
35548
36382
  /**
35549
36383
  * Construct an instance pre-configured for Twilio Media Streams.
@@ -37776,9 +38610,11 @@ var PLAN_REQUIRED_MSG = "ElevenLabs WS streaming requires a Pro plan or higher (
37776
38610
  function sanitiseLogStr(value, limit = 200) {
37777
38611
  return String(value).replace(/[\r\n\x00]/g, " ").slice(0, limit);
37778
38612
  }
37779
- var CARRIER_NATIVE_FORMAT = {
38613
+ var CARRIER_NATIVE_FORMAT2 = {
37780
38614
  twilio: "ulaw_8000",
37781
- telnyx: "pcm_16000"
38615
+ telnyx: "pcm_16000",
38616
+ // Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
38617
+ plivo: "ulaw_8000"
37782
38618
  };
37783
38619
  var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
37784
38620
  static providerKey = "elevenlabs_ws";
@@ -37862,7 +38698,7 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
37862
38698
  */
37863
38699
  setTelephonyCarrier(carrier) {
37864
38700
  if (this._outputFormatExplicit) return;
37865
- const native = CARRIER_NATIVE_FORMAT[carrier];
38701
+ const native = CARRIER_NATIVE_FORMAT2[carrier];
37866
38702
  if (!native) return;
37867
38703
  this._outputFormat = native;
37868
38704
  }
@@ -40024,7 +40860,7 @@ var KrispVivaFilter = class {
40024
40860
 
40025
40861
  // src/telephony/twilio.ts
40026
40862
  init_cjs_shims();
40027
- var Carrier = class {
40863
+ var Carrier2 = class {
40028
40864
  kind = "twilio";
40029
40865
  accountSid;
40030
40866
  authToken;
@@ -40048,7 +40884,7 @@ var Carrier = class {
40048
40884
 
40049
40885
  // src/telephony/telnyx.ts
40050
40886
  init_cjs_shims();
40051
- var Carrier2 = class {
40887
+ var Carrier3 = class {
40052
40888
  kind = "telnyx";
40053
40889
  apiKey;
40054
40890
  connectionId;
@@ -40074,6 +40910,7 @@ var Carrier2 = class {
40074
40910
  };
40075
40911
 
40076
40912
  // src/index.ts
40913
+ init_plivo();
40077
40914
  init_openai_realtime_2();
40078
40915
 
40079
40916
  // src/public-api.ts
@@ -40132,9 +40969,9 @@ init_tunnel();
40132
40969
 
40133
40970
  // src/chat-context.ts
40134
40971
  init_cjs_shims();
40135
- var import_node_crypto4 = require("crypto");
40972
+ var import_node_crypto5 = require("crypto");
40136
40973
  function generateId() {
40137
- return (0, import_node_crypto4.randomUUID)().replace(/-/g, "").slice(0, 12);
40974
+ return (0, import_node_crypto5.randomUUID)().replace(/-/g, "").slice(0, 12);
40138
40975
  }
40139
40976
  function createMessage(role, content, options) {
40140
40977
  return Object.freeze({
@@ -40914,7 +41751,7 @@ var TwilioAdapter = class _TwilioAdapter {
40914
41751
 
40915
41752
  // src/providers/telnyx-adapter.ts
40916
41753
  init_cjs_shims();
40917
- var import_node_crypto5 = require("crypto");
41754
+ var import_node_crypto6 = require("crypto");
40918
41755
  init_logger();
40919
41756
  var TELNYX_API_BASE2 = "https://api.telnyx.com/v2";
40920
41757
  var TelnyxAdapter = class {
@@ -41018,7 +41855,7 @@ var TelnyxAdapter = class {
41018
41855
  if (!callControlId) throw new Error("TelnyxAdapter: callControlId is required");
41019
41856
  const encoded = encodeURIComponent(callControlId);
41020
41857
  const body = {
41021
- command_id: opts.commandId ?? (0, import_node_crypto5.randomUUID)()
41858
+ command_id: opts.commandId ?? (0, import_node_crypto6.randomUUID)()
41022
41859
  };
41023
41860
  try {
41024
41861
  await this.request(
@@ -41035,6 +41872,9 @@ var TelnyxAdapter = class {
41035
41872
  }
41036
41873
  };
41037
41874
 
41875
+ // src/index.ts
41876
+ init_plivo_adapter();
41877
+
41038
41878
  // src/providers/telnyx-stt.ts
41039
41879
  init_cjs_shims();
41040
41880
  var import_ws12 = __toESM(require("ws"));
@@ -41352,6 +42192,8 @@ init_event_bus();
41352
42192
  PatterTool,
41353
42193
  PcmCarry,
41354
42194
  PipelineHookExecutor,
42195
+ Plivo,
42196
+ PlivoAdapter,
41355
42197
  PricingUnit,
41356
42198
  ProvisionError,
41357
42199
  RateLimitError,