@oneinbox/web-sdk 0.1.0-beta.1 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,15 +52,18 @@ await oi.start({ token, serverUrl }); // from your backend
52
52
  | `start({ token, serverUrl })` | Connect with a token you minted server-side. |
53
53
  | `stop()` | End the call. |
54
54
  | `setMuted(muted)` / `isMuted()` | Toggle / read mic state. |
55
+ | `resumeAudio()` | Resume playback if the browser blocked autoplay (call from a click handler). |
55
56
  | `status` | `"idle" \| "connecting" \| "active" \| "ending"`. |
56
57
  | `underlyingRoom` | Escape hatch to the raw `livekit-client` `Room`. |
57
58
  | `on(event, cb)` / `once` / `off` | Typed event subscription; `on` returns an unsubscribe fn. |
58
59
 
60
+ > **Errors come through the `error` event, not exceptions.** `start()` never throws/rejects — it resolves once the attempt settles, and every failure (including "a call is already in progress") is delivered once via `oi.on("error", …)`. Check `status` or the `call-start` event for success.
61
+
59
62
  ### Events
60
63
 
61
- `call-start`, `call-end` (`reason?`), `status` (`CallStatus`), `connection-state` (LiveKit `ConnectionState`), `transcript` (`{ role, text, final, language? }`), `speech-start` / `speech-end` (`"user" | "agent"`), `volume-level` (`number`, 0..1), `error` (`{ code, message, cause? }`).
64
+ `call-start`, `call-end` (`reason?`), `status` (`CallStatus`), `connection-state` (LiveKit `ConnectionState`), `transcript` (`{ role, text, final, language? }` where `role` is `"user" | "agent" | "unknown"`), `speech-start` / `speech-end` (`TranscriptRole`), `volume-level` (`number`, 0..1), `error` (`{ code, message, cause? }`).
62
65
 
63
- Error codes: `TOKEN_FETCH_FAILED`, `ORIGIN_NOT_ALLOWED`, `INVALID_PUBLISHABLE_KEY`, `CONNECT_FAILED`, `MIC_PUBLISH_FAILED`, `MIC_TOGGLE_FAILED`, `DISCONNECTED` (plus server `error_code`s passed through from the token endpoint).
66
+ Error codes: `TOKEN_FETCH_FAILED`, `ORIGIN_NOT_ALLOWED`, `INVALID_PUBLISHABLE_KEY`, `CONNECT_FAILED`, `MIC_PUBLISH_FAILED`, `MIC_TOGGLE_FAILED`, `CALL_IN_PROGRESS`, `AUDIO_PLAYBACK_BLOCKED`, `DISCONNECTED` (plus server `error_code`s passed through from the token endpoint).
64
67
 
65
68
  ## License
66
69
 
package/dist/index.cjs CHANGED
@@ -1,8 +1,34 @@
1
- 'use strict';
2
-
3
- var livekitClient = require('livekit-client');
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
4
19
 
5
20
  // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ConnectionState: () => import_livekit_client.ConnectionState,
24
+ DisconnectReason: () => import_livekit_client.DisconnectReason,
25
+ OneInbox: () => OneInbox,
26
+ RoomEvent: () => import_livekit_client.RoomEvent,
27
+ Track: () => import_livekit_client.Track,
28
+ TypedEmitter: () => TypedEmitter
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var import_livekit_client = require("livekit-client");
6
32
 
7
33
  // src/emitter.ts
8
34
  var TypedEmitter = class {
@@ -51,6 +77,9 @@ var OneInbox = class extends TypedEmitter {
51
77
  _muted = false;
52
78
  volumeTimer = null;
53
79
  speaking = /* @__PURE__ */ new Set();
80
+ // Audio <audio> elements created for the agent's remote tracks, so we can
81
+ // play them and tear them down on call end.
82
+ audioElements = /* @__PURE__ */ new Set();
54
83
  /** Current high-level call status. */
55
84
  get status() {
56
85
  return this._status;
@@ -65,7 +94,8 @@ var OneInbox = class extends TypedEmitter {
65
94
  }
66
95
  async start(arg, opts) {
67
96
  if (this._status !== "idle") {
68
- throw new Error("A call is already in progress \u2014 call stop() first.");
97
+ this.fail("CALL_IN_PROGRESS", "A call is already in progress \u2014 call stop() first.");
98
+ return;
69
99
  }
70
100
  this.setStatus("connecting");
71
101
  try {
@@ -83,9 +113,8 @@ var OneInbox = class extends TypedEmitter {
83
113
  autoMic = arg.autoPublishMicrophone !== false;
84
114
  }
85
115
  await this.connectRoom(serverUrl, token, autoMic);
86
- } catch (err) {
116
+ } catch {
87
117
  this.setStatus("idle");
88
- throw err;
89
118
  }
90
119
  }
91
120
  /** End the call and release resources. The backend marks the call completed. */
@@ -97,12 +126,23 @@ var OneInbox = class extends TypedEmitter {
97
126
  this.setStatus("ending");
98
127
  await this.room.disconnect();
99
128
  }
129
+ /**
130
+ * Resume audio playback if the browser blocked autoplay (call from a click /
131
+ * tap handler). No-op when audio is already playing.
132
+ */
133
+ async resumeAudio() {
134
+ if (this.room && !this.room.canPlaybackAudio) {
135
+ await this.room.startAudio();
136
+ }
137
+ }
100
138
  /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */
101
139
  setMuted(muted) {
102
- this._muted = muted;
103
140
  const lp = this.room?.localParticipant;
141
+ const previous = this._muted;
142
+ this._muted = muted;
104
143
  if (!lp) return;
105
144
  void lp.setMicrophoneEnabled(!muted).catch((err) => {
145
+ this._muted = previous;
106
146
  this.emit("error", {
107
147
  code: "MIC_TOGGLE_FAILED",
108
148
  message: err?.message ?? "Failed to toggle microphone",
@@ -144,7 +184,7 @@ var OneInbox = class extends TypedEmitter {
144
184
  return res.json();
145
185
  }
146
186
  async connectRoom(serverUrl, token, autoMic) {
147
- const room = new livekitClient.Room({ adaptiveStream: true, dynacast: true });
187
+ const room = new import_livekit_client.Room({ adaptiveStream: true, dynacast: true });
148
188
  this.room = room;
149
189
  this.bindRoomEvents(room);
150
190
  try {
@@ -170,10 +210,32 @@ var OneInbox = class extends TypedEmitter {
170
210
  }
171
211
  }
172
212
  bindRoomEvents(room) {
173
- room.on(livekitClient.RoomEvent.ConnectionStateChanged, (state) => {
213
+ room.on(import_livekit_client.RoomEvent.ConnectionStateChanged, (state) => {
174
214
  this.emit("connection-state", state);
175
215
  });
176
- room.on(livekitClient.RoomEvent.TranscriptionReceived, (segments, participant) => {
216
+ room.on(import_livekit_client.RoomEvent.TrackSubscribed, (track) => {
217
+ if (track.kind !== import_livekit_client.Track.Kind.Audio) return;
218
+ const el = track.attach();
219
+ el.setAttribute("data-oneinbox", "agent-audio");
220
+ if (typeof document !== "undefined") document.body.appendChild(el);
221
+ this.audioElements.add(el);
222
+ });
223
+ room.on(import_livekit_client.RoomEvent.TrackUnsubscribed, (track) => {
224
+ if (track.kind !== import_livekit_client.Track.Kind.Audio) return;
225
+ for (const el of track.detach()) {
226
+ el.remove();
227
+ this.audioElements.delete(el);
228
+ }
229
+ });
230
+ room.on(import_livekit_client.RoomEvent.AudioPlaybackStatusChanged, () => {
231
+ if (!room.canPlaybackAudio) {
232
+ this.emit("error", {
233
+ code: "AUDIO_PLAYBACK_BLOCKED",
234
+ message: "Browser blocked audio autoplay \u2014 call resumeAudio() after a user gesture."
235
+ });
236
+ }
237
+ });
238
+ room.on(import_livekit_client.RoomEvent.TranscriptionReceived, (segments, participant) => {
177
239
  const role = roleFor(participant);
178
240
  for (const seg of segments) {
179
241
  this.emit("transcript", {
@@ -184,7 +246,7 @@ var OneInbox = class extends TypedEmitter {
184
246
  });
185
247
  }
186
248
  });
187
- room.on(livekitClient.RoomEvent.ActiveSpeakersChanged, (speakers) => {
249
+ room.on(import_livekit_client.RoomEvent.ActiveSpeakersChanged, (speakers) => {
188
250
  const now = new Set(speakers.map(roleFor));
189
251
  for (const role of now) {
190
252
  if (!this.speaking.has(role)) this.emit("speech-start", role);
@@ -194,15 +256,15 @@ var OneInbox = class extends TypedEmitter {
194
256
  }
195
257
  this.speaking = now;
196
258
  });
197
- room.on(livekitClient.RoomEvent.Disconnected, (reason) => {
198
- const clientInitiated = reason === void 0 || reason === livekitClient.DisconnectReason.CLIENT_INITIATED;
259
+ room.on(import_livekit_client.RoomEvent.Disconnected, (reason) => {
260
+ const clientInitiated = reason === void 0 || reason === import_livekit_client.DisconnectReason.CLIENT_INITIATED;
199
261
  if (!clientInitiated) {
200
262
  this.emit("error", {
201
263
  code: "DISCONNECTED",
202
- message: `Room disconnected: ${livekitClient.DisconnectReason[reason] ?? reason}`
264
+ message: `Room disconnected: ${import_livekit_client.DisconnectReason[reason] ?? reason}`
203
265
  });
204
266
  }
205
- this.teardown(reason !== void 0 ? livekitClient.DisconnectReason[reason] : void 0);
267
+ this.teardown(reason !== void 0 ? import_livekit_client.DisconnectReason[reason] : void 0);
206
268
  });
207
269
  }
208
270
  startVolumeSampling() {
@@ -221,6 +283,12 @@ var OneInbox = class extends TypedEmitter {
221
283
  teardown(reason) {
222
284
  this.stopVolumeSampling();
223
285
  this.speaking.clear();
286
+ for (const el of this.audioElements) {
287
+ el.pause();
288
+ el.srcObject = null;
289
+ el.remove();
290
+ }
291
+ this.audioElements.clear();
224
292
  this.room = null;
225
293
  this.setStatus("idle");
226
294
  this.emit("call-end", reason);
@@ -237,27 +305,16 @@ var OneInbox = class extends TypedEmitter {
237
305
  }
238
306
  };
239
307
  function roleFor(participant) {
240
- if (!participant) return "agent";
308
+ if (!participant) return "unknown";
241
309
  return participant.isLocal ? "user" : "agent";
242
310
  }
243
-
244
- Object.defineProperty(exports, "ConnectionState", {
245
- enumerable: true,
246
- get: function () { return livekitClient.ConnectionState; }
247
- });
248
- Object.defineProperty(exports, "DisconnectReason", {
249
- enumerable: true,
250
- get: function () { return livekitClient.DisconnectReason; }
251
- });
252
- Object.defineProperty(exports, "RoomEvent", {
253
- enumerable: true,
254
- get: function () { return livekitClient.RoomEvent; }
255
- });
256
- Object.defineProperty(exports, "Track", {
257
- enumerable: true,
258
- get: function () { return livekitClient.Track; }
311
+ // Annotate the CommonJS export names for ESM import in node:
312
+ 0 && (module.exports = {
313
+ ConnectionState,
314
+ DisconnectReason,
315
+ OneInbox,
316
+ RoomEvent,
317
+ Track,
318
+ TypedEmitter
259
319
  });
260
- exports.OneInbox = OneInbox;
261
- exports.TypedEmitter = TypedEmitter;
262
- //# sourceMappingURL=index.cjs.map
263
320
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/emitter.ts","../src/index.ts"],"names":["Room","RoomEvent","DisconnectReason"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Tiny strongly-typed event emitter. Zero runtime deps so the SDK stays\n * tree-shakeable and installs without pulling `events`/`eventemitter3`.\n *\n * `E` maps event name → listener signature, e.g.\n * { \"call-start\": () => void; \"volume-level\": (v: number) => void }\n */\nexport type EventMap = Record<string, (...args: never[]) => void>;\n\nexport class TypedEmitter<E extends EventMap> {\n private readonly listeners: { [K in keyof E]?: Set<E[K]> } = {};\n\n /** Subscribe to `event`. Returns an unsubscribe function. */\n on<K extends keyof E>(event: K, fn: E[K]): () => void {\n (this.listeners[event] ??= new Set<E[K]>()).add(fn);\n return () => this.off(event, fn);\n }\n\n /** Subscribe once; auto-unsubscribes after the first emission. */\n once<K extends keyof E>(event: K, fn: E[K]): () => void {\n const wrapper = ((...args: Parameters<E[K]>) => {\n this.off(event, wrapper);\n (fn as (...a: Parameters<E[K]>) => void)(...args);\n }) as E[K];\n return this.on(event, wrapper);\n }\n\n /** Remove a specific listener. */\n off<K extends keyof E>(event: K, fn: E[K]): void {\n this.listeners[event]?.delete(fn);\n }\n\n /** Remove all listeners (all events, or just `event`). */\n removeAllListeners<K extends keyof E>(event?: K): void {\n if (event) this.listeners[event]?.clear();\n else (Object.keys(this.listeners) as K[]).forEach((k) => this.listeners[k]?.clear());\n }\n\n protected emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void {\n const set = this.listeners[event];\n if (!set) return;\n // Copy so a listener that unsubscribes mid-emit doesn't skip the next one.\n for (const fn of [...set]) (fn as (...a: Parameters<E[K]>) => void)(...args);\n }\n}\n","import {\n ConnectionState,\n DisconnectReason,\n type LocalParticipant,\n type Participant,\n type RemoteParticipant,\n Room,\n RoomEvent,\n Track,\n type TranscriptionSegment,\n} from \"livekit-client\";\n\nimport { TypedEmitter } from \"./emitter\";\n\nexport type TranscriptRole = \"user\" | \"agent\";\n\nexport interface TranscriptEvent {\n role: TranscriptRole;\n text: string;\n final: boolean;\n language?: string;\n}\n\nexport interface OneInboxError {\n /** Stable machine code, e.g. CONNECT_FAILED, ORIGIN_NOT_ALLOWED, MIC_PUBLISH_FAILED. */\n code: string;\n message: string;\n cause?: unknown;\n}\n\n/** High-level call lifecycle, mirrored to the `status` event. */\nexport type CallStatus = \"idle\" | \"connecting\" | \"active\" | \"ending\";\n\n/**\n * Typed event map. Listen with `oi.on(\"transcript\", cb)` — the callback type\n * is inferred per event (Vapi/LiveKit style).\n */\nexport type OneInboxEvents = {\n /** Connected to the room; the agent is joining. */\n \"call-start\": () => void;\n /** Call ended (locally or remotely). `reason` is the LiveKit disconnect reason when known. */\n \"call-end\": (reason?: string) => void;\n /** Underlying LiveKit connection state changed. */\n \"connection-state\": (state: ConnectionState) => void;\n /** High-level status changed. */\n status: (status: CallStatus) => void;\n /** Incremental transcript chunk for the caller or the agent (`final` marks end-of-utterance). */\n transcript: (ev: TranscriptEvent) => void;\n /** Someone started speaking (best-effort, from active-speaker detection). */\n \"speech-start\": (who: TranscriptRole) => void;\n /** Someone stopped speaking. */\n \"speech-end\": (who: TranscriptRole) => void;\n /** Local microphone level, 0..1, sampled ~10x/sec while active (for mic UIs). */\n \"volume-level\": (level: number) => void;\n /** Any error (connection, token fetch, mic publish, server disconnect). */\n error: (err: OneInboxError) => void;\n};\n\nexport interface OneInboxConfig {\n /** OneInbox API base URL. Default `https://api.oneinbox.ai`. */\n baseUrl?: string;\n}\n\nexport interface StartOptions {\n /** Per-call template variables interpolated into the agent prompt. */\n variables?: Record<string, unknown>;\n /** Free-form metadata stored on the call record. */\n metadata?: Record<string, unknown>;\n /** Auto-publish the user's microphone on connect. Default `true`. */\n autoPublishMicrophone?: boolean;\n}\n\n/** Bring-your-own-token escape hatch: connect with a token your backend minted. */\nexport interface TokenStartOptions {\n /** From `POST /v1/web-calls/token` (or `/v1/calls/web`) → `participant_token`. */\n token: string;\n /** → `server_url`. */\n serverUrl: string;\n autoPublishMicrophone?: boolean;\n}\n\nconst VOLUME_SAMPLE_MS = 100;\n\n/**\n * Browser client for OneInbox voice agents over WebRTC.\n *\n * ```ts\n * const oi = new OneInbox(\"oi_pk_…\");\n * oi.on(\"transcript\", (t) => console.log(t.role, t.text));\n * await oi.start(agentId); // mic publishes, the agent answers\n * // …\n * await oi.stop();\n * ```\n */\nexport class OneInbox extends TypedEmitter<OneInboxEvents> {\n private room: Room | null = null;\n private _status: CallStatus = \"idle\";\n private _muted = false;\n private volumeTimer: ReturnType<typeof setInterval> | null = null;\n private speaking = new Set<TranscriptRole>();\n\n constructor(\n private readonly publishableKey: string,\n private readonly config: OneInboxConfig = {},\n ) {\n super();\n }\n\n /** Current high-level call status. */\n get status(): CallStatus {\n return this._status;\n }\n\n /** Whether the local microphone is currently muted. */\n isMuted(): boolean {\n return this._muted;\n }\n\n /** Underlying LiveKit room (escape hatch for advanced use). */\n get underlyingRoom(): Room | null {\n return this.room;\n }\n\n /**\n * Start a call.\n *\n * - `start(agentId, opts?)` — publishable-key path: fetches a short-lived\n * token from the OneInbox API, then connects.\n * - `start({ token, serverUrl })` — connect with a token your backend minted.\n */\n async start(agentId: string, opts?: StartOptions): Promise<void>;\n async start(opts: TokenStartOptions): Promise<void>;\n async start(arg: string | TokenStartOptions, opts?: StartOptions): Promise<void> {\n if (this._status !== \"idle\") {\n throw new Error(\"A call is already in progress — call stop() first.\");\n }\n this.setStatus(\"connecting\");\n try {\n let serverUrl: string;\n let token: string;\n let autoMic: boolean;\n if (typeof arg === \"string\") {\n const res = await this.fetchToken(arg, opts);\n serverUrl = res.server_url;\n token = res.participant_token;\n autoMic = opts?.autoPublishMicrophone !== false;\n } else {\n serverUrl = arg.serverUrl;\n token = arg.token;\n autoMic = arg.autoPublishMicrophone !== false;\n }\n await this.connectRoom(serverUrl, token, autoMic);\n } catch (err) {\n this.setStatus(\"idle\");\n throw err;\n }\n }\n\n /** End the call and release resources. The backend marks the call completed. */\n async stop(): Promise<void> {\n if (!this.room) {\n this.setStatus(\"idle\");\n return;\n }\n this.setStatus(\"ending\");\n await this.room.disconnect();\n // teardown is finalized in the Disconnected handler\n }\n\n /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */\n setMuted(muted: boolean): void {\n this._muted = muted;\n const lp = this.room?.localParticipant;\n if (!lp) return;\n void lp.setMicrophoneEnabled(!muted).catch((err: unknown) => {\n this.emit(\"error\", {\n code: \"MIC_TOGGLE_FAILED\",\n message: (err as Error)?.message ?? \"Failed to toggle microphone\",\n cause: err,\n });\n });\n }\n\n // ---- internals -------------------------------------------------------\n\n private async fetchToken(\n agentId: string,\n opts?: StartOptions,\n ): Promise<{ server_url: string; participant_token: string }> {\n const base = (this.config.baseUrl ?? \"https://api.oneinbox.ai\").replace(/\\/+$/, \"\");\n let res: Response;\n try {\n res = await fetch(`${base}/v1/web-calls/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.publishableKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n agent_id: agentId,\n variables: opts?.variables ?? {},\n metadata: opts?.metadata ?? {},\n }),\n });\n } catch (err) {\n throw this.fail(\"TOKEN_FETCH_FAILED\", (err as Error)?.message ?? \"Network error\", err);\n }\n if (!res.ok) {\n let code = `HTTP_${res.status}`;\n let detail = res.statusText;\n try {\n const body = (await res.json()) as { error_code?: string; detail?: string };\n code = body.error_code ?? code;\n detail = body.detail ?? detail;\n } catch {\n /* non-JSON error body */\n }\n throw this.fail(code, detail);\n }\n return res.json() as Promise<{ server_url: string; participant_token: string }>;\n }\n\n private async connectRoom(serverUrl: string, token: string, autoMic: boolean): Promise<void> {\n const room = new Room({ adaptiveStream: true, dynacast: true });\n this.room = room;\n this.bindRoomEvents(room);\n\n try {\n await room.connect(serverUrl, token);\n } catch (err) {\n this.room = null;\n throw this.fail(\"CONNECT_FAILED\", (err as Error)?.message ?? \"Connection failed\", err);\n }\n\n this.setStatus(\"active\");\n this.emit(\"call-start\");\n this.startVolumeSampling();\n\n if (autoMic) {\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n this._muted = false;\n } catch (err) {\n this.emit(\"error\", {\n code: \"MIC_PUBLISH_FAILED\",\n message: (err as Error)?.message ?? \"Failed to publish microphone\",\n cause: err,\n });\n }\n }\n }\n\n private bindRoomEvents(room: Room): void {\n room.on(RoomEvent.ConnectionStateChanged, (state) => {\n this.emit(\"connection-state\", state);\n });\n\n room.on(RoomEvent.TranscriptionReceived, (segments, participant) => {\n const role = roleFor(participant);\n for (const seg of segments) {\n this.emit(\"transcript\", {\n role,\n text: seg.text,\n final: seg.final,\n language: seg.language,\n });\n }\n });\n\n room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {\n const now = new Set<TranscriptRole>(speakers.map(roleFor));\n for (const role of now) {\n if (!this.speaking.has(role)) this.emit(\"speech-start\", role);\n }\n for (const role of this.speaking) {\n if (!now.has(role)) this.emit(\"speech-end\", role);\n }\n this.speaking = now;\n });\n\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n const clientInitiated =\n reason === undefined || reason === DisconnectReason.CLIENT_INITIATED;\n if (!clientInitiated) {\n this.emit(\"error\", {\n code: \"DISCONNECTED\",\n message: `Room disconnected: ${DisconnectReason[reason] ?? reason}`,\n });\n }\n this.teardown(reason !== undefined ? DisconnectReason[reason] : undefined);\n });\n }\n\n private startVolumeSampling(): void {\n this.stopVolumeSampling();\n this.volumeTimer = setInterval(() => {\n const lp = this.room?.localParticipant;\n if (lp) this.emit(\"volume-level\", lp.audioLevel);\n }, VOLUME_SAMPLE_MS);\n }\n\n private stopVolumeSampling(): void {\n if (this.volumeTimer !== null) {\n clearInterval(this.volumeTimer);\n this.volumeTimer = null;\n }\n }\n\n private teardown(reason?: string): void {\n this.stopVolumeSampling();\n this.speaking.clear();\n this.room = null;\n this.setStatus(\"idle\");\n this.emit(\"call-end\", reason);\n }\n\n private setStatus(status: CallStatus): void {\n if (this._status === status) return;\n this._status = status;\n this.emit(\"status\", status);\n }\n\n private fail(code: string, message: string, cause?: unknown): OneInboxError {\n const err: OneInboxError = { code, message, cause };\n this.emit(\"error\", err);\n return err;\n }\n}\n\nfunction roleFor(participant: Participant | undefined): TranscriptRole {\n // The human is the local participant; the agent joins as a remote participant.\n if (!participant) return \"agent\";\n return participant.isLocal ? \"user\" : \"agent\";\n}\n\n// Re-export upstream types callers commonly need so they don't have to add a\n// direct livekit-client import just for types.\nexport { ConnectionState, DisconnectReason, RoomEvent, Track };\nexport type { LocalParticipant, Participant, RemoteParticipant, Room, TranscriptionSegment };\nexport { TypedEmitter } from \"./emitter\";\n"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/emitter.ts"],"sourcesContent":["import {\n ConnectionState,\n DisconnectReason,\n type LocalParticipant,\n type Participant,\n type RemoteParticipant,\n Room,\n RoomEvent,\n Track,\n type TranscriptionSegment,\n} from \"livekit-client\";\n\nimport { TypedEmitter } from \"./emitter\";\n\nexport type TranscriptRole = \"user\" | \"agent\" | \"unknown\";\n\nexport interface TranscriptEvent {\n role: TranscriptRole;\n text: string;\n final: boolean;\n language?: string;\n}\n\nexport interface OneInboxError {\n /** Stable machine code, e.g. CONNECT_FAILED, ORIGIN_NOT_ALLOWED, MIC_PUBLISH_FAILED. */\n code: string;\n message: string;\n cause?: unknown;\n}\n\n/** High-level call lifecycle, mirrored to the `status` event. */\nexport type CallStatus = \"idle\" | \"connecting\" | \"active\" | \"ending\";\n\n/**\n * Typed event map. Listen with `oi.on(\"transcript\", cb)` — the callback type\n * is inferred per event (Vapi/LiveKit style).\n */\nexport type OneInboxEvents = {\n /** Connected to the room; the agent is joining. */\n \"call-start\": () => void;\n /** Call ended (locally or remotely). `reason` is the LiveKit disconnect reason when known. */\n \"call-end\": (reason?: string) => void;\n /** Underlying LiveKit connection state changed. */\n \"connection-state\": (state: ConnectionState) => void;\n /** High-level status changed. */\n status: (status: CallStatus) => void;\n /** Incremental transcript chunk for the caller or the agent (`final` marks end-of-utterance). */\n transcript: (ev: TranscriptEvent) => void;\n /** Someone started speaking (best-effort, from active-speaker detection). */\n \"speech-start\": (who: TranscriptRole) => void;\n /** Someone stopped speaking. */\n \"speech-end\": (who: TranscriptRole) => void;\n /** Local microphone level, 0..1, sampled ~10x/sec while active (for mic UIs). */\n \"volume-level\": (level: number) => void;\n /** Any error (connection, token fetch, mic publish, server disconnect). */\n error: (err: OneInboxError) => void;\n};\n\nexport interface OneInboxConfig {\n /** OneInbox API base URL. Default `https://api.oneinbox.ai`. */\n baseUrl?: string;\n}\n\nexport interface StartOptions {\n /** Per-call template variables interpolated into the agent prompt. */\n variables?: Record<string, unknown>;\n /** Free-form metadata stored on the call record. */\n metadata?: Record<string, unknown>;\n /** Auto-publish the user's microphone on connect. Default `true`. */\n autoPublishMicrophone?: boolean;\n}\n\n/** Bring-your-own-token escape hatch: connect with a token your backend minted. */\nexport interface TokenStartOptions {\n /** From `POST /v1/web-calls/token` (or `/v1/calls/web`) → `participant_token`. */\n token: string;\n /** → `server_url`. */\n serverUrl: string;\n autoPublishMicrophone?: boolean;\n}\n\nconst VOLUME_SAMPLE_MS = 100;\n\n/**\n * Browser client for OneInbox voice agents over WebRTC.\n *\n * ```ts\n * const oi = new OneInbox(\"oi_pk_…\");\n * oi.on(\"transcript\", (t) => console.log(t.role, t.text));\n * await oi.start(agentId); // mic publishes, the agent answers\n * // …\n * await oi.stop();\n * ```\n */\nexport class OneInbox extends TypedEmitter<OneInboxEvents> {\n private room: Room | null = null;\n private _status: CallStatus = \"idle\";\n private _muted = false;\n private volumeTimer: ReturnType<typeof setInterval> | null = null;\n private speaking = new Set<TranscriptRole>();\n // Audio <audio> elements created for the agent's remote tracks, so we can\n // play them and tear them down on call end.\n private audioElements = new Set<HTMLMediaElement>();\n\n constructor(\n private readonly publishableKey: string,\n private readonly config: OneInboxConfig = {},\n ) {\n super();\n }\n\n /** Current high-level call status. */\n get status(): CallStatus {\n return this._status;\n }\n\n /** Whether the local microphone is currently muted. */\n isMuted(): boolean {\n return this._muted;\n }\n\n /** Underlying LiveKit room (escape hatch for advanced use). */\n get underlyingRoom(): Room | null {\n return this.room;\n }\n\n /**\n * Start a call.\n *\n * - `start(agentId, opts?)` — publishable-key path: fetches a short-lived\n * token from the OneInbox API, then connects.\n * - `start({ token, serverUrl })` — connect with a token your backend minted.\n *\n * Errors are delivered through the `error` event (never thrown), so a single\n * `oi.on(\"error\", …)` handler catches every failure path consistently. The\n * returned promise resolves once the attempt settles (check `status` or the\n * `call-start` event for success).\n */\n async start(agentId: string, opts?: StartOptions): Promise<void>;\n async start(opts: TokenStartOptions): Promise<void>;\n async start(arg: string | TokenStartOptions, opts?: StartOptions): Promise<void> {\n if (this._status !== \"idle\") {\n // Bug-fix: emit \"error\" like every other failure path instead of throwing\n // (a throw here was an unhandled rejection for `oi.on(\"error\")` users).\n this.fail(\"CALL_IN_PROGRESS\", \"A call is already in progress — call stop() first.\");\n return;\n }\n this.setStatus(\"connecting\");\n try {\n let serverUrl: string;\n let token: string;\n let autoMic: boolean;\n if (typeof arg === \"string\") {\n const res = await this.fetchToken(arg, opts);\n serverUrl = res.server_url;\n token = res.participant_token;\n autoMic = opts?.autoPublishMicrophone !== false;\n } else {\n serverUrl = arg.serverUrl;\n token = arg.token;\n autoMic = arg.autoPublishMicrophone !== false;\n }\n await this.connectRoom(serverUrl, token, autoMic);\n } catch {\n // fetchToken / connectRoom already emitted \"error\" via fail(); just reset\n // status. We deliberately do NOT re-throw — that double-delivered the\n // same failure (once via the event, once via the rejection).\n this.setStatus(\"idle\");\n }\n }\n\n /** End the call and release resources. The backend marks the call completed. */\n async stop(): Promise<void> {\n if (!this.room) {\n this.setStatus(\"idle\");\n return;\n }\n this.setStatus(\"ending\");\n await this.room.disconnect();\n // teardown is finalized in the Disconnected handler\n }\n\n /**\n * Resume audio playback if the browser blocked autoplay (call from a click /\n * tap handler). No-op when audio is already playing.\n */\n async resumeAudio(): Promise<void> {\n if (this.room && !this.room.canPlaybackAudio) {\n await this.room.startAudio();\n }\n }\n\n /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */\n setMuted(muted: boolean): void {\n const lp = this.room?.localParticipant;\n const previous = this._muted;\n this._muted = muted;\n if (!lp) return;\n void lp.setMicrophoneEnabled(!muted).catch((err: unknown) => {\n // Bug-fix: the track toggle failed, so isMuted() must not keep reporting\n // the requested-but-never-applied state — revert to the previous value.\n this._muted = previous;\n this.emit(\"error\", {\n code: \"MIC_TOGGLE_FAILED\",\n message: (err as Error)?.message ?? \"Failed to toggle microphone\",\n cause: err,\n });\n });\n }\n\n // ---- internals -------------------------------------------------------\n\n private async fetchToken(\n agentId: string,\n opts?: StartOptions,\n ): Promise<{ server_url: string; participant_token: string }> {\n const base = (this.config.baseUrl ?? \"https://api.oneinbox.ai\").replace(/\\/+$/, \"\");\n let res: Response;\n try {\n res = await fetch(`${base}/v1/web-calls/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.publishableKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n agent_id: agentId,\n variables: opts?.variables ?? {},\n metadata: opts?.metadata ?? {},\n }),\n });\n } catch (err) {\n throw this.fail(\"TOKEN_FETCH_FAILED\", (err as Error)?.message ?? \"Network error\", err);\n }\n if (!res.ok) {\n let code = `HTTP_${res.status}`;\n let detail = res.statusText;\n try {\n const body = (await res.json()) as { error_code?: string; detail?: string };\n code = body.error_code ?? code;\n detail = body.detail ?? detail;\n } catch {\n /* non-JSON error body */\n }\n throw this.fail(code, detail);\n }\n return res.json() as Promise<{ server_url: string; participant_token: string }>;\n }\n\n private async connectRoom(serverUrl: string, token: string, autoMic: boolean): Promise<void> {\n const room = new Room({ adaptiveStream: true, dynacast: true });\n this.room = room;\n this.bindRoomEvents(room);\n\n try {\n await room.connect(serverUrl, token);\n } catch (err) {\n this.room = null;\n throw this.fail(\"CONNECT_FAILED\", (err as Error)?.message ?? \"Connection failed\", err);\n }\n\n this.setStatus(\"active\");\n this.emit(\"call-start\");\n this.startVolumeSampling();\n\n if (autoMic) {\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n this._muted = false;\n } catch (err) {\n this.emit(\"error\", {\n code: \"MIC_PUBLISH_FAILED\",\n message: (err as Error)?.message ?? \"Failed to publish microphone\",\n cause: err,\n });\n }\n }\n }\n\n private bindRoomEvents(room: Room): void {\n room.on(RoomEvent.ConnectionStateChanged, (state) => {\n this.emit(\"connection-state\", state);\n });\n\n // Play the agent's audio. livekit-client does NOT auto-play remote tracks —\n // we must attach each subscribed audio track to an <audio> element. Without\n // this the agent is heard by no one even though transcripts arrive.\n room.on(RoomEvent.TrackSubscribed, (track) => {\n if (track.kind !== Track.Kind.Audio) return;\n const el = track.attach(); // creates an autoplaying <audio> element\n el.setAttribute(\"data-oneinbox\", \"agent-audio\");\n // Some browsers need the element in the DOM to actually play.\n if (typeof document !== \"undefined\") document.body.appendChild(el);\n this.audioElements.add(el);\n });\n\n room.on(RoomEvent.TrackUnsubscribed, (track) => {\n if (track.kind !== Track.Kind.Audio) return;\n for (const el of track.detach()) {\n el.remove();\n this.audioElements.delete(el);\n }\n });\n\n // If the browser blocks autoplay (no prior gesture), surface it so the app\n // can prompt a tap. Calling startAudio() resumes playback.\n room.on(RoomEvent.AudioPlaybackStatusChanged, () => {\n if (!room.canPlaybackAudio) {\n this.emit(\"error\", {\n code: \"AUDIO_PLAYBACK_BLOCKED\",\n message: \"Browser blocked audio autoplay — call resumeAudio() after a user gesture.\",\n });\n }\n });\n\n room.on(RoomEvent.TranscriptionReceived, (segments, participant) => {\n const role = roleFor(participant);\n for (const seg of segments) {\n this.emit(\"transcript\", {\n role,\n text: seg.text,\n final: seg.final,\n language: seg.language,\n });\n }\n });\n\n room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {\n const now = new Set<TranscriptRole>(speakers.map(roleFor));\n for (const role of now) {\n if (!this.speaking.has(role)) this.emit(\"speech-start\", role);\n }\n for (const role of this.speaking) {\n if (!now.has(role)) this.emit(\"speech-end\", role);\n }\n this.speaking = now;\n });\n\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n const clientInitiated =\n reason === undefined || reason === DisconnectReason.CLIENT_INITIATED;\n if (!clientInitiated) {\n this.emit(\"error\", {\n code: \"DISCONNECTED\",\n message: `Room disconnected: ${DisconnectReason[reason] ?? reason}`,\n });\n }\n this.teardown(reason !== undefined ? DisconnectReason[reason] : undefined);\n });\n }\n\n private startVolumeSampling(): void {\n this.stopVolumeSampling();\n this.volumeTimer = setInterval(() => {\n const lp = this.room?.localParticipant;\n if (lp) this.emit(\"volume-level\", lp.audioLevel);\n }, VOLUME_SAMPLE_MS);\n }\n\n private stopVolumeSampling(): void {\n if (this.volumeTimer !== null) {\n clearInterval(this.volumeTimer);\n this.volumeTimer = null;\n }\n }\n\n private teardown(reason?: string): void {\n this.stopVolumeSampling();\n this.speaking.clear();\n for (const el of this.audioElements) {\n el.pause();\n el.srcObject = null;\n el.remove();\n }\n this.audioElements.clear();\n this.room = null;\n this.setStatus(\"idle\");\n this.emit(\"call-end\", reason);\n }\n\n private setStatus(status: CallStatus): void {\n if (this._status === status) return;\n this._status = status;\n this.emit(\"status\", status);\n }\n\n private fail(code: string, message: string, cause?: unknown): OneInboxError {\n const err: OneInboxError = { code, message, cause };\n this.emit(\"error\", err);\n return err;\n }\n}\n\nfunction roleFor(participant: Participant | undefined): TranscriptRole {\n // The human is the local participant; the agent joins as a remote participant.\n // Bug-fix: when LiveKit fires without a participant we genuinely don't know\n // who spoke — return \"unknown\" rather than mislabelling it as the agent.\n if (!participant) return \"unknown\";\n return participant.isLocal ? \"user\" : \"agent\";\n}\n\n// Re-export upstream types callers commonly need so they don't have to add a\n// direct livekit-client import just for types.\nexport { ConnectionState, DisconnectReason, RoomEvent, Track };\nexport type { LocalParticipant, Participant, RemoteParticipant, Room, TranscriptionSegment };\nexport { TypedEmitter } from \"./emitter\";\n","/**\n * Tiny strongly-typed event emitter. Zero runtime deps so the SDK stays\n * tree-shakeable and installs without pulling `events`/`eventemitter3`.\n *\n * `E` maps event name → listener signature, e.g.\n * { \"call-start\": () => void; \"volume-level\": (v: number) => void }\n */\nexport type EventMap = Record<string, (...args: never[]) => void>;\n\nexport class TypedEmitter<E extends EventMap> {\n private readonly listeners: { [K in keyof E]?: Set<E[K]> } = {};\n\n /** Subscribe to `event`. Returns an unsubscribe function. */\n on<K extends keyof E>(event: K, fn: E[K]): () => void {\n (this.listeners[event] ??= new Set<E[K]>()).add(fn);\n return () => this.off(event, fn);\n }\n\n /** Subscribe once; auto-unsubscribes after the first emission. */\n once<K extends keyof E>(event: K, fn: E[K]): () => void {\n const wrapper = ((...args: Parameters<E[K]>) => {\n this.off(event, wrapper);\n (fn as (...a: Parameters<E[K]>) => void)(...args);\n }) as E[K];\n return this.on(event, wrapper);\n }\n\n /** Remove a specific listener. */\n off<K extends keyof E>(event: K, fn: E[K]): void {\n this.listeners[event]?.delete(fn);\n }\n\n /** Remove all listeners (all events, or just `event`). */\n removeAllListeners<K extends keyof E>(event?: K): void {\n if (event) this.listeners[event]?.clear();\n else (Object.keys(this.listeners) as K[]).forEach((k) => this.listeners[k]?.clear());\n }\n\n protected emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void {\n const set = this.listeners[event];\n if (!set) return;\n // Copy so a listener that unsubscribes mid-emit doesn't skip the next one.\n for (const fn of [...set]) (fn as (...a: Parameters<E[K]>) => void)(...args);\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -22,7 +22,7 @@ declare class TypedEmitter<E extends EventMap> {
22
22
  protected emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void;
23
23
  }
24
24
 
25
- type TranscriptRole = "user" | "agent";
25
+ type TranscriptRole = "user" | "agent" | "unknown";
26
26
  interface TranscriptEvent {
27
27
  role: TranscriptRole;
28
28
  text: string;
@@ -100,6 +100,7 @@ declare class OneInbox extends TypedEmitter<OneInboxEvents> {
100
100
  private _muted;
101
101
  private volumeTimer;
102
102
  private speaking;
103
+ private audioElements;
103
104
  constructor(publishableKey: string, config?: OneInboxConfig);
104
105
  /** Current high-level call status. */
105
106
  get status(): CallStatus;
@@ -113,11 +114,21 @@ declare class OneInbox extends TypedEmitter<OneInboxEvents> {
113
114
  * - `start(agentId, opts?)` — publishable-key path: fetches a short-lived
114
115
  * token from the OneInbox API, then connects.
115
116
  * - `start({ token, serverUrl })` — connect with a token your backend minted.
117
+ *
118
+ * Errors are delivered through the `error` event (never thrown), so a single
119
+ * `oi.on("error", …)` handler catches every failure path consistently. The
120
+ * returned promise resolves once the attempt settles (check `status` or the
121
+ * `call-start` event for success).
116
122
  */
117
123
  start(agentId: string, opts?: StartOptions): Promise<void>;
118
124
  start(opts: TokenStartOptions): Promise<void>;
119
125
  /** End the call and release resources. The backend marks the call completed. */
120
126
  stop(): Promise<void>;
127
+ /**
128
+ * Resume audio playback if the browser blocked autoplay (call from a click /
129
+ * tap handler). No-op when audio is already playing.
130
+ */
131
+ resumeAudio(): Promise<void>;
121
132
  /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */
122
133
  setMuted(muted: boolean): void;
123
134
  private fetchToken;
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ declare class TypedEmitter<E extends EventMap> {
22
22
  protected emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void;
23
23
  }
24
24
 
25
- type TranscriptRole = "user" | "agent";
25
+ type TranscriptRole = "user" | "agent" | "unknown";
26
26
  interface TranscriptEvent {
27
27
  role: TranscriptRole;
28
28
  text: string;
@@ -100,6 +100,7 @@ declare class OneInbox extends TypedEmitter<OneInboxEvents> {
100
100
  private _muted;
101
101
  private volumeTimer;
102
102
  private speaking;
103
+ private audioElements;
103
104
  constructor(publishableKey: string, config?: OneInboxConfig);
104
105
  /** Current high-level call status. */
105
106
  get status(): CallStatus;
@@ -113,11 +114,21 @@ declare class OneInbox extends TypedEmitter<OneInboxEvents> {
113
114
  * - `start(agentId, opts?)` — publishable-key path: fetches a short-lived
114
115
  * token from the OneInbox API, then connects.
115
116
  * - `start({ token, serverUrl })` — connect with a token your backend minted.
117
+ *
118
+ * Errors are delivered through the `error` event (never thrown), so a single
119
+ * `oi.on("error", …)` handler catches every failure path consistently. The
120
+ * returned promise resolves once the attempt settles (check `status` or the
121
+ * `call-start` event for success).
116
122
  */
117
123
  start(agentId: string, opts?: StartOptions): Promise<void>;
118
124
  start(opts: TokenStartOptions): Promise<void>;
119
125
  /** End the call and release resources. The backend marks the call completed. */
120
126
  stop(): Promise<void>;
127
+ /**
128
+ * Resume audio playback if the browser blocked autoplay (call from a click /
129
+ * tap handler). No-op when audio is already playing.
130
+ */
131
+ resumeAudio(): Promise<void>;
121
132
  /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */
122
133
  setMuted(muted: boolean): void;
123
134
  private fetchToken;
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
- import { Room, RoomEvent, DisconnectReason } from 'livekit-client';
2
- export { ConnectionState, DisconnectReason, RoomEvent, Track } from 'livekit-client';
3
-
4
1
  // src/index.ts
2
+ import {
3
+ ConnectionState,
4
+ DisconnectReason,
5
+ Room,
6
+ RoomEvent,
7
+ Track
8
+ } from "livekit-client";
5
9
 
6
10
  // src/emitter.ts
7
11
  var TypedEmitter = class {
@@ -50,6 +54,9 @@ var OneInbox = class extends TypedEmitter {
50
54
  _muted = false;
51
55
  volumeTimer = null;
52
56
  speaking = /* @__PURE__ */ new Set();
57
+ // Audio <audio> elements created for the agent's remote tracks, so we can
58
+ // play them and tear them down on call end.
59
+ audioElements = /* @__PURE__ */ new Set();
53
60
  /** Current high-level call status. */
54
61
  get status() {
55
62
  return this._status;
@@ -64,7 +71,8 @@ var OneInbox = class extends TypedEmitter {
64
71
  }
65
72
  async start(arg, opts) {
66
73
  if (this._status !== "idle") {
67
- throw new Error("A call is already in progress \u2014 call stop() first.");
74
+ this.fail("CALL_IN_PROGRESS", "A call is already in progress \u2014 call stop() first.");
75
+ return;
68
76
  }
69
77
  this.setStatus("connecting");
70
78
  try {
@@ -82,9 +90,8 @@ var OneInbox = class extends TypedEmitter {
82
90
  autoMic = arg.autoPublishMicrophone !== false;
83
91
  }
84
92
  await this.connectRoom(serverUrl, token, autoMic);
85
- } catch (err) {
93
+ } catch {
86
94
  this.setStatus("idle");
87
- throw err;
88
95
  }
89
96
  }
90
97
  /** End the call and release resources. The backend marks the call completed. */
@@ -96,12 +103,23 @@ var OneInbox = class extends TypedEmitter {
96
103
  this.setStatus("ending");
97
104
  await this.room.disconnect();
98
105
  }
106
+ /**
107
+ * Resume audio playback if the browser blocked autoplay (call from a click /
108
+ * tap handler). No-op when audio is already playing.
109
+ */
110
+ async resumeAudio() {
111
+ if (this.room && !this.room.canPlaybackAudio) {
112
+ await this.room.startAudio();
113
+ }
114
+ }
99
115
  /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */
100
116
  setMuted(muted) {
101
- this._muted = muted;
102
117
  const lp = this.room?.localParticipant;
118
+ const previous = this._muted;
119
+ this._muted = muted;
103
120
  if (!lp) return;
104
121
  void lp.setMicrophoneEnabled(!muted).catch((err) => {
122
+ this._muted = previous;
105
123
  this.emit("error", {
106
124
  code: "MIC_TOGGLE_FAILED",
107
125
  message: err?.message ?? "Failed to toggle microphone",
@@ -172,6 +190,28 @@ var OneInbox = class extends TypedEmitter {
172
190
  room.on(RoomEvent.ConnectionStateChanged, (state) => {
173
191
  this.emit("connection-state", state);
174
192
  });
193
+ room.on(RoomEvent.TrackSubscribed, (track) => {
194
+ if (track.kind !== Track.Kind.Audio) return;
195
+ const el = track.attach();
196
+ el.setAttribute("data-oneinbox", "agent-audio");
197
+ if (typeof document !== "undefined") document.body.appendChild(el);
198
+ this.audioElements.add(el);
199
+ });
200
+ room.on(RoomEvent.TrackUnsubscribed, (track) => {
201
+ if (track.kind !== Track.Kind.Audio) return;
202
+ for (const el of track.detach()) {
203
+ el.remove();
204
+ this.audioElements.delete(el);
205
+ }
206
+ });
207
+ room.on(RoomEvent.AudioPlaybackStatusChanged, () => {
208
+ if (!room.canPlaybackAudio) {
209
+ this.emit("error", {
210
+ code: "AUDIO_PLAYBACK_BLOCKED",
211
+ message: "Browser blocked audio autoplay \u2014 call resumeAudio() after a user gesture."
212
+ });
213
+ }
214
+ });
175
215
  room.on(RoomEvent.TranscriptionReceived, (segments, participant) => {
176
216
  const role = roleFor(participant);
177
217
  for (const seg of segments) {
@@ -220,6 +260,12 @@ var OneInbox = class extends TypedEmitter {
220
260
  teardown(reason) {
221
261
  this.stopVolumeSampling();
222
262
  this.speaking.clear();
263
+ for (const el of this.audioElements) {
264
+ el.pause();
265
+ el.srcObject = null;
266
+ el.remove();
267
+ }
268
+ this.audioElements.clear();
223
269
  this.room = null;
224
270
  this.setStatus("idle");
225
271
  this.emit("call-end", reason);
@@ -236,10 +282,15 @@ var OneInbox = class extends TypedEmitter {
236
282
  }
237
283
  };
238
284
  function roleFor(participant) {
239
- if (!participant) return "agent";
285
+ if (!participant) return "unknown";
240
286
  return participant.isLocal ? "user" : "agent";
241
287
  }
242
-
243
- export { OneInbox, TypedEmitter };
244
- //# sourceMappingURL=index.js.map
288
+ export {
289
+ ConnectionState,
290
+ DisconnectReason,
291
+ OneInbox,
292
+ RoomEvent,
293
+ Track,
294
+ TypedEmitter
295
+ };
245
296
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/emitter.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tiny strongly-typed event emitter. Zero runtime deps so the SDK stays\n * tree-shakeable and installs without pulling `events`/`eventemitter3`.\n *\n * `E` maps event name → listener signature, e.g.\n * { \"call-start\": () => void; \"volume-level\": (v: number) => void }\n */\nexport type EventMap = Record<string, (...args: never[]) => void>;\n\nexport class TypedEmitter<E extends EventMap> {\n private readonly listeners: { [K in keyof E]?: Set<E[K]> } = {};\n\n /** Subscribe to `event`. Returns an unsubscribe function. */\n on<K extends keyof E>(event: K, fn: E[K]): () => void {\n (this.listeners[event] ??= new Set<E[K]>()).add(fn);\n return () => this.off(event, fn);\n }\n\n /** Subscribe once; auto-unsubscribes after the first emission. */\n once<K extends keyof E>(event: K, fn: E[K]): () => void {\n const wrapper = ((...args: Parameters<E[K]>) => {\n this.off(event, wrapper);\n (fn as (...a: Parameters<E[K]>) => void)(...args);\n }) as E[K];\n return this.on(event, wrapper);\n }\n\n /** Remove a specific listener. */\n off<K extends keyof E>(event: K, fn: E[K]): void {\n this.listeners[event]?.delete(fn);\n }\n\n /** Remove all listeners (all events, or just `event`). */\n removeAllListeners<K extends keyof E>(event?: K): void {\n if (event) this.listeners[event]?.clear();\n else (Object.keys(this.listeners) as K[]).forEach((k) => this.listeners[k]?.clear());\n }\n\n protected emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void {\n const set = this.listeners[event];\n if (!set) return;\n // Copy so a listener that unsubscribes mid-emit doesn't skip the next one.\n for (const fn of [...set]) (fn as (...a: Parameters<E[K]>) => void)(...args);\n }\n}\n","import {\n ConnectionState,\n DisconnectReason,\n type LocalParticipant,\n type Participant,\n type RemoteParticipant,\n Room,\n RoomEvent,\n Track,\n type TranscriptionSegment,\n} from \"livekit-client\";\n\nimport { TypedEmitter } from \"./emitter\";\n\nexport type TranscriptRole = \"user\" | \"agent\";\n\nexport interface TranscriptEvent {\n role: TranscriptRole;\n text: string;\n final: boolean;\n language?: string;\n}\n\nexport interface OneInboxError {\n /** Stable machine code, e.g. CONNECT_FAILED, ORIGIN_NOT_ALLOWED, MIC_PUBLISH_FAILED. */\n code: string;\n message: string;\n cause?: unknown;\n}\n\n/** High-level call lifecycle, mirrored to the `status` event. */\nexport type CallStatus = \"idle\" | \"connecting\" | \"active\" | \"ending\";\n\n/**\n * Typed event map. Listen with `oi.on(\"transcript\", cb)` — the callback type\n * is inferred per event (Vapi/LiveKit style).\n */\nexport type OneInboxEvents = {\n /** Connected to the room; the agent is joining. */\n \"call-start\": () => void;\n /** Call ended (locally or remotely). `reason` is the LiveKit disconnect reason when known. */\n \"call-end\": (reason?: string) => void;\n /** Underlying LiveKit connection state changed. */\n \"connection-state\": (state: ConnectionState) => void;\n /** High-level status changed. */\n status: (status: CallStatus) => void;\n /** Incremental transcript chunk for the caller or the agent (`final` marks end-of-utterance). */\n transcript: (ev: TranscriptEvent) => void;\n /** Someone started speaking (best-effort, from active-speaker detection). */\n \"speech-start\": (who: TranscriptRole) => void;\n /** Someone stopped speaking. */\n \"speech-end\": (who: TranscriptRole) => void;\n /** Local microphone level, 0..1, sampled ~10x/sec while active (for mic UIs). */\n \"volume-level\": (level: number) => void;\n /** Any error (connection, token fetch, mic publish, server disconnect). */\n error: (err: OneInboxError) => void;\n};\n\nexport interface OneInboxConfig {\n /** OneInbox API base URL. Default `https://api.oneinbox.ai`. */\n baseUrl?: string;\n}\n\nexport interface StartOptions {\n /** Per-call template variables interpolated into the agent prompt. */\n variables?: Record<string, unknown>;\n /** Free-form metadata stored on the call record. */\n metadata?: Record<string, unknown>;\n /** Auto-publish the user's microphone on connect. Default `true`. */\n autoPublishMicrophone?: boolean;\n}\n\n/** Bring-your-own-token escape hatch: connect with a token your backend minted. */\nexport interface TokenStartOptions {\n /** From `POST /v1/web-calls/token` (or `/v1/calls/web`) → `participant_token`. */\n token: string;\n /** → `server_url`. */\n serverUrl: string;\n autoPublishMicrophone?: boolean;\n}\n\nconst VOLUME_SAMPLE_MS = 100;\n\n/**\n * Browser client for OneInbox voice agents over WebRTC.\n *\n * ```ts\n * const oi = new OneInbox(\"oi_pk_…\");\n * oi.on(\"transcript\", (t) => console.log(t.role, t.text));\n * await oi.start(agentId); // mic publishes, the agent answers\n * // …\n * await oi.stop();\n * ```\n */\nexport class OneInbox extends TypedEmitter<OneInboxEvents> {\n private room: Room | null = null;\n private _status: CallStatus = \"idle\";\n private _muted = false;\n private volumeTimer: ReturnType<typeof setInterval> | null = null;\n private speaking = new Set<TranscriptRole>();\n\n constructor(\n private readonly publishableKey: string,\n private readonly config: OneInboxConfig = {},\n ) {\n super();\n }\n\n /** Current high-level call status. */\n get status(): CallStatus {\n return this._status;\n }\n\n /** Whether the local microphone is currently muted. */\n isMuted(): boolean {\n return this._muted;\n }\n\n /** Underlying LiveKit room (escape hatch for advanced use). */\n get underlyingRoom(): Room | null {\n return this.room;\n }\n\n /**\n * Start a call.\n *\n * - `start(agentId, opts?)` — publishable-key path: fetches a short-lived\n * token from the OneInbox API, then connects.\n * - `start({ token, serverUrl })` — connect with a token your backend minted.\n */\n async start(agentId: string, opts?: StartOptions): Promise<void>;\n async start(opts: TokenStartOptions): Promise<void>;\n async start(arg: string | TokenStartOptions, opts?: StartOptions): Promise<void> {\n if (this._status !== \"idle\") {\n throw new Error(\"A call is already in progress — call stop() first.\");\n }\n this.setStatus(\"connecting\");\n try {\n let serverUrl: string;\n let token: string;\n let autoMic: boolean;\n if (typeof arg === \"string\") {\n const res = await this.fetchToken(arg, opts);\n serverUrl = res.server_url;\n token = res.participant_token;\n autoMic = opts?.autoPublishMicrophone !== false;\n } else {\n serverUrl = arg.serverUrl;\n token = arg.token;\n autoMic = arg.autoPublishMicrophone !== false;\n }\n await this.connectRoom(serverUrl, token, autoMic);\n } catch (err) {\n this.setStatus(\"idle\");\n throw err;\n }\n }\n\n /** End the call and release resources. The backend marks the call completed. */\n async stop(): Promise<void> {\n if (!this.room) {\n this.setStatus(\"idle\");\n return;\n }\n this.setStatus(\"ending\");\n await this.room.disconnect();\n // teardown is finalized in the Disconnected handler\n }\n\n /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */\n setMuted(muted: boolean): void {\n this._muted = muted;\n const lp = this.room?.localParticipant;\n if (!lp) return;\n void lp.setMicrophoneEnabled(!muted).catch((err: unknown) => {\n this.emit(\"error\", {\n code: \"MIC_TOGGLE_FAILED\",\n message: (err as Error)?.message ?? \"Failed to toggle microphone\",\n cause: err,\n });\n });\n }\n\n // ---- internals -------------------------------------------------------\n\n private async fetchToken(\n agentId: string,\n opts?: StartOptions,\n ): Promise<{ server_url: string; participant_token: string }> {\n const base = (this.config.baseUrl ?? \"https://api.oneinbox.ai\").replace(/\\/+$/, \"\");\n let res: Response;\n try {\n res = await fetch(`${base}/v1/web-calls/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.publishableKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n agent_id: agentId,\n variables: opts?.variables ?? {},\n metadata: opts?.metadata ?? {},\n }),\n });\n } catch (err) {\n throw this.fail(\"TOKEN_FETCH_FAILED\", (err as Error)?.message ?? \"Network error\", err);\n }\n if (!res.ok) {\n let code = `HTTP_${res.status}`;\n let detail = res.statusText;\n try {\n const body = (await res.json()) as { error_code?: string; detail?: string };\n code = body.error_code ?? code;\n detail = body.detail ?? detail;\n } catch {\n /* non-JSON error body */\n }\n throw this.fail(code, detail);\n }\n return res.json() as Promise<{ server_url: string; participant_token: string }>;\n }\n\n private async connectRoom(serverUrl: string, token: string, autoMic: boolean): Promise<void> {\n const room = new Room({ adaptiveStream: true, dynacast: true });\n this.room = room;\n this.bindRoomEvents(room);\n\n try {\n await room.connect(serverUrl, token);\n } catch (err) {\n this.room = null;\n throw this.fail(\"CONNECT_FAILED\", (err as Error)?.message ?? \"Connection failed\", err);\n }\n\n this.setStatus(\"active\");\n this.emit(\"call-start\");\n this.startVolumeSampling();\n\n if (autoMic) {\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n this._muted = false;\n } catch (err) {\n this.emit(\"error\", {\n code: \"MIC_PUBLISH_FAILED\",\n message: (err as Error)?.message ?? \"Failed to publish microphone\",\n cause: err,\n });\n }\n }\n }\n\n private bindRoomEvents(room: Room): void {\n room.on(RoomEvent.ConnectionStateChanged, (state) => {\n this.emit(\"connection-state\", state);\n });\n\n room.on(RoomEvent.TranscriptionReceived, (segments, participant) => {\n const role = roleFor(participant);\n for (const seg of segments) {\n this.emit(\"transcript\", {\n role,\n text: seg.text,\n final: seg.final,\n language: seg.language,\n });\n }\n });\n\n room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {\n const now = new Set<TranscriptRole>(speakers.map(roleFor));\n for (const role of now) {\n if (!this.speaking.has(role)) this.emit(\"speech-start\", role);\n }\n for (const role of this.speaking) {\n if (!now.has(role)) this.emit(\"speech-end\", role);\n }\n this.speaking = now;\n });\n\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n const clientInitiated =\n reason === undefined || reason === DisconnectReason.CLIENT_INITIATED;\n if (!clientInitiated) {\n this.emit(\"error\", {\n code: \"DISCONNECTED\",\n message: `Room disconnected: ${DisconnectReason[reason] ?? reason}`,\n });\n }\n this.teardown(reason !== undefined ? DisconnectReason[reason] : undefined);\n });\n }\n\n private startVolumeSampling(): void {\n this.stopVolumeSampling();\n this.volumeTimer = setInterval(() => {\n const lp = this.room?.localParticipant;\n if (lp) this.emit(\"volume-level\", lp.audioLevel);\n }, VOLUME_SAMPLE_MS);\n }\n\n private stopVolumeSampling(): void {\n if (this.volumeTimer !== null) {\n clearInterval(this.volumeTimer);\n this.volumeTimer = null;\n }\n }\n\n private teardown(reason?: string): void {\n this.stopVolumeSampling();\n this.speaking.clear();\n this.room = null;\n this.setStatus(\"idle\");\n this.emit(\"call-end\", reason);\n }\n\n private setStatus(status: CallStatus): void {\n if (this._status === status) return;\n this._status = status;\n this.emit(\"status\", status);\n }\n\n private fail(code: string, message: string, cause?: unknown): OneInboxError {\n const err: OneInboxError = { code, message, cause };\n this.emit(\"error\", err);\n return err;\n }\n}\n\nfunction roleFor(participant: Participant | undefined): TranscriptRole {\n // The human is the local participant; the agent joins as a remote participant.\n if (!participant) return \"agent\";\n return participant.isLocal ? \"user\" : \"agent\";\n}\n\n// Re-export upstream types callers commonly need so they don't have to add a\n// direct livekit-client import just for types.\nexport { ConnectionState, DisconnectReason, RoomEvent, Track };\nexport type { LocalParticipant, Participant, RemoteParticipant, Room, TranscriptionSegment };\nexport { TypedEmitter } from \"./emitter\";\n"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/emitter.ts"],"sourcesContent":["import {\n ConnectionState,\n DisconnectReason,\n type LocalParticipant,\n type Participant,\n type RemoteParticipant,\n Room,\n RoomEvent,\n Track,\n type TranscriptionSegment,\n} from \"livekit-client\";\n\nimport { TypedEmitter } from \"./emitter\";\n\nexport type TranscriptRole = \"user\" | \"agent\" | \"unknown\";\n\nexport interface TranscriptEvent {\n role: TranscriptRole;\n text: string;\n final: boolean;\n language?: string;\n}\n\nexport interface OneInboxError {\n /** Stable machine code, e.g. CONNECT_FAILED, ORIGIN_NOT_ALLOWED, MIC_PUBLISH_FAILED. */\n code: string;\n message: string;\n cause?: unknown;\n}\n\n/** High-level call lifecycle, mirrored to the `status` event. */\nexport type CallStatus = \"idle\" | \"connecting\" | \"active\" | \"ending\";\n\n/**\n * Typed event map. Listen with `oi.on(\"transcript\", cb)` — the callback type\n * is inferred per event (Vapi/LiveKit style).\n */\nexport type OneInboxEvents = {\n /** Connected to the room; the agent is joining. */\n \"call-start\": () => void;\n /** Call ended (locally or remotely). `reason` is the LiveKit disconnect reason when known. */\n \"call-end\": (reason?: string) => void;\n /** Underlying LiveKit connection state changed. */\n \"connection-state\": (state: ConnectionState) => void;\n /** High-level status changed. */\n status: (status: CallStatus) => void;\n /** Incremental transcript chunk for the caller or the agent (`final` marks end-of-utterance). */\n transcript: (ev: TranscriptEvent) => void;\n /** Someone started speaking (best-effort, from active-speaker detection). */\n \"speech-start\": (who: TranscriptRole) => void;\n /** Someone stopped speaking. */\n \"speech-end\": (who: TranscriptRole) => void;\n /** Local microphone level, 0..1, sampled ~10x/sec while active (for mic UIs). */\n \"volume-level\": (level: number) => void;\n /** Any error (connection, token fetch, mic publish, server disconnect). */\n error: (err: OneInboxError) => void;\n};\n\nexport interface OneInboxConfig {\n /** OneInbox API base URL. Default `https://api.oneinbox.ai`. */\n baseUrl?: string;\n}\n\nexport interface StartOptions {\n /** Per-call template variables interpolated into the agent prompt. */\n variables?: Record<string, unknown>;\n /** Free-form metadata stored on the call record. */\n metadata?: Record<string, unknown>;\n /** Auto-publish the user's microphone on connect. Default `true`. */\n autoPublishMicrophone?: boolean;\n}\n\n/** Bring-your-own-token escape hatch: connect with a token your backend minted. */\nexport interface TokenStartOptions {\n /** From `POST /v1/web-calls/token` (or `/v1/calls/web`) → `participant_token`. */\n token: string;\n /** → `server_url`. */\n serverUrl: string;\n autoPublishMicrophone?: boolean;\n}\n\nconst VOLUME_SAMPLE_MS = 100;\n\n/**\n * Browser client for OneInbox voice agents over WebRTC.\n *\n * ```ts\n * const oi = new OneInbox(\"oi_pk_…\");\n * oi.on(\"transcript\", (t) => console.log(t.role, t.text));\n * await oi.start(agentId); // mic publishes, the agent answers\n * // …\n * await oi.stop();\n * ```\n */\nexport class OneInbox extends TypedEmitter<OneInboxEvents> {\n private room: Room | null = null;\n private _status: CallStatus = \"idle\";\n private _muted = false;\n private volumeTimer: ReturnType<typeof setInterval> | null = null;\n private speaking = new Set<TranscriptRole>();\n // Audio <audio> elements created for the agent's remote tracks, so we can\n // play them and tear them down on call end.\n private audioElements = new Set<HTMLMediaElement>();\n\n constructor(\n private readonly publishableKey: string,\n private readonly config: OneInboxConfig = {},\n ) {\n super();\n }\n\n /** Current high-level call status. */\n get status(): CallStatus {\n return this._status;\n }\n\n /** Whether the local microphone is currently muted. */\n isMuted(): boolean {\n return this._muted;\n }\n\n /** Underlying LiveKit room (escape hatch for advanced use). */\n get underlyingRoom(): Room | null {\n return this.room;\n }\n\n /**\n * Start a call.\n *\n * - `start(agentId, opts?)` — publishable-key path: fetches a short-lived\n * token from the OneInbox API, then connects.\n * - `start({ token, serverUrl })` — connect with a token your backend minted.\n *\n * Errors are delivered through the `error` event (never thrown), so a single\n * `oi.on(\"error\", …)` handler catches every failure path consistently. The\n * returned promise resolves once the attempt settles (check `status` or the\n * `call-start` event for success).\n */\n async start(agentId: string, opts?: StartOptions): Promise<void>;\n async start(opts: TokenStartOptions): Promise<void>;\n async start(arg: string | TokenStartOptions, opts?: StartOptions): Promise<void> {\n if (this._status !== \"idle\") {\n // Bug-fix: emit \"error\" like every other failure path instead of throwing\n // (a throw here was an unhandled rejection for `oi.on(\"error\")` users).\n this.fail(\"CALL_IN_PROGRESS\", \"A call is already in progress — call stop() first.\");\n return;\n }\n this.setStatus(\"connecting\");\n try {\n let serverUrl: string;\n let token: string;\n let autoMic: boolean;\n if (typeof arg === \"string\") {\n const res = await this.fetchToken(arg, opts);\n serverUrl = res.server_url;\n token = res.participant_token;\n autoMic = opts?.autoPublishMicrophone !== false;\n } else {\n serverUrl = arg.serverUrl;\n token = arg.token;\n autoMic = arg.autoPublishMicrophone !== false;\n }\n await this.connectRoom(serverUrl, token, autoMic);\n } catch {\n // fetchToken / connectRoom already emitted \"error\" via fail(); just reset\n // status. We deliberately do NOT re-throw — that double-delivered the\n // same failure (once via the event, once via the rejection).\n this.setStatus(\"idle\");\n }\n }\n\n /** End the call and release resources. The backend marks the call completed. */\n async stop(): Promise<void> {\n if (!this.room) {\n this.setStatus(\"idle\");\n return;\n }\n this.setStatus(\"ending\");\n await this.room.disconnect();\n // teardown is finalized in the Disconnected handler\n }\n\n /**\n * Resume audio playback if the browser blocked autoplay (call from a click /\n * tap handler). No-op when audio is already playing.\n */\n async resumeAudio(): Promise<void> {\n if (this.room && !this.room.canPlaybackAudio) {\n await this.room.startAudio();\n }\n }\n\n /** Mute/unmute the local microphone (fire-and-forget, like Vapi's setMuted). */\n setMuted(muted: boolean): void {\n const lp = this.room?.localParticipant;\n const previous = this._muted;\n this._muted = muted;\n if (!lp) return;\n void lp.setMicrophoneEnabled(!muted).catch((err: unknown) => {\n // Bug-fix: the track toggle failed, so isMuted() must not keep reporting\n // the requested-but-never-applied state — revert to the previous value.\n this._muted = previous;\n this.emit(\"error\", {\n code: \"MIC_TOGGLE_FAILED\",\n message: (err as Error)?.message ?? \"Failed to toggle microphone\",\n cause: err,\n });\n });\n }\n\n // ---- internals -------------------------------------------------------\n\n private async fetchToken(\n agentId: string,\n opts?: StartOptions,\n ): Promise<{ server_url: string; participant_token: string }> {\n const base = (this.config.baseUrl ?? \"https://api.oneinbox.ai\").replace(/\\/+$/, \"\");\n let res: Response;\n try {\n res = await fetch(`${base}/v1/web-calls/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.publishableKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n agent_id: agentId,\n variables: opts?.variables ?? {},\n metadata: opts?.metadata ?? {},\n }),\n });\n } catch (err) {\n throw this.fail(\"TOKEN_FETCH_FAILED\", (err as Error)?.message ?? \"Network error\", err);\n }\n if (!res.ok) {\n let code = `HTTP_${res.status}`;\n let detail = res.statusText;\n try {\n const body = (await res.json()) as { error_code?: string; detail?: string };\n code = body.error_code ?? code;\n detail = body.detail ?? detail;\n } catch {\n /* non-JSON error body */\n }\n throw this.fail(code, detail);\n }\n return res.json() as Promise<{ server_url: string; participant_token: string }>;\n }\n\n private async connectRoom(serverUrl: string, token: string, autoMic: boolean): Promise<void> {\n const room = new Room({ adaptiveStream: true, dynacast: true });\n this.room = room;\n this.bindRoomEvents(room);\n\n try {\n await room.connect(serverUrl, token);\n } catch (err) {\n this.room = null;\n throw this.fail(\"CONNECT_FAILED\", (err as Error)?.message ?? \"Connection failed\", err);\n }\n\n this.setStatus(\"active\");\n this.emit(\"call-start\");\n this.startVolumeSampling();\n\n if (autoMic) {\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n this._muted = false;\n } catch (err) {\n this.emit(\"error\", {\n code: \"MIC_PUBLISH_FAILED\",\n message: (err as Error)?.message ?? \"Failed to publish microphone\",\n cause: err,\n });\n }\n }\n }\n\n private bindRoomEvents(room: Room): void {\n room.on(RoomEvent.ConnectionStateChanged, (state) => {\n this.emit(\"connection-state\", state);\n });\n\n // Play the agent's audio. livekit-client does NOT auto-play remote tracks —\n // we must attach each subscribed audio track to an <audio> element. Without\n // this the agent is heard by no one even though transcripts arrive.\n room.on(RoomEvent.TrackSubscribed, (track) => {\n if (track.kind !== Track.Kind.Audio) return;\n const el = track.attach(); // creates an autoplaying <audio> element\n el.setAttribute(\"data-oneinbox\", \"agent-audio\");\n // Some browsers need the element in the DOM to actually play.\n if (typeof document !== \"undefined\") document.body.appendChild(el);\n this.audioElements.add(el);\n });\n\n room.on(RoomEvent.TrackUnsubscribed, (track) => {\n if (track.kind !== Track.Kind.Audio) return;\n for (const el of track.detach()) {\n el.remove();\n this.audioElements.delete(el);\n }\n });\n\n // If the browser blocks autoplay (no prior gesture), surface it so the app\n // can prompt a tap. Calling startAudio() resumes playback.\n room.on(RoomEvent.AudioPlaybackStatusChanged, () => {\n if (!room.canPlaybackAudio) {\n this.emit(\"error\", {\n code: \"AUDIO_PLAYBACK_BLOCKED\",\n message: \"Browser blocked audio autoplay — call resumeAudio() after a user gesture.\",\n });\n }\n });\n\n room.on(RoomEvent.TranscriptionReceived, (segments, participant) => {\n const role = roleFor(participant);\n for (const seg of segments) {\n this.emit(\"transcript\", {\n role,\n text: seg.text,\n final: seg.final,\n language: seg.language,\n });\n }\n });\n\n room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {\n const now = new Set<TranscriptRole>(speakers.map(roleFor));\n for (const role of now) {\n if (!this.speaking.has(role)) this.emit(\"speech-start\", role);\n }\n for (const role of this.speaking) {\n if (!now.has(role)) this.emit(\"speech-end\", role);\n }\n this.speaking = now;\n });\n\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n const clientInitiated =\n reason === undefined || reason === DisconnectReason.CLIENT_INITIATED;\n if (!clientInitiated) {\n this.emit(\"error\", {\n code: \"DISCONNECTED\",\n message: `Room disconnected: ${DisconnectReason[reason] ?? reason}`,\n });\n }\n this.teardown(reason !== undefined ? DisconnectReason[reason] : undefined);\n });\n }\n\n private startVolumeSampling(): void {\n this.stopVolumeSampling();\n this.volumeTimer = setInterval(() => {\n const lp = this.room?.localParticipant;\n if (lp) this.emit(\"volume-level\", lp.audioLevel);\n }, VOLUME_SAMPLE_MS);\n }\n\n private stopVolumeSampling(): void {\n if (this.volumeTimer !== null) {\n clearInterval(this.volumeTimer);\n this.volumeTimer = null;\n }\n }\n\n private teardown(reason?: string): void {\n this.stopVolumeSampling();\n this.speaking.clear();\n for (const el of this.audioElements) {\n el.pause();\n el.srcObject = null;\n el.remove();\n }\n this.audioElements.clear();\n this.room = null;\n this.setStatus(\"idle\");\n this.emit(\"call-end\", reason);\n }\n\n private setStatus(status: CallStatus): void {\n if (this._status === status) return;\n this._status = status;\n this.emit(\"status\", status);\n }\n\n private fail(code: string, message: string, cause?: unknown): OneInboxError {\n const err: OneInboxError = { code, message, cause };\n this.emit(\"error\", err);\n return err;\n }\n}\n\nfunction roleFor(participant: Participant | undefined): TranscriptRole {\n // The human is the local participant; the agent joins as a remote participant.\n // Bug-fix: when LiveKit fires without a participant we genuinely don't know\n // who spoke — return \"unknown\" rather than mislabelling it as the agent.\n if (!participant) return \"unknown\";\n return participant.isLocal ? \"user\" : \"agent\";\n}\n\n// Re-export upstream types callers commonly need so they don't have to add a\n// direct livekit-client import just for types.\nexport { ConnectionState, DisconnectReason, RoomEvent, Track };\nexport type { LocalParticipant, Participant, RemoteParticipant, Room, TranscriptionSegment };\nexport { TypedEmitter } from \"./emitter\";\n","/**\n * Tiny strongly-typed event emitter. Zero runtime deps so the SDK stays\n * tree-shakeable and installs without pulling `events`/`eventemitter3`.\n *\n * `E` maps event name → listener signature, e.g.\n * { \"call-start\": () => void; \"volume-level\": (v: number) => void }\n */\nexport type EventMap = Record<string, (...args: never[]) => void>;\n\nexport class TypedEmitter<E extends EventMap> {\n private readonly listeners: { [K in keyof E]?: Set<E[K]> } = {};\n\n /** Subscribe to `event`. Returns an unsubscribe function. */\n on<K extends keyof E>(event: K, fn: E[K]): () => void {\n (this.listeners[event] ??= new Set<E[K]>()).add(fn);\n return () => this.off(event, fn);\n }\n\n /** Subscribe once; auto-unsubscribes after the first emission. */\n once<K extends keyof E>(event: K, fn: E[K]): () => void {\n const wrapper = ((...args: Parameters<E[K]>) => {\n this.off(event, wrapper);\n (fn as (...a: Parameters<E[K]>) => void)(...args);\n }) as E[K];\n return this.on(event, wrapper);\n }\n\n /** Remove a specific listener. */\n off<K extends keyof E>(event: K, fn: E[K]): void {\n this.listeners[event]?.delete(fn);\n }\n\n /** Remove all listeners (all events, or just `event`). */\n removeAllListeners<K extends keyof E>(event?: K): void {\n if (event) this.listeners[event]?.clear();\n else (Object.keys(this.listeners) as K[]).forEach((k) => this.listeners[k]?.clear());\n }\n\n protected emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void {\n const set = this.listeners[event];\n if (!set) return;\n // Copy so a listener that unsubscribes mid-emit doesn't skip the next one.\n for (const fn of [...set]) (fn as (...a: Parameters<E[K]>) => void)(...args);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oneinbox/web-sdk",
3
- "version": "0.1.0-beta.1",
4
- "description": "OneInbox browser SDK connect to a voice agent over WebRTC with a publishable key.",
3
+ "version": "0.1.0-beta.3",
4
+ "description": "OneInbox browser SDK \u2014 connect to a voice agent over WebRTC with a publishable key.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -36,7 +36,14 @@
36
36
  "tsup": "^8.0.0",
37
37
  "typescript": "^5.4.0"
38
38
  },
39
- "keywords": ["voice", "ai", "agents", "webrtc", "livekit", "vapi"],
39
+ "keywords": [
40
+ "voice",
41
+ "ai",
42
+ "agents",
43
+ "webrtc",
44
+ "livekit",
45
+ "vapi"
46
+ ],
40
47
  "author": "OneInbox",
41
48
  "homepage": "https://www.npmjs.com/package/@oneinbox/web-sdk",
42
49
  "license": "MIT",