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