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 +37 -0
- 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/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"`
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 ??
|
|
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
|
-
/**
|
|
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}`);
|