@craftedxp/voice-js 0.4.2 → 0.6.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/browser.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { RemoteTrack, LocalVideoTrack } from 'livekit-client';
2
+
1
3
  interface ClientTool {
2
4
  description: string;
3
5
  parameters: Record<string, unknown>;
@@ -79,6 +81,201 @@ interface BuildWsUrlArgs {
79
81
  }
80
82
  declare function buildWsUrl(args: BuildWsUrlArgs): string;
81
83
 
84
+ type SystemMessage = {
85
+ kind: 'room.starting';
86
+ at: string;
87
+ } | {
88
+ kind: 'room.ending.soon';
89
+ minutesRemaining: 5 | 1;
90
+ } | {
91
+ kind: 'room.ended';
92
+ reason: 'duration_reached' | 'manual' | 'empty';
93
+ } | {
94
+ kind: 'role.promoted';
95
+ participantId: string;
96
+ name: string;
97
+ } | {
98
+ kind: 'role.demoted';
99
+ participantId: string;
100
+ name: string;
101
+ } | {
102
+ kind: 'participant.removed';
103
+ participantId: string;
104
+ name: string;
105
+ byHost?: string;
106
+ } | {
107
+ kind: 'notetaker.connected';
108
+ } | {
109
+ kind: 'notetaker.disconnected';
110
+ } | {
111
+ kind: 'notetaker.partial_degraded';
112
+ participantId: string;
113
+ };
114
+ type TranscriptMessage = {
115
+ kind: 'partial';
116
+ participantId: string;
117
+ speakerName: string;
118
+ text: string;
119
+ startedAt: string;
120
+ };
121
+
122
+ interface JoinRoomOptions {
123
+ /** Full HTTPS URL of the Voissia server. Same shape as VoiceClientConfig.apiBase. */
124
+ apiBase: string;
125
+ /** Server-generated room id (`rm_…`). */
126
+ roomId: string;
127
+ /** Shared room join token from the invite link. Mints a fresh participant each call. */
128
+ joinCode: string;
129
+ /** Display name the joiner registers under for this participant. */
130
+ name: string;
131
+ }
132
+ interface RoomParticipantInfo {
133
+ /** Stable participant id (`p_…`); strips any `guest:` LiveKit identity prefix. */
134
+ participantId: string;
135
+ /** Display name as the worker registered it; may be empty. */
136
+ name: string;
137
+ }
138
+ type RoomTrackKind = 'audio' | 'video';
139
+ /** What a track is — lets consumers tell a camera apart from a screen share
140
+ * (a participant can publish both at once). Mirrors livekit `Track.Source`. */
141
+ type RoomTrackSource = 'camera' | 'microphone' | 'screen_share' | 'screen_share_audio' | 'unknown';
142
+ interface RoomTrackEvent {
143
+ /** Stable participant id (`p_…`); `guest:` prefix stripped. */
144
+ participantId: string;
145
+ kind: RoomTrackKind;
146
+ /** Distinguishes camera vs screen_share so each can render as its own tile. */
147
+ source: RoomTrackSource;
148
+ /** livekit-client track — call `.attach(el)` / `.detach()` to render. */
149
+ track: RemoteTrack;
150
+ }
151
+ type RoomEventName = 'participant.joined' | 'participant.left' | 'transcript.partial' | 'transcript.final' | 'system.message' | 'room.ended' | 'track.subscribed' | 'track.unsubscribed' | 'active.speakers';
152
+ interface RoomEventPayloads {
153
+ 'participant.joined': RoomParticipantInfo;
154
+ 'participant.left': RoomParticipantInfo;
155
+ 'transcript.partial': TranscriptMessage;
156
+ /**
157
+ * Reserved — the worker currently emits only partials over the transcript
158
+ * topic. Final-utterance events will land in Phase 8 once the worker
159
+ * publishes a `final` kind; the SDK keeps the slot reserved so consumers
160
+ * can register handlers today.
161
+ */
162
+ 'transcript.final': {
163
+ participantId: string;
164
+ speakerName: string;
165
+ text: string;
166
+ startedAt: string;
167
+ };
168
+ 'system.message': SystemMessage;
169
+ 'room.ended': undefined;
170
+ 'track.subscribed': RoomTrackEvent;
171
+ 'track.unsubscribed': RoomTrackEvent;
172
+ /** participantIds currently speaking (drives an active-speaker UI). */
173
+ 'active.speakers': string[];
174
+ }
175
+ type Handler<E extends RoomEventName> = (payload: RoomEventPayloads[E]) => void;
176
+ interface RoomSession {
177
+ /** This session's own stable participant id (`p_…`). Useful to filter
178
+ * yourself out of `active.speakers`, which includes the local participant. */
179
+ readonly participantId: string;
180
+ /** Snapshot of the remote participants currently connected. */
181
+ readonly participants: RoomParticipantInfo[];
182
+ /** Subscribe to a typed event. No unsubscribe surface yet (mirrors `Call.onX`). */
183
+ on<E extends RoomEventName>(event: E, handler: Handler<E>): void;
184
+ /** Publish the local mic track. Resolves once the track is live on LiveKit. */
185
+ publishMic(): Promise<void>;
186
+ /** Publish the local camera track. */
187
+ publishCamera(): Promise<void>;
188
+ /** Mid-call mute/unmute of the local mic. */
189
+ setMicEnabled(on: boolean): Promise<void>;
190
+ /** Mid-call camera on/off. */
191
+ setCameraEnabled(on: boolean): Promise<void>;
192
+ /** Current local mic state (for toggle UI). */
193
+ isMicEnabled(): boolean;
194
+ /** Current local camera state (for toggle UI). */
195
+ isCameraEnabled(): boolean;
196
+ /** The local camera track for self-view, or null before publishCamera resolves. */
197
+ getLocalCameraTrack(): LocalVideoTrack | null;
198
+ /**
199
+ * Remote tracks already subscribed at this moment. A late joiner misses the
200
+ * live `track.subscribed` events for tracks published before it connected
201
+ * (LiveKit delivers them during `connect`, before consumer listeners attach).
202
+ * Call this right after registering `track.subscribed` to backfill them.
203
+ */
204
+ getRemoteTracks(): RoomTrackEvent[];
205
+ /**
206
+ * Start/stop sharing the screen (via `getDisplayMedia`). Pass `{ audio: true }`
207
+ * to also capture shared/system audio where the browser allows it (Chrome:
208
+ * tab or system audio; macOS Chrome is tab-audio only; Safari/Firefox don't
209
+ * capture share audio). Publishes a `screen_share` video track (+ optional
210
+ * `screen_share_audio`); remote peers receive them via `track.subscribed`.
211
+ */
212
+ setScreenShareEnabled(on: boolean, opts?: {
213
+ audio?: boolean;
214
+ }): Promise<void>;
215
+ /** Current local screen-share state (for toggle UI). */
216
+ isScreenShareEnabled(): boolean;
217
+ /** The local screen-share video track for self-preview, or null when off. */
218
+ getLocalScreenTrack(): LocalVideoTrack | null;
219
+ /** Disconnect from LiveKit. Idempotent. Triggers `room.ended` via Disconnected. */
220
+ leave(): Promise<void>;
221
+ }
222
+ declare const joinRoom: (opts: JoinRoomOptions) => Promise<RoomSession>;
223
+
224
+ /**
225
+ * Browser-friendly text-channel chat session. Mint a `ct_` token with
226
+ * `channel: 'text'` on your backend, then call `startTextSession({...})`
227
+ * to open the SSE stream.
228
+ *
229
+ * Each `.send(text)` is a fresh POST; SSE-per-turn means the connection
230
+ * closes when each turn ends. Conversation state lives server-side on the
231
+ * underlying CallRecord.
232
+ */
233
+ type ChatEvent = {
234
+ type: 'chat.started';
235
+ chatId: string;
236
+ callId: string;
237
+ } | {
238
+ type: 'token';
239
+ text: string;
240
+ } | {
241
+ type: 'tool.call';
242
+ name: string;
243
+ args: unknown;
244
+ } | {
245
+ type: 'tool.result';
246
+ name: string;
247
+ ok?: boolean;
248
+ [key: string]: unknown;
249
+ } | {
250
+ type: 'turn.end';
251
+ finishReason: 'stop' | 'aborted' | 'length' | 'tool_error';
252
+ committedText?: string;
253
+ } | {
254
+ type: 'error';
255
+ code: string;
256
+ message: string;
257
+ };
258
+ interface StartTextSessionOpts {
259
+ baseUrl: string;
260
+ token: string;
261
+ agentId: string;
262
+ /** Optional inline first user message; otherwise the agent's greeting opens the stream. */
263
+ text?: string;
264
+ /** Override the global fetch (useful for tests; defaults to globalThis.fetch). */
265
+ fetch?: typeof fetch;
266
+ }
267
+ interface TextSession {
268
+ id: string;
269
+ callId: string;
270
+ /** Async iterable for the opening turn — greeting tokens / first reply if text was inlined. */
271
+ greeting: AsyncIterable<ChatEvent>;
272
+ /** Send a user message; returns an async iterable for the agent's reply. */
273
+ send(text: string): Promise<AsyncIterable<ChatEvent>>;
274
+ /** End the session — DELETE /v1/calls/:callId. */
275
+ end(): Promise<void>;
276
+ }
277
+ declare function startTextSession(opts: StartTextSessionOpts): Promise<TextSession>;
278
+
82
279
  interface FetchTokenArgs {
83
280
  /** The agent the SDK is about to call. */
84
281
  agentId: string;
@@ -228,6 +425,25 @@ interface VoiceClientFactory {
228
425
  * don't reject this promise.
229
426
  */
230
427
  startCall: (options: StartCallOptions) => Promise<Call>;
428
+ /**
429
+ * Phase 7 (multi-party rooms). Browser only. Exchange a single-use
430
+ * joinCode for a LiveKit JWT and connect to the room. The returned
431
+ * `RoomSession` exposes a typed event surface
432
+ * (participant.joined / participant.left / transcript.partial /
433
+ * transcript.final / system.message / room.ended) plus
434
+ * publishMic / publishCamera / leave. The Node bundle does NOT
435
+ * implement this — livekit-client is a browser-only WebRTC client.
436
+ */
437
+ joinRoom?: (options: Omit<JoinRoomOptions, 'apiBase'>) => Promise<RoomSession>;
438
+ /**
439
+ * Open a text-channel chat session (no microphone / audio required).
440
+ * Mint a `ct_` token with `channel: 'text'` server-side, then call
441
+ * this to connect. Returns a `TextSession` with:
442
+ * - `.greeting` — async iterable for the opening turn
443
+ * - `.send(text)` — send a user message; returns an async iterable for the reply
444
+ * - `.end()` — close the session (DELETE /v1/calls/:callId)
445
+ */
446
+ startTextSession?: (opts: Omit<StartTextSessionOpts, 'baseUrl' | 'fetch'>) => Promise<TextSession>;
231
447
  }
232
448
 
233
449
  type OnChunk = (pcm: ArrayBuffer) => void;
@@ -307,6 +523,31 @@ declare const createReconnectingWebSocket: (options: RWSOptions, onEvent: (ev: R
307
523
  };
308
524
  type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>;
309
525
 
526
+ /**
527
+ * Canonical payload a tenant places in their VoIP/FCM push so an
528
+ * agent-initiated call can connect. It is the `callTokens.mint` result
529
+ * (token + transport) plus two optional display fields for the native
530
+ * incoming-call UI. The host receives the push, runs `parseIncomingCall`,
531
+ * and on accept passes `token` (+ transport / webrtcGatewayBase) into the
532
+ * voice client. Web background-wake is best-effort (Web Push); native is
533
+ * the real target. See docs/sdks.md "Agent-initiated calls".
534
+ */
535
+ interface IncomingCallPayload {
536
+ token: string;
537
+ agentId: string;
538
+ transport: 'ws' | 'webrtc';
539
+ webrtcGatewayBase?: string;
540
+ expiresAt?: number;
541
+ agentName?: string;
542
+ agentAvatarUrl?: string;
543
+ }
544
+ /**
545
+ * Validate + normalise a raw push payload into an IncomingCallPayload.
546
+ * Throws synchronously on malformed input. Unknown transports fall back to
547
+ * 'ws'; webrtcGatewayBase is ignored unless transport === 'webrtc'.
548
+ */
549
+ declare const parseIncomingCall: (raw: unknown) => IncomingCallPayload;
550
+
310
551
  /**
311
552
  * One-time SDK setup. Returns a factory you call `startCall` on for
312
553
  * every voice call.
@@ -334,4 +575,4 @@ type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>;
334
575
  */
335
576
  declare function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory;
336
577
 
337
- export { type Call, type CallEndEvent, type CallEndReason, type CallError, type CallErrorCode, type CallState, type CaptureController, type CaptureOptions, type ClientTool, type ClientToolMap, type FetchToken, type FetchTokenArgs, type FetchTokenResult, type OnAgentSpeakingChange, type OnChunk, type OnError, type OnVolume$1 as OnVolume, type PlaybackController, type PlaybackOptions, type ProtocolCallbacks, type ProtocolState, type RWSEvent, type RWSOptions, type ReconnectingWebSocket, type ServerMessage, type StartCallOptions, type TranscriptEntry, type VoiceClientConfig, type VoiceClientFactory, type VolumeEvent, type WebSocketFactory, type WebSocketLike, buildWsUrl, configureVoiceClient, createAudioCapture, createAudioPlayback, createProtocolState, createReconnectingWebSocket, handleServerMessage };
578
+ export { type Call, type CallEndEvent, type CallEndReason, type CallError, type CallErrorCode, type CallState, type CaptureController, type CaptureOptions, type ChatEvent, type ClientTool, type ClientToolMap, type FetchToken, type FetchTokenArgs, type FetchTokenResult, type IncomingCallPayload, type JoinRoomOptions, type OnAgentSpeakingChange, type OnChunk, type OnError, type OnVolume$1 as OnVolume, type PlaybackController, type PlaybackOptions, type ProtocolCallbacks, type ProtocolState, type RWSEvent, type RWSOptions, type ReconnectingWebSocket, type RoomEventName, type RoomEventPayloads, type RoomParticipantInfo, type RoomSession, type ServerMessage, type StartCallOptions, type StartTextSessionOpts, type SystemMessage, type TextSession, type TranscriptEntry, type TranscriptMessage, type VoiceClientConfig, type VoiceClientFactory, type VolumeEvent, type WebSocketFactory, type WebSocketLike, buildWsUrl, configureVoiceClient, createAudioCapture, createAudioPlayback, createProtocolState, createReconnectingWebSocket, handleServerMessage, joinRoom, parseIncomingCall, startTextSession };
package/dist/browser.js CHANGED
@@ -26,7 +26,10 @@ __export(browser_exports, {
26
26
  createAudioPlayback: () => createAudioPlayback,
27
27
  createProtocolState: () => createProtocolState,
28
28
  createReconnectingWebSocket: () => createReconnectingWebSocket,
29
- handleServerMessage: () => handleServerMessage
29
+ handleServerMessage: () => handleServerMessage,
30
+ joinRoom: () => joinRoom,
31
+ parseIncomingCall: () => parseIncomingCall,
32
+ startTextSession: () => startTextSession
30
33
  });
31
34
  module.exports = __toCommonJS(browser_exports);
32
35
 
@@ -1047,6 +1050,284 @@ async function createWebRtcCall(opts) {
1047
1050
  };
1048
1051
  }
1049
1052
 
1053
+ // src/room.ts
1054
+ var import_livekit_client = require("livekit-client");
1055
+
1056
+ // src/roomProtocol.ts
1057
+ var SYSTEM_TOPIC = "system";
1058
+ var TRANSCRIPT_TOPIC = "transcript";
1059
+ var decodeSystem = (bytes) => {
1060
+ try {
1061
+ const v = JSON.parse(new TextDecoder().decode(bytes));
1062
+ if (v && typeof v.kind === "string") return v;
1063
+ return null;
1064
+ } catch {
1065
+ return null;
1066
+ }
1067
+ };
1068
+ var decodeTranscript = (bytes) => {
1069
+ try {
1070
+ const v = JSON.parse(new TextDecoder().decode(bytes));
1071
+ if (v && v.kind === "partial") return v;
1072
+ return null;
1073
+ } catch {
1074
+ return null;
1075
+ }
1076
+ };
1077
+
1078
+ // src/room.ts
1079
+ var identityToPid = (identity) => identity.startsWith("guest:") ? identity.slice("guest:".length) : identity;
1080
+ var joinRoom = async (opts) => {
1081
+ const exchangeUrl = `${opts.apiBase.replace(/\/+$/, "")}/v1/rooms/${encodeURIComponent(
1082
+ opts.roomId
1083
+ )}/join`;
1084
+ const exchangeRes = await fetch(exchangeUrl, {
1085
+ method: "POST",
1086
+ headers: { "Content-Type": "application/json" },
1087
+ body: JSON.stringify({ code: opts.joinCode, name: opts.name })
1088
+ });
1089
+ if (!exchangeRes.ok) {
1090
+ const err = await exchangeRes.json().catch(() => ({}));
1091
+ throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`);
1092
+ }
1093
+ const exchange = await exchangeRes.json();
1094
+ const handlers = /* @__PURE__ */ new Map();
1095
+ const emit = (e, payload) => {
1096
+ handlers.get(e)?.forEach((h) => {
1097
+ try {
1098
+ h(payload);
1099
+ } catch {
1100
+ }
1101
+ });
1102
+ };
1103
+ const room = new import_livekit_client.Room({ adaptiveStream: true, dynacast: true });
1104
+ room.on(
1105
+ import_livekit_client.RoomEvent.ParticipantConnected,
1106
+ (p) => emit("participant.joined", {
1107
+ participantId: identityToPid(p.identity),
1108
+ name: p.name ?? ""
1109
+ })
1110
+ );
1111
+ room.on(
1112
+ import_livekit_client.RoomEvent.ParticipantDisconnected,
1113
+ (p) => emit("participant.left", {
1114
+ participantId: identityToPid(p.identity),
1115
+ name: p.name ?? ""
1116
+ })
1117
+ );
1118
+ room.on(import_livekit_client.RoomEvent.Disconnected, () => emit("room.ended", void 0));
1119
+ room.on(import_livekit_client.RoomEvent.DataReceived, (data, _participant, _kind, topic) => {
1120
+ if (topic === SYSTEM_TOPIC) {
1121
+ const m = decodeSystem(data);
1122
+ if (m) emit("system.message", m);
1123
+ } else if (topic === TRANSCRIPT_TOPIC) {
1124
+ const m = decodeTranscript(data);
1125
+ if (m) emit("transcript.partial", m);
1126
+ }
1127
+ });
1128
+ const trackKind = (t) => t.kind === import_livekit_client.Track.Kind.Video ? "video" : "audio";
1129
+ const trackSource = (s) => {
1130
+ switch (s) {
1131
+ case import_livekit_client.Track.Source.Camera:
1132
+ return "camera";
1133
+ case import_livekit_client.Track.Source.Microphone:
1134
+ return "microphone";
1135
+ case import_livekit_client.Track.Source.ScreenShare:
1136
+ return "screen_share";
1137
+ case import_livekit_client.Track.Source.ScreenShareAudio:
1138
+ return "screen_share_audio";
1139
+ default:
1140
+ return "unknown";
1141
+ }
1142
+ };
1143
+ room.on(
1144
+ import_livekit_client.RoomEvent.TrackSubscribed,
1145
+ (track, pub, participant) => emit("track.subscribed", {
1146
+ participantId: identityToPid(participant.identity),
1147
+ kind: trackKind(track),
1148
+ source: trackSource(pub.source),
1149
+ track
1150
+ })
1151
+ );
1152
+ room.on(
1153
+ import_livekit_client.RoomEvent.TrackUnsubscribed,
1154
+ (track, pub, participant) => emit("track.unsubscribed", {
1155
+ participantId: identityToPid(participant.identity),
1156
+ kind: trackKind(track),
1157
+ source: trackSource(pub.source),
1158
+ track
1159
+ })
1160
+ );
1161
+ room.on(
1162
+ import_livekit_client.RoomEvent.ActiveSpeakersChanged,
1163
+ (speakers) => emit(
1164
+ "active.speakers",
1165
+ speakers.map((p) => identityToPid(p.identity))
1166
+ )
1167
+ );
1168
+ await room.connect(exchange.livekit.url, exchange.livekit.token);
1169
+ return {
1170
+ participantId: exchange.participantId,
1171
+ get participants() {
1172
+ return [...room.remoteParticipants.values()].map((p) => ({
1173
+ participantId: identityToPid(p.identity),
1174
+ name: p.name ?? ""
1175
+ }));
1176
+ },
1177
+ on(event, handler) {
1178
+ const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
1179
+ set.add(handler);
1180
+ handlers.set(event, set);
1181
+ },
1182
+ publishMic: async () => {
1183
+ await room.localParticipant.setMicrophoneEnabled(true);
1184
+ },
1185
+ publishCamera: async () => {
1186
+ await room.localParticipant.setCameraEnabled(true);
1187
+ },
1188
+ setMicEnabled: async (on) => {
1189
+ await room.localParticipant.setMicrophoneEnabled(on);
1190
+ },
1191
+ setCameraEnabled: async (on) => {
1192
+ await room.localParticipant.setCameraEnabled(on);
1193
+ },
1194
+ isMicEnabled: () => room.localParticipant.isMicrophoneEnabled,
1195
+ isCameraEnabled: () => room.localParticipant.isCameraEnabled,
1196
+ getLocalCameraTrack: () => room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.Camera)?.videoTrack ?? null,
1197
+ getRemoteTracks: () => {
1198
+ const out = [];
1199
+ for (const p of room.remoteParticipants.values()) {
1200
+ for (const pub of p.trackPublications.values()) {
1201
+ const track = pub.track;
1202
+ if (!track) continue;
1203
+ out.push({
1204
+ participantId: identityToPid(p.identity),
1205
+ kind: trackKind(track),
1206
+ source: trackSource(pub.source),
1207
+ track
1208
+ });
1209
+ }
1210
+ }
1211
+ return out;
1212
+ },
1213
+ setScreenShareEnabled: async (on, opts2) => {
1214
+ await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false });
1215
+ },
1216
+ isScreenShareEnabled: () => room.localParticipant.isScreenShareEnabled,
1217
+ getLocalScreenTrack: () => room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.ScreenShare)?.videoTrack ?? null,
1218
+ leave: async () => {
1219
+ await room.disconnect();
1220
+ }
1221
+ };
1222
+ };
1223
+
1224
+ // src/textSession.ts
1225
+ async function startTextSession(opts) {
1226
+ const f = opts.fetch ?? fetch;
1227
+ const tokenQs = `?token=${encodeURIComponent(opts.token)}`;
1228
+ const startUrl = `${opts.baseUrl}/v1/agents/${opts.agentId}/chat${tokenQs}`;
1229
+ const startBody = opts.text ? JSON.stringify({ text: opts.text }) : "{}";
1230
+ const res = await f(startUrl, {
1231
+ method: "POST",
1232
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
1233
+ body: startBody
1234
+ });
1235
+ if (!res.ok || !res.body) {
1236
+ const text = await res.text().catch(() => "");
1237
+ throw new Error(`startTextSession failed: ${res.status} ${text}`);
1238
+ }
1239
+ const iter = parseSse(res.body);
1240
+ let chatId = "";
1241
+ let callId = "";
1242
+ const buffered = [];
1243
+ const it = iter[Symbol.asyncIterator]();
1244
+ while (true) {
1245
+ const { value, done } = await it.next();
1246
+ if (done) break;
1247
+ if (value.type === "chat.started") {
1248
+ chatId = value.chatId;
1249
+ callId = value.callId;
1250
+ break;
1251
+ }
1252
+ buffered.push(value);
1253
+ }
1254
+ return {
1255
+ id: chatId,
1256
+ callId,
1257
+ greeting: replayThen(buffered, { [Symbol.asyncIterator]: () => it }),
1258
+ async send(text) {
1259
+ const r = await f(`${opts.baseUrl}/v1/chats/${chatId}/messages${tokenQs}`, {
1260
+ method: "POST",
1261
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
1262
+ body: JSON.stringify({ text })
1263
+ });
1264
+ if (!r.ok || !r.body) {
1265
+ const errText = await r.text().catch(() => "");
1266
+ throw new Error(`send failed: ${r.status} ${errText}`);
1267
+ }
1268
+ return parseSse(r.body);
1269
+ },
1270
+ async end() {
1271
+ await f(`${opts.baseUrl}/v1/calls/${callId}`, { method: "DELETE" });
1272
+ }
1273
+ };
1274
+ }
1275
+ async function* parseSse(body) {
1276
+ const reader = body.getReader();
1277
+ const decoder = new TextDecoder();
1278
+ let buf = "";
1279
+ while (true) {
1280
+ const { value, done } = await reader.read();
1281
+ if (done) return;
1282
+ buf += decoder.decode(value, { stream: true });
1283
+ let idx;
1284
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
1285
+ const chunk = buf.slice(0, idx);
1286
+ buf = buf.slice(idx + 2);
1287
+ let event = "message";
1288
+ let data = "";
1289
+ for (const line of chunk.split("\n")) {
1290
+ if (line.startsWith(":")) continue;
1291
+ if (line.startsWith("event:")) event = line.slice(6).trim();
1292
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
1293
+ }
1294
+ if (!data) continue;
1295
+ try {
1296
+ const parsed = JSON.parse(data);
1297
+ yield { type: event, ...parsed };
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ async function* replayThen(buffered, rest) {
1304
+ for (const x of buffered) yield x;
1305
+ for await (const x of rest) yield x;
1306
+ }
1307
+
1308
+ // src/incomingCall.ts
1309
+ var parseIncomingCall = (raw) => {
1310
+ if (typeof raw !== "object" || raw === null) {
1311
+ throw new Error("parseIncomingCall: payload must be an object");
1312
+ }
1313
+ const p = raw;
1314
+ if (typeof p.token !== "string" || !p.token.startsWith("ct_")) {
1315
+ throw new Error("parseIncomingCall: missing or invalid `token` (expected a ct_ string)");
1316
+ }
1317
+ if (typeof p.agentId !== "string" || p.agentId.length === 0) {
1318
+ throw new Error("parseIncomingCall: missing `agentId`");
1319
+ }
1320
+ const transport = p.transport === "webrtc" ? "webrtc" : "ws";
1321
+ const out = { token: p.token, agentId: p.agentId, transport };
1322
+ if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
1323
+ out.webrtcGatewayBase = p.webrtcGatewayBase;
1324
+ }
1325
+ if (typeof p.expiresAt === "number") out.expiresAt = p.expiresAt;
1326
+ if (typeof p.agentName === "string") out.agentName = p.agentName;
1327
+ if (typeof p.agentAvatarUrl === "string") out.agentAvatarUrl = p.agentAvatarUrl;
1328
+ return out;
1329
+ };
1330
+
1050
1331
  // src/browser.ts
1051
1332
  var browserWsFactory = (url) => new globalThis.WebSocket(url);
1052
1333
  var BrowserVoiceFactory = class {
@@ -1104,6 +1385,21 @@ var BrowserVoiceFactory = class {
1104
1385
  await client.start();
1105
1386
  return client;
1106
1387
  };
1388
+ // Multi-party rooms (Phase 7 video).
1389
+ //
1390
+ // The guest's browser calls this with the roomId + joinCode it parsed
1391
+ // out of the invite link. The SDK exchanges the code for a LiveKit
1392
+ // JWT against `${apiBase}/v1/rooms/:roomId/join` (an AUTH-EXEMPT
1393
+ // endpoint — the opaque code is the only credential), then connects
1394
+ // to LiveKit and returns a typed event surface.
1395
+ this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts });
1396
+ // Text-channel chat session (no microphone / audio).
1397
+ // Mint a `ct_` token with `channel: 'text'` on your backend, then call
1398
+ // this to open an SSE stream against the chat API.
1399
+ this.startTextSession = (opts) => startTextSession({
1400
+ ...opts,
1401
+ baseUrl: this.config.apiBase
1402
+ });
1107
1403
  this.config = config;
1108
1404
  }
1109
1405
  };
@@ -1118,6 +1414,9 @@ function configureVoiceClient(config) {
1118
1414
  createAudioPlayback,
1119
1415
  createProtocolState,
1120
1416
  createReconnectingWebSocket,
1121
- handleServerMessage
1417
+ handleServerMessage,
1418
+ joinRoom,
1419
+ parseIncomingCall,
1420
+ startTextSession
1122
1421
  });
1123
1422
  //# sourceMappingURL=browser.js.map