supafone-labs 0.5.4 → 0.6.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.
package/dist/cjs/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * A dependency-free TypeScript client for creating hosted Supafone agents
6
6
  * through the Supafone API, including managed phone numbers, voices, stages,
7
7
  * tools, recordings, transcripts, widgets, and Supafone Supervisor. It also
8
- * includes the Labs cloud sidecar oracle, hosted TTS/STT, live multilingual
8
+ * includes Supafone Supervisor, hosted TTS/STT, live multilingual
9
9
  * transcription, telemetry, agent builder, and objective-driven optimizer.
10
10
  *
11
11
  * Works in Node 18+ (native fetch/WebSocket) and the browser.
@@ -157,6 +157,19 @@ class SupafoneLabsError extends Error {
157
157
  }
158
158
  }
159
159
  exports.SupafoneLabsError = SupafoneLabsError;
160
+ function errorDetailMessage(detail, fallback) {
161
+ if (detail && typeof detail === "object" && !Array.isArray(detail)) {
162
+ const value = detail;
163
+ for (const key of ["message", "detail", "error", "code"]) {
164
+ const candidate = value[key];
165
+ if (typeof candidate === "string" && candidate.trim())
166
+ return candidate.trim();
167
+ }
168
+ }
169
+ if (typeof detail === "string" && detail.trim())
170
+ return detail.trim();
171
+ return fallback;
172
+ }
160
173
  const DEFAULT_BASE = "https://api.labs.supafone.ai";
161
174
  const DEFAULT_SUPAFONE_API_BASE = "https://api.supafone.ai";
162
175
  const COACH_SYSTEM = "You are the coaching core of a supervisor for a live voice agent. Read the " +
@@ -358,8 +371,8 @@ class SupafoneLabs {
358
371
  const text = await res.text();
359
372
  const parsed = text ? safeJson(text) : {};
360
373
  if (!res.ok) {
361
- const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
362
- throw new SupafoneLabsError(`${method} ${path}: ${detail}`, res.status, parsed);
374
+ const detail = parsed?.detail ?? text;
375
+ throw new SupafoneLabsError(`${method} ${path}: ${errorDetailMessage(detail, `HTTP ${res.status}`)}`, res.status, parsed);
363
376
  }
364
377
  return parsed;
365
378
  }
@@ -381,8 +394,8 @@ class SupafoneLabs {
381
394
  const text = await res.text();
382
395
  const parsed = text ? safeJson(text) : {};
383
396
  if (!res.ok) {
384
- const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
385
- throw new SupafoneLabsError(`${method} ${path}: ${detail}`, res.status, parsed);
397
+ const detail = parsed?.detail ?? text;
398
+ throw new SupafoneLabsError(`${method} ${path}: ${errorDetailMessage(detail, `HTTP ${res.status}`)}`, res.status, parsed);
386
399
  }
387
400
  return parsed;
388
401
  }
@@ -408,8 +421,8 @@ class SupafoneLabs {
408
421
  const text = await res.text();
409
422
  const parsed = text ? safeJson(text) : {};
410
423
  if (!res.ok) {
411
- const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
412
- throw new SupafoneLabsError(`POST ${path}: ${detail}`, res.status, parsed);
424
+ const detail = parsed?.detail ?? text;
425
+ throw new SupafoneLabsError(`POST ${path}: ${errorDetailMessage(detail, `HTTP ${res.status}`)}`, res.status, parsed);
413
426
  }
414
427
  return parsed;
415
428
  }
@@ -430,8 +443,8 @@ class SupafoneLabs {
430
443
  if (!res.ok) {
431
444
  const text = await res.text();
432
445
  const parsed = text ? safeJson(text) : {};
433
- const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
434
- throw new SupafoneLabsError(`GET ${path}: ${detail}`, res.status, parsed);
446
+ const detail = parsed?.detail ?? text;
447
+ throw new SupafoneLabsError(`GET ${path}: ${errorDetailMessage(detail, `HTTP ${res.status}`)}`, res.status, parsed);
435
448
  }
436
449
  return {
437
450
  content: await res.arrayBuffer(),
@@ -494,8 +507,8 @@ class SupafoneLabs {
494
507
  const text = await res.text();
495
508
  const parsed = text ? safeJson(text) : {};
496
509
  if (!res.ok) {
497
- const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
498
- throw new SupafoneLabsError(`POST ${path}: ${detail}`, res.status, parsed);
510
+ const detail = parsed?.detail ?? text;
511
+ throw new SupafoneLabsError(`POST ${path}: ${errorDetailMessage(detail, `HTTP ${res.status}`)}`, res.status, parsed);
499
512
  }
500
513
  return parsed;
501
514
  };
@@ -528,8 +541,8 @@ class SupafoneLabs {
528
541
  const text = await res.text();
529
542
  const parsed = text ? safeJson(text) : {};
530
543
  if (!res.ok) {
531
- const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
532
- throw new SupafoneLabsError(`${method} ${path}: ${detail}`, res.status, parsed);
544
+ const detail = parsed?.detail ?? text;
545
+ throw new SupafoneLabsError(`${method} ${path}: ${errorDetailMessage(detail, `HTTP ${res.status}`)}`, res.status, parsed);
533
546
  }
534
547
  return parsed;
535
548
  }
@@ -604,22 +617,26 @@ class SupafoneLabs {
604
617
  }
605
618
  return this.requestAccountApi("POST", "/api/v1/agents/generate-intake", payload);
606
619
  }
607
- /** Raw oracle completion — full control over messages and model. */
608
- async oracle(req) {
609
- return this.request("POST", "/v1/oracle/complete", {
620
+ /** Raw Supervisor completion with either Supafone-managed or BYOK inference. */
621
+ async completeWithSupervisor(req) {
622
+ return this.request("POST", "/v1/supervisor/complete", {
610
623
  messages: req.messages,
611
- model: req.model ?? "supafone-labs-oracle",
624
+ model: req.model ?? "supafone-supervisor",
612
625
  max_tokens: req.maxTokens ?? 256,
613
626
  ...(req.temperature !== undefined ? { temperature: req.temperature } : {}),
614
627
  });
615
628
  }
629
+ /** @deprecated Use completeWithSupervisor(). */
630
+ async oracle(req) {
631
+ return this.completeWithSupervisor(req);
632
+ }
616
633
  /**
617
634
  * The one-liner: hand it the running transcript, get back a silent directive
618
635
  * (empty string when the agent is doing fine).
619
636
  */
620
637
  async whisper(transcript, opts = {}) {
621
638
  const system = opts.guardrails ? `${COACH_SYSTEM}\n\nOperator rules:\n${opts.guardrails}` : COACH_SYSTEM;
622
- const out = await this.oracle({
639
+ const out = await this.completeWithSupervisor({
623
640
  model: opts.model,
624
641
  maxTokens: opts.maxTokens ?? 120,
625
642
  ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
@@ -645,7 +662,7 @@ class SupafoneLabs {
645
662
  directiveContractPrompt(contract),
646
663
  operatorRules.length ? `Operator rules:\n${operatorRules.join("\n")}` : "",
647
664
  ].filter(Boolean).join("\n\n");
648
- const out = await this.oracle({
665
+ const out = await this.completeWithSupervisor({
649
666
  model: opts.model,
650
667
  maxTokens: opts.maxTokens ?? 320,
651
668
  ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
@@ -728,7 +745,7 @@ class SupafoneLabs {
728
745
  balance() {
729
746
  return this.request("GET", "/v1/billing/balance");
730
747
  }
731
- /** Today's usage against your plan caps (oracle/tts/stt/…). */
748
+ /** Today's usage across Supervisor, TTS, STT, and managed runtime meters. */
732
749
  usage() {
733
750
  return this.request("GET", "/v1/usage");
734
751
  }
@@ -795,7 +812,7 @@ class SupafoneLabs {
795
812
  *
796
813
  * With `postCallAnalysis: true` on the client and a transcript (or
797
814
  * messages) present, the call is automatically classified first: the
798
- * oracle labels it against the agent's objective (achieved/missed,
815
+ * Supervisor labels it against the agent's objective (achieved/missed,
799
816
  * per-criterion verdicts, failure reasons) and files the enriched report
800
817
  * server-side. The generated labels come back on `analysis`. Analysis is
801
818
  * best-effort — on any failure the plain zero-billed report still lands.
@@ -828,7 +845,7 @@ class SupafoneLabs {
828
845
  * objective and get labels back — achieved/missed, per-criterion verdicts,
829
846
  * failure reasons, and the blended objective value. Files an enriched call
830
847
  * report server-side (feeding optimizer.improve() and objective stats).
831
- * Billed one oracle call.
848
+ * Billed one Supervisor inference.
832
849
  */
833
850
  classifyCall(input) {
834
851
  return this.request("POST", "/v1/calls/classify", compact({
@@ -840,7 +857,7 @@ class SupafoneLabs {
840
857
  nudges: input.nudges,
841
858
  }));
842
859
  }
843
- /** Available oracle model ids (live vendor catalog). */
860
+ /** Available managed and BYOK Supervisor model ids. */
844
861
  async models() {
845
862
  const d = await this.request("GET", "/v1/models");
846
863
  return d.models.map((m) => (typeof m === "string" ? m : m.id));
@@ -1145,6 +1162,10 @@ class LabsBillingNamespace {
1145
1162
  }
1146
1163
  /** Start hosted Stripe Checkout. MCP callers should render checkout_url as a link. */
1147
1164
  checkout(input = {}) {
1165
+ const sku = input.sku ?? input.packageSku ?? input.package_sku;
1166
+ if (sku) {
1167
+ return this.sm.request("POST", "/v1/billing/checkout", { sku });
1168
+ }
1148
1169
  return this.sm.request("POST", "/v1/billing/checkout", compact({
1149
1170
  kind: input.kind ?? "plan",
1150
1171
  plan_key: input.plan_key ?? input.planKey,
@@ -1155,6 +1176,12 @@ class LabsBillingNamespace {
1155
1176
  cancel_url: input.cancel_url ?? input.cancelUrl,
1156
1177
  }));
1157
1178
  }
1179
+ /** Start Stripe Checkout for one prepaid managed-minute package. */
1180
+ topUp(sku = "sf_voice_minutes_400_v1") {
1181
+ if (!sku.trim())
1182
+ return Promise.reject(new SupafoneLabsError("sku is required"));
1183
+ return this.checkout({ sku });
1184
+ }
1158
1185
  status(checkoutSessionId) {
1159
1186
  if (!checkoutSessionId?.trim())
1160
1187
  throw new SupafoneLabsError("checkoutSessionId is required");
@@ -1269,6 +1296,11 @@ class LabsAgentsNamespace {
1269
1296
  const suffix = q.toString() ? `?${q}` : "";
1270
1297
  return this.sm.requestSupafoneApi("GET", `/api/v1/labs/agents${suffix}`);
1271
1298
  }
1299
+ /** Start a rate-limited authenticated browser preview for this hosted agent. */
1300
+ testCall(agentKey, opts = {}) {
1301
+ const query = opts.agencyId ? `?agency_id=${encodeURIComponent(opts.agencyId)}` : "";
1302
+ return this.sm.requestSupafoneApi("POST", `/api/v1/labs/agents/${encodeURIComponent(agentKey)}/test-call${query}`);
1303
+ }
1272
1304
  /** Fetch one durable agent by key. */
1273
1305
  get(agentKey, opts = {}) {
1274
1306
  const q = new URLSearchParams();
@@ -1644,6 +1676,37 @@ class LabsPhoneNumbersNamespace {
1644
1676
  const suffix = q.toString() ? `?${q}` : "";
1645
1677
  return this.sm.requestSupafoneApi("GET", `/api/v1/labs/phone-numbers${suffix}`);
1646
1678
  }
1679
+ /**
1680
+ * Read the explicitly enrolled shared developer-number pool. This inventory
1681
+ * never infers customer or merely-unassigned numbers into the response.
1682
+ */
1683
+ pool() {
1684
+ return this.sm.requestSupafoneApi("GET", "/api/v1/labs/phone-numbers/pool");
1685
+ }
1686
+ /**
1687
+ * Connect to realtime pool state using a short-lived pool-only token. The
1688
+ * Supafone API key is never placed in the WebSocket URL.
1689
+ */
1690
+ async connectPool(opts = {}) {
1691
+ const snapshot = await this.pool();
1692
+ const stream = snapshot.stream;
1693
+ if (!stream?.url || !stream.token) {
1694
+ throw new SupafoneLabsError("Phone-pool response did not include stream metadata");
1695
+ }
1696
+ const WS = opts.WebSocketImpl ?? globalThis.WebSocket;
1697
+ if (!WS) {
1698
+ throw new SupafoneLabsError("No WebSocket available - pass opts.WebSocketImpl (for example, the ws package)");
1699
+ }
1700
+ const url = new URL(stream.url, `${this.sm.supafoneApiBaseUrl}/`);
1701
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1702
+ url.searchParams.set("token", stream.token);
1703
+ const socket = new WS(url.toString(), stream.protocol || "developer_phone_pool_v1");
1704
+ await new Promise((resolve, reject) => {
1705
+ socket.addEventListener("open", () => resolve(), { once: true });
1706
+ socket.addEventListener("error", () => reject(new SupafoneLabsError("Could not connect to the phone-pool stream")), { once: true });
1707
+ });
1708
+ return socket;
1709
+ }
1647
1710
  /** Search Supafone-managed inventory. This uses Supafone's master telephony account. */
1648
1711
  search(opts = {}) {
1649
1712
  return this.sm.requestSupafoneApi("POST", "/api/v1/labs/phone-numbers/search", phoneNumberSearchPayload(opts));
@@ -1702,21 +1765,31 @@ class LabsPhoneNumbersNamespace {
1702
1765
  * it to the supplied agent. This is the zero-Twilio-account happy path.
1703
1766
  */
1704
1767
  async buyAndAssign(input) {
1768
+ const strategy = input.number_strategy ?? input.numberStrategy ?? (input.premium ? "premium" : "default_pool");
1705
1769
  let phoneNumber = input.phoneNumber ?? input.phone_number ?? "";
1706
1770
  if (!phoneNumber) {
1707
- const found = await this.search({
1708
- ...(input.search ?? {}),
1709
- agencyId: input.agencyId ?? input.agency_id ?? input.search?.agencyId,
1710
- limit: input.search?.limit ?? 1,
1711
- });
1712
- phoneNumber = found.numbers[0]?.phone_number ?? "";
1771
+ if (strategy === "default_pool") {
1772
+ const found = await this.pool();
1773
+ const areaCode = String(input.search?.areaCode ?? input.search?.area_code ?? "");
1774
+ phoneNumber = found.numbers.find((number) => number.available && (!areaCode || number.phone_number.startsWith(`+1${areaCode}`)))?.phone_number ?? "";
1775
+ }
1776
+ else {
1777
+ const found = await this.search({
1778
+ ...(input.search ?? {}),
1779
+ agencyId: input.agencyId ?? input.agency_id ?? input.search?.agencyId,
1780
+ limit: input.search?.limit ?? 1,
1781
+ });
1782
+ phoneNumber = found.numbers[0]?.phone_number ?? "";
1783
+ }
1713
1784
  if (!phoneNumber) {
1714
- throw new SupafoneLabsError("No Supafone-managed phone numbers matched the search");
1785
+ const source = strategy === "default_pool" ? "shared developer" : "Supafone-managed";
1786
+ throw new SupafoneLabsError(`No ${source} phone numbers matched the request`);
1715
1787
  }
1716
1788
  }
1717
1789
  return this.buy({
1718
1790
  ...input,
1719
1791
  phoneNumber,
1792
+ numberStrategy: strategy,
1720
1793
  telephony: input.telephony ?? { mode: "supafone_managed", provider: "supafone" },
1721
1794
  });
1722
1795
  }
@@ -1974,6 +2047,7 @@ function campaignSettingsPayload(settings, mode) {
1974
2047
  }
1975
2048
  function labsAgentPayload(input) {
1976
2049
  const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
2050
+ const supervisor = input.supervisor;
1977
2051
  return compact({
1978
2052
  agency_id: input.agency_id ?? input.agencyId,
1979
2053
  agent_key: input.agent_key ?? input.agentKey,
@@ -1992,6 +2066,7 @@ function labsAgentPayload(input) {
1992
2066
  direction: input.direction,
1993
2067
  preset_key: input.preset_key ?? input.presetKey,
1994
2068
  runtime_mode: input.runtime_mode ?? input.runtimeMode,
2069
+ realtime: input.realtime,
1995
2070
  call_stages: callStagesPayload(input),
1996
2071
  stage_generation: input.stage_generation ?? input.stageGeneration,
1997
2072
  stage_count: input.stage_count ?? input.stageCount,
@@ -2017,7 +2092,12 @@ function labsAgentPayload(input) {
2017
2092
  email: input.email ? emailPayload(input.email) : undefined,
2018
2093
  labs: input.labs ? labsPayload(input.labs) : undefined,
2019
2094
  ultravox: input.ultravox ? ultravoxPayload(input.ultravox) : undefined,
2020
- voice_watcher: input.supervisor ?? input.voice_watcher ?? input.voiceWatcher,
2095
+ supervisor: supervisor && typeof supervisor === "object" ? supervisorPayload(supervisor) : undefined,
2096
+ voice_watcher: typeof supervisor === "boolean"
2097
+ ? supervisor
2098
+ : supervisor && typeof supervisor === "object"
2099
+ ? supervisor.enabled ?? true
2100
+ : input.voice_watcher ?? input.voiceWatcher,
2021
2101
  voice_watcher_model: input.voice_watcher_model ?? input.voiceWatcherModel,
2022
2102
  metadata: labsAgentMetadataPayload(input),
2023
2103
  });
@@ -2144,8 +2224,13 @@ function callStagesPayload(input) {
2144
2224
  return explicit.map(callStagePayload);
2145
2225
  if (explicit === false || auto === false)
2146
2226
  return false;
2147
- if (explicit === "oracle" || explicit === "template" || explicit === "off")
2227
+ if (explicit === "managed" || explicit === "template" || explicit === "off")
2148
2228
  return explicit;
2229
+ if (explicit === "oracle")
2230
+ return "managed";
2231
+ // The S2S backend owns its validated intake → booking → confirmation flow.
2232
+ if (input.realtime)
2233
+ return undefined;
2149
2234
  // Omitted means the private Supafone API generates and compiles the plan.
2150
2235
  return undefined;
2151
2236
  }
@@ -2312,6 +2397,10 @@ function providerKeysPayload(input) {
2312
2397
  deepgram_api_key: input.deepgram_api_key ?? input.deepgramApiKey,
2313
2398
  anthropic: input.anthropic,
2314
2399
  anthropic_api_key: input.anthropic_api_key ?? input.anthropicApiKey,
2400
+ google: input.google,
2401
+ google_api_key: input.google_api_key ?? input.googleApiKey,
2402
+ gemini: input.gemini,
2403
+ gemini_api_key: input.gemini_api_key ?? input.geminiApiKey,
2315
2404
  openai: input.openai,
2316
2405
  openai_api_key: input.openai_api_key ?? input.openaiApiKey,
2317
2406
  xai: input.xai,
@@ -2427,9 +2516,19 @@ function artifactsPayload(input) {
2427
2516
  });
2428
2517
  }
2429
2518
  function labsPayload(input) {
2519
+ const nestedSupervisor = input.supervisor && typeof input.supervisor === "object"
2520
+ ? input.supervisor
2521
+ : input.provider
2522
+ ? input
2523
+ : undefined;
2430
2524
  return compact({
2431
2525
  enabled: input.enabled,
2432
- voice_watcher: input.supervisor ?? input.voice_watcher ?? input.voiceWatcher,
2526
+ supervisor: nestedSupervisor ? supervisorPayload(nestedSupervisor) : undefined,
2527
+ voice_watcher: typeof input.supervisor === "boolean"
2528
+ ? input.supervisor
2529
+ : nestedSupervisor
2530
+ ? nestedSupervisor.enabled ?? true
2531
+ : input.voice_watcher ?? input.voiceWatcher,
2433
2532
  api_key: input.api_key ?? input.apiKey,
2434
2533
  model: input.model,
2435
2534
  mode: input.mode,
@@ -2441,6 +2540,16 @@ function labsPayload(input) {
2441
2540
  label: input.label,
2442
2541
  });
2443
2542
  }
2543
+ function supervisorPayload(input) {
2544
+ return compact({
2545
+ enabled: input.enabled,
2546
+ mode: input.mode,
2547
+ provider: input.provider,
2548
+ model: input.model,
2549
+ api_key: input.api_key ?? input.apiKey,
2550
+ api_key_configured: input.api_key_configured ?? input.apiKeyConfigured,
2551
+ });
2552
+ }
2444
2553
  function ultravoxPayload(input) {
2445
2554
  return compact({
2446
2555
  model: input.model,