@spekoai/sdk 0.5.1 → 0.5.2
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/CHANGELOG.md +6 -0
- package/README.md +88 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/client.d.ts +13 -1
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +22 -2
- package/dist/lib/http.d.ts +11 -3
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +29 -12
- package/dist/lib/resources/agents.js +1 -0
- package/dist/lib/resources/calls.d.ts +19 -1
- package/dist/lib/resources/calls.d.ts.map +1 -1
- package/dist/lib/resources/calls.js +22 -0
- package/dist/lib/resources/phone-numbers.d.ts +2 -1
- package/dist/lib/resources/phone-numbers.d.ts.map +1 -1
- package/dist/lib/resources/phone-numbers.js +2 -1
- package/dist/lib/resources/realtime.d.ts +3 -5
- package/dist/lib/resources/realtime.d.ts.map +1 -1
- package/dist/lib/resources/realtime.js +829 -91
- package/dist/lib/resources/sessions.d.ts +53 -0
- package/dist/lib/resources/sessions.d.ts.map +1 -0
- package/dist/lib/resources/sessions.js +166 -0
- package/dist/lib/resources/sms.d.ts +80 -0
- package/dist/lib/resources/sms.d.ts.map +1 -0
- package/dist/lib/resources/sms.js +152 -0
- package/dist/lib/resources/transcribe.d.ts.map +1 -1
- package/dist/lib/resources/transcribe.js +5 -2
- package/dist/lib/resources/voice.d.ts +283 -1
- package/dist/lib/resources/voice.d.ts.map +1 -1
- package/dist/lib/resources/voice.js +345 -0
- package/dist/lib/resources/webhooks.d.ts +25 -0
- package/dist/lib/resources/webhooks.d.ts.map +1 -0
- package/dist/lib/resources/webhooks.js +46 -0
- package/dist/lib/types/index.d.ts +944 -9
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/voice-contract.d.ts +280 -0
- package/dist/lib/voice-contract.d.ts.map +1 -0
- package/dist/lib/voice-contract.js +115 -0
- package/package.json +2 -1
- package/src/index.ts +212 -0
- package/src/lib/client.ts +169 -0
- package/src/lib/errors.ts +28 -0
- package/src/lib/http.ts +442 -0
- package/src/lib/resources/agents.ts +211 -0
- package/src/lib/resources/callbacks.ts +40 -0
- package/src/lib/resources/calls.ts +113 -0
- package/src/lib/resources/complete.ts +63 -0
- package/src/lib/resources/credits.ts +41 -0
- package/src/lib/resources/knowledge-bases.ts +199 -0
- package/src/lib/resources/phone-numbers.ts +109 -0
- package/src/lib/resources/realtime-globals.d.ts +31 -0
- package/src/lib/resources/realtime.spec.ts +565 -0
- package/src/lib/resources/realtime.ts +1169 -0
- package/src/lib/resources/sessions.ts +191 -0
- package/src/lib/resources/sms.ts +214 -0
- package/src/lib/resources/synthesize.ts +101 -0
- package/src/lib/resources/transcribe.ts +91 -0
- package/src/lib/resources/usage.ts +24 -0
- package/src/lib/resources/voice.ts +426 -0
- package/src/lib/resources/voices.ts +32 -0
- package/src/lib/resources/webhooks.ts +67 -0
- package/src/lib/types/index.ts +2409 -0
- package/src/lib/voice-contract.ts +358 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmable voice — the wire contract.
|
|
3
|
+
*
|
|
4
|
+
* A hand-kept mirror of the platform's own `voice-contract.ts`. The SDK ships
|
|
5
|
+
* standalone to npm, so these shapes are re-declared here rather than imported
|
|
6
|
+
* across the monorepo; when the platform contract moves, this file moves with
|
|
7
|
+
* it.
|
|
8
|
+
*
|
|
9
|
+
* Every wire shape here is member-for-member identical to the platform's. Two
|
|
10
|
+
* things there are deliberately not mirrored, because they exist to serve the
|
|
11
|
+
* server and would only be dead surface on a client: the `as const` arrays the
|
|
12
|
+
* server derives its zod enums from (`CALL_STATUSES` and friends — the unions
|
|
13
|
+
* below have exactly those members), and `isBrokerReachable`, whose verdict
|
|
14
|
+
* already arrives precomputed as {@link BrokerPresenceResource.reachable}.
|
|
15
|
+
* `CALL_COMMANDS` and `CALL_EVENTS` *are* mirrored as arrays, because a client
|
|
16
|
+
* does enumerate those.
|
|
17
|
+
*
|
|
18
|
+
* The model is Telnyx Call Control's, deliberately: every leg of a call carries
|
|
19
|
+
* an opaque `controlId`, and commands are addressed to that handle rather than
|
|
20
|
+
* to a room or a participant identity. A migrating integration maps one command
|
|
21
|
+
* to one command. What is underneath (LiveKit) never leaks into the API.
|
|
22
|
+
*
|
|
23
|
+
* Two divergences from Telnyx are worth knowing before you port anything, both
|
|
24
|
+
* forced by the transport:
|
|
25
|
+
*
|
|
26
|
+
* - `hold` is synthesized by isolating subscriptions in both directions;
|
|
27
|
+
* there is no LiveKit hold primitive. Without a music-on-hold source
|
|
28
|
+
* configured, **hold is silent** — the far end hears nothing at all, not
|
|
29
|
+
* hold music.
|
|
30
|
+
* - There is **no SIP registrar**. A third-party SIP endpoint (a desk phone, a
|
|
31
|
+
* softphone, another PBX) cannot register against Speko and be rung. Humans
|
|
32
|
+
* join from the browser; `browser` and `pstn` are the only human-reachable
|
|
33
|
+
* leg kinds.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// --- Resources -------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Which way a call or leg was set up. `internal` is broker-to-broker. */
|
|
39
|
+
export type CallDirection = 'inbound' | 'outbound' | 'internal';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lifecycle of a call. Terminal states distinguish the three ways a call can
|
|
43
|
+
* die unanswered, so a missed-call list can tell them apart: `declined` (a
|
|
44
|
+
* human said no), `missed` (the ring timed out), `cancelled` (the originator
|
|
45
|
+
* hung up first).
|
|
46
|
+
*/
|
|
47
|
+
export type CallStatus =
|
|
48
|
+
| 'initiating'
|
|
49
|
+
| 'ringing'
|
|
50
|
+
| 'active'
|
|
51
|
+
| 'ended'
|
|
52
|
+
| 'declined'
|
|
53
|
+
| 'missed'
|
|
54
|
+
| 'cancelled'
|
|
55
|
+
| 'failed';
|
|
56
|
+
|
|
57
|
+
/** Per-leg lifecycle. `held` is a state, not a flag — see {@link CallLegResource.onHold}. */
|
|
58
|
+
export type CallLegStatus = 'initiating' | 'ringing' | 'active' | 'held' | 'ended' | 'failed';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What is on the far end of a leg. `browser` is a human on the WebRTC
|
|
62
|
+
* softphone, `pstn` a phone reached over SIP, `agent` a Speko AI worker — which
|
|
63
|
+
* is what makes escalating an AI call to a human an ordinary bridge rather than
|
|
64
|
+
* a special case.
|
|
65
|
+
*/
|
|
66
|
+
export type CallLegKind = 'browser' | 'pstn' | 'agent';
|
|
67
|
+
|
|
68
|
+
/** Valid org-defined broker ids accepted by the human-calling API. */
|
|
69
|
+
export const BROKER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
70
|
+
|
|
71
|
+
/** One party on a call. The unit every command is addressed to. */
|
|
72
|
+
export interface CallLegResource {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
/** The command handle. Telnyx `call_control_id` equivalent. */
|
|
75
|
+
readonly controlId: string;
|
|
76
|
+
readonly callId: string;
|
|
77
|
+
readonly kind: CallLegKind;
|
|
78
|
+
readonly direction: CallDirection;
|
|
79
|
+
readonly status: CallLegStatus;
|
|
80
|
+
/** Org-defined broker identity when `kind === 'browser'`; null otherwise. */
|
|
81
|
+
readonly brokerId: string | null;
|
|
82
|
+
/** E.164 of the far end, when `kind === 'pstn'`. */
|
|
83
|
+
readonly phoneNumber: string | null;
|
|
84
|
+
readonly muted: boolean;
|
|
85
|
+
readonly onHold: boolean;
|
|
86
|
+
readonly answeredAt: string | null;
|
|
87
|
+
readonly endedAt: string | null;
|
|
88
|
+
readonly endReason: string | null;
|
|
89
|
+
readonly createdAt: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A call and every leg on it. */
|
|
93
|
+
export interface CallResource {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly direction: CallDirection;
|
|
96
|
+
readonly status: CallStatus;
|
|
97
|
+
readonly answeredAt: string | null;
|
|
98
|
+
readonly endedAt: string | null;
|
|
99
|
+
readonly endReason: string | null;
|
|
100
|
+
readonly legs: readonly CallLegResource[];
|
|
101
|
+
readonly createdAt: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What a browser client needs to actually join a room — a live credential, so
|
|
106
|
+
* mint it at the moment you connect and never stockpile it.
|
|
107
|
+
*
|
|
108
|
+
* Returned by {@link CallControl.dial} (alongside the call, because the dialing
|
|
109
|
+
* broker must already be in the room when the far end answers) and by
|
|
110
|
+
* {@link CallControl.join} (for a leg you were rung on). Either way it is minted
|
|
111
|
+
* per join, short-lived, and issued only to the leg's own owner: a token is a
|
|
112
|
+
* live credential for a room that may carry a customer conversation.
|
|
113
|
+
*
|
|
114
|
+
* Hand `token` and `url` to `@spekoai/client`, or to `livekit-client` directly.
|
|
115
|
+
*/
|
|
116
|
+
export interface CallJoinCredentials {
|
|
117
|
+
readonly token: string;
|
|
118
|
+
readonly url: string;
|
|
119
|
+
readonly identity: string;
|
|
120
|
+
readonly roomName: string;
|
|
121
|
+
readonly expiresAt: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Commands --------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Every command a live leg accepts. Names mirror Telnyx Call Control verbs so a
|
|
128
|
+
* migrating integration can be mapped mechanically.
|
|
129
|
+
*/
|
|
130
|
+
export const CALL_COMMANDS = [
|
|
131
|
+
'answer',
|
|
132
|
+
'hangup',
|
|
133
|
+
'bridge',
|
|
134
|
+
'hold',
|
|
135
|
+
'unhold',
|
|
136
|
+
'mute',
|
|
137
|
+
'unmute',
|
|
138
|
+
'dtmf',
|
|
139
|
+
'transfer',
|
|
140
|
+
] as const;
|
|
141
|
+
|
|
142
|
+
export type CallCommand = (typeof CALL_COMMANDS)[number];
|
|
143
|
+
|
|
144
|
+
/** Digits accepted in a DTMF payload: 0-9, `*`, `#`, and `w` for a pause. */
|
|
145
|
+
export const DTMF_PATTERN = /^[0-9*#w,]{1,64}$/;
|
|
146
|
+
|
|
147
|
+
export interface BridgeCommandPayload {
|
|
148
|
+
/** `controlId` of the leg to bridge this one to. */
|
|
149
|
+
readonly bridgeTo: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface DtmfCommandPayload {
|
|
153
|
+
/** Matches {@link DTMF_PATTERN}: 0-9, `*`, `#`, `,`, and `w` for a pause. */
|
|
154
|
+
readonly digits: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface TransferCommandPayload {
|
|
158
|
+
/**
|
|
159
|
+
* Where to send the leg — an E.164 number for a PSTN transfer, or the
|
|
160
|
+
* `controlId` of a broker leg for an internal one.
|
|
161
|
+
*/
|
|
162
|
+
readonly to: string;
|
|
163
|
+
/**
|
|
164
|
+
* `blind` hands the call off and drops this leg immediately. `warm` parks the
|
|
165
|
+
* far end and opens a consultation room first, so the two humans can talk
|
|
166
|
+
* before the handoff completes.
|
|
167
|
+
*/
|
|
168
|
+
readonly mode: 'blind' | 'warm';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface HangupCommandPayload {
|
|
172
|
+
/** Free-text, surfaced as the leg's `endReason`. */
|
|
173
|
+
readonly reason?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type CallCommandPayload =
|
|
177
|
+
| BridgeCommandPayload
|
|
178
|
+
| DtmfCommandPayload
|
|
179
|
+
| TransferCommandPayload
|
|
180
|
+
| HangupCommandPayload
|
|
181
|
+
| Record<string, never>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Commands are idempotent by intent, not by replay: issuing `mute` twice is a
|
|
185
|
+
* no-op, but the second call still returns the current leg. Every command
|
|
186
|
+
* resolves to the leg's post-command state so a client never has to re-read.
|
|
187
|
+
*/
|
|
188
|
+
export interface CallCommandResult {
|
|
189
|
+
readonly leg: CallLegResource;
|
|
190
|
+
/** Present when the command changed nothing, e.g. muting a muted leg. */
|
|
191
|
+
readonly noop?: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Events ----------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The event stream. Assembled from LiveKit room webhooks plus SIP call status —
|
|
198
|
+
* which is coarser than a carrier's own signalling, so a few Telnyx events have
|
|
199
|
+
* no faithful equivalent and are deliberately absent rather than approximated:
|
|
200
|
+
* there is no early-media event, and no distinction between a 486 Busy and a
|
|
201
|
+
* generic rejection.
|
|
202
|
+
*/
|
|
203
|
+
export const CALL_EVENTS = [
|
|
204
|
+
'call.initiated',
|
|
205
|
+
'call.ringing',
|
|
206
|
+
'call.answered',
|
|
207
|
+
'call.bridged',
|
|
208
|
+
'call.hold',
|
|
209
|
+
'call.unhold',
|
|
210
|
+
'call.mute',
|
|
211
|
+
'call.unmute',
|
|
212
|
+
'call.dtmf.received',
|
|
213
|
+
'call.dtmf.sent',
|
|
214
|
+
'call.transfer.initiated',
|
|
215
|
+
'call.transfer.completed',
|
|
216
|
+
'call.transfer.failed',
|
|
217
|
+
'call.leg.hangup',
|
|
218
|
+
'call.hangup',
|
|
219
|
+
] as const;
|
|
220
|
+
|
|
221
|
+
export type CallEventType = (typeof CALL_EVENTS)[number];
|
|
222
|
+
|
|
223
|
+
export interface CallEventResource {
|
|
224
|
+
readonly id: string;
|
|
225
|
+
readonly type: CallEventType;
|
|
226
|
+
readonly callId: string;
|
|
227
|
+
/** Null for call-scoped events that belong to no single leg. */
|
|
228
|
+
readonly controlId: string | null;
|
|
229
|
+
readonly payload: Record<string, unknown>;
|
|
230
|
+
readonly occurredAt: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Presence --------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
export type BrokerPresenceStatus = 'available' | 'busy' | 'away' | 'offline';
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* How long a broker's heartbeat stays valid. Past this, inbound routing treats
|
|
239
|
+
* them as offline whatever their stored status says — a crashed tab must not
|
|
240
|
+
* black-hole calls. Heartbeat comfortably inside this window (a third of it is
|
|
241
|
+
* a good default).
|
|
242
|
+
*/
|
|
243
|
+
export const PRESENCE_STALE_AFTER_MS = 90_000;
|
|
244
|
+
|
|
245
|
+
export interface BrokerPresenceResource {
|
|
246
|
+
readonly brokerId: string;
|
|
247
|
+
readonly status: BrokerPresenceStatus;
|
|
248
|
+
readonly lastSeenAt: string;
|
|
249
|
+
/** False once the heartbeat has gone stale, regardless of `status`. */
|
|
250
|
+
readonly reachable: boolean;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Presence wire messages ------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* What the server publishes into a broker's presence room — the ring channel's
|
|
257
|
+
* wire format, and the reason a browser softphone needs more than the HTTP
|
|
258
|
+
* surface. Read these off the LiveKit data channel you opened with
|
|
259
|
+
* {@link CallControl.presenceToken}; there is no HTTP equivalent, because a ring
|
|
260
|
+
* has to arrive unsolicited.
|
|
261
|
+
*
|
|
262
|
+
* Discriminate on `type`. New members can appear without a major version, so
|
|
263
|
+
* treat an unrecognized `type` as "ignore", never as an error.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* import type { PresenceMessage } from '@spekoai/sdk';
|
|
268
|
+
*
|
|
269
|
+
* room.on('dataReceived', async (payload) => {
|
|
270
|
+
* const message = JSON.parse(new TextDecoder().decode(payload)) as PresenceMessage;
|
|
271
|
+
* switch (message.type) {
|
|
272
|
+
* case 'incoming_call': {
|
|
273
|
+
* // Join the room first, then answer — see CallControl.join.
|
|
274
|
+
* const credentials = await speko.callControl.join(message.controlId);
|
|
275
|
+
* await connectTo(credentials);
|
|
276
|
+
* await speko.callControl.answer(message.controlId);
|
|
277
|
+
* break;
|
|
278
|
+
* }
|
|
279
|
+
* case 'call_event':
|
|
280
|
+
* // Also how a ring is cancelled: a caller who gives up before you answer
|
|
281
|
+
* // ends your ringing leg, which arrives here as `call.leg.hangup`.
|
|
282
|
+
* if (message.event.type === 'call.leg.hangup') dismissIncomingCallPrompt(message.event.callId);
|
|
283
|
+
* applyToUi(message.event);
|
|
284
|
+
* break;
|
|
285
|
+
* }
|
|
286
|
+
* });
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
export type PresenceMessage = RingOffer | CallEventNotice;
|
|
290
|
+
|
|
291
|
+
/** Published to the callee when a call starts ringing them. */
|
|
292
|
+
export interface RingOffer {
|
|
293
|
+
readonly type: 'incoming_call';
|
|
294
|
+
readonly callId: string;
|
|
295
|
+
/**
|
|
296
|
+
* The callee's OWN browser leg — the handle {@link CallControl.answer},
|
|
297
|
+
* {@link CallControl.join} and {@link CallControl.hangup} are addressed to.
|
|
298
|
+
* A ring without it is unanswerable, so treat its absence as a malformed
|
|
299
|
+
* packet rather than rendering an answer button that cannot work.
|
|
300
|
+
*/
|
|
301
|
+
readonly controlId: string;
|
|
302
|
+
readonly roomName: string;
|
|
303
|
+
readonly caller: {
|
|
304
|
+
/** Org-defined broker id on an internal call; null when the caller is a phone. */
|
|
305
|
+
readonly brokerId: string | null;
|
|
306
|
+
readonly name: string | null;
|
|
307
|
+
/** E.164 of the calling phone, when the call arrived over the PSTN. */
|
|
308
|
+
readonly phoneNumber: string | null;
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A call-lifecycle event mirrored into the broker's presence room so the
|
|
314
|
+
* softphone reflects state changes it did not initiate — the far end hanging up,
|
|
315
|
+
* a supervisor bridging in, a transfer completing.
|
|
316
|
+
*
|
|
317
|
+
* The event is carried verbatim as a {@link CallEventResource}, the same shape
|
|
318
|
+
* {@link CallControl.events} returns, so one parser serves both the live feed
|
|
319
|
+
* and the history. History is authoritative: a dropped packet costs freshness,
|
|
320
|
+
* never a record.
|
|
321
|
+
*/
|
|
322
|
+
export interface CallEventNotice {
|
|
323
|
+
readonly type: 'call_event';
|
|
324
|
+
readonly event: CallEventResource;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Error codes -----------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Stable codes so you can branch without string-matching prose. They arrive on
|
|
331
|
+
* `SpekoApiError.code`.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* import { SpekoApiError, VOICE_ERROR_CODES } from '@spekoai/sdk';
|
|
336
|
+
*
|
|
337
|
+
* try {
|
|
338
|
+
* await speko.callControl.hold(controlId);
|
|
339
|
+
* } catch (err) {
|
|
340
|
+
* if (err instanceof SpekoApiError && err.code === VOICE_ERROR_CODES.legNotLive) {
|
|
341
|
+
* // The leg hung up between render and click — drop the control, don't retry.
|
|
342
|
+
* }
|
|
343
|
+
* }
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
export const VOICE_ERROR_CODES = {
|
|
347
|
+
disabled: 'HUMAN_CALLING_DISABLED',
|
|
348
|
+
unconfigured: 'HUMAN_CALLING_UNCONFIGURED',
|
|
349
|
+
notFound: 'NOT_FOUND',
|
|
350
|
+
validation: 'VALIDATION_ERROR',
|
|
351
|
+
legNotLive: 'LEG_NOT_LIVE',
|
|
352
|
+
callNotLive: 'CALL_NOT_LIVE',
|
|
353
|
+
unsupportedCommand: 'UNSUPPORTED_COMMAND',
|
|
354
|
+
bridgeTargetInvalid: 'BRIDGE_TARGET_INVALID',
|
|
355
|
+
noBrokerAvailable: 'NO_BROKER_AVAILABLE',
|
|
356
|
+
} as const;
|
|
357
|
+
|
|
358
|
+
export type VoiceErrorCode = (typeof VOICE_ERROR_CODES)[keyof typeof VOICE_ERROR_CODES];
|