@craftedxp/voice-js 0.2.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/node.mjs ADDED
@@ -0,0 +1,450 @@
1
+ // src/config.ts
2
+ function normalizeConfig(config) {
3
+ if (!config) throw new Error("configureVoiceClient: config is required");
4
+ if ("apiKey" in config) {
5
+ throw new Error(
6
+ "configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead \u2014 see the @craftedxp/voice-js README for the migration recipe."
7
+ );
8
+ }
9
+ if (!config.apiBase) {
10
+ throw new Error("configureVoiceClient: apiBase is required");
11
+ }
12
+ if (typeof config.fetchToken !== "function") {
13
+ throw new Error("configureVoiceClient: fetchToken must be a function");
14
+ }
15
+ return {
16
+ ...config,
17
+ apiBase: config.apiBase.replace(/\/+$/, "")
18
+ };
19
+ }
20
+ function mergeStartCallContext(factory, call) {
21
+ const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
22
+ const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
23
+ return { context, metadata };
24
+ }
25
+
26
+ // src/ReconnectingWebSocket.ts
27
+ var READYSTATE_OPEN = 1;
28
+ var READYSTATE_CLOSED = 3;
29
+ var createReconnectingWebSocket = (options, onEvent) => {
30
+ const maxRetries = options.maxRetries ?? 3;
31
+ const initialBackoff = options.initialBackoffMs ?? 500;
32
+ const maxBackoff = options.maxBackoffMs ?? 8e3;
33
+ let ws = null;
34
+ let intentionalClose = false;
35
+ let retries = 0;
36
+ let backoff = initialBackoff;
37
+ let reconnectTimer = null;
38
+ const openOnce = () => {
39
+ ws = options.wsFactory(options.url);
40
+ ws.binaryType = "arraybuffer";
41
+ ws.onopen = () => {
42
+ if (retries === 0) onEvent({ type: "open" });
43
+ else onEvent({ type: "reconnected" });
44
+ retries = 0;
45
+ backoff = initialBackoff;
46
+ };
47
+ ws.onmessage = (ev) => {
48
+ onEvent({ type: "message", data: ev.data });
49
+ };
50
+ ws.onerror = () => {
51
+ onEvent({ type: "error", error: new Error("WebSocket error") });
52
+ };
53
+ ws.onclose = (ev) => {
54
+ ws = null;
55
+ const shouldRetry = !intentionalClose && retries < maxRetries;
56
+ if (!shouldRetry) {
57
+ onEvent({
58
+ type: "close",
59
+ code: ev.code,
60
+ reason: ev.reason,
61
+ permanent: true
62
+ });
63
+ return;
64
+ }
65
+ onEvent({
66
+ type: "close",
67
+ code: ev.code,
68
+ reason: ev.reason,
69
+ permanent: false
70
+ });
71
+ retries++;
72
+ const delay = Math.min(backoff, maxBackoff);
73
+ backoff = Math.min(backoff * 2, maxBackoff);
74
+ reconnectTimer = setTimeout(openOnce, delay);
75
+ };
76
+ };
77
+ openOnce();
78
+ return {
79
+ send: (data) => {
80
+ if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
81
+ },
82
+ close: (code = 1e3, reason = "client-requested") => {
83
+ intentionalClose = true;
84
+ if (reconnectTimer) {
85
+ clearTimeout(reconnectTimer);
86
+ reconnectTimer = null;
87
+ }
88
+ try {
89
+ ws?.close(code, reason);
90
+ } catch {
91
+ }
92
+ },
93
+ readyState: () => ws?.readyState ?? READYSTATE_CLOSED
94
+ };
95
+ };
96
+
97
+ // src/protocol.ts
98
+ var createProtocolState = () => ({
99
+ state: "idle",
100
+ transcript: [],
101
+ agentBubbleId: null,
102
+ idCounter: 0,
103
+ endReason: null
104
+ });
105
+ var mapEndReason = (raw) => {
106
+ if (raw === "agent_ended") return "agent_ended";
107
+ if (raw === "caller_hung_up") return "user_hangup";
108
+ if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
109
+ return "error";
110
+ };
111
+ function handleServerMessage(raw, state, cb) {
112
+ let msg;
113
+ try {
114
+ msg = JSON.parse(raw);
115
+ } catch {
116
+ return;
117
+ }
118
+ switch (msg.type) {
119
+ case "connected":
120
+ setState(state, "listening", cb);
121
+ return;
122
+ case "transcript": {
123
+ const text = msg.text ?? "";
124
+ if (!text) return;
125
+ const isFinal = !!msg.isFinal;
126
+ if (!isFinal) setState(state, "user_speaking", cb);
127
+ upsertUserPartial(state, text, isFinal);
128
+ cb.onTranscript(state.transcript);
129
+ return;
130
+ }
131
+ case "agent_turn_start": {
132
+ const id = `m${state.idCounter++}`;
133
+ state.agentBubbleId = id;
134
+ state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
135
+ cb.onTranscript(state.transcript);
136
+ cb.onAgentTurnStart();
137
+ setState(state, "agent_speaking", cb);
138
+ return;
139
+ }
140
+ case "agent_text": {
141
+ const delta = msg.text ?? "";
142
+ if (!delta || !state.agentBubbleId) return;
143
+ const id = state.agentBubbleId;
144
+ state.transcript = state.transcript.map(
145
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
146
+ );
147
+ cb.onTranscript(state.transcript);
148
+ return;
149
+ }
150
+ case "agent_turn_end":
151
+ state.agentBubbleId = null;
152
+ setState(state, "listening", cb);
153
+ return;
154
+ case "interrupt":
155
+ cb.onInterrupt();
156
+ return;
157
+ case "agent_turn_abort": {
158
+ const committed = (msg.committedText ?? "").trim();
159
+ if (state.agentBubbleId) {
160
+ const id = state.agentBubbleId;
161
+ if (committed) {
162
+ state.transcript = state.transcript.map(
163
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
164
+ );
165
+ } else {
166
+ state.transcript = state.transcript.filter((e) => e.id !== id);
167
+ }
168
+ cb.onTranscript(state.transcript);
169
+ }
170
+ state.agentBubbleId = null;
171
+ return;
172
+ }
173
+ case "tool_call":
174
+ state.transcript = [
175
+ ...state.transcript,
176
+ {
177
+ id: `m${state.idCounter++}`,
178
+ role: "tool",
179
+ text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
180
+ }
181
+ ];
182
+ cb.onTranscript(state.transcript);
183
+ return;
184
+ case "tool_result":
185
+ state.transcript = [
186
+ ...state.transcript,
187
+ {
188
+ id: `m${state.idCounter++}`,
189
+ role: "tool",
190
+ text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
191
+ }
192
+ ];
193
+ cb.onTranscript(state.transcript);
194
+ return;
195
+ case "call_end": {
196
+ const reasonRaw = String(msg.reason ?? "");
197
+ const reason = mapEndReason(reasonRaw);
198
+ state.endReason = reason;
199
+ state.transcript = [
200
+ ...state.transcript,
201
+ {
202
+ id: `m${state.idCounter++}`,
203
+ role: "system",
204
+ text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
205
+ }
206
+ ];
207
+ cb.onTranscript(state.transcript);
208
+ cb.onCallEnd(reason);
209
+ return;
210
+ }
211
+ case "error": {
212
+ const code = msg.code ?? "server_error";
213
+ const message = msg.message ?? "server error";
214
+ cb.onError({ code, message });
215
+ return;
216
+ }
217
+ }
218
+ }
219
+ var setState = (state, next, cb) => {
220
+ if (state.state === next) return;
221
+ state.state = next;
222
+ cb.onState(next);
223
+ };
224
+ var upsertUserPartial = (state, text, isFinal) => {
225
+ let idx = -1;
226
+ for (let i = state.transcript.length - 1; i >= 0; i--) {
227
+ const e = state.transcript[i];
228
+ if (e.role === "user" && e.committed === false) {
229
+ idx = i;
230
+ break;
231
+ }
232
+ }
233
+ if (idx === -1) {
234
+ state.transcript = [
235
+ ...state.transcript,
236
+ { id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
237
+ ];
238
+ return;
239
+ }
240
+ const target = state.transcript[idx];
241
+ const next = [...state.transcript];
242
+ next[idx] = { ...target, text, committed: isFinal };
243
+ state.transcript = next;
244
+ };
245
+ function buildWsUrl(args) {
246
+ const base = new URL(args.apiBase);
247
+ const proto = base.protocol === "https:" ? "wss:" : "ws:";
248
+ const bargeQS = args.bargeIn === false ? "&barge=off" : "";
249
+ return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
250
+ }
251
+
252
+ // src/NodeVoiceClient.ts
253
+ var NodeVoiceClient = class {
254
+ constructor(args) {
255
+ this.rws = null;
256
+ this.muted = false;
257
+ this.startedAt = null;
258
+ this.endedFired = false;
259
+ this.lastError = null;
260
+ this.end = () => {
261
+ this.teardown("user_hangup");
262
+ };
263
+ this.mute = () => {
264
+ this.muted = true;
265
+ };
266
+ this.unmute = () => {
267
+ this.muted = false;
268
+ };
269
+ // ---------------------------------------------------------------
270
+ // Node-only raw audio surface
271
+ // ---------------------------------------------------------------
272
+ this.sendAudioChunk = (pcm) => {
273
+ if (!this.rws) return false;
274
+ if (this.muted) {
275
+ const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength;
276
+ this.rws.send(new ArrayBuffer(len));
277
+ return true;
278
+ }
279
+ this.rws.send(pcm);
280
+ return true;
281
+ };
282
+ // ---------------------------------------------------------------
283
+ // Internal
284
+ // ---------------------------------------------------------------
285
+ this.setState = (next) => {
286
+ if (this.proto.state === next) return;
287
+ this.proto.state = next;
288
+ this.args.options.onStateChange?.(next);
289
+ };
290
+ this.emitError = (err) => {
291
+ this.lastError = err;
292
+ this.args.options.onError?.(err);
293
+ };
294
+ this.handleSocketEvent = (ev) => {
295
+ switch (ev.type) {
296
+ case "open":
297
+ break;
298
+ case "reconnected":
299
+ this.proto.transcript = [];
300
+ this.proto.agentBubbleId = null;
301
+ this.args.options.onTranscript?.(this.proto.transcript);
302
+ this.setState("listening");
303
+ break;
304
+ case "message":
305
+ if (typeof ev.data === "string") {
306
+ handleServerMessage(ev.data, this.proto, {
307
+ onState: this.setState,
308
+ onTranscript: (entries) => this.args.options.onTranscript?.(entries),
309
+ onError: this.emitError,
310
+ onInterrupt: () => void 0,
311
+ onAgentTurnStart: () => void 0,
312
+ onCallEnd: (reason) => this.teardown(reason)
313
+ });
314
+ } else {
315
+ this.args.options.onAudioChunk?.(ev.data);
316
+ }
317
+ break;
318
+ case "close":
319
+ if (ev.permanent) {
320
+ const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
321
+ this.teardown(reason);
322
+ }
323
+ break;
324
+ case "error":
325
+ this.emitError({ code: "socket_error", message: ev.error.message });
326
+ break;
327
+ }
328
+ };
329
+ this.teardown = (reason) => {
330
+ try {
331
+ this.rws?.close(1e3, reason);
332
+ } catch {
333
+ }
334
+ this.rws = null;
335
+ this.setState("ended");
336
+ this.fireEndOnce(reason);
337
+ };
338
+ this.fireEndOnce = (reason) => {
339
+ if (this.endedFired) return;
340
+ this.endedFired = true;
341
+ const startedAt = this.startedAt ?? Date.now();
342
+ this.args.options.onEnd?.({
343
+ reason,
344
+ errorCode: reason === "error" ? this.lastError?.code : void 0,
345
+ durationMs: Date.now() - startedAt
346
+ });
347
+ };
348
+ this.args = args;
349
+ this.proto = createProtocolState();
350
+ }
351
+ // ---------------------------------------------------------------
352
+ // Call interface
353
+ // ---------------------------------------------------------------
354
+ get state() {
355
+ return this.proto.state;
356
+ }
357
+ get transcript() {
358
+ return this.proto.transcript.slice();
359
+ }
360
+ get isMuted() {
361
+ return this.muted;
362
+ }
363
+ // ---------------------------------------------------------------
364
+ // Lifecycle
365
+ // ---------------------------------------------------------------
366
+ async start() {
367
+ this.setState("connecting");
368
+ this.startedAt = Date.now();
369
+ const url = buildWsUrl({
370
+ apiBase: this.args.config.apiBase,
371
+ agentId: this.args.options.agentId,
372
+ token: this.args.token,
373
+ bargeIn: this.args.options.bargeIn
374
+ });
375
+ this.rws = createReconnectingWebSocket(
376
+ {
377
+ url,
378
+ wsFactory: this.args.wsFactory,
379
+ maxRetries: 3
380
+ },
381
+ (ev) => this.handleSocketEvent(ev)
382
+ );
383
+ }
384
+ };
385
+
386
+ // src/node.ts
387
+ var cachedWsCtor = null;
388
+ var loadWsCtor = async () => {
389
+ if (cachedWsCtor) return cachedWsCtor;
390
+ try {
391
+ const mod = await import("ws");
392
+ const ctor = mod.WebSocket ?? mod.default;
393
+ if (!ctor) {
394
+ throw new Error("imported `ws` but neither default nor named WebSocket export was found");
395
+ }
396
+ cachedWsCtor = ctor;
397
+ return ctor;
398
+ } catch (err) {
399
+ throw new Error(
400
+ "@craftedxp/voice-js (node): missing optional peer `ws`. Install it with `npm install ws` (ws is declared as `peerDependenciesMeta.optional` so npm doesn't install it automatically). Original: " + (err instanceof Error ? err.message : String(err))
401
+ );
402
+ }
403
+ };
404
+ var NodeVoiceFactory = class {
405
+ constructor(config) {
406
+ this.startCall = async (options) => {
407
+ if (!options.agentId) {
408
+ throw new Error("startCall: agentId is required");
409
+ }
410
+ const WsCtor = await loadWsCtor();
411
+ const wsFactory = (url) => new WsCtor(url);
412
+ const { context, metadata } = mergeStartCallContext(this.config, options);
413
+ const fetchArgs = {
414
+ agentId: options.agentId,
415
+ userId: options.userId,
416
+ context,
417
+ metadata
418
+ };
419
+ let token;
420
+ if (options.token) {
421
+ token = options.token;
422
+ } else {
423
+ token = await this.config.fetchToken(fetchArgs);
424
+ if (!token) {
425
+ throw new Error("configureVoiceClient.fetchToken returned empty token");
426
+ }
427
+ }
428
+ const client = new NodeVoiceClient({
429
+ config: this.config,
430
+ options: { ...options, context, metadata },
431
+ token,
432
+ wsFactory
433
+ });
434
+ await client.start();
435
+ return client;
436
+ };
437
+ this.config = config;
438
+ }
439
+ };
440
+ function configureVoiceClient(config) {
441
+ return new NodeVoiceFactory(normalizeConfig(config));
442
+ }
443
+ export {
444
+ buildWsUrl,
445
+ configureVoiceClient,
446
+ createProtocolState,
447
+ createReconnectingWebSocket,
448
+ handleServerMessage
449
+ };
450
+ //# sourceMappingURL=node.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/ReconnectingWebSocket.ts","../src/protocol.ts","../src/NodeVoiceClient.ts","../src/node.ts"],"sourcesContent":["// Public configuration surface.\n//\n// `configureVoiceClient` returns a small factory that knows how to mint\n// `ct_` tokens via your callback. Per-call you call `factory.startCall`.\n//\n// Parity with @craftedxp/voice-rn:\n// voice-rn calls `configureVoiceClient` once at app startup as a\n// side-effect (singleton), because in RN there is exactly one host\n// app and one running `<AgentCall>` at a time. In JS environments\n// the same process can drive multiple clients (multi-tenant\n// dashboards, terminal multiplexers, electron apps with several\n// panels), so the JS SDK returns the factory rather than mutating a\n// module-level singleton. Same option names, same callback shape.\n//\n// Auth model:\n// - `apiKey` is INTENTIONALLY ABSENT from the public surface.\n// Pre-0.2 had it; baking an `sk_` into a JS bundle ships your\n// server-grade credentials to every client. The right pattern is\n// `fetchToken` — your code asks YOUR backend for a short-lived\n// `ct_` and your backend uses its `sk_` (via @craftedxp/sdk-node)\n// to mint it.\n// - For tests + local prototypes a bare `token` may be passed to\n// `startCall` directly, bypassing `fetchToken`. Don't ship that to\n// production — `ct_` lifetimes are short and you want the SDK to\n// re-mint on expiry.\n\nimport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\n// ---------------------------------------------------------------------------\n// fetchToken contract — matches voice-rn FetchTokenArgs verbatim.\n// ---------------------------------------------------------------------------\n\nexport interface FetchTokenArgs {\n /** The agent the SDK is about to call. */\n agentId: string\n /**\n * Optional consumer-side user identifier. Round-tripped to the server\n * as `contactId` for Phase 11 contact memory. The SDK does not\n * inspect this; your backend uses it to scope the token mint.\n */\n userId?: string\n /**\n * Per-call structured context lowered into the agent's effective\n * system prompt server-side at session open. Opaque to the SDK.\n */\n context?: Record<string, unknown>\n /**\n * String key/value pairs round-tripped on the `call.ended` webhook.\n * Capped at 1 KB total server-side. NOT lowered into the system prompt.\n */\n metadata?: Record<string, string>\n}\n\nexport type FetchToken = (args: FetchTokenArgs) => Promise<string>\n\n// ---------------------------------------------------------------------------\n// Factory configuration\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientConfig {\n /**\n * Full HTTPS URL of the Voxline server. The WebSocket scheme is\n * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.\n */\n apiBase: string\n /**\n * Called by the SDK whenever it needs a fresh `ct_` token (initial\n * connect; mid-call refresh on `token_expired`). Your implementation\n * should hit YOUR backend, which holds the `sk_` API key and mints\n * via `POST /v1/call-tokens` (or `client.callTokens.mint` from\n * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a\n * client.\n */\n fetchToken: FetchToken\n /**\n * Optional metadata applied to EVERY startCall. Per-call `metadata`\n * in `startCall` is merged on top (per-call wins on key conflicts).\n * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.\n */\n defaultMetadata?: Record<string, string>\n /**\n * Optional context applied to EVERY startCall. Per-call `context` in\n * `startCall` is merged on top. Useful for cross-call invariants like\n * the signed-in user's locale.\n */\n defaultContext?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Per-call options — startCall({ ... })\n// ---------------------------------------------------------------------------\n\nexport interface StartCallOptions {\n /** The agent to call. */\n agentId: string\n /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */\n userId?: string\n /**\n * Per-call structured context. Merged on top of `defaultContext`\n * configured at factory time.\n */\n context?: Record<string, unknown>\n /**\n * Per-call metadata. Merged on top of `defaultMetadata` configured\n * at factory time.\n */\n metadata?: Record<string, string>\n /**\n * When false, the SDK + server stay full-duplex but barge-in is\n * suppressed. Useful for alarm-style flows where the user shouldn't\n * accidentally interrupt the script. Default true.\n */\n bargeIn?: boolean\n /**\n * Test-only escape hatch — pass a pre-minted `ct_` directly and skip\n * the `fetchToken` call. Don't use this in production code: tokens\n * expire and the SDK can't re-mint without the callback.\n */\n token?: string\n\n // Event callbacks — same shape as voice-rn UseVoiceCallOptions.\n onStateChange?: (state: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (err: CallError) => void\n onEnd?: (end: CallEndEvent) => void\n /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */\n onVolume?: (vol: VolumeEvent) => void\n}\n\n// ---------------------------------------------------------------------------\n// Call handle returned from startCall — matches voice-rn\n// VoiceCallController where it makes sense.\n// ---------------------------------------------------------------------------\n\nexport interface Call {\n /** Current state. Snapshot — subscribe via onStateChange for live updates. */\n readonly state: CallState\n /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */\n readonly transcript: TranscriptEntry[]\n /** True after `mute()` and before `unmute()`. */\n readonly isMuted: boolean\n /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */\n end: () => void\n /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */\n mute: () => void\n /** Unmute mic frames. Idempotent. */\n unmute: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Factory contract — what configureVoiceClient returns. The actual\n// implementation differs between browser (audio-equipped) and node (raw\n// PCM) bundles; both satisfy this interface.\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientFactory {\n /** Read back the resolved config (post trailing-slash normalisation). */\n readonly config: VoiceClientConfig\n /**\n * Open a fresh call. Returns when the WS is open; rejects on\n * pre-flight failure (missing config, fetchToken throw, etc). Mid-\n * call failures arrive via the per-call `onError` callback — they\n * don't reject this promise.\n */\n startCall: (options: StartCallOptions) => Promise<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pre-flight validation. Pulled out so both bundles share the exact\n// same \"missing field\" error messages.\n// ---------------------------------------------------------------------------\n\nexport function normalizeConfig(config: VoiceClientConfig): VoiceClientConfig {\n if (!config) throw new Error('configureVoiceClient: config is required')\n if ('apiKey' in (config as object)) {\n throw new Error(\n \"configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead — see the @craftedxp/voice-js README for the migration recipe.\",\n )\n }\n if (!config.apiBase) {\n throw new Error('configureVoiceClient: apiBase is required')\n }\n if (typeof config.fetchToken !== 'function') {\n throw new Error('configureVoiceClient: fetchToken must be a function')\n }\n return {\n ...config,\n apiBase: config.apiBase.replace(/\\/+$/, ''),\n }\n}\n\n// Merge factory-level defaults with per-call overrides. Per-call wins.\nexport function mergeStartCallContext(\n factory: VoiceClientConfig,\n call: StartCallOptions,\n): { context?: Record<string, unknown>; metadata?: Record<string, string> } {\n const context =\n factory.defaultContext || call.context\n ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }\n : undefined\n const metadata =\n factory.defaultMetadata || call.metadata\n ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }\n : undefined\n return { context, metadata }\n}\n","// Minimal auto-reconnecting WebSocket wrapper.\n//\n// Scope: transparent reconnection on unexpected drops (network blip,\n// server restart). We deliberately do NOT try to preserve call state\n// across reconnects — the server's AgentCallHandler is session-scoped\n// and won't resume where we left off. If the consumer needs mid-call\n// resilience, that's a server-side resumable-session feature, not a\n// client-side retry.\n//\n// In v1 we reconnect only when `connect()` has been called and\n// `.close(1000, ...)` has NOT been called by the consumer (i.e., we drop\n// because of network, not because the user hung up). On unclean close\n// the consumer gets a fresh connection + a `reconnected` event so they\n// can re-issue any greeting / context.\n//\n// Transport agnostic — accepts a WebSocket-constructor factory so the\n// browser bundle uses native `WebSocket` and the node bundle injects the\n// `ws` package.\n\nexport type RWSEvent =\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error }\n\n// Minimal WebSocket-like contract. Both browser `WebSocket` and the\n// `ws` package's WebSocket satisfy it.\nexport interface WebSocketLike {\n binaryType: string\n readyState: number\n onopen: ((ev: unknown) => void) | null\n onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null\n onerror: ((ev: unknown) => void) | null\n onclose: ((ev: { code: number; reason: string }) => void) | null\n send: (data: string | ArrayBuffer | ArrayBufferView) => void\n close: (code?: number, reason?: string) => void\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike\n\nexport interface RWSOptions {\n url: string\n // Factory so we can swap browser-native WebSocket for the `ws` package\n // in node. Browser entry pre-fills with a wrapper around globalThis.WebSocket.\n wsFactory: WebSocketFactory\n // Cap the number of auto-reconnect attempts. `0` disables retry entirely\n // (closer to native WebSocket semantics). Default: 3.\n maxRetries?: number\n // Initial backoff; doubles up to maxBackoffMs.\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst READYSTATE_OPEN = 1\nconst READYSTATE_CLOSED = 3\n\nexport const createReconnectingWebSocket = (\n options: RWSOptions,\n onEvent: (ev: RWSEvent) => void,\n) => {\n const maxRetries = options.maxRetries ?? 3\n const initialBackoff = options.initialBackoffMs ?? 500\n const maxBackoff = options.maxBackoffMs ?? 8000\n\n let ws: WebSocketLike | null = null\n let intentionalClose = false\n let retries = 0\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n\n const openOnce = () => {\n ws = options.wsFactory(options.url)\n // Binary frames arrive as Int16 PCM from the server. ArrayBuffer is\n // more convenient than Blob (one fewer async read) — we `.arrayBuffer()`\n // a Blob manually only if we receive one unexpectedly.\n ws.binaryType = 'arraybuffer'\n ws.onopen = () => {\n if (retries === 0) onEvent({ type: 'open' })\n else onEvent({ type: 'reconnected' })\n retries = 0\n backoff = initialBackoff\n }\n ws.onmessage = (ev) => {\n onEvent({ type: 'message', data: ev.data as string | ArrayBuffer })\n }\n ws.onerror = () => {\n onEvent({ type: 'error', error: new Error('WebSocket error') })\n }\n ws.onclose = (ev) => {\n ws = null\n const shouldRetry = !intentionalClose && retries < maxRetries\n if (!shouldRetry) {\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: true,\n })\n return\n }\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: false,\n })\n retries++\n const delay = Math.min(backoff, maxBackoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n reconnectTimer = setTimeout(openOnce, delay)\n }\n }\n\n openOnce()\n\n return {\n send: (data: string | ArrayBuffer | ArrayBufferView) => {\n if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)\n },\n close: (code = 1000, reason = 'client-requested') => {\n intentionalClose = true\n if (reconnectTimer) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n try {\n ws?.close(code, reason)\n } catch {\n // already closed\n }\n },\n readyState: () => ws?.readyState ?? READYSTATE_CLOSED,\n }\n}\n\nexport type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>\n","// Wire-protocol types + stateless transcript reducer.\n//\n// Shared between the browser and node bundles. The protocol surface is\n// matched (where it makes sense) to @craftedxp/voice-rn 0.3.x so a\n// consumer can write the same `onTranscript` / `onStateChange` /\n// `onError` / `onEnd` shape for both web and RN clients.\n\n// ---------------------------------------------------------------------------\n// State machine\n// ---------------------------------------------------------------------------\n\nexport type CallState =\n | 'idle'\n | 'connecting'\n | 'listening'\n | 'user_speaking'\n | 'agent_speaking'\n | 'ended'\n | 'error'\n\n// ---------------------------------------------------------------------------\n// Transcript model — matches voice-rn TranscriptEntry\n// ---------------------------------------------------------------------------\n\nexport type TranscriptEntry =\n | { id: string; role: 'user'; text: string; committed: boolean }\n | { id: string; role: 'agent'; text: string; interrupted?: boolean }\n | { id: string; role: 'tool'; text: string }\n | { id: string; role: 'system'; text: string }\n\n// ---------------------------------------------------------------------------\n// Stable error code contract — matches voice-rn CallErrorCode where the\n// failure modes overlap. Web-specific codes (mic_denied via getUserMedia\n// rejection, etc.) keep their voice-rn names so cross-platform consumers\n// can write one switch statement.\n// ---------------------------------------------------------------------------\n\nexport type CallErrorCode =\n // Programming errors — surface loudly to the host's developer.\n | 'missing_credentials'\n | 'forbidden'\n // Browser audio failures.\n | 'mic_denied'\n | 'mic_start_failed'\n | 'audio_session_failed'\n // Auth lifecycle.\n | 'token_expired'\n | 'token_invalid'\n | 'unauthorized'\n // Network / connectivity.\n | 'network_unreachable'\n | 'socket_error'\n // Business state.\n | 'payment_required'\n | 'not_found'\n // End-of-call states surfaced via onError (also via onEnd reason='timeout').\n | 'silence_timeout'\n // Catch-all for unexpected server / 5xx / 1011.\n | 'server_error'\n\nexport interface CallError {\n code: CallErrorCode\n message: string\n}\n\n// ---------------------------------------------------------------------------\n// End-of-call signal — matches voice-rn CallEndReason / CallEndEvent\n// ---------------------------------------------------------------------------\n\nexport type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error'\n\nexport interface CallEndEvent {\n reason: CallEndReason\n // Present iff reason === 'error'. Mirrors the code from the most\n // recent onError.\n errorCode?: CallErrorCode\n // Wallclock from start() resolving to the WS close. Useful for billing\n // / \"you spoke for 1m23s\" UIs without forcing the host to track\n // timestamps themselves.\n durationMs: number\n}\n\n// ---------------------------------------------------------------------------\n// Volume meter event (browser bundle only — node bundle leaves volume to\n// the consumer if they're processing PCM themselves).\n// ---------------------------------------------------------------------------\n\nexport interface VolumeEvent {\n // 0-1 RMS over the last ~100ms. Bind to a waveform / level meter.\n input: number\n output: number\n}\n\n// ---------------------------------------------------------------------------\n// Server → client message envelope. Loose typing — the server can add new\n// `type` values without breaking the SDK; unknown types are ignored by\n// the dispatch in handleServerMessage.\n// ---------------------------------------------------------------------------\n\nexport type ServerMessage = Record<string, unknown> & { type?: string }\n\n// ---------------------------------------------------------------------------\n// Stateless transcript reducer + state-machine helpers. Both the browser\n// and node clients call into these so the shape of the transcript stays\n// identical across environments.\n// ---------------------------------------------------------------------------\n\nexport interface ProtocolState {\n state: CallState\n transcript: TranscriptEntry[]\n agentBubbleId: string | null\n idCounter: number\n // Reason latched from the server's call_end frame. Read by the\n // surrounding client when the WS finally closes so onEnd fires with the\n // right reason instead of falling back to user_hangup.\n endReason: CallEndReason | null\n}\n\nexport const createProtocolState = (): ProtocolState => ({\n state: 'idle',\n transcript: [],\n agentBubbleId: null,\n idCounter: 0,\n endReason: null,\n})\n\n// Side-effect callbacks the protocol layer fires as it processes server\n// frames. The surrounding client wires these up to event emitters\n// (browser) or to user-supplied callbacks (node).\nexport interface ProtocolCallbacks {\n onState: (next: CallState) => void\n onTranscript: (entries: TranscriptEntry[]) => void\n onError: (err: CallError) => void\n // Fires on `interrupt` — caller should flush its audio playback queue.\n onInterrupt: () => void\n // Fires on `agent_turn_start` — caller may want to reset its turn\n // anchor for \"agent has been speaking N ms\" UIs.\n onAgentTurnStart: () => void\n // Fires on `call_end` — caller closes its WS and resolves onEnd.\n onCallEnd: (reason: CallEndReason) => void\n}\n\n// Map server-supplied endReason strings onto our SDK-side CallEndReason.\nconst mapEndReason = (raw: string): CallEndReason => {\n if (raw === 'agent_ended') return 'agent_ended'\n if (raw === 'caller_hung_up') return 'user_hangup'\n if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'\n return 'error'\n}\n\n// Pure-ish transcript reducer + dispatcher. Mutates `state` in place to\n// match the imperative pattern used by both clients; returns nothing.\n//\n// The \"agent_text\" / \"transcript\" interim handling is identical to\n// voice-rn's useVoiceCall — one growing user bubble while interim, one\n// growing agent bubble per turn.\nexport function handleServerMessage(\n raw: string,\n state: ProtocolState,\n cb: ProtocolCallbacks,\n): void {\n let msg: ServerMessage\n try {\n msg = JSON.parse(raw)\n } catch {\n return\n }\n\n switch (msg.type) {\n case 'connected':\n setState(state, 'listening', cb)\n return\n\n case 'transcript': {\n const text = (msg.text as string) ?? ''\n if (!text) return\n const isFinal = !!msg.isFinal\n if (!isFinal) setState(state, 'user_speaking', cb)\n upsertUserPartial(state, text, isFinal)\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_start': {\n const id = `m${state.idCounter++}`\n state.agentBubbleId = id\n state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]\n cb.onTranscript(state.transcript)\n cb.onAgentTurnStart()\n setState(state, 'agent_speaking', cb)\n return\n }\n\n case 'agent_text': {\n const delta = (msg.text as string) ?? ''\n if (!delta || !state.agentBubbleId) return\n const id = state.agentBubbleId\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,\n )\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_end':\n state.agentBubbleId = null\n setState(state, 'listening', cb)\n return\n\n case 'interrupt':\n cb.onInterrupt()\n return\n\n case 'agent_turn_abort': {\n const committed = ((msg.committedText as string) ?? '').trim()\n if (state.agentBubbleId) {\n const id = state.agentBubbleId\n if (committed) {\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,\n )\n } else {\n state.transcript = state.transcript.filter((e) => e.id !== id)\n }\n cb.onTranscript(state.transcript)\n }\n state.agentBubbleId = null\n return\n }\n\n case 'tool_call':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `→ ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'tool_result':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `${msg.ok ? '✓' : '✗'} ${String(msg.tool ?? '?')}`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'call_end': {\n const reasonRaw = String(msg.reason ?? '')\n const reason = mapEndReason(reasonRaw)\n state.endReason = reason\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'system',\n text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,\n },\n ]\n cb.onTranscript(state.transcript)\n cb.onCallEnd(reason)\n return\n }\n\n case 'error': {\n const code = (msg.code as CallErrorCode) ?? 'server_error'\n const message = (msg.message as string) ?? 'server error'\n cb.onError({ code, message })\n return\n }\n }\n}\n\nconst setState = (state: ProtocolState, next: CallState, cb: ProtocolCallbacks) => {\n if (state.state === next) return\n state.state = next\n cb.onState(next)\n}\n\n// Find the last uncommitted user bubble and grow it; or append a new\n// uncommitted bubble. Mirrors voice-rn upsertUserPartial.\nconst upsertUserPartial = (state: ProtocolState, text: string, isFinal: boolean) => {\n let idx = -1\n for (let i = state.transcript.length - 1; i >= 0; i--) {\n const e = state.transcript[i]\n if (e.role === 'user' && e.committed === false) {\n idx = i\n break\n }\n }\n if (idx === -1) {\n state.transcript = [\n ...state.transcript,\n { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },\n ]\n return\n }\n const target = state.transcript[idx] as Extract<TranscriptEntry, { role: 'user' }>\n const next = [...state.transcript]\n next[idx] = { ...target, text, committed: isFinal }\n state.transcript = next\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket URL builder. Identical between browser + node bundles —\n// kept here so the rare server-side change to the path / query shape is\n// a one-line edit.\n// ---------------------------------------------------------------------------\n\nexport interface BuildWsUrlArgs {\n apiBase: string\n agentId: string\n token: string\n bargeIn?: boolean\n}\n\nexport function buildWsUrl(args: BuildWsUrlArgs): string {\n const base = new URL(args.apiBase)\n const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'\n const bargeQS = args.bargeIn === false ? '&barge=off' : ''\n return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`\n}\n","// Node VoiceClient — drives one in-progress call from a Node.js or\n// Electron-main environment.\n//\n// Same WS protocol + transcript state as the browser bundle, but with\n// NO built-in audio I/O. Node consumers feed mic frames in via\n// `sendAudioChunk(buf)` and consume the agent's TTS PCM via the\n// `onAudioChunk` callback they passed to `startCall`. Bring your own\n// audio adapter (sox, PortAudio, OS-native, RTP relay — whatever fits\n// your host).\n//\n// `mute()` / `unmute()` here just gate `sendAudioChunk` — when muted, the\n// next call replaces the buffer with zeroed silence of the same length\n// so the server's endpointing keeps seeing wire frames at the expected\n// cadence (matches voice-rn behaviour).\n\nimport {\n createReconnectingWebSocket,\n type ReconnectingWebSocket,\n type WebSocketFactory,\n} from './ReconnectingWebSocket'\nimport {\n buildWsUrl,\n createProtocolState,\n handleServerMessage,\n type CallEndReason,\n type CallError,\n type ProtocolState,\n} from './protocol'\nimport type { Call, StartCallOptions, VoiceClientConfig } from './config'\n\n// Node-specific extensions to the per-call options. Surfaced as a\n// separate type so the browser StartCallOptions stays clean.\nexport interface NodeStartCallOptions extends StartCallOptions {\n /**\n * Fires for each binary PCM frame the server pushes (Int16 LE mono\n * @ 16 kHz — same as the browser playback path). Wire to your\n * preferred output: write to a `sox -t raw -r 16000 -e signed -b 16\n * -c 1 - default` subprocess, queue into PortAudio, relay over RTP,\n * etc. If you don't supply this callback, agent audio is dropped on\n * the floor.\n */\n onAudioChunk?: (pcm: ArrayBuffer) => void\n}\n\n// Node consumers receive a richer Call handle that includes the\n// raw-PCM control surface.\nexport interface NodeCall extends Call {\n /**\n * Push one mic frame to the server. Expected: Int16 LE mono PCM @\n * 16 kHz. Capture cadence ~100 ms / ~3.2 KB per frame is fine.\n * Returns `false` if the WS isn't open yet (caller may want to\n * back-pressure or drop).\n */\n sendAudioChunk: (pcm: ArrayBuffer | ArrayBufferView) => boolean\n}\n\ninterface NodeVoiceClientArgs {\n config: VoiceClientConfig\n options: NodeStartCallOptions\n token: string\n wsFactory: WebSocketFactory\n}\n\nexport class NodeVoiceClient implements NodeCall {\n private readonly args: NodeVoiceClientArgs\n private readonly proto: ProtocolState\n\n private rws: ReconnectingWebSocket | null = null\n\n private muted = false\n private startedAt: number | null = null\n private endedFired = false\n private lastError: CallError | null = null\n\n constructor(args: NodeVoiceClientArgs) {\n this.args = args\n this.proto = createProtocolState()\n }\n\n // ---------------------------------------------------------------\n // Call interface\n // ---------------------------------------------------------------\n\n get state() {\n return this.proto.state\n }\n\n get transcript() {\n return this.proto.transcript.slice()\n }\n\n get isMuted() {\n return this.muted\n }\n\n end = () => {\n this.teardown('user_hangup')\n }\n\n mute = () => {\n this.muted = true\n }\n\n unmute = () => {\n this.muted = false\n }\n\n // ---------------------------------------------------------------\n // Node-only raw audio surface\n // ---------------------------------------------------------------\n\n sendAudioChunk = (pcm: ArrayBuffer | ArrayBufferView): boolean => {\n if (!this.rws) return false\n if (this.muted) {\n // Silence at the wire cadence — server endpointing depends on a\n // steady frame rhythm; going silent confuses it.\n const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength\n this.rws.send(new ArrayBuffer(len))\n return true\n }\n this.rws.send(pcm)\n return true\n }\n\n // ---------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------\n\n async start(): Promise<void> {\n this.setState('connecting')\n this.startedAt = Date.now()\n\n const url = buildWsUrl({\n apiBase: this.args.config.apiBase,\n agentId: this.args.options.agentId,\n token: this.args.token,\n bargeIn: this.args.options.bargeIn,\n })\n\n this.rws = createReconnectingWebSocket(\n {\n url,\n wsFactory: this.args.wsFactory,\n maxRetries: 3,\n },\n (ev) => this.handleSocketEvent(ev),\n )\n }\n\n // ---------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------\n\n private setState = (next: ProtocolState['state']) => {\n if (this.proto.state === next) return\n this.proto.state = next\n this.args.options.onStateChange?.(next)\n }\n\n private emitError = (err: CallError) => {\n this.lastError = err\n this.args.options.onError?.(err)\n }\n\n private handleSocketEvent = (\n ev:\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error },\n ) => {\n switch (ev.type) {\n case 'open':\n // No mic to start — consumer drives sendAudioChunk themselves.\n break\n case 'reconnected':\n this.proto.transcript = []\n this.proto.agentBubbleId = null\n this.args.options.onTranscript?.(this.proto.transcript)\n this.setState('listening')\n break\n case 'message':\n if (typeof ev.data === 'string') {\n handleServerMessage(ev.data, this.proto, {\n onState: this.setState,\n onTranscript: (entries) => this.args.options.onTranscript?.(entries),\n onError: this.emitError,\n onInterrupt: () => undefined,\n onAgentTurnStart: () => undefined,\n onCallEnd: (reason) => this.teardown(reason),\n })\n } else {\n // Binary frame — agent TTS PCM. Hand to consumer's audio sink.\n this.args.options.onAudioChunk?.(ev.data)\n }\n break\n case 'close':\n if (ev.permanent) {\n const reason: CallEndReason =\n this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')\n this.teardown(reason)\n }\n break\n case 'error':\n this.emitError({ code: 'socket_error', message: ev.error.message })\n break\n }\n }\n\n private teardown = (reason: CallEndReason) => {\n try {\n this.rws?.close(1000, reason)\n } catch {\n // already closed\n }\n this.rws = null\n this.setState('ended')\n this.fireEndOnce(reason)\n }\n\n private fireEndOnce = (reason: CallEndReason) => {\n if (this.endedFired) return\n this.endedFired = true\n const startedAt = this.startedAt ?? Date.now()\n this.args.options.onEnd?.({\n reason,\n errorCode: reason === 'error' ? this.lastError?.code : undefined,\n durationMs: Date.now() - startedAt,\n })\n }\n}\n","// Node entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n// (under the `node` condition; or `from '@craftedxp/voice-js/node'`\n// if your bundler doesn't honour conditional exports).\n//\n// Pre-injects:\n// - `ws` package as the WebSocket transport (declared as an OPTIONAL\n// peer — loaded via dynamic import() so a missing peer surfaces a\n// clear install hint at startCall time rather than crashing at\n// module load).\n//\n// Audio is intentionally NOT injected. Node consumers feed mic frames\n// in via `call.sendAudioChunk(buf)` and consume agent audio via\n// `onAudioChunk` they passed to startCall. See NodeVoiceClient for the\n// raw PCM contract.\n\nimport {\n mergeStartCallContext,\n normalizeConfig,\n type Call,\n type FetchToken,\n type StartCallOptions,\n type VoiceClientConfig,\n type VoiceClientFactory,\n} from './config'\nimport type { WebSocketFactory, WebSocketLike } from './ReconnectingWebSocket'\nimport {\n NodeVoiceClient,\n type NodeCall,\n type NodeStartCallOptions,\n} from './NodeVoiceClient'\n\n// Lazy + cached. The first startCall pays the dynamic-import cost; later\n// calls reuse the resolved constructor.\nlet cachedWsCtor: { new (url: string): WebSocketLike } | null = null\n\nconst loadWsCtor = async (): Promise<{ new (url: string): WebSocketLike }> => {\n if (cachedWsCtor) return cachedWsCtor\n try {\n // Dynamic import survives bundler tree-shaking and lets us emit a\n // clean error message for the missing-peer case. Browser bundles\n // never reach this code path because they pick the `browser`\n // condition in package.json `exports`.\n const mod = (await import('ws')) as unknown as {\n default?: { new (url: string): WebSocketLike }\n WebSocket?: { new (url: string): WebSocketLike }\n }\n const ctor = mod.WebSocket ?? mod.default\n if (!ctor) {\n throw new Error('imported `ws` but neither default nor named WebSocket export was found')\n }\n cachedWsCtor = ctor\n return ctor\n } catch (err) {\n throw new Error(\n \"@craftedxp/voice-js (node): missing optional peer `ws`. Install it with `npm install ws` (ws is declared as `peerDependenciesMeta.optional` so npm doesn't install it automatically). Original: \" +\n (err instanceof Error ? err.message : String(err)),\n )\n }\n}\n\nclass NodeVoiceFactory implements VoiceClientFactory {\n readonly config: VoiceClientConfig\n\n constructor(config: VoiceClientConfig) {\n this.config = config\n }\n\n startCall = async (options: NodeStartCallOptions): Promise<NodeCall> => {\n if (!options.agentId) {\n throw new Error('startCall: agentId is required')\n }\n\n // Resolve `ws` before the network round-trip so the missing-peer\n // failure surfaces early.\n const WsCtor = await loadWsCtor()\n const wsFactory: WebSocketFactory = (url) => new WsCtor(url)\n\n const { context, metadata } = mergeStartCallContext(this.config, options)\n const fetchArgs = {\n agentId: options.agentId,\n userId: options.userId,\n context,\n metadata,\n }\n\n let token: string\n if (options.token) {\n token = options.token\n } else {\n token = await this.config.fetchToken(fetchArgs)\n if (!token) {\n throw new Error('configureVoiceClient.fetchToken returned empty token')\n }\n }\n\n const client = new NodeVoiceClient({\n config: this.config,\n options: { ...options, context, metadata },\n token,\n wsFactory,\n })\n await client.start()\n return client\n }\n}\n\n/**\n * One-time SDK setup for Node.js / Electron-main consumers. Returns a\n * factory you call `startCall` on for every voice call. Same shape as\n * the browser entry but the returned `Call` has an extra\n * `sendAudioChunk` method for raw-PCM input, and `startCall` accepts\n * an `onAudioChunk` callback for raw-PCM output.\n *\n * Example (vterm-style CLI, sox sub-process for I/O):\n *\n * import { configureVoiceClient } from '@craftedxp/voice-js/node'\n * import { spawn } from 'child_process'\n *\n * const voice = configureVoiceClient({\n * apiBase: 'https://api.your-server.com',\n * fetchToken: async () => mintFromMyBackend(),\n * })\n *\n * const mic = spawn('sox', [...recArgs, '-r', '16000', '-c', '1', '-b', '16', '-e', 'signed', '-t', 'raw', '-'])\n * const spk = spawn('sox', ['-t', 'raw', '-r', '16000', '-c', '1', '-b', '16', '-e', 'signed', '-', ...playArgs])\n *\n * const call = await voice.startCall({\n * agentId: 'agt_xxx',\n * onAudioChunk: (pcm) => spk.stdin.write(Buffer.from(pcm)),\n * onEnd: () => { mic.kill(); spk.stdin.end() },\n * })\n *\n * mic.stdout.on('data', (chunk) => call.sendAudioChunk(chunk))\n */\nexport function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory {\n return new NodeVoiceFactory(normalizeConfig(config))\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — types + advanced primitives. Mirror of browser entry\n// minus the AudioCapture/AudioPlayback exports (browser-only).\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n FetchToken,\n FetchTokenArgs,\n StartCallOptions,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type { NodeCall, NodeStartCallOptions } from './NodeVoiceClient'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n"],"mappings":";AAmLO,SAAS,gBAAgB,QAA8C;AAC5E,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,0CAA0C;AACvE,MAAI,YAAa,QAAmB;AAClC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AACA,MAAI,OAAO,OAAO,eAAe,YAAY;AAC3C,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AAAA,EAC5C;AACF;AAGO,SAAS,sBACd,SACA,MAC0E;AAC1E,QAAM,UACJ,QAAQ,kBAAkB,KAAK,UAC3B,EAAE,GAAI,QAAQ,kBAAkB,CAAC,GAAI,GAAI,KAAK,WAAW,CAAC,EAAG,IAC7D;AACN,QAAM,WACJ,QAAQ,mBAAmB,KAAK,WAC5B,EAAE,GAAI,QAAQ,mBAAmB,CAAC,GAAI,GAAI,KAAK,YAAY,CAAC,EAAG,IAC/D;AACN,SAAO,EAAE,SAAS,SAAS;AAC7B;;;AC9JA,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAEnB,IAAM,8BAA8B,CACzC,SACA,YACG;AACH,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,iBAAiB,QAAQ,oBAAoB;AACnD,QAAM,aAAa,QAAQ,gBAAgB;AAE3C,MAAI,KAA2B;AAC/B,MAAI,mBAAmB;AACvB,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,iBAAuD;AAE3D,QAAM,WAAW,MAAM;AACrB,SAAK,QAAQ,UAAU,QAAQ,GAAG;AAIlC,OAAG,aAAa;AAChB,OAAG,SAAS,MAAM;AAChB,UAAI,YAAY,EAAG,SAAQ,EAAE,MAAM,OAAO,CAAC;AAAA,UACtC,SAAQ,EAAE,MAAM,cAAc,CAAC;AACpC,gBAAU;AACV,gBAAU;AAAA,IACZ;AACA,OAAG,YAAY,CAAC,OAAO;AACrB,cAAQ,EAAE,MAAM,WAAW,MAAM,GAAG,KAA6B,CAAC;AAAA,IACpE;AACA,OAAG,UAAU,MAAM;AACjB,cAAQ,EAAE,MAAM,SAAS,OAAO,IAAI,MAAM,iBAAiB,EAAE,CAAC;AAAA,IAChE;AACA,OAAG,UAAU,CAAC,OAAO;AACnB,WAAK;AACL,YAAM,cAAc,CAAC,oBAAoB,UAAU;AACnD,UAAI,CAAC,aAAa;AAChB,gBAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM,GAAG;AAAA,UACT,QAAQ,GAAG;AAAA,UACX,WAAW;AAAA,QACb,CAAC;AACD;AAAA,MACF;AACA,cAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM,GAAG;AAAA,QACT,QAAQ,GAAG;AAAA,QACX,WAAW;AAAA,MACb,CAAC;AACD;AACA,YAAM,QAAQ,KAAK,IAAI,SAAS,UAAU;AAC1C,gBAAU,KAAK,IAAI,UAAU,GAAG,UAAU;AAC1C,uBAAiB,WAAW,UAAU,KAAK;AAAA,IAC7C;AAAA,EACF;AAEA,WAAS;AAET,SAAO;AAAA,IACL,MAAM,CAAC,SAAiD;AACtD,UAAI,MAAM,GAAG,eAAe,gBAAiB,IAAG,KAAK,IAAI;AAAA,IAC3D;AAAA,IACA,OAAO,CAAC,OAAO,KAAM,SAAS,uBAAuB;AACnD,yBAAmB;AACnB,UAAI,gBAAgB;AAClB,qBAAa,cAAc;AAC3B,yBAAiB;AAAA,MACnB;AACA,UAAI;AACF,YAAI,MAAM,MAAM,MAAM;AAAA,MACxB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,YAAY,MAAM,IAAI,cAAc;AAAA,EACtC;AACF;;;AChBO,IAAM,sBAAsB,OAAsB;AAAA,EACvD,OAAO;AAAA,EACP,YAAY,CAAC;AAAA,EACb,eAAe;AAAA,EACf,WAAW;AAAA,EACX,WAAW;AACb;AAmBA,IAAM,eAAe,CAAC,QAA+B;AACnD,MAAI,QAAQ,cAAe,QAAO;AAClC,MAAI,QAAQ,iBAAkB,QAAO;AACrC,MAAI,QAAQ,qBAAqB,QAAQ,eAAgB,QAAO;AAChE,SAAO;AACT;AAQO,SAAS,oBACd,KACA,OACA,IACM;AACN,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,GAAG;AAAA,EACtB,QAAQ;AACN;AAAA,EACF;AAEA,UAAQ,IAAI,MAAM;AAAA,IAChB,KAAK;AACH,eAAS,OAAO,aAAa,EAAE;AAC/B;AAAA,IAEF,KAAK,cAAc;AACjB,YAAM,OAAQ,IAAI,QAAmB;AACrC,UAAI,CAAC,KAAM;AACX,YAAM,UAAU,CAAC,CAAC,IAAI;AACtB,UAAI,CAAC,QAAS,UAAS,OAAO,iBAAiB,EAAE;AACjD,wBAAkB,OAAO,MAAM,OAAO;AACtC,SAAG,aAAa,MAAM,UAAU;AAChC;AAAA,IACF;AAAA,IAEA,KAAK,oBAAoB;AACvB,YAAM,KAAK,IAAI,MAAM,WAAW;AAChC,YAAM,gBAAgB;AACtB,YAAM,aAAa,CAAC,GAAG,MAAM,YAAY,EAAE,IAAI,MAAM,SAAS,MAAM,GAAG,CAAC;AACxE,SAAG,aAAa,MAAM,UAAU;AAChC,SAAG,iBAAiB;AACpB,eAAS,OAAO,kBAAkB,EAAE;AACpC;AAAA,IACF;AAAA,IAEA,KAAK,cAAc;AACjB,YAAM,QAAS,IAAI,QAAmB;AACtC,UAAI,CAAC,SAAS,CAAC,MAAM,cAAe;AACpC,YAAM,KAAK,MAAM;AACjB,YAAM,aAAa,MAAM,WAAW;AAAA,QAAI,CAAC,MACvC,EAAE,OAAO,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,GAAG,MAAM,EAAE,OAAO,MAAM,IAAI;AAAA,MACvE;AACA,SAAG,aAAa,MAAM,UAAU;AAChC;AAAA,IACF;AAAA,IAEA,KAAK;AACH,YAAM,gBAAgB;AACtB,eAAS,OAAO,aAAa,EAAE;AAC/B;AAAA,IAEF,KAAK;AACH,SAAG,YAAY;AACf;AAAA,IAEF,KAAK,oBAAoB;AACvB,YAAM,aAAc,IAAI,iBAA4B,IAAI,KAAK;AAC7D,UAAI,MAAM,eAAe;AACvB,cAAM,KAAK,MAAM;AACjB,YAAI,WAAW;AACb,gBAAM,aAAa,MAAM,WAAW;AAAA,YAAI,CAAC,MACvC,EAAE,OAAO,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,GAAG,MAAM,WAAW,aAAa,KAAK,IAAI;AAAA,UACrF;AAAA,QACF,OAAO;AACL,gBAAM,aAAa,MAAM,WAAW,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AAAA,QAC/D;AACA,WAAG,aAAa,MAAM,UAAU;AAAA,MAClC;AACA,YAAM,gBAAgB;AACtB;AAAA,IACF;AAAA,IAEA,KAAK;AACH,YAAM,aAAa;AAAA,QACjB,GAAG,MAAM;AAAA,QACT;AAAA,UACE,IAAI,IAAI,MAAM,WAAW;AAAA,UACzB,MAAM;AAAA,UACN,MAAM,UAAK,OAAO,IAAI,QAAQ,GAAG,CAAC,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,IAAI,IAAI,EAAE;AAAA,QAChF;AAAA,MACF;AACA,SAAG,aAAa,MAAM,UAAU;AAChC;AAAA,IAEF,KAAK;AACH,YAAM,aAAa;AAAA,QACjB,GAAG,MAAM;AAAA,QACT;AAAA,UACE,IAAI,IAAI,MAAM,WAAW;AAAA,UACzB,MAAM;AAAA,UACN,MAAM,GAAG,IAAI,KAAK,WAAM,QAAG,IAAI,OAAO,IAAI,QAAQ,GAAG,CAAC;AAAA,QACxD;AAAA,MACF;AACA,SAAG,aAAa,MAAM,UAAU;AAChC;AAAA,IAEF,KAAK,YAAY;AACf,YAAM,YAAY,OAAO,IAAI,UAAU,EAAE;AACzC,YAAM,SAAS,aAAa,SAAS;AACrC,YAAM,YAAY;AAClB,YAAM,aAAa;AAAA,QACjB,GAAG,MAAM;AAAA,QACT;AAAA,UACE,IAAI,IAAI,MAAM,WAAW;AAAA,UACzB,MAAM;AAAA,UACN,MAAM,aAAa,YAAY,KAAK,SAAS,MAAM,EAAE;AAAA,QACvD;AAAA,MACF;AACA,SAAG,aAAa,MAAM,UAAU;AAChC,SAAG,UAAU,MAAM;AACnB;AAAA,IACF;AAAA,IAEA,KAAK,SAAS;AACZ,YAAM,OAAQ,IAAI,QAA0B;AAC5C,YAAM,UAAW,IAAI,WAAsB;AAC3C,SAAG,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC5B;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAM,WAAW,CAAC,OAAsB,MAAiB,OAA0B;AACjF,MAAI,MAAM,UAAU,KAAM;AAC1B,QAAM,QAAQ;AACd,KAAG,QAAQ,IAAI;AACjB;AAIA,IAAM,oBAAoB,CAAC,OAAsB,MAAc,YAAqB;AAClF,MAAI,MAAM;AACV,WAAS,IAAI,MAAM,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;AACrD,UAAM,IAAI,MAAM,WAAW,CAAC;AAC5B,QAAI,EAAE,SAAS,UAAU,EAAE,cAAc,OAAO;AAC9C,YAAM;AACN;AAAA,IACF;AAAA,EACF;AACA,MAAI,QAAQ,IAAI;AACd,UAAM,aAAa;AAAA,MACjB,GAAG,MAAM;AAAA,MACT,EAAE,IAAI,IAAI,MAAM,WAAW,IAAI,MAAM,QAAQ,MAAM,WAAW,QAAQ;AAAA,IACxE;AACA;AAAA,EACF;AACA,QAAM,SAAS,MAAM,WAAW,GAAG;AACnC,QAAM,OAAO,CAAC,GAAG,MAAM,UAAU;AACjC,OAAK,GAAG,IAAI,EAAE,GAAG,QAAQ,MAAM,WAAW,QAAQ;AAClD,QAAM,aAAa;AACrB;AAeO,SAAS,WAAW,MAA8B;AACvD,QAAM,OAAO,IAAI,IAAI,KAAK,OAAO;AACjC,QAAM,QAAQ,KAAK,aAAa,WAAW,SAAS;AACpD,QAAM,UAAU,KAAK,YAAY,QAAQ,eAAe;AACxD,SAAO,GAAG,KAAK,KAAK,KAAK,IAAI,cAAc,mBAAmB,KAAK,OAAO,CAAC,eAAe,mBAAmB,KAAK,KAAK,CAAC,GAAG,OAAO;AACpI;;;ACzQO,IAAM,kBAAN,MAA0C;AAAA,EAW/C,YAAY,MAA2B;AAPvC,SAAQ,MAAoC;AAE5C,SAAQ,QAAQ;AAChB,SAAQ,YAA2B;AACnC,SAAQ,aAAa;AACrB,SAAQ,YAA8B;AAuBtC,eAAM,MAAM;AACV,WAAK,SAAS,aAAa;AAAA,IAC7B;AAEA,gBAAO,MAAM;AACX,WAAK,QAAQ;AAAA,IACf;AAEA,kBAAS,MAAM;AACb,WAAK,QAAQ;AAAA,IACf;AAMA;AAAA;AAAA;AAAA,0BAAiB,CAAC,QAAgD;AAChE,UAAI,CAAC,KAAK,IAAK,QAAO;AACtB,UAAI,KAAK,OAAO;AAGd,cAAM,MAAM,YAAY,OAAO,GAAG,IAAI,IAAI,aAAa,IAAI;AAC3D,aAAK,IAAI,KAAK,IAAI,YAAY,GAAG,CAAC;AAClC,eAAO;AAAA,MACT;AACA,WAAK,IAAI,KAAK,GAAG;AACjB,aAAO;AAAA,IACT;AA+BA;AAAA;AAAA;AAAA,SAAQ,WAAW,CAAC,SAAiC;AACnD,UAAI,KAAK,MAAM,UAAU,KAAM;AAC/B,WAAK,MAAM,QAAQ;AACnB,WAAK,KAAK,QAAQ,gBAAgB,IAAI;AAAA,IACxC;AAEA,SAAQ,YAAY,CAAC,QAAmB;AACtC,WAAK,YAAY;AACjB,WAAK,KAAK,QAAQ,UAAU,GAAG;AAAA,IACjC;AAEA,SAAQ,oBAAoB,CAC1B,OAMG;AACH,cAAQ,GAAG,MAAM;AAAA,QACf,KAAK;AAEH;AAAA,QACF,KAAK;AACH,eAAK,MAAM,aAAa,CAAC;AACzB,eAAK,MAAM,gBAAgB;AAC3B,eAAK,KAAK,QAAQ,eAAe,KAAK,MAAM,UAAU;AACtD,eAAK,SAAS,WAAW;AACzB;AAAA,QACF,KAAK;AACH,cAAI,OAAO,GAAG,SAAS,UAAU;AAC/B,gCAAoB,GAAG,MAAM,KAAK,OAAO;AAAA,cACvC,SAAS,KAAK;AAAA,cACd,cAAc,CAAC,YAAY,KAAK,KAAK,QAAQ,eAAe,OAAO;AAAA,cACnE,SAAS,KAAK;AAAA,cACd,aAAa,MAAM;AAAA,cACnB,kBAAkB,MAAM;AAAA,cACxB,WAAW,CAAC,WAAW,KAAK,SAAS,MAAM;AAAA,YAC7C,CAAC;AAAA,UACH,OAAO;AAEL,iBAAK,KAAK,QAAQ,eAAe,GAAG,IAAI;AAAA,UAC1C;AACA;AAAA,QACF,KAAK;AACH,cAAI,GAAG,WAAW;AAChB,kBAAM,SACJ,KAAK,MAAM,cAAc,KAAK,YAAY,UAAU;AACtD,iBAAK,SAAS,MAAM;AAAA,UACtB;AACA;AAAA,QACF,KAAK;AACH,eAAK,UAAU,EAAE,MAAM,gBAAgB,SAAS,GAAG,MAAM,QAAQ,CAAC;AAClE;AAAA,MACJ;AAAA,IACF;AAEA,SAAQ,WAAW,CAAC,WAA0B;AAC5C,UAAI;AACF,aAAK,KAAK,MAAM,KAAM,MAAM;AAAA,MAC9B,QAAQ;AAAA,MAER;AACA,WAAK,MAAM;AACX,WAAK,SAAS,OAAO;AACrB,WAAK,YAAY,MAAM;AAAA,IACzB;AAEA,SAAQ,cAAc,CAAC,WAA0B;AAC/C,UAAI,KAAK,WAAY;AACrB,WAAK,aAAa;AAClB,YAAM,YAAY,KAAK,aAAa,KAAK,IAAI;AAC7C,WAAK,KAAK,QAAQ,QAAQ;AAAA,QACxB;AAAA,QACA,WAAW,WAAW,UAAU,KAAK,WAAW,OAAO;AAAA,QACvD,YAAY,KAAK,IAAI,IAAI;AAAA,MAC3B,CAAC;AAAA,IACH;AA3JE,SAAK,OAAO;AACZ,SAAK,QAAQ,oBAAoB;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,QAAQ;AACV,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAI,aAAa;AACf,WAAO,KAAK,MAAM,WAAW,MAAM;AAAA,EACrC;AAAA,EAEA,IAAI,UAAU;AACZ,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAmCA,MAAM,QAAuB;AAC3B,SAAK,SAAS,YAAY;AAC1B,SAAK,YAAY,KAAK,IAAI;AAE1B,UAAM,MAAM,WAAW;AAAA,MACrB,SAAS,KAAK,KAAK,OAAO;AAAA,MAC1B,SAAS,KAAK,KAAK,QAAQ;AAAA,MAC3B,OAAO,KAAK,KAAK;AAAA,MACjB,SAAS,KAAK,KAAK,QAAQ;AAAA,IAC7B,CAAC;AAED,SAAK,MAAM;AAAA,MACT;AAAA,QACE;AAAA,QACA,WAAW,KAAK,KAAK;AAAA,QACrB,YAAY;AAAA,MACd;AAAA,MACA,CAAC,OAAO,KAAK,kBAAkB,EAAE;AAAA,IACnC;AAAA,EACF;AAoFF;;;ACtMA,IAAI,eAA4D;AAEhE,IAAM,aAAa,YAA2D;AAC5E,MAAI,aAAc,QAAO;AACzB,MAAI;AAKF,UAAM,MAAO,MAAM,OAAO,IAAI;AAI9B,UAAM,OAAO,IAAI,aAAa,IAAI;AAClC,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,wEAAwE;AAAA,IAC1F;AACA,mBAAe;AACf,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,sMACG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACpD;AAAA,EACF;AACF;AAEA,IAAM,mBAAN,MAAqD;AAAA,EAGnD,YAAY,QAA2B;AAIvC,qBAAY,OAAO,YAAqD;AACtE,UAAI,CAAC,QAAQ,SAAS;AACpB,cAAM,IAAI,MAAM,gCAAgC;AAAA,MAClD;AAIA,YAAM,SAAS,MAAM,WAAW;AAChC,YAAM,YAA8B,CAAC,QAAQ,IAAI,OAAO,GAAG;AAE3D,YAAM,EAAE,SAAS,SAAS,IAAI,sBAAsB,KAAK,QAAQ,OAAO;AACxE,YAAM,YAAY;AAAA,QAChB,SAAS,QAAQ;AAAA,QACjB,QAAQ,QAAQ;AAAA,QAChB;AAAA,QACA;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,QAAQ,OAAO;AACjB,gBAAQ,QAAQ;AAAA,MAClB,OAAO;AACL,gBAAQ,MAAM,KAAK,OAAO,WAAW,SAAS;AAC9C,YAAI,CAAC,OAAO;AACV,gBAAM,IAAI,MAAM,sDAAsD;AAAA,QACxE;AAAA,MACF;AAEA,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,QAAQ,KAAK;AAAA,QACb,SAAS,EAAE,GAAG,SAAS,SAAS,SAAS;AAAA,QACzC;AAAA,QACA;AAAA,MACF,CAAC;AACD,YAAM,OAAO,MAAM;AACnB,aAAO;AAAA,IACT;AAvCE,SAAK,SAAS;AAAA,EAChB;AAuCF;AA8BO,SAAS,qBAAqB,QAA+C;AAClF,SAAO,IAAI,iBAAiB,gBAAgB,MAAM,CAAC;AACrD;","names":[]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@craftedxp/voice-js",
3
+ "version": "0.2.0",
4
+ "description": "JS SDK for embedding a voice agent call in any JS environment — browser, Node.js, Electron. Zero framework dependencies. Drop-in companion to @craftedxp/voice-rn (React Native).",
5
+ "author": "Crafted XP",
6
+ "license": "MIT",
7
+ "main": "dist/browser.js",
8
+ "module": "dist/browser.mjs",
9
+ "types": "dist/browser.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/browser.d.ts",
13
+ "browser": "./dist/browser.mjs",
14
+ "node": {
15
+ "types": "./dist/node.d.ts",
16
+ "import": "./dist/node.mjs",
17
+ "require": "./dist/node.js"
18
+ },
19
+ "import": "./dist/browser.mjs",
20
+ "require": "./dist/browser.js",
21
+ "default": "./dist/browser.mjs"
22
+ },
23
+ "./node": {
24
+ "types": "./dist/node.d.ts",
25
+ "import": "./dist/node.mjs",
26
+ "require": "./dist/node.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "CONSUMING.md",
34
+ "DEVELOPING.md"
35
+ ],
36
+ "sideEffects": false,
37
+ "keywords": [
38
+ "voice",
39
+ "voice-ai",
40
+ "agent",
41
+ "websocket",
42
+ "browser",
43
+ "node",
44
+ "electron",
45
+ "craftedxp"
46
+ ],
47
+ "peerDependencies": {
48
+ "ws": "^8.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "ws": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/ws": "^8.5.10",
57
+ "tsup": "^8.3.5",
58
+ "typescript": "^5.3.3",
59
+ "ws": "^8.18.0"
60
+ },
61
+ "scripts": {
62
+ "clean": "rm -rf dist",
63
+ "build": "npm run clean && tsup",
64
+ "dev": "tsup --watch",
65
+ "typecheck": "tsc --noEmit",
66
+ "embed:copy": "cp dist/embed.iife.js ../../web/embed.js"
67
+ }
68
+ }