@craftedxp/voice-js 0.3.1 → 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,592 +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
- state.state = next;
271
- cb.onState(next);
272
- };
285
+ if (state.state === next) return
286
+ cb.onState(next)
287
+ }
273
288
  var upsertUserPartial = (state, text, isFinal) => {
274
- let idx = -1;
289
+ let idx = -1
275
290
  for (let i = state.transcript.length - 1; i >= 0; i--) {
276
- const e = state.transcript[i];
277
- if (e.role === "user" && e.committed === false) {
278
- idx = i;
279
- break;
291
+ const e = state.transcript[i]
292
+ if (e.role === 'user' && e.committed === false) {
293
+ idx = i
294
+ break
280
295
  }
281
296
  }
282
297
  if (idx === -1) {
283
298
  state.transcript = [
284
299
  ...state.transcript,
285
- { id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
286
- ];
287
- return;
300
+ { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },
301
+ ]
302
+ return
288
303
  }
289
- const target = state.transcript[idx];
290
- const next = [...state.transcript];
291
- next[idx] = { ...target, text, committed: isFinal };
292
- state.transcript = next;
293
- };
304
+ const target = state.transcript[idx]
305
+ const next = [...state.transcript]
306
+ next[idx] = { ...target, text, committed: isFinal }
307
+ state.transcript = next
308
+ }
294
309
  function buildWsUrl(args) {
295
- const base = new URL(args.apiBase);
296
- const proto = base.protocol === "https:" ? "wss:" : "ws:";
297
- const bargeQS = args.bargeIn === false ? "&barge=off" : "";
298
- 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}`
299
314
  }
300
315
 
301
316
  // src/clientTools.ts
302
- var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
303
- var MAX_TOOLS = 64;
304
- var MAX_USAGE = 500;
305
- 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
306
321
  var validateClientToolMap = (tools) => {
307
- if (tools === void 0) return;
308
- if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
309
- 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')
310
325
  }
311
- const entries = Object.entries(tools);
326
+ const entries = Object.entries(tools)
312
327
  if (entries.length > MAX_TOOLS) {
313
- 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})`)
314
329
  }
315
330
  for (const [name, def] of entries) {
316
331
  if (!NAME_RE.test(name)) {
317
332
  throw new Error(
318
- `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
319
- );
333
+ `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,
334
+ )
320
335
  }
321
- if (!def || typeof def !== "object") {
322
- 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`)
323
338
  }
324
- if (typeof def.description !== "string" || def.description.length === 0) {
325
- 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`)
326
341
  }
327
- if (typeof def.handler !== "function") {
328
- 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`)
329
344
  }
330
345
  if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
331
- throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
346
+ throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
332
347
  }
333
- if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
334
- 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]`)
335
353
  }
336
354
  }
337
- };
355
+ }
338
356
  var buildRegisterFrame = (tools) => ({
339
- type: "client_tools_register",
357
+ type: 'client_tools_register',
340
358
  tools: Object.entries(tools).map(([name, def]) => ({
341
359
  name,
342
360
  description: def.description,
343
361
  parameters: def.parameters,
344
- ...def.usage !== void 0 ? { usage: def.usage } : {},
345
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
346
- }))
347
- });
362
+ ...(def.usage !== void 0 ? { usage: def.usage } : {}),
363
+ ...(def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}),
364
+ })),
365
+ })
348
366
  var dispatchClientToolCall = (send, tools, frame) => {
349
367
  const safeSend = (payload) => {
350
368
  try {
351
- send(payload);
352
- } catch {
353
- }
354
- };
355
- const tool = tools[frame.name];
369
+ send(payload)
370
+ } catch {}
371
+ }
372
+ const tool = tools[frame.name]
356
373
  if (!tool) {
357
374
  safeSend({
358
- type: "client_tool_result",
375
+ type: 'client_tool_result',
359
376
  toolCallId: frame.toolCallId,
360
- error: `No handler for ${frame.name}`
361
- });
362
- return;
377
+ error: `No handler for ${frame.name}`,
378
+ })
379
+ return
363
380
  }
364
381
  void (async () => {
365
382
  try {
366
- const out = await tool.handler(frame.args);
383
+ const out = await tool.handler(frame.args)
367
384
  safeSend({
368
- type: "client_tool_result",
385
+ type: 'client_tool_result',
369
386
  toolCallId: frame.toolCallId,
370
- result: typeof out === "string" ? out : JSON.stringify(out)
371
- });
387
+ result: typeof out === 'string' ? out : JSON.stringify(out),
388
+ })
372
389
  } catch (err) {
373
390
  safeSend({
374
- type: "client_tool_result",
391
+ type: 'client_tool_result',
375
392
  toolCallId: frame.toolCallId,
376
- error: err instanceof Error ? err.message : String(err)
377
- });
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)
378
457
  }
379
- })();
380
- };
458
+ pendingFirstOutboundAt = null
459
+ }
460
+ return {
461
+ markFirstOutboundAudio,
462
+ markFirstAudibleOutput,
463
+ onAgentTurnStart,
464
+ onAgentTurnEnd,
465
+ flush,
466
+ }
467
+ }
381
468
 
382
469
  // src/NodeVoiceClient.ts
383
470
  var NodeVoiceClient = class {
384
471
  constructor(args) {
385
- this.rws = null;
386
- this.muted = false;
387
- this.startedAt = null;
388
- this.endedFired = false;
389
- this.lastError = null;
472
+ this.rws = null
473
+ this.muted = false
474
+ this.startedAt = null
475
+ this.endedFired = false
476
+ this.lastError = null
390
477
  this.end = () => {
391
- this.teardown("user_hangup");
392
- };
478
+ this.teardown('user_hangup')
479
+ }
393
480
  this.mute = () => {
394
- this.muted = true;
395
- };
481
+ this.muted = true
482
+ }
396
483
  this.unmute = () => {
397
- this.muted = false;
398
- };
484
+ this.muted = false
485
+ }
399
486
  // ---------------------------------------------------------------
400
487
  // Node-only raw audio surface
401
488
  // ---------------------------------------------------------------
402
489
  this.sendAudioChunk = (pcm) => {
403
- if (!this.rws) return false;
490
+ if (!this.rws) return false
491
+ this.marks.markFirstOutboundAudio()
404
492
  if (this.muted) {
405
- const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength;
406
- this.rws.send(new ArrayBuffer(len));
407
- return true;
493
+ const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength
494
+ this.rws.send(new ArrayBuffer(len))
495
+ return true
408
496
  }
409
- this.rws.send(pcm);
410
- return true;
411
- };
497
+ this.rws.send(pcm)
498
+ return true
499
+ }
412
500
  // ---------------------------------------------------------------
413
501
  // Internal
414
502
  // ---------------------------------------------------------------
415
503
  this.setState = (next) => {
416
- if (this.proto.state === next) return;
417
- this.proto.state = next;
418
- this.args.options.onStateChange?.(next);
419
- };
504
+ if (this.proto.state === next) return
505
+ this.proto.state = next
506
+ this.args.options.onStateChange?.(next)
507
+ }
420
508
  this.sendClientToolsRegister = () => {
421
- const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
422
- this.rws?.send(JSON.stringify(frame));
423
- };
509
+ const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
510
+ this.rws?.send(JSON.stringify(frame))
511
+ }
424
512
  this.emitError = (err) => {
425
- this.lastError = err;
426
- this.args.options.onError?.(err);
427
- };
513
+ this.lastError = err
514
+ this.args.options.onError?.(err)
515
+ }
428
516
  this.handleSocketEvent = (ev) => {
429
517
  switch (ev.type) {
430
- case "open":
431
- break;
432
- case "reconnected":
433
- this.proto.transcript = [];
434
- this.proto.agentBubbleId = null;
435
- this.args.options.onTranscript?.(this.proto.transcript);
436
- this.setState("listening");
437
- break;
438
- case "message":
439
- 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') {
440
528
  handleServerMessage(ev.data, this.proto, {
441
529
  onState: this.setState,
442
530
  onTranscript: (entries) => this.args.options.onTranscript?.(entries),
443
531
  onError: this.emitError,
444
532
  onInterrupt: () => this.args.options.onInterrupt?.(),
445
- 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
+ },
446
540
  onCallEnd: (reason) => this.teardown(reason),
447
541
  onConnected: () => this.sendClientToolsRegister(),
448
- onClientToolCall: (frame) => dispatchClientToolCall(
449
- (f) => this.rws?.send(JSON.stringify(f)),
450
- this.args.options.clientTools ?? {},
451
- frame
452
- )
453
- });
542
+ onClientToolCall: (frame) =>
543
+ dispatchClientToolCall(
544
+ (f) => this.rws?.send(JSON.stringify(f)),
545
+ this.args.options.clientTools ?? {},
546
+ frame,
547
+ ),
548
+ })
454
549
  } else {
455
- this.args.options.onAudioChunk?.(ev.data);
550
+ this.marks.markFirstAudibleOutput()
551
+ this.args.options.onAudioChunk?.(ev.data)
456
552
  }
457
- break;
458
- case "close":
553
+ break
554
+ case 'close':
459
555
  if (ev.permanent) {
460
- const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
461
- this.teardown(reason);
556
+ const reason = this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')
557
+ this.teardown(reason)
462
558
  }
463
- break;
464
- case "error":
465
- this.emitError({ code: "socket_error", message: ev.error.message });
466
- break;
559
+ break
560
+ case 'error':
561
+ this.emitError({ code: 'socket_error', message: ev.error.message })
562
+ break
467
563
  }
468
- };
564
+ }
469
565
  this.teardown = (reason) => {
470
566
  try {
471
- this.rws?.close(1e3, reason);
472
- } catch {
473
- }
474
- this.rws = null;
475
- this.setState("ended");
476
- this.fireEndOnce(reason);
477
- };
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
+ }
478
576
  this.fireEndOnce = (reason) => {
479
- if (this.endedFired) return;
480
- this.endedFired = true;
481
- const startedAt = this.startedAt ?? Date.now();
577
+ if (this.endedFired) return
578
+ this.endedFired = true
579
+ const startedAt = this.startedAt ?? Date.now()
482
580
  this.args.options.onEnd?.({
483
581
  reason,
484
- errorCode: reason === "error" ? this.lastError?.code : void 0,
485
- durationMs: Date.now() - startedAt
486
- });
487
- };
488
- this.args = args;
489
- this.proto = createProtocolState();
490
- 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
+ })
491
596
  }
492
597
  // ---------------------------------------------------------------
493
598
  // Call interface
494
599
  // ---------------------------------------------------------------
495
600
  get state() {
496
- return this.proto.state;
601
+ return this.proto.state
497
602
  }
498
603
  get transcript() {
499
- return this.proto.transcript.slice();
604
+ return this.proto.transcript.slice()
500
605
  }
501
606
  get isMuted() {
502
- return this.muted;
607
+ return this.muted
503
608
  }
504
609
  // ---------------------------------------------------------------
505
610
  // Lifecycle
506
611
  // ---------------------------------------------------------------
507
612
  async start() {
508
- this.setState("connecting");
509
- this.startedAt = Date.now();
613
+ this.setState('connecting')
614
+ this.startedAt = Date.now()
510
615
  const url = buildWsUrl({
511
616
  apiBase: this.args.config.apiBase,
512
617
  agentId: this.args.options.agentId,
513
618
  token: this.args.token,
514
- bargeIn: this.args.options.bargeIn
515
- });
619
+ bargeIn: this.args.options.bargeIn,
620
+ })
516
621
  this.rws = createReconnectingWebSocket(
517
622
  {
518
623
  url,
519
624
  wsFactory: this.args.wsFactory,
520
- maxRetries: 3
625
+ maxRetries: 3,
521
626
  },
522
- (ev) => this.handleSocketEvent(ev)
523
- );
627
+ (ev) => this.handleSocketEvent(ev),
628
+ )
524
629
  }
525
- };
630
+ }
526
631
 
527
632
  // src/node.ts
528
- var cachedWsCtor = null;
633
+ var cachedWsCtor = null
529
634
  var loadWsCtor = async () => {
530
- if (cachedWsCtor) return cachedWsCtor;
635
+ if (cachedWsCtor) return cachedWsCtor
531
636
  try {
532
- const mod = await import("ws");
533
- const ctor = mod.WebSocket ?? mod.default;
637
+ const mod = await import('ws')
638
+ const ctor = mod.WebSocket ?? mod.default
534
639
  if (!ctor) {
535
- 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')
536
641
  }
537
- cachedWsCtor = ctor;
538
- return ctor;
642
+ cachedWsCtor = ctor
643
+ return ctor
539
644
  } catch (err) {
540
645
  throw new Error(
541
- "@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))
542
- );
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
+ )
543
649
  }
544
- };
650
+ }
545
651
  var NodeVoiceFactory = class {
546
652
  constructor(config) {
547
653
  this.startCall = async (options) => {
548
654
  if (!options.agentId) {
549
- throw new Error("startCall: agentId is required");
655
+ throw new Error('startCall: agentId is required')
550
656
  }
551
- const WsCtor = await loadWsCtor();
552
- const wsFactory = (url) => new WsCtor(url);
553
- 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)
554
660
  const fetchArgs = {
555
661
  agentId: options.agentId,
556
662
  userId: options.userId,
557
663
  context,
558
- metadata
559
- };
560
- let token;
664
+ metadata,
665
+ }
666
+ let token
561
667
  if (options.token) {
562
- token = options.token;
668
+ token = options.token
563
669
  } else {
564
- 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
565
675
  if (!token) {
566
- throw new Error("configureVoiceClient.fetchToken returned empty token");
676
+ throw new Error('configureVoiceClient.fetchToken returned an object without `token`')
567
677
  }
568
678
  }
569
679
  const client = new NodeVoiceClient({
570
680
  config: this.config,
571
681
  options: { ...options, context, metadata },
572
682
  token,
573
- wsFactory
574
- });
575
- await client.start();
576
- return client;
577
- };
578
- this.config = config;
683
+ wsFactory,
684
+ })
685
+ await client.start()
686
+ return client
687
+ }
688
+ this.config = config
579
689
  }
580
- };
690
+ }
581
691
  function configureVoiceClient(config) {
582
- return new NodeVoiceFactory(normalizeConfig(config));
692
+ return new NodeVoiceFactory(normalizeConfig(config))
583
693
  }
584
694
  // Annotate the CommonJS export names for ESM import in node:
585
- 0 && (module.exports = {
586
- buildWsUrl,
587
- configureVoiceClient,
588
- createProtocolState,
589
- createReconnectingWebSocket,
590
- handleServerMessage
591
- });
592
- //# 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