supafone-labs 0.3.2 → 0.4.0
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.d.ts +177 -10
- package/dist/cjs/index.js +223 -9
- package/dist/index.d.ts +177 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +223 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +328 -14
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
export interface SupafoneLabsOptions {
|
|
24
24
|
/** Your key from https://labs.supafone.ai/get-key.html */
|
|
25
|
-
apiKey
|
|
25
|
+
apiKey?: string;
|
|
26
26
|
/** Override the gateway (default: the hosted cloud). */
|
|
27
27
|
baseUrl?: string;
|
|
28
28
|
/** Supafone app/API key for hosted agent provisioning. Defaults to apiKey. */
|
|
@@ -33,6 +33,17 @@ export interface SupafoneLabsOptions {
|
|
|
33
33
|
timeoutMs?: number;
|
|
34
34
|
/** Optional pre-obtained session token (else use login()). */
|
|
35
35
|
sessionToken?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Account (app.supafone.ai) auth — powers campaigns + real calls. Pass a
|
|
38
|
+
* JWT directly, or accountEmail + accountPassword and the client logs in
|
|
39
|
+
* lazily (and re-logs-in once when the token expires). With account auth
|
|
40
|
+
* present, apiKey becomes optional.
|
|
41
|
+
*/
|
|
42
|
+
accountToken?: string;
|
|
43
|
+
accountEmail?: string;
|
|
44
|
+
accountPassword?: string;
|
|
45
|
+
/** Portal base for listen/monitor links (default https://app.supafone.ai). */
|
|
46
|
+
appUrl?: string;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
export interface ChatMessage {
|
|
@@ -881,28 +892,44 @@ const COACH_SYSTEM =
|
|
|
881
892
|
export class SupafoneLabs {
|
|
882
893
|
readonly baseUrl: string;
|
|
883
894
|
readonly supafoneApiBaseUrl: string;
|
|
895
|
+
readonly appUrl: string;
|
|
884
896
|
private readonly apiKey: string;
|
|
885
897
|
private readonly supafoneApiKey: string;
|
|
886
898
|
private readonly timeoutMs: number;
|
|
887
899
|
private sessionToken?: string;
|
|
900
|
+
private accountToken?: string;
|
|
901
|
+
private accountSessionToken?: string;
|
|
902
|
+
private readonly accountEmail?: string;
|
|
903
|
+
private readonly accountPassword?: string;
|
|
888
904
|
|
|
889
905
|
readonly labs: LabsNamespace;
|
|
890
906
|
readonly builder: BuilderNamespace;
|
|
891
907
|
readonly qa: QANamespace;
|
|
892
908
|
readonly optimizer: OptimizerNamespace;
|
|
909
|
+
readonly campaigns: CampaignsNamespace;
|
|
893
910
|
|
|
894
911
|
constructor(opts: SupafoneLabsOptions) {
|
|
895
|
-
|
|
896
|
-
|
|
912
|
+
const hasAccountAuth = !!(opts?.accountToken || (opts?.accountEmail && opts?.accountPassword));
|
|
913
|
+
if (!opts?.apiKey && !hasAccountAuth) {
|
|
914
|
+
throw new SupafoneLabsError(
|
|
915
|
+
"apiKey is required — or, for campaigns/calls, pass accountToken or accountEmail + accountPassword",
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
this.apiKey = opts.apiKey ?? "";
|
|
897
919
|
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
898
|
-
this.supafoneApiKey = opts.supafoneApiKey ??
|
|
920
|
+
this.supafoneApiKey = opts.supafoneApiKey ?? this.apiKey;
|
|
899
921
|
this.supafoneApiBaseUrl = (opts.supafoneApiBaseUrl ?? DEFAULT_SUPAFONE_API_BASE).replace(/\/$/, "");
|
|
922
|
+
this.appUrl = (opts.appUrl ?? "https://app.supafone.ai").replace(/\/$/, "");
|
|
900
923
|
this.timeoutMs = opts.timeoutMs ?? 30_000;
|
|
901
924
|
this.sessionToken = opts.sessionToken;
|
|
925
|
+
this.accountToken = opts.accountToken;
|
|
926
|
+
this.accountEmail = opts.accountEmail;
|
|
927
|
+
this.accountPassword = opts.accountPassword;
|
|
902
928
|
this.labs = new LabsNamespace(this);
|
|
903
929
|
this.builder = new BuilderNamespace(this);
|
|
904
930
|
this.qa = new QANamespace(this);
|
|
905
931
|
this.optimizer = new OptimizerNamespace(this);
|
|
932
|
+
this.campaigns = new CampaignsNamespace(this);
|
|
906
933
|
}
|
|
907
934
|
|
|
908
935
|
/** True once login() (or a passed sessionToken) is in effect. */
|
|
@@ -967,6 +994,93 @@ export class SupafoneLabs {
|
|
|
967
994
|
}
|
|
968
995
|
}
|
|
969
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Exchange the account email/password for a product-API JWT (the same login
|
|
999
|
+
* as app.supafone.ai). Called lazily by campaigns/calls — call directly only
|
|
1000
|
+
* to fail fast.
|
|
1001
|
+
*/
|
|
1002
|
+
async accountLogin(email?: string, password?: string): Promise<string> {
|
|
1003
|
+
const useEmail = email ?? this.accountEmail;
|
|
1004
|
+
const usePassword = password ?? this.accountPassword;
|
|
1005
|
+
if (!useEmail || !usePassword) {
|
|
1006
|
+
throw new SupafoneLabsError(
|
|
1007
|
+
"Not authenticated: pass accountToken, or accountEmail + accountPassword",
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
const body = await this.accountHttp<{ token?: string; access_token?: string }>(
|
|
1011
|
+
"POST",
|
|
1012
|
+
"/api/v1/auth/login",
|
|
1013
|
+
{ email: useEmail, password: usePassword },
|
|
1014
|
+
"",
|
|
1015
|
+
);
|
|
1016
|
+
const token = body.access_token || body.token;
|
|
1017
|
+
if (!token) throw new SupafoneLabsError("Login succeeded but returned no token");
|
|
1018
|
+
this.accountSessionToken = token;
|
|
1019
|
+
return token;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* @internal JSON request to the Supafone product API with the ACCOUNT JWT
|
|
1024
|
+
* (campaigns + real calls). A minted token that expires gets one transparent
|
|
1025
|
+
* re-login; an explicit accountToken is the caller's to refresh.
|
|
1026
|
+
*/
|
|
1027
|
+
async requestAccountApi<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
1028
|
+
const token = this.accountToken || this.accountSessionToken || (await this.accountLogin());
|
|
1029
|
+
try {
|
|
1030
|
+
return await this.accountHttp<T>(method, path, body, token);
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
const expired = err instanceof SupafoneLabsError && err.status === 401;
|
|
1033
|
+
if (expired && !this.accountToken && this.accountEmail && this.accountPassword) {
|
|
1034
|
+
this.accountSessionToken = undefined;
|
|
1035
|
+
return this.accountHttp<T>(method, path, body, await this.accountLogin());
|
|
1036
|
+
}
|
|
1037
|
+
throw err;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** @internal Raw product-API request with an explicit bearer ("" = none). */
|
|
1042
|
+
private async accountHttp<T>(method: string, path: string, body: unknown, token: string): Promise<T> {
|
|
1043
|
+
const ctrl = new AbortController();
|
|
1044
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
1045
|
+
try {
|
|
1046
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1047
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1048
|
+
const res = await fetch(this.supafoneApiBaseUrl + path, {
|
|
1049
|
+
method,
|
|
1050
|
+
signal: ctrl.signal,
|
|
1051
|
+
headers,
|
|
1052
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
1053
|
+
});
|
|
1054
|
+
const text = await res.text();
|
|
1055
|
+
const parsed = text ? safeJson(text) : {};
|
|
1056
|
+
if (!res.ok) {
|
|
1057
|
+
const detail = (parsed as { detail?: string })?.detail ?? text ?? `HTTP ${res.status}`;
|
|
1058
|
+
throw new SupafoneLabsError(`${method} ${path}: ${detail}`, res.status, parsed);
|
|
1059
|
+
}
|
|
1060
|
+
return parsed as T;
|
|
1061
|
+
} finally {
|
|
1062
|
+
clearTimeout(timer);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* PLACE A REAL OUTBOUND PHONE CALL: dials toNumber from the account's
|
|
1068
|
+
* calling provider and bridges the voice agent onto the line.
|
|
1069
|
+
*/
|
|
1070
|
+
async placeCall(opts: { agentId: string; toNumber: string }): Promise<PlaceCallResult> {
|
|
1071
|
+
if (!opts?.agentId) throw new SupafoneLabsError("agentId is required (see listVoiceAgents())");
|
|
1072
|
+
if (!opts?.toNumber) throw new SupafoneLabsError("toNumber is required (E.164, e.g. +15551234567)");
|
|
1073
|
+
return this.requestAccountApi<PlaceCallResult>("POST", "/api/v1/phone/test-call", {
|
|
1074
|
+
agent_id: opts.agentId,
|
|
1075
|
+
to_number: opts.toNumber,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** The account's voice agents — pick an agent id for campaigns/calls. */
|
|
1080
|
+
async listVoiceAgents(): Promise<{ agents: Record<string, unknown>[] }> {
|
|
1081
|
+
return this.requestAccountApi("GET", "/api/v1/agents");
|
|
1082
|
+
}
|
|
1083
|
+
|
|
970
1084
|
/** Raw oracle completion — full control over messages and model. */
|
|
971
1085
|
async oracle(req: OracleRequest): Promise<OracleResult> {
|
|
972
1086
|
return this.request<OracleResult>("POST", "/v1/oracle/complete", {
|
|
@@ -1227,6 +1341,216 @@ class LiveTranscription {
|
|
|
1227
1341
|
}
|
|
1228
1342
|
|
|
1229
1343
|
/** Programmatic hosted Supafone agents, inside the Supafone API. */
|
|
1344
|
+
// ---------------------------------------------------------------------------
|
|
1345
|
+
// Campaigns — the outbound AI campaign engine the app.supafone.ai builder
|
|
1346
|
+
// drives, packaged. Account-JWT authenticated (accountToken / accountEmail +
|
|
1347
|
+
// accountPassword on the client options).
|
|
1348
|
+
// ---------------------------------------------------------------------------
|
|
1349
|
+
|
|
1350
|
+
export interface PlaceCallResult {
|
|
1351
|
+
success: boolean;
|
|
1352
|
+
simulated?: boolean;
|
|
1353
|
+
call_sid?: string | null;
|
|
1354
|
+
provider?: string;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
export interface CampaignRecipientInput {
|
|
1358
|
+
name?: string;
|
|
1359
|
+
phone?: string;
|
|
1360
|
+
email?: string;
|
|
1361
|
+
/** Warm-outreach consent — required before any voice/email touch. */
|
|
1362
|
+
outreach_consent?: string;
|
|
1363
|
+
[field: string]: unknown;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
export interface CampaignSummary {
|
|
1367
|
+
id: string;
|
|
1368
|
+
name: string;
|
|
1369
|
+
goal: string;
|
|
1370
|
+
status: string;
|
|
1371
|
+
agent_id?: string | null;
|
|
1372
|
+
stats?: Record<string, unknown>;
|
|
1373
|
+
settings?: Record<string, unknown>;
|
|
1374
|
+
[field: string]: unknown;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
export interface CampaignLiveCall {
|
|
1378
|
+
id: string;
|
|
1379
|
+
status: string;
|
|
1380
|
+
/** Portal deep link to watch this call (live transcript while in flight). */
|
|
1381
|
+
listen_url: string;
|
|
1382
|
+
[field: string]: unknown;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
export interface CampaignLiveView {
|
|
1386
|
+
campaign_id: string;
|
|
1387
|
+
in_flight: CampaignLiveCall[];
|
|
1388
|
+
/** Developer-portal link showing this campaign's agents/calls live. */
|
|
1389
|
+
portal_url: string;
|
|
1390
|
+
stats?: Record<string, unknown> | null;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
export interface CampaignUpdateInput {
|
|
1394
|
+
name?: string;
|
|
1395
|
+
goal?: string;
|
|
1396
|
+
agentId?: string;
|
|
1397
|
+
emailSubject?: string;
|
|
1398
|
+
emailBody?: string;
|
|
1399
|
+
cadence?: { channel: "voice" | "email"; delay_hours: number }[];
|
|
1400
|
+
settings?: Record<string, unknown>;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Typical flow:
|
|
1405
|
+
* ```ts
|
|
1406
|
+
* const sf = new Supafone({ accountEmail, accountPassword });
|
|
1407
|
+
* const { agents } = await sf.listVoiceAgents();
|
|
1408
|
+
* const { campaign } = await sf.campaigns.create({ name: "Q3 win-back", goal: "reengage", agentId: agents[0].id });
|
|
1409
|
+
* await sf.campaigns.applyPreset(campaign.id, "win_back");
|
|
1410
|
+
* await sf.campaigns.addRecipients(campaign.id, [{ name: "Jane", phone: "+15551234567", outreach_consent: "yes" }]);
|
|
1411
|
+
* await sf.campaigns.launch(campaign.id);
|
|
1412
|
+
* const live = await sf.campaigns.live(campaign.id); // in-flight calls + portal links
|
|
1413
|
+
* ```
|
|
1414
|
+
*/
|
|
1415
|
+
class CampaignsNamespace {
|
|
1416
|
+
constructor(private sm: SupafoneLabs) {}
|
|
1417
|
+
|
|
1418
|
+
list(opts: { accountId?: string } = {}): Promise<{ campaigns: CampaignSummary[] }> {
|
|
1419
|
+
const query = opts.accountId ? `?${new URLSearchParams({ account_id: opts.accountId })}` : "";
|
|
1420
|
+
return this.sm.requestAccountApi("GET", `/api/v1/campaigns${query}`);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
create(opts: { name?: string; goal?: string; agentId?: string; accountId?: string } = {}): Promise<{ campaign: CampaignSummary }> {
|
|
1424
|
+
return this.sm.requestAccountApi("POST", "/api/v1/campaigns", compact({
|
|
1425
|
+
name: opts.name ?? "New campaign",
|
|
1426
|
+
goal: opts.goal ?? "book",
|
|
1427
|
+
agent_id: opts.agentId,
|
|
1428
|
+
account_id: opts.accountId,
|
|
1429
|
+
}));
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
get(campaignId: string): Promise<{ campaign: CampaignSummary }> {
|
|
1433
|
+
return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
update(campaignId: string, input: CampaignUpdateInput): Promise<{ campaign: CampaignSummary }> {
|
|
1437
|
+
const payload = compact({
|
|
1438
|
+
name: input.name,
|
|
1439
|
+
goal: input.goal,
|
|
1440
|
+
agent_id: input.agentId,
|
|
1441
|
+
email_subject: input.emailSubject,
|
|
1442
|
+
email_body: input.emailBody,
|
|
1443
|
+
cadence: input.cadence,
|
|
1444
|
+
settings: input.settings,
|
|
1445
|
+
});
|
|
1446
|
+
if (!Object.keys(payload as Record<string, unknown>).length) {
|
|
1447
|
+
throw new SupafoneLabsError("Nothing to update — pass name, goal, agentId, emailSubject, emailBody, cadence, or settings");
|
|
1448
|
+
}
|
|
1449
|
+
return this.sm.requestAccountApi("PUT", `/api/v1/campaigns/${encodeURIComponent(campaignId)}`, payload);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** Add consented leads: [{name, phone, email, outreach_consent: "yes"}]. */
|
|
1453
|
+
addRecipients(campaignId: string, recipients: CampaignRecipientInput[]): Promise<{ added: number; stats: Record<string, unknown> }> {
|
|
1454
|
+
if (!Array.isArray(recipients) || !recipients.length) {
|
|
1455
|
+
throw new SupafoneLabsError("recipients must be a non-empty array of lead rows");
|
|
1456
|
+
}
|
|
1457
|
+
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients`, { recipients });
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
recipients(campaignId: string): Promise<{ recipients: Record<string, unknown>[] }> {
|
|
1461
|
+
return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** Starts REAL calls/emails on the cadence immediately. */
|
|
1465
|
+
launch(campaignId: string): Promise<{ campaign: CampaignSummary }> {
|
|
1466
|
+
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/launch`, {});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
pause(campaignId: string): Promise<{ campaign: CampaignSummary }> {
|
|
1470
|
+
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/pause`, {});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** Built-in playbooks + the account's saved custom presets. */
|
|
1474
|
+
async presets(): Promise<{ built_in: Record<string, unknown>[]; custom: Record<string, unknown>[] }> {
|
|
1475
|
+
const builtIn = await this.sm.requestAccountApi<{ presets: Record<string, unknown>[] }>(
|
|
1476
|
+
"GET",
|
|
1477
|
+
"/api/v1/campaigns/outbound-presets",
|
|
1478
|
+
);
|
|
1479
|
+
let custom: Record<string, unknown>[] = [];
|
|
1480
|
+
try {
|
|
1481
|
+
const mine = await this.sm.requestAccountApi<{ presets: Record<string, unknown>[] }>(
|
|
1482
|
+
"GET",
|
|
1483
|
+
"/api/v1/campaigns/custom-presets",
|
|
1484
|
+
);
|
|
1485
|
+
custom = mine.presets ?? [];
|
|
1486
|
+
} catch {
|
|
1487
|
+
/* custom presets need account scope — built-ins still return */
|
|
1488
|
+
}
|
|
1489
|
+
return { built_in: builtIn.presets ?? [], custom };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Materialize a preset (goal, questions, scripts, signing doc) in one write. */
|
|
1493
|
+
applyPreset(campaignId: string, presetId: string): Promise<{ campaign: CampaignSummary }> {
|
|
1494
|
+
return this.sm.requestAccountApi("POST", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/apply-preset`, {
|
|
1495
|
+
preset_id: presetId,
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
stats(campaignId: string): Promise<{ stats: Record<string, unknown> }> {
|
|
1500
|
+
return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/stats`);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/** The live funnel + the campaign's most recent calls (newest first). */
|
|
1504
|
+
activity(campaignId: string): Promise<{ stats?: Record<string, unknown>; calls?: Record<string, unknown>[] }> {
|
|
1505
|
+
return this.sm.requestAccountApi("GET", `/api/v1/campaigns/${encodeURIComponent(campaignId)}/activity`);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* In-flight calls right now, each with a portal link to watch/listen. Poll
|
|
1510
|
+
* getCall(callId) (or open the link) for the transcript as it grows.
|
|
1511
|
+
*/
|
|
1512
|
+
async live(campaignId: string): Promise<CampaignLiveView> {
|
|
1513
|
+
const activity = await this.activity(campaignId);
|
|
1514
|
+
const inFlight: CampaignLiveCall[] = [];
|
|
1515
|
+
for (const call of activity.calls ?? []) {
|
|
1516
|
+
const status = String((call as { status?: unknown }).status ?? "");
|
|
1517
|
+
if (status === "initiated" || status === "dialing" || status === "in_progress") {
|
|
1518
|
+
const id = String((call as { id?: unknown }).id ?? "");
|
|
1519
|
+
inFlight.push({
|
|
1520
|
+
...(call as Record<string, unknown>),
|
|
1521
|
+
id,
|
|
1522
|
+
status,
|
|
1523
|
+
listen_url: `${this.sm.appUrl}/app/calls?call=${encodeURIComponent(id)}`,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
campaign_id: campaignId,
|
|
1529
|
+
in_flight: inFlight,
|
|
1530
|
+
portal_url: `${this.sm.appUrl}/app/developer?campaign=${encodeURIComponent(campaignId)}`,
|
|
1531
|
+
stats: activity.stats ?? null,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/** One call — while in_progress the transcript grows on each poll. */
|
|
1536
|
+
getCall(callId: string): Promise<{ call: Record<string, unknown> }> {
|
|
1537
|
+
return this.sm.requestAccountApi("GET", `/api/v1/calls/${encodeURIComponent(callId)}`);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/** Mint a recipient's tracked tap-to-sign link (inherits the campaign's signing PDF). */
|
|
1541
|
+
createSignLink(
|
|
1542
|
+
campaignId: string,
|
|
1543
|
+
recipientId: string,
|
|
1544
|
+
opts: { title?: string; message?: string } = {},
|
|
1545
|
+
): Promise<{ link: Record<string, unknown> }> {
|
|
1546
|
+
return this.sm.requestAccountApi(
|
|
1547
|
+
"POST",
|
|
1548
|
+
`/api/v1/campaigns/${encodeURIComponent(campaignId)}/recipients/${encodeURIComponent(recipientId)}/sign-link`,
|
|
1549
|
+
compact({ title: opts.title, message: opts.message }),
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1230
1554
|
class LabsNamespace {
|
|
1231
1555
|
readonly agents: LabsAgentsNamespace;
|
|
1232
1556
|
readonly presets: LabsPresetsNamespace;
|
|
@@ -1656,16 +1980,6 @@ class OptimizerNamespace {
|
|
|
1656
1980
|
standing(agent = "builder"): Promise<{ version: number; text: string }> {
|
|
1657
1981
|
return this.sm.request("GET", `/v1/optimizer/standing?agent=${encodeURIComponent(agent)}`);
|
|
1658
1982
|
}
|
|
1659
|
-
/** SSR grade distribution: five nominal levels folded into a real score distribution. */
|
|
1660
|
-
distribution(agent = "builder", limit = 500): Promise<{
|
|
1661
|
-
agent: string; calls: number; counts: Record<string, number>;
|
|
1662
|
-
mean_score: number; buckets: number[]; bucket_edges: number[];
|
|
1663
|
-
}> {
|
|
1664
|
-
return this.sm.request(
|
|
1665
|
-
"GET",
|
|
1666
|
-
`/v1/objective/distribution?agent=${encodeURIComponent(agent)}&limit=${limit}`,
|
|
1667
|
-
);
|
|
1668
|
-
}
|
|
1669
1983
|
/** List the post-call reports behind the optimizer. */
|
|
1670
1984
|
reports(agent = "builder", limit = 40): Promise<{ reports: unknown[] }> {
|
|
1671
1985
|
return this.sm.request(
|