supafone-labs 0.3.2 → 0.4.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/README.md CHANGED
@@ -297,6 +297,43 @@ const reports = await supafone.optimizer.reports("builder");
297
297
  All errors throw `SupafoneLabsError` (with `.status` and `.body`); catch it to
298
298
  inspect gateway responses.
299
299
 
300
+ ## Campaigns & real calls (account-scoped)
301
+
302
+ The outbound campaign engine behind app.supafone.ai, packaged. Authenticate
303
+ with your account (not an API key) — pass `accountToken`, or
304
+ `accountEmail` + `accountPassword` and the client logs in lazily (and
305
+ re-logs-in transparently when the token expires):
306
+
307
+ ```ts
308
+ const sf = new Supafone({ accountEmail: "you@company.com", accountPassword: "..." });
309
+
310
+ const { agents } = await sf.listVoiceAgents();
311
+ const { campaign } = await sf.campaigns.create({ name: "Q3 win-back", goal: "reengage", agentId: agents[0].id });
312
+ await sf.campaigns.applyPreset(campaign.id, "win_back"); // or your custom_… preset
313
+ await sf.campaigns.addRecipients(campaign.id, [
314
+ { name: "Jane Doe", phone: "+15551234567", outreach_consent: "yes" },
315
+ ]);
316
+ await sf.campaigns.launch(campaign.id); // real calls + emails begin
317
+
318
+ const live = await sf.campaigns.live(campaign.id); // in-flight calls + listen links
319
+ await sf.placeCall({ agentId: agents[0].id, toNumber: "+15551234567" }); // ring a phone right now
320
+ ```
321
+
322
+ `campaigns.live()` returns a portal link (`app.supafone.ai/app/developer`) and
323
+ a listen link per in-flight call; poll `campaigns.getCall(id)` to follow the
324
+ live transcript while a call is in progress.
325
+
326
+ ## Prefer natural language? Use the MCP server
327
+
328
+ The repo ships an MCP stdio server (`services/supafone-labs/mcp/supafone_mcp.py`)
329
+ exposing this same surface — plus hosted-agent provisioning — as tools for
330
+ Claude Desktop / Claude Code. Configure it with `SUPAFONE_EMAIL` +
331
+ `SUPAFONE_PASSWORD` (campaigns/calls) and/or `SUPAFONE_API_KEY` (hosted
332
+ agents), then just ask: *"create a win-back campaign, add these leads, launch
333
+ it, and show me the calls as they happen"* — Claude replies with developer-
334
+ portal links to watch the calls live. Full tool reference lives in the repo's
335
+ `gitbook/mcp-server.md`.
336
+
300
337
  ## Module format
301
338
 
302
339
  Ships **both ESM and CommonJS**. `import { Supafone } from "supafone-labs"`
@@ -21,7 +21,7 @@
21
21
  */
22
22
  export interface SupafoneLabsOptions {
23
23
  /** Your key from https://labs.supafone.ai/get-key.html */
24
- apiKey: string;
24
+ apiKey?: string;
25
25
  /** Override the gateway (default: the hosted cloud). */
26
26
  baseUrl?: string;
27
27
  /** Supafone app/API key for hosted agent provisioning. Defaults to apiKey. */
@@ -32,6 +32,17 @@ export interface SupafoneLabsOptions {
32
32
  timeoutMs?: number;
33
33
  /** Optional pre-obtained session token (else use login()). */
34
34
  sessionToken?: string;
35
+ /**
36
+ * Account (app.supafone.ai) auth — powers campaigns + real calls. Pass a
37
+ * JWT directly, or accountEmail + accountPassword and the client logs in
38
+ * lazily (and re-logs-in once when the token expires). With account auth
39
+ * present, apiKey becomes optional.
40
+ */
41
+ accountToken?: string;
42
+ accountEmail?: string;
43
+ accountPassword?: string;
44
+ /** Portal base for listen/monitor links (default https://app.supafone.ai). */
45
+ appUrl?: string;
35
46
  }
36
47
  export interface ChatMessage {
37
48
  role: "system" | "user" | "assistant";
@@ -807,14 +818,20 @@ export declare class SupafoneLabsError extends Error {
807
818
  export declare class SupafoneLabs {
808
819
  readonly baseUrl: string;
809
820
  readonly supafoneApiBaseUrl: string;
821
+ readonly appUrl: string;
810
822
  private readonly apiKey;
811
823
  private readonly supafoneApiKey;
812
824
  private readonly timeoutMs;
813
825
  private sessionToken?;
826
+ private accountToken?;
827
+ private accountSessionToken?;
828
+ private readonly accountEmail?;
829
+ private readonly accountPassword?;
814
830
  readonly labs: LabsNamespace;
815
831
  readonly builder: BuilderNamespace;
816
832
  readonly qa: QANamespace;
817
833
  readonly optimizer: OptimizerNamespace;
834
+ readonly campaigns: CampaignsNamespace;
818
835
  constructor(opts: SupafoneLabsOptions);
819
836
  /** True once login() (or a passed sessionToken) is in effect. */
820
837
  get isLoggedIn(): boolean;
@@ -827,6 +844,32 @@ export declare class SupafoneLabs {
827
844
  request<T>(method: string, path: string, body?: unknown, useSession?: boolean): Promise<T>;
828
845
  /** @internal Authenticated JSON request to the Supafone app API (`/api/v1/labs/*`). */
829
846
  requestSupafoneApi<T>(method: string, path: string, body?: unknown): Promise<T>;
847
+ /**
848
+ * Exchange the account email/password for a product-API JWT (the same login
849
+ * as app.supafone.ai). Called lazily by campaigns/calls — call directly only
850
+ * to fail fast.
851
+ */
852
+ accountLogin(email?: string, password?: string): Promise<string>;
853
+ /**
854
+ * @internal JSON request to the Supafone product API with the ACCOUNT JWT
855
+ * (campaigns + real calls). A minted token that expires gets one transparent
856
+ * re-login; an explicit accountToken is the caller's to refresh.
857
+ */
858
+ requestAccountApi<T>(method: string, path: string, body?: unknown): Promise<T>;
859
+ /** @internal Raw product-API request with an explicit bearer ("" = none). */
860
+ private accountHttp;
861
+ /**
862
+ * PLACE A REAL OUTBOUND PHONE CALL: dials toNumber from the account's
863
+ * calling provider and bridges the voice agent onto the line.
864
+ */
865
+ placeCall(opts: {
866
+ agentId: string;
867
+ toNumber: string;
868
+ }): Promise<PlaceCallResult>;
869
+ /** The account's voice agents — pick an agent id for campaigns/calls. */
870
+ listVoiceAgents(): Promise<{
871
+ agents: Record<string, unknown>[];
872
+ }>;
830
873
  /** Raw oracle completion — full control over messages and model. */
831
874
  oracle(req: OracleRequest): Promise<OracleResult>;
832
875
  /**
@@ -905,6 +948,139 @@ declare class LiveTranscription {
905
948
  get socket(): WebSocket;
906
949
  }
907
950
  /** Programmatic hosted Supafone agents, inside the Supafone API. */
951
+ export interface PlaceCallResult {
952
+ success: boolean;
953
+ simulated?: boolean;
954
+ call_sid?: string | null;
955
+ provider?: string;
956
+ }
957
+ export interface CampaignRecipientInput {
958
+ name?: string;
959
+ phone?: string;
960
+ email?: string;
961
+ /** Warm-outreach consent — required before any voice/email touch. */
962
+ outreach_consent?: string;
963
+ [field: string]: unknown;
964
+ }
965
+ export interface CampaignSummary {
966
+ id: string;
967
+ name: string;
968
+ goal: string;
969
+ status: string;
970
+ agent_id?: string | null;
971
+ stats?: Record<string, unknown>;
972
+ settings?: Record<string, unknown>;
973
+ [field: string]: unknown;
974
+ }
975
+ export interface CampaignLiveCall {
976
+ id: string;
977
+ status: string;
978
+ /** Portal deep link to watch this call (live transcript while in flight). */
979
+ listen_url: string;
980
+ [field: string]: unknown;
981
+ }
982
+ export interface CampaignLiveView {
983
+ campaign_id: string;
984
+ in_flight: CampaignLiveCall[];
985
+ /** Developer-portal link showing this campaign's agents/calls live. */
986
+ portal_url: string;
987
+ stats?: Record<string, unknown> | null;
988
+ }
989
+ export interface CampaignUpdateInput {
990
+ name?: string;
991
+ goal?: string;
992
+ agentId?: string;
993
+ emailSubject?: string;
994
+ emailBody?: string;
995
+ cadence?: {
996
+ channel: "voice" | "email";
997
+ delay_hours: number;
998
+ }[];
999
+ settings?: Record<string, unknown>;
1000
+ }
1001
+ /**
1002
+ * Typical flow:
1003
+ * ```ts
1004
+ * const sf = new Supafone({ accountEmail, accountPassword });
1005
+ * const { agents } = await sf.listVoiceAgents();
1006
+ * const { campaign } = await sf.campaigns.create({ name: "Q3 win-back", goal: "reengage", agentId: agents[0].id });
1007
+ * await sf.campaigns.applyPreset(campaign.id, "win_back");
1008
+ * await sf.campaigns.addRecipients(campaign.id, [{ name: "Jane", phone: "+15551234567", outreach_consent: "yes" }]);
1009
+ * await sf.campaigns.launch(campaign.id);
1010
+ * const live = await sf.campaigns.live(campaign.id); // in-flight calls + portal links
1011
+ * ```
1012
+ */
1013
+ declare class CampaignsNamespace {
1014
+ private sm;
1015
+ constructor(sm: SupafoneLabs);
1016
+ list(opts?: {
1017
+ accountId?: string;
1018
+ }): Promise<{
1019
+ campaigns: CampaignSummary[];
1020
+ }>;
1021
+ create(opts?: {
1022
+ name?: string;
1023
+ goal?: string;
1024
+ agentId?: string;
1025
+ accountId?: string;
1026
+ }): Promise<{
1027
+ campaign: CampaignSummary;
1028
+ }>;
1029
+ get(campaignId: string): Promise<{
1030
+ campaign: CampaignSummary;
1031
+ }>;
1032
+ update(campaignId: string, input: CampaignUpdateInput): Promise<{
1033
+ campaign: CampaignSummary;
1034
+ }>;
1035
+ /** Add consented leads: [{name, phone, email, outreach_consent: "yes"}]. */
1036
+ addRecipients(campaignId: string, recipients: CampaignRecipientInput[]): Promise<{
1037
+ added: number;
1038
+ stats: Record<string, unknown>;
1039
+ }>;
1040
+ recipients(campaignId: string): Promise<{
1041
+ recipients: Record<string, unknown>[];
1042
+ }>;
1043
+ /** Starts REAL calls/emails on the cadence immediately. */
1044
+ launch(campaignId: string): Promise<{
1045
+ campaign: CampaignSummary;
1046
+ }>;
1047
+ pause(campaignId: string): Promise<{
1048
+ campaign: CampaignSummary;
1049
+ }>;
1050
+ /** Built-in playbooks + the account's saved custom presets. */
1051
+ presets(): Promise<{
1052
+ built_in: Record<string, unknown>[];
1053
+ custom: Record<string, unknown>[];
1054
+ }>;
1055
+ /** Materialize a preset (goal, questions, scripts, signing doc) in one write. */
1056
+ applyPreset(campaignId: string, presetId: string): Promise<{
1057
+ campaign: CampaignSummary;
1058
+ }>;
1059
+ stats(campaignId: string): Promise<{
1060
+ stats: Record<string, unknown>;
1061
+ }>;
1062
+ /** The live funnel + the campaign's most recent calls (newest first). */
1063
+ activity(campaignId: string): Promise<{
1064
+ stats?: Record<string, unknown>;
1065
+ calls?: Record<string, unknown>[];
1066
+ }>;
1067
+ /**
1068
+ * In-flight calls right now, each with a portal link to watch/listen. Poll
1069
+ * getCall(callId) (or open the link) for the transcript as it grows.
1070
+ */
1071
+ live(campaignId: string): Promise<CampaignLiveView>;
1072
+ /** One call — while in_progress the transcript grows on each poll. */
1073
+ getCall(callId: string): Promise<{
1074
+ call: Record<string, unknown>;
1075
+ }>;
1076
+ /** Mint a recipient's tracked tap-to-sign link (inherits the campaign's signing PDF). */
1077
+ createSignLink(campaignId: string, recipientId: string, opts?: {
1078
+ title?: string;
1079
+ message?: string;
1080
+ }): Promise<{
1081
+ link: Record<string, unknown>;
1082
+ }>;
1083
+ }
908
1084
  declare class LabsNamespace {
909
1085
  private sm;
910
1086
  readonly agents: LabsAgentsNamespace;
@@ -1075,15 +1251,6 @@ declare class OptimizerNamespace {
1075
1251
  version: number;
1076
1252
  text: string;
1077
1253
  }>;
1078
- /** SSR grade distribution: five nominal levels folded into a real score distribution. */
1079
- distribution(agent?: string, limit?: number): Promise<{
1080
- agent: string;
1081
- calls: number;
1082
- counts: Record<string, number>;
1083
- mean_score: number;
1084
- buckets: number[];
1085
- bucket_edges: number[];
1086
- }>;
1087
1254
  /** List the post-call reports behind the optimizer. */
1088
1255
  reports(agent?: string, limit?: number): Promise<{
1089
1256
  reports: unknown[];
package/dist/cjs/index.js CHANGED
@@ -43,27 +43,40 @@ const COACH_SYSTEM = "You are the coaching core of a second mind for a live voic
43
43
  class SupafoneLabs {
44
44
  baseUrl;
45
45
  supafoneApiBaseUrl;
46
+ appUrl;
46
47
  apiKey;
47
48
  supafoneApiKey;
48
49
  timeoutMs;
49
50
  sessionToken;
51
+ accountToken;
52
+ accountSessionToken;
53
+ accountEmail;
54
+ accountPassword;
50
55
  labs;
51
56
  builder;
52
57
  qa;
53
58
  optimizer;
59
+ campaigns;
54
60
  constructor(opts) {
55
- if (!opts?.apiKey)
56
- throw new SupafoneLabsError("apiKey is required");
57
- this.apiKey = opts.apiKey;
61
+ const hasAccountAuth = !!(opts?.accountToken || (opts?.accountEmail && opts?.accountPassword));
62
+ if (!opts?.apiKey && !hasAccountAuth) {
63
+ throw new SupafoneLabsError("apiKey is required — or, for campaigns/calls, pass accountToken or accountEmail + accountPassword");
64
+ }
65
+ this.apiKey = opts.apiKey ?? "";
58
66
  this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
59
- this.supafoneApiKey = opts.supafoneApiKey ?? opts.apiKey;
67
+ this.supafoneApiKey = opts.supafoneApiKey ?? this.apiKey;
60
68
  this.supafoneApiBaseUrl = (opts.supafoneApiBaseUrl ?? DEFAULT_SUPAFONE_API_BASE).replace(/\/$/, "");
69
+ this.appUrl = (opts.appUrl ?? "https://app.supafone.ai").replace(/\/$/, "");
61
70
  this.timeoutMs = opts.timeoutMs ?? 30_000;
62
71
  this.sessionToken = opts.sessionToken;
72
+ this.accountToken = opts.accountToken;
73
+ this.accountEmail = opts.accountEmail;
74
+ this.accountPassword = opts.accountPassword;
63
75
  this.labs = new LabsNamespace(this);
64
76
  this.builder = new BuilderNamespace(this);
65
77
  this.qa = new QANamespace(this);
66
78
  this.optimizer = new OptimizerNamespace(this);
79
+ this.campaigns = new CampaignsNamespace(this);
67
80
  }
68
81
  /** True once login() (or a passed sessionToken) is in effect. */
69
82
  get isLoggedIn() {
@@ -126,6 +139,87 @@ class SupafoneLabs {
126
139
  clearTimeout(timer);
127
140
  }
128
141
  }
142
+ /**
143
+ * Exchange the account email/password for a product-API JWT (the same login
144
+ * as app.supafone.ai). Called lazily by campaigns/calls — call directly only
145
+ * to fail fast.
146
+ */
147
+ async accountLogin(email, password) {
148
+ const useEmail = email ?? this.accountEmail;
149
+ const usePassword = password ?? this.accountPassword;
150
+ if (!useEmail || !usePassword) {
151
+ throw new SupafoneLabsError("Not authenticated: pass accountToken, or accountEmail + accountPassword");
152
+ }
153
+ const body = await this.accountHttp("POST", "/api/v1/auth/login", { email: useEmail, password: usePassword }, "");
154
+ const token = body.access_token || body.token;
155
+ if (!token)
156
+ throw new SupafoneLabsError("Login succeeded but returned no token");
157
+ this.accountSessionToken = token;
158
+ return token;
159
+ }
160
+ /**
161
+ * @internal JSON request to the Supafone product API with the ACCOUNT JWT
162
+ * (campaigns + real calls). A minted token that expires gets one transparent
163
+ * re-login; an explicit accountToken is the caller's to refresh.
164
+ */
165
+ async requestAccountApi(method, path, body) {
166
+ const token = this.accountToken || this.accountSessionToken || (await this.accountLogin());
167
+ try {
168
+ return await this.accountHttp(method, path, body, token);
169
+ }
170
+ catch (err) {
171
+ const expired = err instanceof SupafoneLabsError && err.status === 401;
172
+ if (expired && !this.accountToken && this.accountEmail && this.accountPassword) {
173
+ this.accountSessionToken = undefined;
174
+ return this.accountHttp(method, path, body, await this.accountLogin());
175
+ }
176
+ throw err;
177
+ }
178
+ }
179
+ /** @internal Raw product-API request with an explicit bearer ("" = none). */
180
+ async accountHttp(method, path, body, token) {
181
+ const ctrl = new AbortController();
182
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
183
+ try {
184
+ const headers = { "Content-Type": "application/json" };
185
+ if (token)
186
+ headers.Authorization = `Bearer ${token}`;
187
+ const res = await fetch(this.supafoneApiBaseUrl + path, {
188
+ method,
189
+ signal: ctrl.signal,
190
+ headers,
191
+ body: body === undefined ? undefined : JSON.stringify(body),
192
+ });
193
+ const text = await res.text();
194
+ const parsed = text ? safeJson(text) : {};
195
+ if (!res.ok) {
196
+ const detail = parsed?.detail ?? text ?? `HTTP ${res.status}`;
197
+ throw new SupafoneLabsError(`${method} ${path}: ${detail}`, res.status, parsed);
198
+ }
199
+ return parsed;
200
+ }
201
+ finally {
202
+ clearTimeout(timer);
203
+ }
204
+ }
205
+ /**
206
+ * PLACE A REAL OUTBOUND PHONE CALL: dials toNumber from the account's
207
+ * calling provider and bridges the voice agent onto the line.
208
+ */
209
+ async placeCall(opts) {
210
+ if (!opts?.agentId)
211
+ throw new SupafoneLabsError("agentId is required (see listVoiceAgents())");
212
+ if (!opts?.toNumber)
213
+ throw new SupafoneLabsError("toNumber is required (E.164, e.g. +15551234567)");
214
+ return this.requestAccountApi("POST", "/api/v1/phone/test-call", {
215
+ agent_id: opts.agentId,
216
+ to_number: opts.toNumber,
217
+ });
218
+ }
219
+ /** The account's voice agents — pick an agent id for campaigns/calls. */
220
+ async listVoiceAgents() {
221
+ return this.requestAccountApi("GET", "/api/v1/agents");
222
+ }
129
223
  /** Raw oracle completion — full control over messages and model. */
130
224
  async oracle(req) {
131
225
  return this.request("POST", "/v1/oracle/complete", {
@@ -341,7 +435,131 @@ class LiveTranscription {
341
435
  return this.ws;
342
436
  }
343
437
  }
344
- /** Programmatic hosted Supafone agents, inside the Supafone API. */
438
+ /**
439
+ * Typical flow:
440
+ * ```ts
441
+ * const sf = new Supafone({ accountEmail, accountPassword });
442
+ * const { agents } = await sf.listVoiceAgents();
443
+ * const { campaign } = await sf.campaigns.create({ name: "Q3 win-back", goal: "reengage", agentId: agents[0].id });
444
+ * await sf.campaigns.applyPreset(campaign.id, "win_back");
445
+ * await sf.campaigns.addRecipients(campaign.id, [{ name: "Jane", phone: "+15551234567", outreach_consent: "yes" }]);
446
+ * await sf.campaigns.launch(campaign.id);
447
+ * const live = await sf.campaigns.live(campaign.id); // in-flight calls + portal links
448
+ * ```
449
+ */
450
+ class CampaignsNamespace {
451
+ sm;
452
+ constructor(sm) {
453
+ this.sm = sm;
454
+ }
455
+ list(opts = {}) {
456
+ const query = opts.accountId ? `?${new URLSearchParams({ account_id: opts.accountId })}` : "";
457
+ return this.sm.requestAccountApi("GET", `/api/v1/campaigns${query}`);
458
+ }
459
+ create(opts = {}) {
460
+ return this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
461
+ name: opts.name ?? "New campaign",
462
+ goal: opts.goal ?? "book",
463
+ agent_id: opts.agentId,
464
+ account_id: opts.accountId,
465
+ }));
466
+ }
467
+ get(campaignId) {
468
+ return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`);
469
+ }
470
+ update(campaignId, input) {
471
+ const payload = compact({
472
+ name: input.name,
473
+ goal: input.goal,
474
+ agent_id: input.agentId,
475
+ email_subject: input.emailSubject,
476
+ email_body: input.emailBody,
477
+ cadence: input.cadence,
478
+ settings: input.settings,
479
+ });
480
+ if (!Object.keys(payload).length) {
481
+ throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, or settings");
482
+ }
483
+ return this.sm.requestAccountApi("PUT", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`, payload);
484
+ }
485
+ /** Add consented leads: [{name, phone, email, outreach_consent: "yes"}]. */
486
+ addRecipients(campaignId, recipients) {
487
+ if (!Array.isArray(recipients) || !recipients.length) {
488
+ throw new SupafoneLabsError("recipients must be a non-empty array of lead rows");
489
+ }
490
+ return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients`, { recipients });
491
+ }
492
+ recipients(campaignId) {
493
+ return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients`);
494
+ }
495
+ /** Starts REAL calls/emails on the cadence immediately. */
496
+ launch(campaignId) {
497
+ return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/launch`, {});
498
+ }
499
+ pause(campaignId) {
500
+ return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/pause`, {});
501
+ }
502
+ /** Built-in playbooks + the account's saved custom presets. */
503
+ async presets() {
504
+ const builtIn = await this.sm.requestAccountApi("GET", "/api/v1/campaigns/outbound-presets");
505
+ let custom = [];
506
+ try {
507
+ const mine = await this.sm.requestAccountApi("GET", "/api/v1/campaigns/custom-presets");
508
+ custom = mine.presets ?? [];
509
+ }
510
+ catch {
511
+ /* custom presets need account scope — built-ins still return */
512
+ }
513
+ return { built_in: builtIn.presets ?? [], custom };
514
+ }
515
+ /** Materialize a preset (goal, questions, scripts, signing doc) in one write. */
516
+ applyPreset(campaignId, presetId) {
517
+ return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/apply-preset`, {
518
+ preset_id: presetId,
519
+ });
520
+ }
521
+ stats(campaignId) {
522
+ return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/stats`);
523
+ }
524
+ /** The live funnel + the campaign's most recent calls (newest first). */
525
+ activity(campaignId) {
526
+ return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/activity`);
527
+ }
528
+ /**
529
+ * In-flight calls right now, each with a portal link to watch/listen. Poll
530
+ * getCall(callId) (or open the link) for the transcript as it grows.
531
+ */
532
+ async live(campaignId) {
533
+ const activity = await this.activity(campaignId);
534
+ const inFlight = [];
535
+ for (const call of activity.calls ?? []) {
536
+ const status = String(call.status ?? "");
537
+ if (status === "initiated" || status === "dialing" || status === "in_progress") {
538
+ const id = String(call.id ?? "");
539
+ inFlight.push({
540
+ ...call,
541
+ id,
542
+ status,
543
+ listen_url: `${this.sm.appUrl}/app/calls?call=${encodeURIComponent(id)}`,
544
+ });
545
+ }
546
+ }
547
+ return {
548
+ campaign_id: campaignId,
549
+ in_flight: inFlight,
550
+ portal_url: `${this.sm.appUrl}/app/developer?campaign=${encodeURIComponent(campaignId)}`,
551
+ stats: activity.stats ?? null,
552
+ };
553
+ }
554
+ /** One call — while in_progress the transcript grows on each poll. */
555
+ getCall(callId) {
556
+ return this.sm.requestAccountApi("GET", `/api/v1/calls/${encodeURIComponent(callId)}`);
557
+ }
558
+ /** Mint a recipient's tracked tap-to-sign link (inherits the campaign's signing PDF). */
559
+ createSignLink(campaignId, recipientId, opts = {}) {
560
+ return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients/${encodeURIComponent(recipientId)}/sign-link`, compact({ title: opts.title, message: opts.message }));
561
+ }
562
+ }
345
563
  class LabsNamespace {
346
564
  sm;
347
565
  agents;
@@ -726,10 +944,6 @@ class OptimizerNamespace {
726
944
  standing(agent = "builder") {
727
945
  return this.sm.request("GET", `/v1/optimizer/standing?agent=${encodeURIComponent(agent)}`);
728
946
  }
729
- /** SSR grade distribution: five nominal levels folded into a real score distribution. */
730
- distribution(agent = "builder", limit = 500) {
731
- return this.sm.request("GET", `/v1/objective/distribution?agent=${encodeURIComponent(agent)}&limit=${limit}`);
732
- }
733
947
  /** List the post-call reports behind the optimizer. */
734
948
  reports(agent = "builder", limit = 40) {
735
949
  return this.sm.request("GET", `/v1/optimizer/reports?agent=${encodeURIComponent(agent)}&limit=${limit}`);