@craftedxp/voice-js 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js CHANGED
@@ -1,703 +1,694 @@
1
- 'use strict'
2
- var __create = Object.create
3
- var __defProp = Object.defineProperty
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor
5
- var __getOwnPropNames = Object.getOwnPropertyNames
6
- var __getProtoOf = Object.getPrototypeOf
7
- var __hasOwnProp = Object.prototype.hasOwnProperty
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __export = (target, all) => {
9
- for (var name in all) __defProp(target, name, { get: all[name], enumerable: true })
10
- }
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
11
12
  var __copyProps = (to, from, except, desc) => {
12
- if ((from && typeof from === 'object') || typeof from === 'function') {
13
+ if (from && typeof from === "object" || typeof from === "function") {
13
14
  for (let key of __getOwnPropNames(from))
14
15
  if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, {
16
- get: () => from[key],
17
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
18
- })
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
17
  }
20
- return to
21
- }
22
- var __toESM = (mod, isNodeMode, target) => (
23
- (target = mod != null ? __create(__getProtoOf(mod)) : {}),
24
- __copyProps(
25
- // If the importer is in node compatibility mode or this is not an ESM
26
- // file that has been converted to a CommonJS file using a Babel-
27
- // compatible transform (i.e. "__esModule" has not been set), then set
28
- // "default" to the CommonJS "module.exports" for node compatibility.
29
- isNodeMode || !mod || !mod.__esModule
30
- ? __defProp(target, 'default', { value: mod, enumerable: true })
31
- : target,
32
- mod,
33
- )
34
- )
35
- var __toCommonJS = (mod) => __copyProps(__defProp({}, '__esModule', { value: true }), mod)
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
29
 
37
30
  // src/node.ts
38
- var node_exports = {}
31
+ var node_exports = {};
39
32
  __export(node_exports, {
40
33
  buildWsUrl: () => buildWsUrl,
41
34
  configureVoiceClient: () => configureVoiceClient,
42
35
  createProtocolState: () => createProtocolState,
43
36
  createReconnectingWebSocket: () => createReconnectingWebSocket,
44
- handleServerMessage: () => handleServerMessage,
45
- })
46
- module.exports = __toCommonJS(node_exports)
37
+ handleServerMessage: () => handleServerMessage
38
+ });
39
+ module.exports = __toCommonJS(node_exports);
47
40
 
48
41
  // src/config.ts
49
42
  function normalizeConfig(config) {
50
- if (!config) throw new Error('configureVoiceClient: config is required')
51
- if ('apiKey' in config) {
43
+ if (!config) throw new Error("configureVoiceClient: config is required");
44
+ if ("apiKey" in config) {
52
45
  throw new Error(
53
- '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.',
54
- )
46
+ "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."
47
+ );
55
48
  }
56
49
  if (!config.apiBase) {
57
- throw new Error('configureVoiceClient: apiBase is required')
50
+ throw new Error("configureVoiceClient: apiBase is required");
58
51
  }
59
- if (typeof config.fetchToken !== 'function') {
60
- throw new Error('configureVoiceClient: fetchToken must be a function')
52
+ if (typeof config.fetchToken !== "function") {
53
+ throw new Error("configureVoiceClient: fetchToken must be a function");
61
54
  }
62
55
  return {
63
56
  ...config,
64
- apiBase: config.apiBase.replace(/\/+$/, ''),
65
- }
57
+ apiBase: config.apiBase.replace(/\/+$/, "")
58
+ };
66
59
  }
67
60
  function mergeStartCallContext(factory, call) {
68
- const context =
69
- factory.defaultContext || call.context
70
- ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }
71
- : void 0
72
- const metadata =
73
- factory.defaultMetadata || call.metadata
74
- ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }
75
- : void 0
76
- return { context, metadata }
61
+ const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
62
+ const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
63
+ return { context, metadata };
77
64
  }
78
65
 
79
66
  // src/ReconnectingWebSocket.ts
80
- var READYSTATE_OPEN = 1
81
- var READYSTATE_CLOSED = 3
67
+ var READYSTATE_OPEN = 1;
68
+ var READYSTATE_CLOSED = 3;
82
69
  var createReconnectingWebSocket = (options, onEvent) => {
83
- const maxRetries = options.maxRetries ?? 3
84
- const initialBackoff = options.initialBackoffMs ?? 500
85
- const maxBackoff = options.maxBackoffMs ?? 8e3
86
- let ws = null
87
- let intentionalClose = false
88
- let retries = 0
89
- let backoff = initialBackoff
90
- let reconnectTimer = null
70
+ const maxRetries = options.maxRetries ?? 3;
71
+ const initialBackoff = options.initialBackoffMs ?? 500;
72
+ const maxBackoff = options.maxBackoffMs ?? 8e3;
73
+ let ws = null;
74
+ let intentionalClose = false;
75
+ let retries = 0;
76
+ let backoff = initialBackoff;
77
+ let reconnectTimer = null;
91
78
  const openOnce = () => {
92
- ws = options.wsFactory(options.url)
93
- ws.binaryType = 'arraybuffer'
79
+ ws = options.wsFactory(options.url);
80
+ ws.binaryType = "arraybuffer";
94
81
  ws.onopen = () => {
95
- if (retries === 0) onEvent({ type: 'open' })
96
- else onEvent({ type: 'reconnected' })
97
- retries = 0
98
- backoff = initialBackoff
99
- }
82
+ if (retries === 0) onEvent({ type: "open" });
83
+ else onEvent({ type: "reconnected" });
84
+ retries = 0;
85
+ backoff = initialBackoff;
86
+ };
100
87
  ws.onmessage = (ev) => {
101
- onEvent({ type: 'message', data: ev.data })
102
- }
88
+ onEvent({ type: "message", data: ev.data });
89
+ };
103
90
  ws.onerror = () => {
104
- onEvent({ type: 'error', error: new Error('WebSocket error') })
105
- }
91
+ onEvent({ type: "error", error: new Error("WebSocket error") });
92
+ };
106
93
  ws.onclose = (ev) => {
107
- ws = null
108
- const shouldRetry = !intentionalClose && retries < maxRetries
94
+ ws = null;
95
+ const shouldRetry = !intentionalClose && retries < maxRetries;
109
96
  if (!shouldRetry) {
110
97
  onEvent({
111
- type: 'close',
98
+ type: "close",
112
99
  code: ev.code,
113
100
  reason: ev.reason,
114
- permanent: true,
115
- })
116
- return
101
+ permanent: true
102
+ });
103
+ return;
117
104
  }
118
105
  onEvent({
119
- type: 'close',
106
+ type: "close",
120
107
  code: ev.code,
121
108
  reason: ev.reason,
122
- permanent: false,
123
- })
124
- retries++
125
- const delay = Math.min(backoff, maxBackoff)
126
- backoff = Math.min(backoff * 2, maxBackoff)
127
- reconnectTimer = setTimeout(openOnce, delay)
128
- }
129
- }
130
- openOnce()
109
+ permanent: false
110
+ });
111
+ retries++;
112
+ const delay = Math.min(backoff, maxBackoff);
113
+ backoff = Math.min(backoff * 2, maxBackoff);
114
+ reconnectTimer = setTimeout(openOnce, delay);
115
+ };
116
+ };
117
+ openOnce();
131
118
  return {
132
119
  send: (data) => {
133
- if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)
120
+ if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
134
121
  },
135
- close: (code = 1e3, reason = 'client-requested') => {
136
- intentionalClose = true
122
+ close: (code = 1e3, reason = "client-requested") => {
123
+ intentionalClose = true;
137
124
  if (reconnectTimer) {
138
- clearTimeout(reconnectTimer)
139
- reconnectTimer = null
125
+ clearTimeout(reconnectTimer);
126
+ reconnectTimer = null;
140
127
  }
141
128
  try {
142
- ws?.close(code, reason)
143
- } catch {}
129
+ ws?.close(code, reason);
130
+ } catch {
131
+ }
144
132
  },
145
- readyState: () => ws?.readyState ?? READYSTATE_CLOSED,
146
- }
147
- }
133
+ readyState: () => ws?.readyState ?? READYSTATE_CLOSED
134
+ };
135
+ };
148
136
 
149
137
  // src/protocol.ts
150
138
  var createProtocolState = () => ({
151
- state: 'idle',
139
+ state: "idle",
152
140
  transcript: [],
153
141
  agentBubbleId: null,
154
142
  idCounter: 0,
155
- endReason: null,
156
- })
143
+ endReason: null
144
+ });
157
145
  var mapEndReason = (raw) => {
158
- if (raw === 'agent_ended') return 'agent_ended'
159
- if (raw === 'caller_hung_up') return 'user_hangup'
160
- if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'
161
- return 'error'
162
- }
146
+ if (raw === "agent_ended") return "agent_ended";
147
+ if (raw === "caller_hung_up") return "user_hangup";
148
+ if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
149
+ return "error";
150
+ };
163
151
  function handleServerMessage(raw, state, cb) {
164
- let msg
152
+ let msg;
165
153
  try {
166
- msg = JSON.parse(raw)
154
+ msg = JSON.parse(raw);
167
155
  } catch {
168
- return
156
+ return;
169
157
  }
170
158
  switch (msg.type) {
171
- case 'connected':
172
- cb.onConnected()
173
- setState(state, 'listening', cb)
174
- return
175
- case 'transcript': {
176
- const text = msg.text ?? ''
177
- if (!text) return
178
- const isFinal = !!msg.isFinal
179
- if (!isFinal) setState(state, 'user_speaking', cb)
180
- upsertUserPartial(state, text, isFinal)
181
- cb.onTranscript(state.transcript)
182
- return
183
- }
184
- case 'agent_turn_start': {
185
- const id = `m${state.idCounter++}`
186
- state.agentBubbleId = id
187
- state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]
188
- cb.onTranscript(state.transcript)
189
- const seq = typeof msg.seq === 'number' ? msg.seq : void 0
190
- cb.onAgentTurnStart(seq)
191
- setState(state, 'agent_speaking', cb)
192
- return
193
- }
194
- case 'agent_text': {
195
- const delta = msg.text ?? ''
196
- if (!delta || !state.agentBubbleId) return
197
- const id = state.agentBubbleId
198
- state.transcript = state.transcript.map((e) =>
199
- e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,
200
- )
201
- cb.onTranscript(state.transcript)
202
- return
203
- }
204
- case 'agent_turn_end': {
205
- state.agentBubbleId = null
206
- const seq = typeof msg.seq === 'number' ? msg.seq : void 0
207
- cb.onAgentTurnEnd(seq)
208
- setState(state, 'listening', cb)
209
- return
210
- }
211
- case 'interrupt':
212
- cb.onInterrupt()
213
- return
214
- case 'agent_turn_abort': {
215
- const committed = (msg.committedText ?? '').trim()
159
+ case "connected":
160
+ cb.onConnected();
161
+ setState(state, "listening", cb);
162
+ return;
163
+ case "transcript": {
164
+ const text = msg.text ?? "";
165
+ if (!text) return;
166
+ const isFinal = !!msg.isFinal;
167
+ if (!isFinal) setState(state, "user_speaking", cb);
168
+ upsertUserPartial(state, text, isFinal);
169
+ cb.onTranscript(state.transcript);
170
+ return;
171
+ }
172
+ case "agent_turn_start": {
173
+ const id = `m${state.idCounter++}`;
174
+ state.agentBubbleId = id;
175
+ state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
176
+ cb.onTranscript(state.transcript);
177
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
178
+ cb.onAgentTurnStart(seq);
179
+ setState(state, "agent_speaking", cb);
180
+ return;
181
+ }
182
+ case "agent_text": {
183
+ const delta = msg.text ?? "";
184
+ if (!delta || !state.agentBubbleId) return;
185
+ const id = state.agentBubbleId;
186
+ state.transcript = state.transcript.map(
187
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
188
+ );
189
+ cb.onTranscript(state.transcript);
190
+ return;
191
+ }
192
+ case "agent_turn_end": {
193
+ state.agentBubbleId = null;
194
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
195
+ cb.onAgentTurnEnd(seq);
196
+ setState(state, "listening", cb);
197
+ return;
198
+ }
199
+ case "interrupt":
200
+ cb.onInterrupt();
201
+ return;
202
+ case "agent_turn_abort": {
203
+ const committed = (msg.committedText ?? "").trim();
216
204
  if (state.agentBubbleId) {
217
- const id = state.agentBubbleId
205
+ const id = state.agentBubbleId;
218
206
  if (committed) {
219
- state.transcript = state.transcript.map((e) =>
220
- e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,
221
- )
207
+ state.transcript = state.transcript.map(
208
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
209
+ );
222
210
  } else {
223
- state.transcript = state.transcript.filter((e) => e.id !== id)
211
+ state.transcript = state.transcript.filter((e) => e.id !== id);
224
212
  }
225
- cb.onTranscript(state.transcript)
213
+ cb.onTranscript(state.transcript);
226
214
  }
227
- state.agentBubbleId = null
228
- return
215
+ state.agentBubbleId = null;
216
+ return;
229
217
  }
230
- case 'tool_call':
218
+ case "tool_call":
231
219
  state.transcript = [
232
220
  ...state.transcript,
233
221
  {
234
222
  id: `m${state.idCounter++}`,
235
- role: 'tool',
236
- text: `\u2192 ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,
237
- },
238
- ]
239
- cb.onTranscript(state.transcript)
240
- return
241
- case 'tool_result':
223
+ role: "tool",
224
+ text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
225
+ }
226
+ ];
227
+ cb.onTranscript(state.transcript);
228
+ return;
229
+ case "tool_result":
242
230
  state.transcript = [
243
231
  ...state.transcript,
244
232
  {
245
233
  id: `m${state.idCounter++}`,
246
- role: 'tool',
247
- text: `${msg.ok ? '\u2713' : '\u2717'} ${String(msg.tool ?? '?')}`,
248
- },
249
- ]
250
- cb.onTranscript(state.transcript)
251
- return
252
- case 'client_tool_call': {
253
- const toolCallId = String(msg.toolCallId ?? '')
254
- const name = String(msg.name ?? '')
255
- const args = msg.args ?? {}
256
- if (!toolCallId || !name) return
257
- cb.onClientToolCall({ toolCallId, name, args })
258
- return
259
- }
260
- case 'call_end': {
261
- const reasonRaw = String(msg.reason ?? '')
262
- const reason = mapEndReason(reasonRaw)
263
- state.endReason = reason
234
+ role: "tool",
235
+ text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
236
+ }
237
+ ];
238
+ cb.onTranscript(state.transcript);
239
+ return;
240
+ case "client_tool_call": {
241
+ const toolCallId = String(msg.toolCallId ?? "");
242
+ const name = String(msg.name ?? "");
243
+ const args = msg.args ?? {};
244
+ if (!toolCallId || !name) return;
245
+ cb.onClientToolCall({ toolCallId, name, args });
246
+ return;
247
+ }
248
+ case "call_end": {
249
+ const reasonRaw = String(msg.reason ?? "");
250
+ const reason = mapEndReason(reasonRaw);
251
+ state.endReason = reason;
264
252
  state.transcript = [
265
253
  ...state.transcript,
266
254
  {
267
255
  id: `m${state.idCounter++}`,
268
- role: 'system',
269
- text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,
270
- },
271
- ]
272
- cb.onTranscript(state.transcript)
273
- cb.onCallEnd(reason)
274
- return
256
+ role: "system",
257
+ text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
258
+ }
259
+ ];
260
+ cb.onTranscript(state.transcript);
261
+ cb.onCallEnd(reason);
262
+ return;
275
263
  }
276
- case 'error': {
277
- const code = msg.code ?? 'server_error'
278
- const message = msg.message ?? 'server error'
279
- cb.onError({ code, message })
280
- return
264
+ case "error": {
265
+ const code = msg.code ?? "server_error";
266
+ const message = msg.message ?? "server error";
267
+ cb.onError({ code, message });
268
+ return;
281
269
  }
282
270
  }
283
271
  }
284
272
  var setState = (state, next, cb) => {
285
- if (state.state === next) return
286
- cb.onState(next)
287
- }
273
+ if (state.state === next) return;
274
+ cb.onState(next);
275
+ };
288
276
  var upsertUserPartial = (state, text, isFinal) => {
289
- let idx = -1
277
+ let idx = -1;
290
278
  for (let i = state.transcript.length - 1; i >= 0; i--) {
291
- const e = state.transcript[i]
292
- if (e.role === 'user' && e.committed === false) {
293
- idx = i
294
- break
279
+ const e = state.transcript[i];
280
+ if (e.role === "user" && e.committed === false) {
281
+ idx = i;
282
+ break;
295
283
  }
296
284
  }
297
285
  if (idx === -1) {
298
286
  state.transcript = [
299
287
  ...state.transcript,
300
- { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },
301
- ]
302
- return
288
+ { id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
289
+ ];
290
+ return;
303
291
  }
304
- const target = state.transcript[idx]
305
- const next = [...state.transcript]
306
- next[idx] = { ...target, text, committed: isFinal }
307
- state.transcript = next
308
- }
292
+ const target = state.transcript[idx];
293
+ const next = [...state.transcript];
294
+ next[idx] = { ...target, text, committed: isFinal };
295
+ state.transcript = next;
296
+ };
309
297
  function buildWsUrl(args) {
310
- const base = new URL(args.apiBase)
311
- const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'
312
- const bargeQS = args.bargeIn === false ? '&barge=off' : ''
313
- return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`
298
+ const base = new URL(args.apiBase);
299
+ const proto = base.protocol === "https:" ? "wss:" : "ws:";
300
+ const bargeQS = args.bargeIn === false ? "&barge=off" : "";
301
+ return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
314
302
  }
315
303
 
316
304
  // src/clientTools.ts
317
- var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/
318
- var MAX_TOOLS = 64
319
- var MAX_USAGE = 500
320
- var MAX_TIMEOUT_MS = 3e4
305
+ var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
306
+ var MAX_TOOLS = 64;
307
+ var MAX_USAGE = 500;
308
+ var MAX_TIMEOUT_MS = 3e4;
321
309
  var validateClientToolMap = (tools) => {
322
- if (tools === void 0) return
323
- if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {
324
- throw new Error('clientTools must be an object keyed by tool name')
310
+ if (tools === void 0) return;
311
+ if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
312
+ throw new Error("clientTools must be an object keyed by tool name");
325
313
  }
326
- const entries = Object.entries(tools)
314
+ const entries = Object.entries(tools);
327
315
  if (entries.length > MAX_TOOLS) {
328
- throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)
316
+ throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`);
329
317
  }
330
318
  for (const [name, def] of entries) {
331
319
  if (!NAME_RE.test(name)) {
332
320
  throw new Error(
333
- `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,
334
- )
321
+ `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
322
+ );
335
323
  }
336
- if (!def || typeof def !== 'object') {
337
- throw new Error(`clientTools["${name}"]: must be an object`)
324
+ if (!def || typeof def !== "object") {
325
+ throw new Error(`clientTools["${name}"]: must be an object`);
338
326
  }
339
- if (typeof def.description !== 'string' || def.description.length === 0) {
340
- throw new Error(`clientTools["${name}"]: must have a description`)
327
+ if (typeof def.description !== "string" || def.description.length === 0) {
328
+ throw new Error(`clientTools["${name}"]: must have a description`);
341
329
  }
342
- if (typeof def.handler !== 'function') {
343
- throw new Error(`clientTools["${name}"]: must have a handler function`)
330
+ if (typeof def.handler !== "function") {
331
+ throw new Error(`clientTools["${name}"]: must have a handler function`);
344
332
  }
345
333
  if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
346
- throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
334
+ throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
347
335
  }
348
- if (
349
- def.timeoutMs !== void 0 &&
350
- (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)
351
- ) {
352
- throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`)
336
+ if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
337
+ throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`);
353
338
  }
354
339
  }
355
- }
340
+ };
356
341
  var buildRegisterFrame = (tools) => ({
357
- type: 'client_tools_register',
342
+ type: "client_tools_register",
358
343
  tools: Object.entries(tools).map(([name, def]) => ({
359
344
  name,
360
345
  description: def.description,
361
346
  parameters: def.parameters,
362
- ...(def.usage !== void 0 ? { usage: def.usage } : {}),
363
- ...(def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}),
364
- })),
365
- })
347
+ ...def.usage !== void 0 ? { usage: def.usage } : {},
348
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
349
+ }))
350
+ });
366
351
  var dispatchClientToolCall = (send, tools, frame) => {
367
352
  const safeSend = (payload) => {
368
353
  try {
369
- send(payload)
370
- } catch {}
371
- }
372
- const tool = tools[frame.name]
354
+ send(payload);
355
+ } catch {
356
+ }
357
+ };
358
+ const tool = tools[frame.name];
373
359
  if (!tool) {
374
360
  safeSend({
375
- type: 'client_tool_result',
361
+ type: "client_tool_result",
376
362
  toolCallId: frame.toolCallId,
377
- error: `No handler for ${frame.name}`,
378
- })
379
- return
363
+ error: `No handler for ${frame.name}`
364
+ });
365
+ return;
380
366
  }
381
367
  void (async () => {
382
368
  try {
383
- const out = await tool.handler(frame.args)
369
+ const out = await tool.handler(frame.args);
384
370
  safeSend({
385
- type: 'client_tool_result',
371
+ type: "client_tool_result",
386
372
  toolCallId: frame.toolCallId,
387
- result: typeof out === 'string' ? out : JSON.stringify(out),
388
- })
373
+ result: typeof out === "string" ? out : JSON.stringify(out)
374
+ });
389
375
  } catch (err) {
390
376
  safeSend({
391
- type: 'client_tool_result',
377
+ type: "client_tool_result",
392
378
  toolCallId: frame.toolCallId,
393
- error: err instanceof Error ? err.message : String(err),
394
- })
379
+ error: err instanceof Error ? err.message : String(err)
380
+ });
395
381
  }
396
- })()
397
- }
382
+ })();
383
+ };
398
384
 
399
385
  // src/ClientMarksBuffer.ts
400
386
  var createClientMarksBuffer = (args) => {
401
- const now = args.now ?? (() => performance.now())
402
- let pendingFirstOutboundAt = null
403
- const inFlight = /* @__PURE__ */ new Map()
387
+ const now = args.now ?? (() => performance.now());
388
+ let pendingFirstOutboundAt = null;
389
+ const inFlight = /* @__PURE__ */ new Map();
404
390
  const tryEmit = (seq) => {
405
- const slot = inFlight.get(seq)
406
- if (!slot) return
407
- if (!slot.ended) return
408
- const marks = {}
391
+ const slot = inFlight.get(seq);
392
+ if (!slot) return;
393
+ if (!slot.ended) return;
394
+ const marks = {};
409
395
  if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {
410
- marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt
396
+ marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt;
411
397
  }
412
398
  args.send({
413
- type: 'client_marks',
399
+ type: "client_marks",
414
400
  seq,
415
401
  marks,
416
- clientNow: Date.now(),
417
- })
418
- inFlight.delete(seq)
419
- }
402
+ clientNow: Date.now()
403
+ });
404
+ inFlight.delete(seq);
405
+ };
420
406
  const markFirstOutboundAudio = () => {
421
- if (pendingFirstOutboundAt !== null) return
422
- pendingFirstOutboundAt = now()
423
- }
407
+ if (pendingFirstOutboundAt !== null) return;
408
+ pendingFirstOutboundAt = now();
409
+ };
424
410
  const markFirstAudibleOutput = () => {
425
- let target
411
+ let target;
426
412
  for (const slot of inFlight.values()) {
427
413
  if (!slot.ended) {
428
- target = slot
414
+ target = slot;
429
415
  }
430
416
  }
431
- if (!target) return
432
- if (target.firstAudibleAt !== null) return
433
- target.firstAudibleAt = now()
434
- }
417
+ if (!target) return;
418
+ if (target.firstAudibleAt !== null) return;
419
+ target.firstAudibleAt = now();
420
+ };
435
421
  const onAgentTurnStart = (seq) => {
436
422
  inFlight.set(seq, {
437
423
  firstOutboundAt: pendingFirstOutboundAt,
438
424
  firstAudibleAt: null,
439
- ended: false,
440
- })
441
- pendingFirstOutboundAt = null
442
- }
425
+ ended: false
426
+ });
427
+ pendingFirstOutboundAt = null;
428
+ };
443
429
  const onAgentTurnEnd = (seq) => {
444
- const slot = inFlight.get(seq)
430
+ const slot = inFlight.get(seq);
445
431
  if (!slot) {
446
- args.send({ type: 'client_marks', seq, marks: {}, clientNow: Date.now() })
447
- return
432
+ args.send({ type: "client_marks", seq, marks: {}, clientNow: Date.now() });
433
+ return;
448
434
  }
449
- slot.ended = true
450
- tryEmit(seq)
451
- }
435
+ slot.ended = true;
436
+ tryEmit(seq);
437
+ };
452
438
  const flush = () => {
453
439
  for (const seq of [...inFlight.keys()]) {
454
- const slot = inFlight.get(seq)
455
- slot.ended = true
456
- tryEmit(seq)
440
+ const slot = inFlight.get(seq);
441
+ slot.ended = true;
442
+ tryEmit(seq);
457
443
  }
458
- pendingFirstOutboundAt = null
459
- }
444
+ pendingFirstOutboundAt = null;
445
+ };
460
446
  return {
461
447
  markFirstOutboundAudio,
462
448
  markFirstAudibleOutput,
463
449
  onAgentTurnStart,
464
450
  onAgentTurnEnd,
465
- flush,
466
- }
467
- }
451
+ flush
452
+ };
453
+ };
468
454
 
469
455
  // src/NodeVoiceClient.ts
470
456
  var NodeVoiceClient = class {
471
457
  constructor(args) {
472
- this.rws = null
473
- this.muted = false
474
- this.startedAt = null
475
- this.endedFired = false
476
- this.lastError = null
458
+ this.rws = null;
459
+ this.muted = false;
460
+ this.startedAt = null;
461
+ this.endedFired = false;
462
+ this.lastError = null;
477
463
  this.end = () => {
478
- this.teardown('user_hangup')
479
- }
464
+ this.teardown("user_hangup");
465
+ };
480
466
  this.mute = () => {
481
- this.muted = true
482
- }
467
+ this.muted = true;
468
+ };
483
469
  this.unmute = () => {
484
- this.muted = false
485
- }
470
+ this.muted = false;
471
+ };
486
472
  // ---------------------------------------------------------------
487
473
  // Node-only raw audio surface
488
474
  // ---------------------------------------------------------------
489
475
  this.sendAudioChunk = (pcm) => {
490
- if (!this.rws) return false
491
- this.marks.markFirstOutboundAudio()
476
+ if (!this.rws) return false;
477
+ this.marks.markFirstOutboundAudio();
492
478
  if (this.muted) {
493
- const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength
494
- this.rws.send(new ArrayBuffer(len))
495
- return true
479
+ const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength;
480
+ this.rws.send(new ArrayBuffer(len));
481
+ return true;
496
482
  }
497
- this.rws.send(pcm)
498
- return true
499
- }
483
+ this.rws.send(pcm);
484
+ return true;
485
+ };
500
486
  // ---------------------------------------------------------------
501
487
  // Internal
502
488
  // ---------------------------------------------------------------
503
489
  this.setState = (next) => {
504
- if (this.proto.state === next) return
505
- this.proto.state = next
506
- this.args.options.onStateChange?.(next)
507
- }
490
+ if (this.proto.state === next) return;
491
+ this.proto.state = next;
492
+ this.args.options.onStateChange?.(next);
493
+ };
508
494
  this.sendClientToolsRegister = () => {
509
- const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
510
- this.rws?.send(JSON.stringify(frame))
511
- }
495
+ const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
496
+ this.rws?.send(JSON.stringify(frame));
497
+ };
512
498
  this.emitError = (err) => {
513
- this.lastError = err
514
- this.args.options.onError?.(err)
515
- }
499
+ this.lastError = err;
500
+ this.args.options.onError?.(err);
501
+ };
516
502
  this.handleSocketEvent = (ev) => {
517
503
  switch (ev.type) {
518
- case 'open':
519
- break
520
- case 'reconnected':
521
- this.proto.transcript = []
522
- this.proto.agentBubbleId = null
523
- this.args.options.onTranscript?.(this.proto.transcript)
524
- this.setState('listening')
525
- break
526
- case 'message':
527
- if (typeof ev.data === 'string') {
504
+ case "open":
505
+ break;
506
+ case "reconnected":
507
+ this.proto.transcript = [];
508
+ this.proto.agentBubbleId = null;
509
+ this.args.options.onTranscript?.(this.proto.transcript);
510
+ this.setState("listening");
511
+ break;
512
+ case "message":
513
+ if (typeof ev.data === "string") {
528
514
  handleServerMessage(ev.data, this.proto, {
529
515
  onState: this.setState,
530
516
  onTranscript: (entries) => this.args.options.onTranscript?.(entries),
531
517
  onError: this.emitError,
532
518
  onInterrupt: () => this.args.options.onInterrupt?.(),
533
519
  onAgentTurnStart: (seq) => {
534
- if (typeof seq === 'number') this.marks.onAgentTurnStart(seq)
535
- this.args.options.onAgentTurnStart?.()
520
+ if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
521
+ this.args.options.onAgentTurnStart?.();
536
522
  },
537
523
  onAgentTurnEnd: (seq) => {
538
- if (typeof seq === 'number') this.marks.onAgentTurnEnd(seq)
524
+ if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
539
525
  },
540
526
  onCallEnd: (reason) => this.teardown(reason),
541
527
  onConnected: () => this.sendClientToolsRegister(),
542
- onClientToolCall: (frame) =>
543
- dispatchClientToolCall(
544
- (f) => this.rws?.send(JSON.stringify(f)),
545
- this.args.options.clientTools ?? {},
546
- frame,
547
- ),
548
- })
528
+ onClientToolCall: (frame) => dispatchClientToolCall(
529
+ (f) => this.rws?.send(JSON.stringify(f)),
530
+ this.args.options.clientTools ?? {},
531
+ frame
532
+ )
533
+ });
549
534
  } else {
550
- this.marks.markFirstAudibleOutput()
551
- this.args.options.onAudioChunk?.(ev.data)
535
+ this.marks.markFirstAudibleOutput();
536
+ this.args.options.onAudioChunk?.(ev.data);
552
537
  }
553
- break
554
- case 'close':
538
+ break;
539
+ case "close":
555
540
  if (ev.permanent) {
556
- const reason = this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')
557
- this.teardown(reason)
541
+ const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
542
+ this.teardown(reason);
558
543
  }
559
- break
560
- case 'error':
561
- this.emitError({ code: 'socket_error', message: ev.error.message })
562
- break
544
+ break;
545
+ case "error":
546
+ this.emitError({ code: "socket_error", message: ev.error.message });
547
+ break;
563
548
  }
564
- }
549
+ };
565
550
  this.teardown = (reason) => {
566
551
  try {
567
- this.marks.flush()
568
- } catch {}
552
+ this.marks.flush();
553
+ } catch {
554
+ }
569
555
  try {
570
- this.rws?.close(1e3, reason)
571
- } catch {}
572
- this.rws = null
573
- this.setState('ended')
574
- this.fireEndOnce(reason)
575
- }
556
+ this.rws?.close(1e3, reason);
557
+ } catch {
558
+ }
559
+ this.rws = null;
560
+ this.setState("ended");
561
+ this.fireEndOnce(reason);
562
+ };
576
563
  this.fireEndOnce = (reason) => {
577
- if (this.endedFired) return
578
- this.endedFired = true
579
- const startedAt = this.startedAt ?? Date.now()
564
+ if (this.endedFired) return;
565
+ this.endedFired = true;
566
+ const startedAt = this.startedAt ?? Date.now();
580
567
  this.args.options.onEnd?.({
581
568
  reason,
582
- errorCode: reason === 'error' ? this.lastError?.code : void 0,
583
- durationMs: Date.now() - startedAt,
584
- })
585
- }
586
- this.args = args
587
- this.proto = createProtocolState()
588
- validateClientToolMap(args.options.clientTools)
569
+ errorCode: reason === "error" ? this.lastError?.code : void 0,
570
+ durationMs: Date.now() - startedAt
571
+ });
572
+ };
573
+ this.args = args;
574
+ this.proto = createProtocolState();
575
+ validateClientToolMap(args.options.clientTools);
589
576
  this.marks = createClientMarksBuffer({
590
577
  send: (frame) => {
591
578
  try {
592
- this.rws?.send(JSON.stringify(frame))
593
- } catch {}
594
- },
595
- })
579
+ this.rws?.send(JSON.stringify(frame));
580
+ } catch {
581
+ }
582
+ }
583
+ });
596
584
  }
597
585
  // ---------------------------------------------------------------
598
586
  // Call interface
599
587
  // ---------------------------------------------------------------
600
588
  get state() {
601
- return this.proto.state
589
+ return this.proto.state;
602
590
  }
603
591
  get transcript() {
604
- return this.proto.transcript.slice()
592
+ return this.proto.transcript.slice();
605
593
  }
606
594
  get isMuted() {
607
- return this.muted
595
+ return this.muted;
608
596
  }
609
597
  // ---------------------------------------------------------------
610
598
  // Lifecycle
611
599
  // ---------------------------------------------------------------
612
600
  async start() {
613
- this.setState('connecting')
614
- this.startedAt = Date.now()
601
+ this.setState("connecting");
602
+ this.startedAt = Date.now();
615
603
  const url = buildWsUrl({
616
604
  apiBase: this.args.config.apiBase,
617
605
  agentId: this.args.options.agentId,
618
606
  token: this.args.token,
619
- bargeIn: this.args.options.bargeIn,
620
- })
607
+ bargeIn: this.args.options.bargeIn
608
+ });
621
609
  this.rws = createReconnectingWebSocket(
622
610
  {
623
611
  url,
624
612
  wsFactory: this.args.wsFactory,
625
- maxRetries: 3,
613
+ maxRetries: 3
626
614
  },
627
- (ev) => this.handleSocketEvent(ev),
628
- )
615
+ (ev) => this.handleSocketEvent(ev)
616
+ );
629
617
  }
630
- }
618
+ };
631
619
 
632
620
  // src/node.ts
633
- var cachedWsCtor = null
621
+ var cachedWsCtor = null;
634
622
  var loadWsCtor = async () => {
635
- if (cachedWsCtor) return cachedWsCtor
623
+ if (cachedWsCtor) return cachedWsCtor;
636
624
  try {
637
- const mod = await import('ws')
638
- const ctor = mod.WebSocket ?? mod.default
625
+ const mod = await import("ws");
626
+ const ctor = mod.WebSocket ?? mod.default;
639
627
  if (!ctor) {
640
- throw new Error('imported `ws` but neither default nor named WebSocket export was found')
628
+ throw new Error("imported `ws` but neither default nor named WebSocket export was found");
641
629
  }
642
- cachedWsCtor = ctor
643
- return ctor
630
+ cachedWsCtor = ctor;
631
+ return ctor;
644
632
  } catch (err) {
645
633
  throw new Error(
646
- "@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: " +
647
- (err instanceof Error ? err.message : String(err)),
648
- )
634
+ "@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))
635
+ );
649
636
  }
650
- }
637
+ };
651
638
  var NodeVoiceFactory = class {
652
639
  constructor(config) {
653
640
  this.startCall = async (options) => {
654
641
  if (!options.agentId) {
655
- throw new Error('startCall: agentId is required')
642
+ throw new Error("startCall: agentId is required");
656
643
  }
657
- const WsCtor = await loadWsCtor()
658
- const wsFactory = (url) => new WsCtor(url)
659
- const { context, metadata } = mergeStartCallContext(this.config, options)
644
+ const WsCtor = await loadWsCtor();
645
+ const wsFactory = (url) => new WsCtor(url);
646
+ const { context, metadata } = mergeStartCallContext(this.config, options);
660
647
  const fetchArgs = {
661
648
  agentId: options.agentId,
662
649
  userId: options.userId,
663
650
  context,
664
- metadata,
665
- }
666
- let token
651
+ metadata
652
+ };
653
+ let token;
667
654
  if (options.token) {
668
- token = options.token
655
+ token = options.token;
669
656
  } else {
670
- const r = await this.config.fetchToken(fetchArgs)
657
+ const r = await this.config.fetchToken(fetchArgs);
671
658
  if (!r) {
672
- throw new Error('configureVoiceClient.fetchToken returned empty token')
659
+ throw new Error("configureVoiceClient.fetchToken returned empty token");
673
660
  }
674
- token = typeof r === 'string' ? r : r.token
661
+ token = typeof r === "string" ? r : r.token;
675
662
  if (!token) {
676
- throw new Error('configureVoiceClient.fetchToken returned an object without `token`')
663
+ throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
664
+ }
665
+ if (typeof r !== "string" && r.transport === "webrtc") {
666
+ console.warn(
667
+ "@craftedxp/voice-js (node): agent is configured for WebRTC but the Node bundle only supports WebSocket \u2014 falling back to WS. Use the browser bundle for WebRTC transport."
668
+ );
677
669
  }
678
670
  }
679
671
  const client = new NodeVoiceClient({
680
672
  config: this.config,
681
673
  options: { ...options, context, metadata },
682
674
  token,
683
- wsFactory,
684
- })
685
- await client.start()
686
- return client
687
- }
688
- this.config = config
675
+ wsFactory
676
+ });
677
+ await client.start();
678
+ return client;
679
+ };
680
+ this.config = config;
689
681
  }
690
- }
682
+ };
691
683
  function configureVoiceClient(config) {
692
- return new NodeVoiceFactory(normalizeConfig(config))
684
+ return new NodeVoiceFactory(normalizeConfig(config));
693
685
  }
694
686
  // Annotate the CommonJS export names for ESM import in node:
695
- 0 &&
696
- (module.exports = {
697
- buildWsUrl,
698
- configureVoiceClient,
699
- createProtocolState,
700
- createReconnectingWebSocket,
701
- handleServerMessage,
702
- })
703
- //# sourceMappingURL=node.js.map
687
+ 0 && (module.exports = {
688
+ buildWsUrl,
689
+ configureVoiceClient,
690
+ createProtocolState,
691
+ createReconnectingWebSocket,
692
+ handleServerMessage
693
+ });
694
+ //# sourceMappingURL=node.js.map