@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/CONSUMING.md +1 -1
- package/README.md +8 -7
- package/dist/browser.d.mts +20 -4
- package/dist/browser.d.ts +334 -250
- package/dist/browser.js +818 -541
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +278 -9
- package/dist/browser.mjs.map +1 -1
- package/dist/embed.iife.js +1094 -4
- package/dist/node.d.mts +20 -4
- package/dist/node.d.ts +324 -247
- package/dist/node.js +480 -369
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +103 -6
- package/dist/node.mjs.map +1 -1
- package/package.json +1 -1
package/dist/node.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/ReconnectingWebSocket.ts","../src/protocol.ts","../src/clientTools.ts","../src/NodeVoiceClient.ts","../src/node.ts"],"sourcesContent":["// Public configuration surface.\n//\n// `configureVoiceClient` returns a small factory that knows how to mint\n// `ct_` tokens via your callback. Per-call you call `factory.startCall`.\n//\n// Parity with @craftedxp/voice-rn:\n// voice-rn calls `configureVoiceClient` once at app startup as a\n// side-effect (singleton), because in RN there is exactly one host\n// app and one running `<AgentCall>` at a time. In JS environments\n// the same process can drive multiple clients (multi-tenant\n// dashboards, terminal multiplexers, electron apps with several\n// panels), so the JS SDK returns the factory rather than mutating a\n// module-level singleton. Same option names, same callback shape.\n//\n// Auth model:\n// - `apiKey` is INTENTIONALLY ABSENT from the public surface.\n// Pre-0.2 had it; baking an `sk_` into a JS bundle ships your\n// server-grade credentials to every client. The right pattern is\n// `fetchToken` — your code asks YOUR backend for a short-lived\n// `ct_` and your backend uses its `sk_` (via @craftedxp/sdk-node)\n// to mint it.\n// - For tests + local prototypes a bare `token` may be passed to\n// `startCall` directly, bypassing `fetchToken`. Don't ship that to\n// production — `ct_` lifetimes are short and you want the SDK to\n// re-mint on expiry.\n\nimport type { CallEndEvent, CallError, CallState, TranscriptEntry, VolumeEvent } from './protocol'\nimport type { ClientToolMap } from './clientTools'\n\n// ---------------------------------------------------------------------------\n// fetchToken contract — matches voice-rn FetchTokenArgs verbatim.\n// ---------------------------------------------------------------------------\n\nexport interface FetchTokenArgs {\n /** The agent the SDK is about to call. */\n agentId: string\n /**\n * Optional consumer-side user identifier. Round-tripped to the server\n * as `contactId` for Phase 11 contact memory. The SDK does not\n * inspect this; your backend uses it to scope the token mint.\n */\n userId?: string\n /**\n * Per-call structured context lowered into the agent's effective\n * system prompt server-side at session open. Opaque to the SDK.\n */\n context?: Record<string, unknown>\n /**\n * String key/value pairs round-tripped on the `call.ended` webhook.\n * Capped at 1 KB total server-side. NOT lowered into the system prompt.\n */\n metadata?: Record<string, string>\n}\n\nexport type FetchToken = (args: FetchTokenArgs) => Promise<string>\n\n// ---------------------------------------------------------------------------\n// Factory configuration\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientConfig {\n /**\n * Full HTTPS URL of the Voxline server. The WebSocket scheme is\n * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.\n */\n apiBase: string\n /**\n * Called by the SDK whenever it needs a fresh `ct_` token (initial\n * connect; mid-call refresh on `token_expired`). Your implementation\n * should hit YOUR backend, which holds the `sk_` API key and mints\n * via `POST /v1/call-tokens` (or `client.callTokens.mint` from\n * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a\n * client.\n */\n fetchToken: FetchToken\n /**\n * Optional metadata applied to EVERY startCall. Per-call `metadata`\n * in `startCall` is merged on top (per-call wins on key conflicts).\n * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.\n */\n defaultMetadata?: Record<string, string>\n /**\n * Optional context applied to EVERY startCall. Per-call `context` in\n * `startCall` is merged on top. Useful for cross-call invariants like\n * the signed-in user's locale.\n */\n defaultContext?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Per-call options — startCall({ ... })\n// ---------------------------------------------------------------------------\n\nexport interface StartCallOptions {\n /** The agent to call. */\n agentId: string\n /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */\n userId?: string\n /**\n * Per-call structured context. Merged on top of `defaultContext`\n * configured at factory time.\n */\n context?: Record<string, unknown>\n /**\n * Per-call metadata. Merged on top of `defaultMetadata` configured\n * at factory time.\n */\n metadata?: Record<string, string>\n /**\n * When false, the SDK + server stay full-duplex but barge-in is\n * suppressed. Useful for alarm-style flows where the user shouldn't\n * accidentally interrupt the script. Default true.\n */\n bargeIn?: boolean\n /**\n * Client-side tools the agent's LLM can call mid-conversation. Each\n * tool's handler runs on the consumer's side; result is fed back to\n * the LLM through the existing call WebSocket. Schema and handler\n * colocate. Validated synchronously at startCall — bad input throws.\n *\n * See docs/integration-echocheck.md for the wire protocol and the\n * server-side guarantees.\n */\n clientTools?: ClientToolMap\n /**\n * Test-only escape hatch — pass a pre-minted `ct_` directly and skip\n * the `fetchToken` call. Don't use this in production code: tokens\n * expire and the SDK can't re-mint without the callback.\n */\n token?: string\n\n // Event callbacks — same shape as voice-rn UseVoiceCallOptions.\n onStateChange?: (state: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (err: CallError) => void\n onEnd?: (end: CallEndEvent) => void\n /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */\n onVolume?: (vol: VolumeEvent) => void\n /**\n * Fires when the server signals barge-in (the user started talking\n * mid-agent-turn). The browser bundle automatically flushes its\n * built-in audio playback before this callback runs; the callback is\n * fired regardless. Node / Electron consumers with custom playback\n * should drain their audio queue here so the agent goes silent\n * immediately.\n */\n onInterrupt?: () => void\n /**\n * Fires on `agent_turn_start` — the server has begun a new agent\n * turn. The state-machine transition to `agent_speaking` happens at\n * the same moment via `onStateChange`; use this when you want a\n * precise turn anchor (e.g. \"agent has been speaking for N ms\" UIs)\n * without diffing state.\n */\n onAgentTurnStart?: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Call handle returned from startCall — matches voice-rn\n// VoiceCallController where it makes sense.\n// ---------------------------------------------------------------------------\n\nexport interface Call {\n /** Current state. Snapshot — subscribe via onStateChange for live updates. */\n readonly state: CallState\n /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */\n readonly transcript: TranscriptEntry[]\n /** True after `mute()` and before `unmute()`. */\n readonly isMuted: boolean\n /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */\n end: () => void\n /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */\n mute: () => void\n /** Unmute mic frames. Idempotent. */\n unmute: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Factory contract — what configureVoiceClient returns. The actual\n// implementation differs between browser (audio-equipped) and node (raw\n// PCM) bundles; both satisfy this interface.\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientFactory {\n /** Read back the resolved config (post trailing-slash normalisation). */\n readonly config: VoiceClientConfig\n /**\n * Open a fresh call. Returns when the WS is open; rejects on\n * pre-flight failure (missing config, fetchToken throw, etc). Mid-\n * call failures arrive via the per-call `onError` callback — they\n * don't reject this promise.\n */\n startCall: (options: StartCallOptions) => Promise<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pre-flight validation. Pulled out so both bundles share the exact\n// same \"missing field\" error messages.\n// ---------------------------------------------------------------------------\n\nexport function normalizeConfig(config: VoiceClientConfig): VoiceClientConfig {\n if (!config) throw new Error('configureVoiceClient: config is required')\n if ('apiKey' in (config as object)) {\n throw new Error(\n 'configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead — see the @craftedxp/voice-js README for the migration recipe.',\n )\n }\n if (!config.apiBase) {\n throw new Error('configureVoiceClient: apiBase is required')\n }\n if (typeof config.fetchToken !== 'function') {\n throw new Error('configureVoiceClient: fetchToken must be a function')\n }\n return {\n ...config,\n apiBase: config.apiBase.replace(/\\/+$/, ''),\n }\n}\n\n// Merge factory-level defaults with per-call overrides. Per-call wins.\nexport function mergeStartCallContext(\n factory: VoiceClientConfig,\n call: StartCallOptions,\n): { context?: Record<string, unknown>; metadata?: Record<string, string> } {\n const context =\n factory.defaultContext || call.context\n ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }\n : undefined\n const metadata =\n factory.defaultMetadata || call.metadata\n ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }\n : undefined\n return { context, metadata }\n}\n\nexport type { ClientTool, ClientToolMap } from './clientTools'\n","// Minimal auto-reconnecting WebSocket wrapper.\n//\n// Scope: transparent reconnection on unexpected drops (network blip,\n// server restart). We deliberately do NOT try to preserve call state\n// across reconnects — the server's AgentCallHandler is session-scoped\n// and won't resume where we left off. If the consumer needs mid-call\n// resilience, that's a server-side resumable-session feature, not a\n// client-side retry.\n//\n// In v1 we reconnect only when `connect()` has been called and\n// `.close(1000, ...)` has NOT been called by the consumer (i.e., we drop\n// because of network, not because the user hung up). On unclean close\n// the consumer gets a fresh connection + a `reconnected` event so they\n// can re-issue any greeting / context.\n//\n// Transport agnostic — accepts a WebSocket-constructor factory so the\n// browser bundle uses native `WebSocket` and the node bundle injects the\n// `ws` package.\n\nexport type RWSEvent =\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error }\n\n// Minimal WebSocket-like contract. Both browser `WebSocket` and the\n// `ws` package's WebSocket satisfy it.\nexport interface WebSocketLike {\n binaryType: string\n readyState: number\n onopen: ((ev: unknown) => void) | null\n onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null\n onerror: ((ev: unknown) => void) | null\n onclose: ((ev: { code: number; reason: string }) => void) | null\n send: (data: string | ArrayBuffer | ArrayBufferView) => void\n close: (code?: number, reason?: string) => void\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike\n\nexport interface RWSOptions {\n url: string\n // Factory so we can swap browser-native WebSocket for the `ws` package\n // in node. Browser entry pre-fills with a wrapper around globalThis.WebSocket.\n wsFactory: WebSocketFactory\n // Cap the number of auto-reconnect attempts. `0` disables retry entirely\n // (closer to native WebSocket semantics). Default: 3.\n maxRetries?: number\n // Initial backoff; doubles up to maxBackoffMs.\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst READYSTATE_OPEN = 1\nconst READYSTATE_CLOSED = 3\n\nexport const createReconnectingWebSocket = (\n options: RWSOptions,\n onEvent: (ev: RWSEvent) => void,\n) => {\n const maxRetries = options.maxRetries ?? 3\n const initialBackoff = options.initialBackoffMs ?? 500\n const maxBackoff = options.maxBackoffMs ?? 8000\n\n let ws: WebSocketLike | null = null\n let intentionalClose = false\n let retries = 0\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n\n const openOnce = () => {\n ws = options.wsFactory(options.url)\n // Binary frames arrive as Int16 PCM from the server. ArrayBuffer is\n // more convenient than Blob (one fewer async read) — we `.arrayBuffer()`\n // a Blob manually only if we receive one unexpectedly.\n ws.binaryType = 'arraybuffer'\n ws.onopen = () => {\n if (retries === 0) onEvent({ type: 'open' })\n else onEvent({ type: 'reconnected' })\n retries = 0\n backoff = initialBackoff\n }\n ws.onmessage = (ev) => {\n onEvent({ type: 'message', data: ev.data as string | ArrayBuffer })\n }\n ws.onerror = () => {\n onEvent({ type: 'error', error: new Error('WebSocket error') })\n }\n ws.onclose = (ev) => {\n ws = null\n const shouldRetry = !intentionalClose && retries < maxRetries\n if (!shouldRetry) {\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: true,\n })\n return\n }\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: false,\n })\n retries++\n const delay = Math.min(backoff, maxBackoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n reconnectTimer = setTimeout(openOnce, delay)\n }\n }\n\n openOnce()\n\n return {\n send: (data: string | ArrayBuffer | ArrayBufferView) => {\n if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)\n },\n close: (code = 1000, reason = 'client-requested') => {\n intentionalClose = true\n if (reconnectTimer) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n try {\n ws?.close(code, reason)\n } catch {\n // already closed\n }\n },\n readyState: () => ws?.readyState ?? READYSTATE_CLOSED,\n }\n}\n\nexport type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>\n","// Wire-protocol types + stateless transcript reducer.\n//\n// Shared between the browser and node bundles. The protocol surface is\n// matched (where it makes sense) to @craftedxp/voice-rn 0.3.x so a\n// consumer can write the same `onTranscript` / `onStateChange` /\n// `onError` / `onEnd` shape for both web and RN clients.\n\nimport type { ClientToolCallFrame } from './clientTools'\n\n// ---------------------------------------------------------------------------\n// State machine\n// ---------------------------------------------------------------------------\n\nexport type CallState =\n | 'idle'\n | 'connecting'\n | 'listening'\n | 'user_speaking'\n | 'agent_speaking'\n | 'ended'\n | 'error'\n\n// ---------------------------------------------------------------------------\n// Transcript model — matches voice-rn TranscriptEntry\n// ---------------------------------------------------------------------------\n\nexport type TranscriptEntry =\n | { id: string; role: 'user'; text: string; committed: boolean }\n | { id: string; role: 'agent'; text: string; interrupted?: boolean }\n | { id: string; role: 'tool'; text: string }\n | { id: string; role: 'system'; text: string }\n\n// ---------------------------------------------------------------------------\n// Stable error code contract — matches voice-rn CallErrorCode where the\n// failure modes overlap. Web-specific codes (mic_denied via getUserMedia\n// rejection, etc.) keep their voice-rn names so cross-platform consumers\n// can write one switch statement.\n// ---------------------------------------------------------------------------\n\nexport type CallErrorCode =\n // Programming errors — surface loudly to the host's developer.\n | 'missing_credentials'\n | 'forbidden'\n // Browser audio failures.\n | 'mic_denied'\n | 'mic_start_failed'\n | 'audio_session_failed'\n // Auth lifecycle.\n | 'token_expired'\n | 'token_invalid'\n | 'unauthorized'\n // Network / connectivity.\n | 'network_unreachable'\n | 'socket_error'\n // Business state.\n | 'payment_required'\n | 'not_found'\n // End-of-call states surfaced via onError (also via onEnd reason='timeout').\n | 'silence_timeout'\n // Catch-all for unexpected server / 5xx / 1011.\n | 'server_error'\n\nexport interface CallError {\n code: CallErrorCode\n message: string\n}\n\n// ---------------------------------------------------------------------------\n// End-of-call signal — matches voice-rn CallEndReason / CallEndEvent\n// ---------------------------------------------------------------------------\n\nexport type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error'\n\nexport interface CallEndEvent {\n reason: CallEndReason\n // Present iff reason === 'error'. Mirrors the code from the most\n // recent onError.\n errorCode?: CallErrorCode\n // Wallclock from start() resolving to the WS close. Useful for billing\n // / \"you spoke for 1m23s\" UIs without forcing the host to track\n // timestamps themselves.\n durationMs: number\n}\n\n// ---------------------------------------------------------------------------\n// Volume meter event (browser bundle only — node bundle leaves volume to\n// the consumer if they're processing PCM themselves).\n// ---------------------------------------------------------------------------\n\nexport interface VolumeEvent {\n // 0-1 RMS over the last ~100ms. Bind to a waveform / level meter.\n input: number\n output: number\n}\n\n// ---------------------------------------------------------------------------\n// Server → client message envelope. Loose typing — the server can add new\n// `type` values without breaking the SDK; unknown types are ignored by\n// the dispatch in handleServerMessage.\n// ---------------------------------------------------------------------------\n\nexport type ServerMessage = Record<string, unknown> & { type?: string }\n\n// ---------------------------------------------------------------------------\n// Stateless transcript reducer + state-machine helpers. Both the browser\n// and node clients call into these so the shape of the transcript stays\n// identical across environments.\n// ---------------------------------------------------------------------------\n\nexport interface ProtocolState {\n state: CallState\n transcript: TranscriptEntry[]\n agentBubbleId: string | null\n idCounter: number\n // Reason latched from the server's call_end frame. Read by the\n // surrounding client when the WS finally closes so onEnd fires with the\n // right reason instead of falling back to user_hangup.\n endReason: CallEndReason | null\n}\n\nexport const createProtocolState = (): ProtocolState => ({\n state: 'idle',\n transcript: [],\n agentBubbleId: null,\n idCounter: 0,\n endReason: null,\n})\n\n// Side-effect callbacks the protocol layer fires as it processes server\n// frames. The surrounding client wires these up to event emitters\n// (browser) or to user-supplied callbacks (node).\nexport interface ProtocolCallbacks {\n onState: (next: CallState) => void\n onTranscript: (entries: TranscriptEntry[]) => void\n onError: (err: CallError) => void\n // Fires on `interrupt` — caller should flush its audio playback queue.\n onInterrupt: () => void\n // Fires on `agent_turn_start` — caller may want to reset its turn\n // anchor for \"agent has been speaking N ms\" UIs.\n onAgentTurnStart: () => void\n // Fires on `call_end` — caller closes its WS and resolves onEnd.\n onCallEnd: (reason: CallEndReason) => void\n // Fires on `connected` — caller should send the client_tools_register\n // frame here so the server's first-turn grace window unblocks.\n onConnected: () => void\n // Fires on `client_tool_call` — caller dispatches to the matching\n // tool handler and posts back a client_tool_result frame.\n onClientToolCall: (frame: ClientToolCallFrame) => void\n}\n\n// Map server-supplied endReason strings onto our SDK-side CallEndReason.\nconst mapEndReason = (raw: string): CallEndReason => {\n if (raw === 'agent_ended') return 'agent_ended'\n if (raw === 'caller_hung_up') return 'user_hangup'\n if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'\n return 'error'\n}\n\n// Pure-ish transcript reducer + dispatcher. Mutates `state` in place to\n// match the imperative pattern used by both clients; returns nothing.\n//\n// The \"agent_text\" / \"transcript\" interim handling is identical to\n// voice-rn's useVoiceCall — one growing user bubble while interim, one\n// growing agent bubble per turn.\nexport function handleServerMessage(\n raw: string,\n state: ProtocolState,\n cb: ProtocolCallbacks,\n): void {\n let msg: ServerMessage\n try {\n msg = JSON.parse(raw)\n } catch {\n return\n }\n\n switch (msg.type) {\n case 'connected':\n cb.onConnected()\n setState(state, 'listening', cb)\n return\n\n case 'transcript': {\n const text = (msg.text as string) ?? ''\n if (!text) return\n const isFinal = !!msg.isFinal\n if (!isFinal) setState(state, 'user_speaking', cb)\n upsertUserPartial(state, text, isFinal)\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_start': {\n const id = `m${state.idCounter++}`\n state.agentBubbleId = id\n state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]\n cb.onTranscript(state.transcript)\n cb.onAgentTurnStart()\n setState(state, 'agent_speaking', cb)\n return\n }\n\n case 'agent_text': {\n const delta = (msg.text as string) ?? ''\n if (!delta || !state.agentBubbleId) return\n const id = state.agentBubbleId\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,\n )\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_end':\n state.agentBubbleId = null\n setState(state, 'listening', cb)\n return\n\n case 'interrupt':\n cb.onInterrupt()\n return\n\n case 'agent_turn_abort': {\n const committed = ((msg.committedText as string) ?? '').trim()\n if (state.agentBubbleId) {\n const id = state.agentBubbleId\n if (committed) {\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,\n )\n } else {\n state.transcript = state.transcript.filter((e) => e.id !== id)\n }\n cb.onTranscript(state.transcript)\n }\n state.agentBubbleId = null\n return\n }\n\n case 'tool_call':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `→ ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'tool_result':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `${msg.ok ? '✓' : '✗'} ${String(msg.tool ?? '?')}`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'client_tool_call': {\n const toolCallId = String(msg.toolCallId ?? '')\n const name = String(msg.name ?? '')\n const args = (msg.args as Record<string, unknown>) ?? {}\n if (!toolCallId || !name) return\n cb.onClientToolCall({ toolCallId, name, args })\n return\n }\n\n case 'call_end': {\n const reasonRaw = String(msg.reason ?? '')\n const reason = mapEndReason(reasonRaw)\n state.endReason = reason\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'system',\n text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,\n },\n ]\n cb.onTranscript(state.transcript)\n cb.onCallEnd(reason)\n return\n }\n\n case 'error': {\n const code = (msg.code as CallErrorCode) ?? 'server_error'\n const message = (msg.message as string) ?? 'server error'\n cb.onError({ code, message })\n return\n }\n }\n}\n\nconst setState = (state: ProtocolState, next: CallState, cb: ProtocolCallbacks) => {\n if (state.state === next) return\n state.state = next\n cb.onState(next)\n}\n\n// Find the last uncommitted user bubble and grow it; or append a new\n// uncommitted bubble. Mirrors voice-rn upsertUserPartial.\nconst upsertUserPartial = (state: ProtocolState, text: string, isFinal: boolean) => {\n let idx = -1\n for (let i = state.transcript.length - 1; i >= 0; i--) {\n const e = state.transcript[i]\n if (e.role === 'user' && e.committed === false) {\n idx = i\n break\n }\n }\n if (idx === -1) {\n state.transcript = [\n ...state.transcript,\n { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },\n ]\n return\n }\n const target = state.transcript[idx] as Extract<TranscriptEntry, { role: 'user' }>\n const next = [...state.transcript]\n next[idx] = { ...target, text, committed: isFinal }\n state.transcript = next\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket URL builder. Identical between browser + node bundles —\n// kept here so the rare server-side change to the path / query shape is\n// a one-line edit.\n// ---------------------------------------------------------------------------\n\nexport interface BuildWsUrlArgs {\n apiBase: string\n agentId: string\n token: string\n bargeIn?: boolean\n}\n\nexport function buildWsUrl(args: BuildWsUrlArgs): string {\n const base = new URL(args.apiBase)\n const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'\n const bargeQS = args.bargeIn === false ? '&barge=off' : ''\n return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`\n}\n","// Bundle-agnostic client-tools core for @craftedxp/voice-js.\n//\n// Imports neither `ws` nor DOM `WebSocket` — the dispatcher takes an\n// injected `send` callback so the same code runs in browsers and Node.\n//\n// Wire protocol (matches the server's Zod schemas in\n// server/src/agents/clientToolFrames.ts):\n//\n// client → server client_tools_register { type, tools[] }\n// server → client client_tool_call { type, toolCallId, name, args }\n// client → server client_tool_result { type, toolCallId, result?, error? }\n\nexport interface ClientTool {\n description: string\n parameters: Record<string, unknown> // JSON Schema\n usage?: string // ≤500 chars; appended to agent system prompt\n timeoutMs?: number // server-enforced; (0, 30_000]\n example?: string // surface-only UI hint; not sent on wire\n handler: (args: Record<string, unknown>) => Promise<string | object> | string | object\n}\n\nexport type ClientToolMap = Record<string, ClientTool>\n\n// Server → client frame shape. Used by both protocol.ts (when surfacing\n// the frame to a callback) and dispatchClientToolCall (when consuming it).\nexport interface ClientToolCallFrame {\n toolCallId: string\n name: string\n args: Record<string, unknown>\n}\n\nconst NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/\nconst MAX_TOOLS = 64\nconst MAX_USAGE = 500\nconst MAX_TIMEOUT_MS = 30_000\n\n// Synchronous, throw-on-error validation. Called once at startCall time\n// before the WS opens, so bad input fails fast at the configuration\n// boundary instead of mid-call.\nexport const validateClientToolMap = (tools: ClientToolMap | undefined): void => {\n if (tools === undefined) return\n if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {\n throw new Error('clientTools must be an object keyed by tool name')\n }\n const entries = Object.entries(tools)\n if (entries.length > MAX_TOOLS) {\n throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)\n }\n for (const [name, def] of entries) {\n if (!NAME_RE.test(name)) {\n throw new Error(\n `clientTools[\"${name}\"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,\n )\n }\n if (!def || typeof def !== 'object') {\n throw new Error(`clientTools[\"${name}\"]: must be an object`)\n }\n if (typeof def.description !== 'string' || def.description.length === 0) {\n throw new Error(`clientTools[\"${name}\"]: must have a description`)\n }\n if (typeof def.handler !== 'function') {\n throw new Error(`clientTools[\"${name}\"]: must have a handler function`)\n }\n if (def.usage !== undefined && def.usage.length > MAX_USAGE) {\n throw new Error(`clientTools[\"${name}\"]: usage must be ≤500 chars`)\n }\n if (\n def.timeoutMs !== undefined &&\n (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)\n ) {\n throw new Error(`clientTools[\"${name}\"]: timeoutMs must be in (0, 30000]`)\n }\n }\n}\n\n// Build the registration frame the SDK sends right after `connected`.\n// Strips runtime-only fields (handler, example) and only includes the\n// optional fields when set.\nexport const buildRegisterFrame = (tools: ClientToolMap): object => ({\n type: 'client_tools_register',\n tools: Object.entries(tools).map(([name, def]) => ({\n name,\n description: def.description,\n parameters: def.parameters,\n ...(def.usage !== undefined ? { usage: def.usage } : {}),\n ...(def.timeoutMs !== undefined ? { timeoutMs: def.timeoutMs } : {}),\n })),\n})\n\ntype FrameSender = (frame: object) => void\n\n// Run the matching handler asynchronously and post a result/error frame\n// back through `send`. Send-side throws are swallowed (the WS may have\n// closed mid-call; the server has already cancelled the in-flight\n// invocation on its side).\nexport const dispatchClientToolCall = (\n send: FrameSender,\n tools: ClientToolMap,\n frame: ClientToolCallFrame,\n): void => {\n const safeSend = (payload: object) => {\n try {\n send(payload)\n } catch {\n // WS likely closed mid-handler. Server-side already cancelled.\n }\n }\n\n const tool = tools[frame.name]\n if (!tool) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: `No handler for ${frame.name}`,\n })\n return\n }\n\n void (async () => {\n try {\n const out = await tool.handler(frame.args)\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n result: typeof out === 'string' ? out : JSON.stringify(out),\n })\n } catch (err) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n}\n","// Node VoiceClient — drives one in-progress call from a Node.js or\n// Electron-main environment.\n//\n// Same WS protocol + transcript state as the browser bundle, but with\n// NO built-in audio I/O. Node consumers feed mic frames in via\n// `sendAudioChunk(buf)` and consume the agent's TTS PCM via the\n// `onAudioChunk` callback they passed to `startCall`. Bring your own\n// audio adapter (sox, PortAudio, OS-native, RTP relay — whatever fits\n// your host).\n//\n// `mute()` / `unmute()` here just gate `sendAudioChunk` — when muted, the\n// next call replaces the buffer with zeroed silence of the same length\n// so the server's endpointing keeps seeing wire frames at the expected\n// cadence (matches voice-rn behaviour).\n\nimport {\n createReconnectingWebSocket,\n type ReconnectingWebSocket,\n type WebSocketFactory,\n} from './ReconnectingWebSocket'\nimport {\n buildWsUrl,\n createProtocolState,\n handleServerMessage,\n type CallEndReason,\n type CallError,\n type ProtocolState,\n} from './protocol'\nimport type { Call, StartCallOptions, VoiceClientConfig } from './config'\nimport { buildRegisterFrame, dispatchClientToolCall, validateClientToolMap } from './clientTools'\n\n// Node-specific extensions to the per-call options. Surfaced as a\n// separate type so the browser StartCallOptions stays clean.\nexport interface NodeStartCallOptions extends StartCallOptions {\n /**\n * Fires for each binary PCM frame the server pushes (Int16 LE mono\n * @ 16 kHz — same as the browser playback path). Wire to your\n * preferred output: write to a `sox -t raw -r 16000 -e signed -b 16\n * -c 1 - default` subprocess, queue into PortAudio, relay over RTP,\n * etc. If you don't supply this callback, agent audio is dropped on\n * the floor.\n */\n onAudioChunk?: (pcm: ArrayBuffer) => void\n}\n\n// Node consumers receive a richer Call handle that includes the\n// raw-PCM control surface.\nexport interface NodeCall extends Call {\n /**\n * Push one mic frame to the server. Expected: Int16 LE mono PCM @\n * 16 kHz. Capture cadence ~100 ms / ~3.2 KB per frame is fine.\n * Returns `false` if the WS isn't open yet (caller may want to\n * back-pressure or drop).\n */\n sendAudioChunk: (pcm: ArrayBuffer | ArrayBufferView) => boolean\n}\n\n/**\n * Node bundle's analog of `VoiceClientFactory`. Same shape but\n * `startCall` accepts the richer `NodeStartCallOptions` (which adds\n * `onAudioChunk`) and resolves to a `NodeCall` (which adds\n * `sendAudioChunk`). Returned by `configureVoiceClient` in the Node\n * entry. Browser entry returns the base `VoiceClientFactory` type.\n */\nexport interface NodeVoiceClientFactory {\n readonly config: VoiceClientConfig\n startCall: (options: NodeStartCallOptions) => Promise<NodeCall>\n}\n\ninterface NodeVoiceClientArgs {\n config: VoiceClientConfig\n options: NodeStartCallOptions\n token: string\n wsFactory: WebSocketFactory\n}\n\nexport class NodeVoiceClient implements NodeCall {\n private readonly args: NodeVoiceClientArgs\n private readonly proto: ProtocolState\n\n private rws: ReconnectingWebSocket | null = null\n\n private muted = false\n private startedAt: number | null = null\n private endedFired = false\n private lastError: CallError | null = null\n\n constructor(args: NodeVoiceClientArgs) {\n this.args = args\n this.proto = createProtocolState()\n validateClientToolMap(args.options.clientTools)\n }\n\n // ---------------------------------------------------------------\n // Call interface\n // ---------------------------------------------------------------\n\n get state() {\n return this.proto.state\n }\n\n get transcript() {\n return this.proto.transcript.slice()\n }\n\n get isMuted() {\n return this.muted\n }\n\n end = () => {\n this.teardown('user_hangup')\n }\n\n mute = () => {\n this.muted = true\n }\n\n unmute = () => {\n this.muted = false\n }\n\n // ---------------------------------------------------------------\n // Node-only raw audio surface\n // ---------------------------------------------------------------\n\n sendAudioChunk = (pcm: ArrayBuffer | ArrayBufferView): boolean => {\n if (!this.rws) return false\n if (this.muted) {\n // Silence at the wire cadence — server endpointing depends on a\n // steady frame rhythm; going silent confuses it.\n const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength\n this.rws.send(new ArrayBuffer(len))\n return true\n }\n this.rws.send(pcm)\n return true\n }\n\n // ---------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------\n\n async start(): Promise<void> {\n this.setState('connecting')\n this.startedAt = Date.now()\n\n const url = buildWsUrl({\n apiBase: this.args.config.apiBase,\n agentId: this.args.options.agentId,\n token: this.args.token,\n bargeIn: this.args.options.bargeIn,\n })\n\n this.rws = createReconnectingWebSocket(\n {\n url,\n wsFactory: this.args.wsFactory,\n maxRetries: 3,\n },\n (ev) => this.handleSocketEvent(ev),\n )\n }\n\n // ---------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------\n\n private setState = (next: ProtocolState['state']) => {\n if (this.proto.state === next) return\n this.proto.state = next\n this.args.options.onStateChange?.(next)\n }\n\n private sendClientToolsRegister = () => {\n const frame = buildRegisterFrame(this.args.options.clientTools ?? {})\n this.rws?.send(JSON.stringify(frame))\n }\n\n private emitError = (err: CallError) => {\n this.lastError = err\n this.args.options.onError?.(err)\n }\n\n private handleSocketEvent = (\n ev:\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error },\n ) => {\n switch (ev.type) {\n case 'open':\n // No mic to start — consumer drives sendAudioChunk themselves.\n break\n case 'reconnected':\n this.proto.transcript = []\n this.proto.agentBubbleId = null\n this.args.options.onTranscript?.(this.proto.transcript)\n this.setState('listening')\n break\n case 'message':\n if (typeof ev.data === 'string') {\n handleServerMessage(ev.data, this.proto, {\n onState: this.setState,\n onTranscript: (entries) => this.args.options.onTranscript?.(entries),\n onError: this.emitError,\n onInterrupt: () => this.args.options.onInterrupt?.(),\n onAgentTurnStart: () => this.args.options.onAgentTurnStart?.(),\n onCallEnd: (reason) => this.teardown(reason),\n onConnected: () => this.sendClientToolsRegister(),\n onClientToolCall: (frame) =>\n dispatchClientToolCall(\n (f) => this.rws?.send(JSON.stringify(f)),\n this.args.options.clientTools ?? {},\n frame,\n ),\n })\n } else {\n // Binary frame — agent TTS PCM. Hand to consumer's audio sink.\n this.args.options.onAudioChunk?.(ev.data)\n }\n break\n case 'close':\n if (ev.permanent) {\n const reason: CallEndReason =\n this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')\n this.teardown(reason)\n }\n break\n case 'error':\n this.emitError({ code: 'socket_error', message: ev.error.message })\n break\n }\n }\n\n private teardown = (reason: CallEndReason) => {\n try {\n this.rws?.close(1000, reason)\n } catch {\n // already closed\n }\n this.rws = null\n this.setState('ended')\n this.fireEndOnce(reason)\n }\n\n private fireEndOnce = (reason: CallEndReason) => {\n if (this.endedFired) return\n this.endedFired = true\n const startedAt = this.startedAt ?? Date.now()\n this.args.options.onEnd?.({\n reason,\n errorCode: reason === 'error' ? this.lastError?.code : undefined,\n durationMs: Date.now() - startedAt,\n })\n }\n}\n","// Node entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n// (under the `node` condition; or `from '@craftedxp/voice-js/node'`\n// if your bundler doesn't honour conditional exports).\n//\n// Pre-injects:\n// - `ws` package as the WebSocket transport (declared as an OPTIONAL\n// peer — loaded via dynamic import() so a missing peer surfaces a\n// clear install hint at startCall time rather than crashing at\n// module load).\n//\n// Audio is intentionally NOT injected. Node consumers feed mic frames\n// in via `call.sendAudioChunk(buf)` and consume agent audio via\n// `onAudioChunk` they passed to startCall. See NodeVoiceClient for the\n// raw PCM contract.\n\nimport { mergeStartCallContext, normalizeConfig, type VoiceClientConfig } from './config'\nimport type { WebSocketFactory, WebSocketLike } from './ReconnectingWebSocket'\nimport {\n NodeVoiceClient,\n type NodeCall,\n type NodeStartCallOptions,\n type NodeVoiceClientFactory,\n} from './NodeVoiceClient'\n\n// Lazy + cached. The first startCall pays the dynamic-import cost; later\n// calls reuse the resolved constructor.\nlet cachedWsCtor: { new (url: string): WebSocketLike } | null = null\n\nconst loadWsCtor = async (): Promise<{ new (url: string): WebSocketLike }> => {\n if (cachedWsCtor) return cachedWsCtor\n try {\n // Dynamic import survives bundler tree-shaking and lets us emit a\n // clean error message for the missing-peer case. Browser bundles\n // never reach this code path because they pick the `browser`\n // condition in package.json `exports`.\n const mod = (await import('ws')) as unknown as {\n default?: { new (url: string): WebSocketLike }\n WebSocket?: { new (url: string): WebSocketLike }\n }\n const ctor = mod.WebSocket ?? mod.default\n if (!ctor) {\n throw new Error('imported `ws` but neither default nor named WebSocket export was found')\n }\n cachedWsCtor = ctor\n return ctor\n } catch (err) {\n throw new Error(\n \"@craftedxp/voice-js (node): missing optional peer `ws`. Install it with `npm install ws` (ws is declared as `peerDependenciesMeta.optional` so npm doesn't install it automatically). Original: \" +\n (err instanceof Error ? err.message : String(err)),\n )\n }\n}\n\nclass NodeVoiceFactory implements NodeVoiceClientFactory {\n readonly config: VoiceClientConfig\n\n constructor(config: VoiceClientConfig) {\n this.config = config\n }\n\n startCall = async (options: NodeStartCallOptions): Promise<NodeCall> => {\n if (!options.agentId) {\n throw new Error('startCall: agentId is required')\n }\n\n // Resolve `ws` before the network round-trip so the missing-peer\n // failure surfaces early.\n const WsCtor = await loadWsCtor()\n const wsFactory: WebSocketFactory = (url) => new WsCtor(url)\n\n const { context, metadata } = mergeStartCallContext(this.config, options)\n const fetchArgs = {\n agentId: options.agentId,\n userId: options.userId,\n context,\n metadata,\n }\n\n let token: string\n if (options.token) {\n token = options.token\n } else {\n token = await this.config.fetchToken(fetchArgs)\n if (!token) {\n throw new Error('configureVoiceClient.fetchToken returned empty token')\n }\n }\n\n const client = new NodeVoiceClient({\n config: this.config,\n options: { ...options, context, metadata },\n token,\n wsFactory,\n })\n await client.start()\n return client\n }\n}\n\n/**\n * One-time SDK setup for Node.js / Electron-main consumers. Returns a\n * factory you call `startCall` on for every voice call. Same shape as\n * the browser entry but the returned `Call` has an extra\n * `sendAudioChunk` method for raw-PCM input, and `startCall` accepts\n * an `onAudioChunk` callback for raw-PCM output.\n *\n * Example (vterm-style CLI, sox sub-process for I/O):\n *\n * import { configureVoiceClient } from '@craftedxp/voice-js/node'\n * import { spawn } from 'child_process'\n *\n * const voice = configureVoiceClient({\n * apiBase: 'https://api.your-server.com',\n * fetchToken: async () => mintFromMyBackend(),\n * })\n *\n * const mic = spawn('sox', [...recArgs, '-r', '16000', '-c', '1', '-b', '16', '-e', 'signed', '-t', 'raw', '-'])\n * const spk = spawn('sox', ['-t', 'raw', '-r', '16000', '-c', '1', '-b', '16', '-e', 'signed', '-', ...playArgs])\n *\n * const call = await voice.startCall({\n * agentId: 'agt_xxx',\n * onAudioChunk: (pcm) => spk.stdin.write(Buffer.from(pcm)),\n * onEnd: () => { mic.kill(); spk.stdin.end() },\n * })\n *\n * mic.stdout.on('data', (chunk) => call.sendAudioChunk(chunk))\n */\nexport function configureVoiceClient(config: VoiceClientConfig): NodeVoiceClientFactory {\n return new NodeVoiceFactory(normalizeConfig(config))\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — types + advanced primitives. Mirror of browser entry\n// minus the AudioCapture/AudioPlayback exports (browser-only).\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n ClientTool,\n ClientToolMap,\n FetchToken,\n FetchTokenArgs,\n StartCallOptions,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type { NodeCall, NodeStartCallOptions, NodeVoiceClientFactory } from './NodeVoiceClient'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/ReconnectingWebSocket.ts","../src/protocol.ts","../src/clientTools.ts","../src/ClientMarksBuffer.ts","../src/NodeVoiceClient.ts","../src/node.ts"],"sourcesContent":["// Public configuration surface.\n//\n// `configureVoiceClient` returns a small factory that knows how to mint\n// `ct_` tokens via your callback. Per-call you call `factory.startCall`.\n//\n// Parity with @craftedxp/voice-rn:\n// voice-rn calls `configureVoiceClient` once at app startup as a\n// side-effect (singleton), because in RN there is exactly one host\n// app and one running `<AgentCall>` at a time. In JS environments\n// the same process can drive multiple clients (multi-tenant\n// dashboards, terminal multiplexers, electron apps with several\n// panels), so the JS SDK returns the factory rather than mutating a\n// module-level singleton. Same option names, same callback shape.\n//\n// Auth model:\n// - `apiKey` is INTENTIONALLY ABSENT from the public surface.\n// Pre-0.2 had it; baking an `sk_` into a JS bundle ships your\n// server-grade credentials to every client. The right pattern is\n// `fetchToken` — your code asks YOUR backend for a short-lived\n// `ct_` and your backend uses its `sk_` (via @craftedxp/sdk-node)\n// to mint it.\n// - For tests + local prototypes a bare `token` may be passed to\n// `startCall` directly, bypassing `fetchToken`. Don't ship that to\n// production — `ct_` lifetimes are short and you want the SDK to\n// re-mint on expiry.\n\nimport type { CallEndEvent, CallError, CallState, TranscriptEntry, VolumeEvent } from './protocol'\nimport type { ClientToolMap } from './clientTools'\n\n// ---------------------------------------------------------------------------\n// fetchToken contract — matches voice-rn FetchTokenArgs verbatim.\n// ---------------------------------------------------------------------------\n\nexport interface FetchTokenArgs {\n /** The agent the SDK is about to call. */\n agentId: string\n /**\n * Optional consumer-side user identifier. Round-tripped to the server\n * as `contactId` for Phase 11 contact memory. The SDK does not\n * inspect this; your backend uses it to scope the token mint.\n */\n userId?: string\n /**\n * Per-call structured context lowered into the agent's effective\n * system prompt server-side at session open. Opaque to the SDK.\n */\n context?: Record<string, unknown>\n /**\n * String key/value pairs round-tripped on the `call.ended` webhook.\n * Capped at 1 KB total server-side. NOT lowered into the system prompt.\n */\n metadata?: Record<string, string>\n}\n\n/**\n * What `fetchToken` may return. The rich object form lets the server\n * choose the transport per call. Returning a bare string is backwards-\n * compatible — the SDK treats it as `{ token, transport: 'ws' }`.\n */\nexport interface FetchTokenResult {\n /** Raw `ct_` to feed into the WS open / WebRTC offer. */\n token: string\n /** Server-selected transport. Default `'ws'` if absent. */\n transport?: 'ws' | 'webrtc'\n /** Required when `transport === 'webrtc'` AND the server uses a\n * separate signaling gateway. When omitted on a webrtc result, the\n * SDK falls back to the API base's Phase-1 routes (local dev). */\n webrtcGatewayBase?: string\n}\n\nexport type FetchToken = (args: FetchTokenArgs) => Promise<string | FetchTokenResult>\n\n// ---------------------------------------------------------------------------\n// Factory configuration\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientConfig {\n /**\n * Full HTTPS URL of the Voissia server. The WebSocket scheme is\n * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.\n */\n apiBase: string\n /**\n * Called by the SDK whenever it needs a fresh `ct_` token (initial\n * connect; mid-call refresh on `token_expired`). Your implementation\n * should hit YOUR backend, which holds the `sk_` API key and mints\n * via `POST /v1/call-tokens` (or `client.callTokens.mint` from\n * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a\n * client.\n */\n fetchToken: FetchToken\n /**\n * Optional metadata applied to EVERY startCall. Per-call `metadata`\n * in `startCall` is merged on top (per-call wins on key conflicts).\n * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.\n */\n defaultMetadata?: Record<string, string>\n /**\n * Optional context applied to EVERY startCall. Per-call `context` in\n * `startCall` is merged on top. Useful for cross-call invariants like\n * the signed-in user's locale.\n */\n defaultContext?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Per-call options — startCall({ ... })\n// ---------------------------------------------------------------------------\n\nexport interface StartCallOptions {\n /** The agent to call. */\n agentId: string\n /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */\n userId?: string\n /**\n * Per-call structured context. Merged on top of `defaultContext`\n * configured at factory time.\n */\n context?: Record<string, unknown>\n /**\n * Per-call metadata. Merged on top of `defaultMetadata` configured\n * at factory time.\n */\n metadata?: Record<string, string>\n /**\n * When false, the SDK + server stay full-duplex but barge-in is\n * suppressed. Useful for alarm-style flows where the user shouldn't\n * accidentally interrupt the script. Default true.\n */\n bargeIn?: boolean\n /**\n * Client-side tools the agent's LLM can call mid-conversation. Each\n * tool's handler runs on the consumer's side; result is fed back to\n * the LLM through the existing call WebSocket. Schema and handler\n * colocate. Validated synchronously at startCall — bad input throws.\n *\n * See docs/integration-echocheck.md for the wire protocol and the\n * server-side guarantees.\n */\n clientTools?: ClientToolMap\n /**\n * Test-only escape hatch — pass a pre-minted `ct_` directly and skip\n * the `fetchToken` call. Don't use this in production code: tokens\n * expire and the SDK can't re-mint without the callback.\n */\n token?: string\n\n // Event callbacks — same shape as voice-rn UseVoiceCallOptions.\n onStateChange?: (state: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (err: CallError) => void\n onEnd?: (end: CallEndEvent) => void\n /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */\n onVolume?: (vol: VolumeEvent) => void\n /**\n * Fires when the server signals barge-in (the user started talking\n * mid-agent-turn). The browser bundle automatically flushes its\n * built-in audio playback before this callback runs; the callback is\n * fired regardless. Node / Electron consumers with custom playback\n * should drain their audio queue here so the agent goes silent\n * immediately.\n */\n onInterrupt?: () => void\n /**\n * Fires on `agent_turn_start` — the server has begun a new agent\n * turn. The state-machine transition to `agent_speaking` happens at\n * the same moment via `onStateChange`; use this when you want a\n * precise turn anchor (e.g. \"agent has been speaking for N ms\" UIs)\n * without diffing state.\n */\n onAgentTurnStart?: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Call handle returned from startCall — matches voice-rn\n// VoiceCallController where it makes sense.\n// ---------------------------------------------------------------------------\n\nexport interface Call {\n /** Current state. Snapshot — subscribe via onStateChange for live updates. */\n readonly state: CallState\n /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */\n readonly transcript: TranscriptEntry[]\n /** True after `mute()` and before `unmute()`. */\n readonly isMuted: boolean\n /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */\n end: () => void\n /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */\n mute: () => void\n /** Unmute mic frames. Idempotent. */\n unmute: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Factory contract — what configureVoiceClient returns. The actual\n// implementation differs between browser (audio-equipped) and node (raw\n// PCM) bundles; both satisfy this interface.\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientFactory {\n /** Read back the resolved config (post trailing-slash normalisation). */\n readonly config: VoiceClientConfig\n /**\n * Open a fresh call. Returns when the WS is open; rejects on\n * pre-flight failure (missing config, fetchToken throw, etc). Mid-\n * call failures arrive via the per-call `onError` callback — they\n * don't reject this promise.\n */\n startCall: (options: StartCallOptions) => Promise<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pre-flight validation. Pulled out so both bundles share the exact\n// same \"missing field\" error messages.\n// ---------------------------------------------------------------------------\n\nexport function normalizeConfig(config: VoiceClientConfig): VoiceClientConfig {\n if (!config) throw new Error('configureVoiceClient: config is required')\n if ('apiKey' in (config as object)) {\n throw new Error(\n 'configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead — see the @craftedxp/voice-js README for the migration recipe.',\n )\n }\n if (!config.apiBase) {\n throw new Error('configureVoiceClient: apiBase is required')\n }\n if (typeof config.fetchToken !== 'function') {\n throw new Error('configureVoiceClient: fetchToken must be a function')\n }\n return {\n ...config,\n apiBase: config.apiBase.replace(/\\/+$/, ''),\n }\n}\n\n// Merge factory-level defaults with per-call overrides. Per-call wins.\nexport function mergeStartCallContext(\n factory: VoiceClientConfig,\n call: StartCallOptions,\n): { context?: Record<string, unknown>; metadata?: Record<string, string> } {\n const context =\n factory.defaultContext || call.context\n ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }\n : undefined\n const metadata =\n factory.defaultMetadata || call.metadata\n ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }\n : undefined\n return { context, metadata }\n}\n\nexport type { ClientTool, ClientToolMap } from './clientTools'\n","// Minimal auto-reconnecting WebSocket wrapper.\n//\n// Scope: transparent reconnection on unexpected drops (network blip,\n// server restart). We deliberately do NOT try to preserve call state\n// across reconnects — the server's AgentCallHandler is session-scoped\n// and won't resume where we left off. If the consumer needs mid-call\n// resilience, that's a server-side resumable-session feature, not a\n// client-side retry.\n//\n// In v1 we reconnect only when `connect()` has been called and\n// `.close(1000, ...)` has NOT been called by the consumer (i.e., we drop\n// because of network, not because the user hung up). On unclean close\n// the consumer gets a fresh connection + a `reconnected` event so they\n// can re-issue any greeting / context.\n//\n// Transport agnostic — accepts a WebSocket-constructor factory so the\n// browser bundle uses native `WebSocket` and the node bundle injects the\n// `ws` package.\n\nexport type RWSEvent =\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error }\n\n// Minimal WebSocket-like contract. Both browser `WebSocket` and the\n// `ws` package's WebSocket satisfy it.\nexport interface WebSocketLike {\n binaryType: string\n readyState: number\n onopen: ((ev: unknown) => void) | null\n onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null\n onerror: ((ev: unknown) => void) | null\n onclose: ((ev: { code: number; reason: string }) => void) | null\n send: (data: string | ArrayBuffer | ArrayBufferView) => void\n close: (code?: number, reason?: string) => void\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike\n\nexport interface RWSOptions {\n url: string\n // Factory so we can swap browser-native WebSocket for the `ws` package\n // in node. Browser entry pre-fills with a wrapper around globalThis.WebSocket.\n wsFactory: WebSocketFactory\n // Cap the number of auto-reconnect attempts. `0` disables retry entirely\n // (closer to native WebSocket semantics). Default: 3.\n maxRetries?: number\n // Initial backoff; doubles up to maxBackoffMs.\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst READYSTATE_OPEN = 1\nconst READYSTATE_CLOSED = 3\n\nexport const createReconnectingWebSocket = (\n options: RWSOptions,\n onEvent: (ev: RWSEvent) => void,\n) => {\n const maxRetries = options.maxRetries ?? 3\n const initialBackoff = options.initialBackoffMs ?? 500\n const maxBackoff = options.maxBackoffMs ?? 8000\n\n let ws: WebSocketLike | null = null\n let intentionalClose = false\n let retries = 0\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n\n const openOnce = () => {\n ws = options.wsFactory(options.url)\n // Binary frames arrive as Int16 PCM from the server. ArrayBuffer is\n // more convenient than Blob (one fewer async read) — we `.arrayBuffer()`\n // a Blob manually only if we receive one unexpectedly.\n ws.binaryType = 'arraybuffer'\n ws.onopen = () => {\n if (retries === 0) onEvent({ type: 'open' })\n else onEvent({ type: 'reconnected' })\n retries = 0\n backoff = initialBackoff\n }\n ws.onmessage = (ev) => {\n onEvent({ type: 'message', data: ev.data as string | ArrayBuffer })\n }\n ws.onerror = () => {\n onEvent({ type: 'error', error: new Error('WebSocket error') })\n }\n ws.onclose = (ev) => {\n ws = null\n const shouldRetry = !intentionalClose && retries < maxRetries\n if (!shouldRetry) {\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: true,\n })\n return\n }\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: false,\n })\n retries++\n const delay = Math.min(backoff, maxBackoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n reconnectTimer = setTimeout(openOnce, delay)\n }\n }\n\n openOnce()\n\n return {\n send: (data: string | ArrayBuffer | ArrayBufferView) => {\n if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)\n },\n close: (code = 1000, reason = 'client-requested') => {\n intentionalClose = true\n if (reconnectTimer) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n try {\n ws?.close(code, reason)\n } catch {\n // already closed\n }\n },\n readyState: () => ws?.readyState ?? READYSTATE_CLOSED,\n }\n}\n\nexport type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>\n","// Wire-protocol types + stateless transcript reducer.\n//\n// Shared between the browser and node bundles. The protocol surface is\n// matched (where it makes sense) to @craftedxp/voice-rn 0.3.x so a\n// consumer can write the same `onTranscript` / `onStateChange` /\n// `onError` / `onEnd` shape for both web and RN clients.\n\nimport type { ClientToolCallFrame } from './clientTools'\n\n// Re-export so consumers / dashboards can type the wire shape. The\n// `client_marks` frame itself is purely an internal observability\n// channel between the SDK and server — it's emitted by the SDK and\n// logged on the server, never surfaced to consumer code.\nexport type { ClientMarksFrame } from './ClientMarksBuffer'\n\n// ---------------------------------------------------------------------------\n// State machine\n// ---------------------------------------------------------------------------\n\nexport type CallState =\n | 'idle'\n | 'connecting'\n | 'listening'\n | 'user_speaking'\n | 'agent_speaking'\n | 'ended'\n | 'error'\n\n// ---------------------------------------------------------------------------\n// Transcript model — matches voice-rn TranscriptEntry\n// ---------------------------------------------------------------------------\n\nexport type TranscriptEntry =\n | { id: string; role: 'user'; text: string; committed: boolean }\n | { id: string; role: 'agent'; text: string; interrupted?: boolean }\n | { id: string; role: 'tool'; text: string }\n | { id: string; role: 'system'; text: string }\n\n// ---------------------------------------------------------------------------\n// Stable error code contract — matches voice-rn CallErrorCode where the\n// failure modes overlap. Web-specific codes (mic_denied via getUserMedia\n// rejection, etc.) keep their voice-rn names so cross-platform consumers\n// can write one switch statement.\n// ---------------------------------------------------------------------------\n\nexport type CallErrorCode =\n // Programming errors — surface loudly to the host's developer.\n | 'missing_credentials'\n | 'forbidden'\n // Browser audio failures.\n | 'mic_denied'\n | 'mic_start_failed'\n | 'audio_session_failed'\n // Auth lifecycle.\n | 'token_expired'\n | 'token_invalid'\n | 'unauthorized'\n // Network / connectivity.\n | 'network_unreachable'\n | 'socket_error'\n // Business state.\n | 'payment_required'\n | 'not_found'\n // End-of-call states surfaced via onError (also via onEnd reason='timeout').\n | 'silence_timeout'\n // Catch-all for unexpected server / 5xx / 1011.\n | 'server_error'\n\nexport interface CallError {\n code: CallErrorCode\n message: string\n}\n\n// ---------------------------------------------------------------------------\n// End-of-call signal — matches voice-rn CallEndReason / CallEndEvent\n// ---------------------------------------------------------------------------\n\nexport type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error'\n\nexport interface CallEndEvent {\n reason: CallEndReason\n // Present iff reason === 'error'. Mirrors the code from the most\n // recent onError.\n errorCode?: CallErrorCode\n // Wallclock from start() resolving to the WS close. Useful for billing\n // / \"you spoke for 1m23s\" UIs without forcing the host to track\n // timestamps themselves.\n durationMs: number\n}\n\n// ---------------------------------------------------------------------------\n// Volume meter event (browser bundle only — node bundle leaves volume to\n// the consumer if they're processing PCM themselves).\n// ---------------------------------------------------------------------------\n\nexport interface VolumeEvent {\n // 0-1 RMS over the last ~100ms. Bind to a waveform / level meter.\n input: number\n output: number\n}\n\n// ---------------------------------------------------------------------------\n// Server → client message envelope. Loose typing — the server can add new\n// `type` values without breaking the SDK; unknown types are ignored by\n// the dispatch in handleServerMessage.\n// ---------------------------------------------------------------------------\n\nexport type ServerMessage = Record<string, unknown> & { type?: string }\n\n// ---------------------------------------------------------------------------\n// Stateless transcript reducer + state-machine helpers. Both the browser\n// and node clients call into these so the shape of the transcript stays\n// identical across environments.\n// ---------------------------------------------------------------------------\n\nexport interface ProtocolState {\n state: CallState\n transcript: TranscriptEntry[]\n agentBubbleId: string | null\n idCounter: number\n // Reason latched from the server's call_end frame. Read by the\n // surrounding client when the WS finally closes so onEnd fires with the\n // right reason instead of falling back to user_hangup.\n endReason: CallEndReason | null\n}\n\nexport const createProtocolState = (): ProtocolState => ({\n state: 'idle',\n transcript: [],\n agentBubbleId: null,\n idCounter: 0,\n endReason: null,\n})\n\n// Side-effect callbacks the protocol layer fires as it processes server\n// frames. The surrounding client wires these up to event emitters\n// (browser) or to user-supplied callbacks (node).\nexport interface ProtocolCallbacks {\n onState: (next: CallState) => void\n onTranscript: (entries: TranscriptEntry[]) => void\n onError: (err: CallError) => void\n // Fires on `interrupt` — caller should flush its audio playback queue.\n onInterrupt: () => void\n // Fires on `agent_turn_start` — caller may want to reset its turn\n // anchor for \"agent has been speaking N ms\" UIs. `seq` is the\n // server-assigned turn ID; absent on legacy server builds that don't\n // emit one (we treat that case as \"no correlation possible\").\n onAgentTurnStart: (seq?: number) => void\n // Fires on `agent_turn_end` — caller may use this to flush per-turn\n // observability marks. `seq` matches the corresponding agent_turn_start.\n onAgentTurnEnd: (seq?: number) => void\n // Fires on `call_end` — caller closes its WS and resolves onEnd.\n onCallEnd: (reason: CallEndReason) => void\n // Fires on `connected` — caller should send the client_tools_register\n // frame here so the server's first-turn grace window unblocks.\n onConnected: () => void\n // Fires on `client_tool_call` — caller dispatches to the matching\n // tool handler and posts back a client_tool_result frame.\n onClientToolCall: (frame: ClientToolCallFrame) => void\n}\n\n// Map server-supplied endReason strings onto our SDK-side CallEndReason.\nconst mapEndReason = (raw: string): CallEndReason => {\n if (raw === 'agent_ended') return 'agent_ended'\n if (raw === 'caller_hung_up') return 'user_hangup'\n if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'\n return 'error'\n}\n\n// Pure-ish transcript reducer + dispatcher. Mutates `state` in place to\n// match the imperative pattern used by both clients; returns nothing.\n//\n// The \"agent_text\" / \"transcript\" interim handling is identical to\n// voice-rn's useVoiceCall — one growing user bubble while interim, one\n// growing agent bubble per turn.\nexport function handleServerMessage(\n raw: string,\n state: ProtocolState,\n cb: ProtocolCallbacks,\n): void {\n let msg: ServerMessage\n try {\n msg = JSON.parse(raw)\n } catch {\n return\n }\n\n switch (msg.type) {\n case 'connected':\n cb.onConnected()\n setState(state, 'listening', cb)\n return\n\n case 'transcript': {\n const text = (msg.text as string) ?? ''\n if (!text) return\n const isFinal = !!msg.isFinal\n if (!isFinal) setState(state, 'user_speaking', cb)\n upsertUserPartial(state, text, isFinal)\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_start': {\n const id = `m${state.idCounter++}`\n state.agentBubbleId = id\n state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]\n cb.onTranscript(state.transcript)\n const seq = typeof msg.seq === 'number' ? (msg.seq as number) : undefined\n cb.onAgentTurnStart(seq)\n setState(state, 'agent_speaking', cb)\n return\n }\n\n case 'agent_text': {\n const delta = (msg.text as string) ?? ''\n if (!delta || !state.agentBubbleId) return\n const id = state.agentBubbleId\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,\n )\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_end': {\n state.agentBubbleId = null\n const seq = typeof msg.seq === 'number' ? (msg.seq as number) : undefined\n cb.onAgentTurnEnd(seq)\n setState(state, 'listening', cb)\n return\n }\n\n case 'interrupt':\n cb.onInterrupt()\n return\n\n case 'agent_turn_abort': {\n const committed = ((msg.committedText as string) ?? '').trim()\n if (state.agentBubbleId) {\n const id = state.agentBubbleId\n if (committed) {\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,\n )\n } else {\n state.transcript = state.transcript.filter((e) => e.id !== id)\n }\n cb.onTranscript(state.transcript)\n }\n state.agentBubbleId = null\n return\n }\n\n case 'tool_call':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `→ ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'tool_result':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `${msg.ok ? '✓' : '✗'} ${String(msg.tool ?? '?')}`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'client_tool_call': {\n const toolCallId = String(msg.toolCallId ?? '')\n const name = String(msg.name ?? '')\n const args = (msg.args as Record<string, unknown>) ?? {}\n if (!toolCallId || !name) return\n cb.onClientToolCall({ toolCallId, name, args })\n return\n }\n\n case 'call_end': {\n const reasonRaw = String(msg.reason ?? '')\n const reason = mapEndReason(reasonRaw)\n state.endReason = reason\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'system',\n text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,\n },\n ]\n cb.onTranscript(state.transcript)\n cb.onCallEnd(reason)\n return\n }\n\n case 'error': {\n const code = (msg.code as CallErrorCode) ?? 'server_error'\n const message = (msg.message as string) ?? 'server error'\n cb.onError({ code, message })\n return\n }\n }\n}\n\nconst setState = (state: ProtocolState, next: CallState, cb: ProtocolCallbacks) => {\n if (state.state === next) return\n // The consumer's setState (cb.onState) is responsible for both\n // updating state.state and firing the user's onStateChange callback.\n // We deliberately don't mutate here — if we did, the consumer's\n // setState would early-return on the dedup check and never fire\n // onStateChange for server-driven transitions.\n cb.onState(next)\n}\n\n// Find the last uncommitted user bubble and grow it; or append a new\n// uncommitted bubble. Mirrors voice-rn upsertUserPartial.\nconst upsertUserPartial = (state: ProtocolState, text: string, isFinal: boolean) => {\n let idx = -1\n for (let i = state.transcript.length - 1; i >= 0; i--) {\n const e = state.transcript[i]\n if (e.role === 'user' && e.committed === false) {\n idx = i\n break\n }\n }\n if (idx === -1) {\n state.transcript = [\n ...state.transcript,\n { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },\n ]\n return\n }\n const target = state.transcript[idx] as Extract<TranscriptEntry, { role: 'user' }>\n const next = [...state.transcript]\n next[idx] = { ...target, text, committed: isFinal }\n state.transcript = next\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket URL builder. Identical between browser + node bundles —\n// kept here so the rare server-side change to the path / query shape is\n// a one-line edit.\n// ---------------------------------------------------------------------------\n\nexport interface BuildWsUrlArgs {\n apiBase: string\n agentId: string\n token: string\n bargeIn?: boolean\n}\n\nexport function buildWsUrl(args: BuildWsUrlArgs): string {\n const base = new URL(args.apiBase)\n const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'\n const bargeQS = args.bargeIn === false ? '&barge=off' : ''\n return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`\n}\n","// Bundle-agnostic client-tools core for @craftedxp/voice-js.\n//\n// Imports neither `ws` nor DOM `WebSocket` — the dispatcher takes an\n// injected `send` callback so the same code runs in browsers and Node.\n//\n// Wire protocol (matches the server's Zod schemas in\n// server/src/agents/clientToolFrames.ts):\n//\n// client → server client_tools_register { type, tools[] }\n// server → client client_tool_call { type, toolCallId, name, args }\n// client → server client_tool_result { type, toolCallId, result?, error? }\n\nexport interface ClientTool {\n description: string\n parameters: Record<string, unknown> // JSON Schema\n usage?: string // ≤500 chars; appended to agent system prompt\n timeoutMs?: number // server-enforced; (0, 30_000]\n example?: string // surface-only UI hint; not sent on wire\n handler: (args: Record<string, unknown>) => Promise<string | object> | string | object\n}\n\nexport type ClientToolMap = Record<string, ClientTool>\n\n// Server → client frame shape. Used by both protocol.ts (when surfacing\n// the frame to a callback) and dispatchClientToolCall (when consuming it).\nexport interface ClientToolCallFrame {\n toolCallId: string\n name: string\n args: Record<string, unknown>\n}\n\nconst NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/\nconst MAX_TOOLS = 64\nconst MAX_USAGE = 500\nconst MAX_TIMEOUT_MS = 30_000\n\n// Synchronous, throw-on-error validation. Called once at startCall time\n// before the WS opens, so bad input fails fast at the configuration\n// boundary instead of mid-call.\nexport const validateClientToolMap = (tools: ClientToolMap | undefined): void => {\n if (tools === undefined) return\n if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {\n throw new Error('clientTools must be an object keyed by tool name')\n }\n const entries = Object.entries(tools)\n if (entries.length > MAX_TOOLS) {\n throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)\n }\n for (const [name, def] of entries) {\n if (!NAME_RE.test(name)) {\n throw new Error(\n `clientTools[\"${name}\"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,\n )\n }\n if (!def || typeof def !== 'object') {\n throw new Error(`clientTools[\"${name}\"]: must be an object`)\n }\n if (typeof def.description !== 'string' || def.description.length === 0) {\n throw new Error(`clientTools[\"${name}\"]: must have a description`)\n }\n if (typeof def.handler !== 'function') {\n throw new Error(`clientTools[\"${name}\"]: must have a handler function`)\n }\n if (def.usage !== undefined && def.usage.length > MAX_USAGE) {\n throw new Error(`clientTools[\"${name}\"]: usage must be ≤500 chars`)\n }\n if (\n def.timeoutMs !== undefined &&\n (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)\n ) {\n throw new Error(`clientTools[\"${name}\"]: timeoutMs must be in (0, 30000]`)\n }\n }\n}\n\n// Build the registration frame the SDK sends right after `connected`.\n// Strips runtime-only fields (handler, example) and only includes the\n// optional fields when set.\nexport const buildRegisterFrame = (tools: ClientToolMap): object => ({\n type: 'client_tools_register',\n tools: Object.entries(tools).map(([name, def]) => ({\n name,\n description: def.description,\n parameters: def.parameters,\n ...(def.usage !== undefined ? { usage: def.usage } : {}),\n ...(def.timeoutMs !== undefined ? { timeoutMs: def.timeoutMs } : {}),\n })),\n})\n\ntype FrameSender = (frame: object) => void\n\n// Run the matching handler asynchronously and post a result/error frame\n// back through `send`. Send-side throws are swallowed (the WS may have\n// closed mid-call; the server has already cancelled the in-flight\n// invocation on its side).\nexport const dispatchClientToolCall = (\n send: FrameSender,\n tools: ClientToolMap,\n frame: ClientToolCallFrame,\n): void => {\n const safeSend = (payload: object) => {\n try {\n send(payload)\n } catch {\n // WS likely closed mid-handler. Server-side already cancelled.\n }\n }\n\n const tool = tools[frame.name]\n if (!tool) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: `No handler for ${frame.name}`,\n })\n return\n }\n\n void (async () => {\n try {\n const out = await tool.handler(frame.args)\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n result: typeof out === 'string' ? out : JSON.stringify(out),\n })\n } catch (err) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n}\n","// ClientMarksBuffer — per-turn latency-mark correlator shared between the\n// browser and node SDK clients.\n//\n// Why this exists:\n// The server mints a `seq` (turnId) only AFTER the user finishes\n// speaking, at VoiceOrchestrator.handleTranscript. The client, however,\n// captures its first user-perceived mark — `t_client_mic_to_first_frame`\n// — before any server frame for the turn exists. We therefore buffer\n// client-side marks under a \"pending\" slot and back-fill the seq when\n// the server's `agent_turn_start { seq }` arrives. When `agent_turn_end\n// { seq }` lands AND we've captured the inbound `client_first_audible`\n// mark, the buffer flushes a `client_marks` frame to the server.\n//\n// Lifetime + bounds:\n// - One pending bucket at a time. A new `agent_turn_start` flushes\n// anything still pending (shouldn't happen in normal flow, but is\n// safer than silently dropping marks).\n// - If `agent_turn_end` arrives but we never captured first_audible\n// (e.g. agent had no audio output), we still emit a `client_marks`\n// frame with whatever marks are present. Missing marks are simply\n// omitted from the wire payload.\n// - The buffer is GC'd by replacement on every turn boundary; there is\n// no long-term retention. A turn whose `agent_turn_end` never\n// arrives (server crash mid-turn) is implicitly dropped on the next\n// `agent_turn_start` or when the call ends. We do NOT add an\n// explicit watchdog — observability marks aren't worth the\n// complexity of a timer here.\n//\n// Time source:\n// `performance.now()` in both browser and Node. Wall-clock-monotonic,\n// sub-ms precision. Marks are stored as raw `performance.now()` values;\n// only the deltas between them ever go on the wire.\n//\n// Wire shape (see protocol.ts):\n// { type: 'client_marks', seq, marks: { ... }, clientNow }\n//\n// The CLAUDE.md / functional-style preference: this module is a small\n// factory returning a closure-based controller. No classes, no shared\n// mutable module state.\n\nexport interface ClientMarksFrame {\n type: 'client_marks'\n seq: number\n marks: {\n // The headline number: client-measured wall-clock between the first\n // outbound audio frame for this turn and the first agent-audio\n // chunk committed for playback. Includes mic-encode time, network\n // RTT in both directions, server-side processing (STT + LLM + TTS),\n // and any client-side scheduling delay. Absent if either endpoint\n // wasn't captured.\n client_mic_to_first_audible_ms?: number\n }\n // Wall-clock at send time, ms-since-epoch. Lets the analysis script\n // estimate clock skew between client and server logs even though the\n // marks themselves are deltas.\n clientNow?: number\n}\n\n// What the consumer-facing controller exposes. The browser and node\n// clients both wire to this same surface.\nexport interface ClientMarksController {\n // Outbound mark. Called on every outgoing audio chunk; the buffer\n // gates so only the first chunk PER TURN is timestamped. `markTurnBoundary`\n // resets the gate so the next chunk is treated as the start of the\n // next turn's outbound stream.\n markFirstOutboundAudio: () => void\n\n // Inbound mark. Called every time the SDK commits an agent-audio\n // chunk for playback (browser: AudioContext schedule; node: handed to\n // consumer via onAudioChunk). Gated to first-per-turn.\n markFirstAudibleOutput: () => void\n\n // Called when the SDK receives `agent_turn_start { seq }`. Tags any\n // accumulated outbound marks with this seq. Also resets the\n // first-outbound gate so the NEXT turn's outbound mark can be\n // captured again. (Outbound is captured for a turn before its seq is\n // known; inbound is captured after, with the seq tagged.)\n onAgentTurnStart: (seq: number) => void\n\n // Called when the SDK receives `agent_turn_end { seq }`. Triggers\n // emission of the buffered `client_marks` frame for that seq, with\n // whatever marks are present (missing ones omitted).\n onAgentTurnEnd: (seq: number) => void\n\n // Force-flush any pending state. Called on call end / teardown so\n // the last turn's marks aren't dropped on hangup.\n flush: () => void\n}\n\n// `send` is the raw WS-send sink injected by the surrounding client.\n// Stringified JSON in browser/node. Returns whether the send was\n// accepted (we don't surface failure beyond the boolean — observability\n// frames are best-effort).\nexport type ClientMarksSend = (frame: ClientMarksFrame) => void\n\n// `now` defaults to performance.now() but is injected so unit tests can\n// drive a fake clock deterministically.\nexport interface CreateClientMarksBufferArgs {\n send: ClientMarksSend\n now?: () => number\n}\n\nexport const createClientMarksBuffer = (\n args: CreateClientMarksBufferArgs,\n): ClientMarksController => {\n const now = args.now ?? (() => performance.now())\n\n // Pending bucket — one in-flight turn at a time. We hold the OUTBOUND\n // mark before we know `seq`, then graduate to a per-seq bucket on\n // agent_turn_start so a second turn's inbound marks can't trample the\n // first turn's outbound mark before flush.\n let pendingFirstOutboundAt: number | null = null\n\n // Once agent_turn_start { seq } arrives we move the pending outbound\n // mark into a per-seq slot and start collecting the inbound mark.\n // Map keyed by seq; in practice never holds more than 1-2 entries\n // (one currently-flushing, one just-started).\n const inFlight = new Map<\n number,\n {\n firstOutboundAt: number | null\n firstAudibleAt: number | null\n ended: boolean\n }\n >()\n\n const tryEmit = (seq: number) => {\n const slot = inFlight.get(seq)\n if (!slot) return\n if (!slot.ended) return\n // Build wire payload. Only include marks we actually captured.\n const marks: ClientMarksFrame['marks'] = {}\n if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {\n marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt\n }\n args.send({\n type: 'client_marks',\n seq,\n marks,\n clientNow: Date.now(),\n })\n inFlight.delete(seq)\n }\n\n const markFirstOutboundAudio = () => {\n if (pendingFirstOutboundAt !== null) return // already stamped this turn\n pendingFirstOutboundAt = now()\n }\n\n const markFirstAudibleOutput = () => {\n // Inbound is captured AFTER agent_turn_start, so we know which seq\n // it belongs to: the most recent un-ended in-flight slot. If there's\n // somehow none (race: audio before agent_turn_start), drop silently.\n let target: { firstAudibleAt: number | null; ended: boolean } | undefined\n for (const slot of inFlight.values()) {\n if (!slot.ended) {\n target = slot\n // No break — last un-ended wins, matching the \"most recent\n // turn\" intent if multiple are in flight.\n }\n }\n if (!target) return\n if (target.firstAudibleAt !== null) return // first-per-turn gate\n target.firstAudibleAt = now()\n }\n\n const onAgentTurnStart = (seq: number) => {\n inFlight.set(seq, {\n firstOutboundAt: pendingFirstOutboundAt,\n firstAudibleAt: null,\n ended: false,\n })\n // Reset outbound gate so the NEXT turn captures cleanly. Note:\n // pendingFirstOutboundAt is already moved into the slot above; we\n // don't need to preserve it.\n pendingFirstOutboundAt = null\n }\n\n const onAgentTurnEnd = (seq: number) => {\n const slot = inFlight.get(seq)\n if (!slot) {\n // Got an end with no matching start (server bug, or restart). Best\n // effort: emit an empty marks frame so the analysis script knows\n // the client saw the turn at all.\n args.send({ type: 'client_marks', seq, marks: {}, clientNow: Date.now() })\n return\n }\n slot.ended = true\n tryEmit(seq)\n }\n\n const flush = () => {\n for (const seq of [...inFlight.keys()]) {\n const slot = inFlight.get(seq)!\n slot.ended = true\n tryEmit(seq)\n }\n pendingFirstOutboundAt = null\n }\n\n return {\n markFirstOutboundAudio,\n markFirstAudibleOutput,\n onAgentTurnStart,\n onAgentTurnEnd,\n flush,\n }\n}\n","// Node VoiceClient — drives one in-progress call from a Node.js or\n// Electron-main environment.\n//\n// Same WS protocol + transcript state as the browser bundle, but with\n// NO built-in audio I/O. Node consumers feed mic frames in via\n// `sendAudioChunk(buf)` and consume the agent's TTS PCM via the\n// `onAudioChunk` callback they passed to `startCall`. Bring your own\n// audio adapter (sox, PortAudio, OS-native, RTP relay — whatever fits\n// your host).\n//\n// `mute()` / `unmute()` here just gate `sendAudioChunk` — when muted, the\n// next call replaces the buffer with zeroed silence of the same length\n// so the server's endpointing keeps seeing wire frames at the expected\n// cadence (matches voice-rn behaviour).\n\nimport {\n createReconnectingWebSocket,\n type ReconnectingWebSocket,\n type WebSocketFactory,\n} from './ReconnectingWebSocket'\nimport {\n buildWsUrl,\n createProtocolState,\n handleServerMessage,\n type CallEndReason,\n type CallError,\n type ProtocolState,\n} from './protocol'\nimport type { Call, StartCallOptions, VoiceClientConfig } from './config'\nimport { buildRegisterFrame, dispatchClientToolCall, validateClientToolMap } from './clientTools'\nimport { createClientMarksBuffer, type ClientMarksController } from './ClientMarksBuffer'\n\n// Node-specific extensions to the per-call options. Surfaced as a\n// separate type so the browser StartCallOptions stays clean.\nexport interface NodeStartCallOptions extends StartCallOptions {\n /**\n * Fires for each binary PCM frame the server pushes (Int16 LE mono\n * @ 16 kHz — same as the browser playback path). Wire to your\n * preferred output: write to a `sox -t raw -r 16000 -e signed -b 16\n * -c 1 - default` subprocess, queue into PortAudio, relay over RTP,\n * etc. If you don't supply this callback, agent audio is dropped on\n * the floor.\n */\n onAudioChunk?: (pcm: ArrayBuffer) => void\n}\n\n// Node consumers receive a richer Call handle that includes the\n// raw-PCM control surface.\nexport interface NodeCall extends Call {\n /**\n * Push one mic frame to the server. Expected: Int16 LE mono PCM @\n * 16 kHz. Capture cadence ~100 ms / ~3.2 KB per frame is fine.\n * Returns `false` if the WS isn't open yet (caller may want to\n * back-pressure or drop).\n */\n sendAudioChunk: (pcm: ArrayBuffer | ArrayBufferView) => boolean\n}\n\n/**\n * Node bundle's analog of `VoiceClientFactory`. Same shape but\n * `startCall` accepts the richer `NodeStartCallOptions` (which adds\n * `onAudioChunk`) and resolves to a `NodeCall` (which adds\n * `sendAudioChunk`). Returned by `configureVoiceClient` in the Node\n * entry. Browser entry returns the base `VoiceClientFactory` type.\n */\nexport interface NodeVoiceClientFactory {\n readonly config: VoiceClientConfig\n startCall: (options: NodeStartCallOptions) => Promise<NodeCall>\n}\n\ninterface NodeVoiceClientArgs {\n config: VoiceClientConfig\n options: NodeStartCallOptions\n token: string\n wsFactory: WebSocketFactory\n}\n\nexport class NodeVoiceClient implements NodeCall {\n private readonly args: NodeVoiceClientArgs\n private readonly proto: ProtocolState\n\n private rws: ReconnectingWebSocket | null = null\n\n private muted = false\n private startedAt: number | null = null\n private endedFired = false\n private lastError: CallError | null = null\n private marks: ClientMarksController\n\n constructor(args: NodeVoiceClientArgs) {\n this.args = args\n this.proto = createProtocolState()\n validateClientToolMap(args.options.clientTools)\n this.marks = createClientMarksBuffer({\n send: (frame) => {\n try {\n this.rws?.send(JSON.stringify(frame))\n } catch {\n // WS already gone; drop the frame.\n }\n },\n })\n }\n\n // ---------------------------------------------------------------\n // Call interface\n // ---------------------------------------------------------------\n\n get state() {\n return this.proto.state\n }\n\n get transcript() {\n return this.proto.transcript.slice()\n }\n\n get isMuted() {\n return this.muted\n }\n\n end = () => {\n this.teardown('user_hangup')\n }\n\n mute = () => {\n this.muted = true\n }\n\n unmute = () => {\n this.muted = false\n }\n\n // ---------------------------------------------------------------\n // Node-only raw audio surface\n // ---------------------------------------------------------------\n\n sendAudioChunk = (pcm: ArrayBuffer | ArrayBufferView): boolean => {\n if (!this.rws) return false\n // Outbound mark — first frame leaving for the current pending turn.\n // Buffer gates so only first-per-turn is timestamped. Same semantic\n // as browser: this is \"moment audio leaves the SDK\", which for Node\n // is moment-of-WS-send (the consumer is responsible for whatever\n // capture pipeline feeds this method).\n this.marks.markFirstOutboundAudio()\n if (this.muted) {\n // Silence at the wire cadence — server endpointing depends on a\n // steady frame rhythm; going silent confuses it.\n const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength\n this.rws.send(new ArrayBuffer(len))\n return true\n }\n this.rws.send(pcm)\n return true\n }\n\n // ---------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------\n\n async start(): Promise<void> {\n this.setState('connecting')\n this.startedAt = Date.now()\n\n const url = buildWsUrl({\n apiBase: this.args.config.apiBase,\n agentId: this.args.options.agentId,\n token: this.args.token,\n bargeIn: this.args.options.bargeIn,\n })\n\n this.rws = createReconnectingWebSocket(\n {\n url,\n wsFactory: this.args.wsFactory,\n maxRetries: 3,\n },\n (ev) => this.handleSocketEvent(ev),\n )\n }\n\n // ---------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------\n\n private setState = (next: ProtocolState['state']) => {\n if (this.proto.state === next) return\n this.proto.state = next\n this.args.options.onStateChange?.(next)\n }\n\n private sendClientToolsRegister = () => {\n const frame = buildRegisterFrame(this.args.options.clientTools ?? {})\n this.rws?.send(JSON.stringify(frame))\n }\n\n private emitError = (err: CallError) => {\n this.lastError = err\n this.args.options.onError?.(err)\n }\n\n private handleSocketEvent = (\n ev:\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error },\n ) => {\n switch (ev.type) {\n case 'open':\n // No mic to start — consumer drives sendAudioChunk themselves.\n break\n case 'reconnected':\n this.proto.transcript = []\n this.proto.agentBubbleId = null\n this.args.options.onTranscript?.(this.proto.transcript)\n this.setState('listening')\n break\n case 'message':\n if (typeof ev.data === 'string') {\n handleServerMessage(ev.data, this.proto, {\n onState: this.setState,\n onTranscript: (entries) => this.args.options.onTranscript?.(entries),\n onError: this.emitError,\n onInterrupt: () => this.args.options.onInterrupt?.(),\n onAgentTurnStart: (seq) => {\n if (typeof seq === 'number') this.marks.onAgentTurnStart(seq)\n this.args.options.onAgentTurnStart?.()\n },\n onAgentTurnEnd: (seq) => {\n if (typeof seq === 'number') this.marks.onAgentTurnEnd(seq)\n },\n onCallEnd: (reason) => this.teardown(reason),\n onConnected: () => this.sendClientToolsRegister(),\n onClientToolCall: (frame) =>\n dispatchClientToolCall(\n (f) => this.rws?.send(JSON.stringify(f)),\n this.args.options.clientTools ?? {},\n frame,\n ),\n })\n } else {\n // Binary frame — agent TTS PCM. Inbound mark fires the moment\n // we COMMIT the chunk to the consumer's sink (closest we can\n // get to \"user heard it\" without owning the playback path).\n this.marks.markFirstAudibleOutput()\n this.args.options.onAudioChunk?.(ev.data)\n }\n break\n case 'close':\n if (ev.permanent) {\n const reason: CallEndReason =\n this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')\n this.teardown(reason)\n }\n break\n case 'error':\n this.emitError({ code: 'socket_error', message: ev.error.message })\n break\n }\n }\n\n private teardown = (reason: CallEndReason) => {\n try {\n this.marks.flush()\n } catch {\n // never fatal\n }\n try {\n this.rws?.close(1000, reason)\n } catch {\n // already closed\n }\n this.rws = null\n this.setState('ended')\n this.fireEndOnce(reason)\n }\n\n private fireEndOnce = (reason: CallEndReason) => {\n if (this.endedFired) return\n this.endedFired = true\n const startedAt = this.startedAt ?? Date.now()\n this.args.options.onEnd?.({\n reason,\n errorCode: reason === 'error' ? this.lastError?.code : undefined,\n durationMs: Date.now() - startedAt,\n })\n }\n}\n","// Node entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n// (under the `node` condition; or `from '@craftedxp/voice-js/node'`\n// if your bundler doesn't honour conditional exports).\n//\n// Pre-injects:\n// - `ws` package as the WebSocket transport (declared as an OPTIONAL\n// peer — loaded via dynamic import() so a missing peer surfaces a\n// clear install hint at startCall time rather than crashing at\n// module load).\n//\n// Audio is intentionally NOT injected. Node consumers feed mic frames\n// in via `call.sendAudioChunk(buf)` and consume agent audio via\n// `onAudioChunk` they passed to startCall. See NodeVoiceClient for the\n// raw PCM contract.\n\nimport { mergeStartCallContext, normalizeConfig, type VoiceClientConfig } from './config'\nimport type { WebSocketFactory, WebSocketLike } from './ReconnectingWebSocket'\nimport {\n NodeVoiceClient,\n type NodeCall,\n type NodeStartCallOptions,\n type NodeVoiceClientFactory,\n} from './NodeVoiceClient'\n\n// Lazy + cached. The first startCall pays the dynamic-import cost; later\n// calls reuse the resolved constructor.\nlet cachedWsCtor: { new (url: string): WebSocketLike } | null = null\n\nconst loadWsCtor = async (): Promise<{ new (url: string): WebSocketLike }> => {\n if (cachedWsCtor) return cachedWsCtor\n try {\n // Dynamic import survives bundler tree-shaking and lets us emit a\n // clean error message for the missing-peer case. Browser bundles\n // never reach this code path because they pick the `browser`\n // condition in package.json `exports`.\n const mod = (await import('ws')) as unknown as {\n default?: { new (url: string): WebSocketLike }\n WebSocket?: { new (url: string): WebSocketLike }\n }\n const ctor = mod.WebSocket ?? mod.default\n if (!ctor) {\n throw new Error('imported `ws` but neither default nor named WebSocket export was found')\n }\n cachedWsCtor = ctor\n return ctor\n } catch (err) {\n throw new Error(\n \"@craftedxp/voice-js (node): missing optional peer `ws`. Install it with `npm install ws` (ws is declared as `peerDependenciesMeta.optional` so npm doesn't install it automatically). Original: \" +\n (err instanceof Error ? err.message : String(err)),\n )\n }\n}\n\nclass NodeVoiceFactory implements NodeVoiceClientFactory {\n readonly config: VoiceClientConfig\n\n constructor(config: VoiceClientConfig) {\n this.config = config\n }\n\n startCall = async (options: NodeStartCallOptions): Promise<NodeCall> => {\n if (!options.agentId) {\n throw new Error('startCall: agentId is required')\n }\n\n // Resolve `ws` before the network round-trip so the missing-peer\n // failure surfaces early.\n const WsCtor = await loadWsCtor()\n const wsFactory: WebSocketFactory = (url) => new WsCtor(url)\n\n const { context, metadata } = mergeStartCallContext(this.config, options)\n const fetchArgs = {\n agentId: options.agentId,\n userId: options.userId,\n context,\n metadata,\n }\n\n let token: string\n if (options.token) {\n token = options.token\n } else {\n const r = await this.config.fetchToken(fetchArgs)\n if (!r) {\n throw new Error('configureVoiceClient.fetchToken returned empty token')\n }\n token = typeof r === 'string' ? r : r.token\n if (!token) {\n throw new Error('configureVoiceClient.fetchToken returned an object without `token`')\n }\n }\n\n const client = new NodeVoiceClient({\n config: this.config,\n options: { ...options, context, metadata },\n token,\n wsFactory,\n })\n await client.start()\n return client\n }\n}\n\n/**\n * One-time SDK setup for Node.js / Electron-main consumers. Returns a\n * factory you call `startCall` on for every voice call. Same shape as\n * the browser entry but the returned `Call` has an extra\n * `sendAudioChunk` method for raw-PCM input, and `startCall` accepts\n * an `onAudioChunk` callback for raw-PCM output.\n *\n * Example (vterm-style CLI, sox sub-process for I/O):\n *\n * import { configureVoiceClient } from '@craftedxp/voice-js/node'\n * import { spawn } from 'child_process'\n *\n * const voice = configureVoiceClient({\n * apiBase: 'https://api.your-server.com',\n * fetchToken: async () => mintFromMyBackend(),\n * })\n *\n * const mic = spawn('sox', [...recArgs, '-r', '16000', '-c', '1', '-b', '16', '-e', 'signed', '-t', 'raw', '-'])\n * const spk = spawn('sox', ['-t', 'raw', '-r', '16000', '-c', '1', '-b', '16', '-e', 'signed', '-', ...playArgs])\n *\n * const call = await voice.startCall({\n * agentId: 'agt_xxx',\n * onAudioChunk: (pcm) => spk.stdin.write(Buffer.from(pcm)),\n * onEnd: () => { mic.kill(); spk.stdin.end() },\n * })\n *\n * mic.stdout.on('data', (chunk) => call.sendAudioChunk(chunk))\n */\nexport function configureVoiceClient(config: VoiceClientConfig): NodeVoiceClientFactory {\n return new NodeVoiceFactory(normalizeConfig(config))\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — types + advanced primitives. Mirror of browser entry\n// minus the AudioCapture/AudioPlayback exports (browser-only).\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n ClientTool,\n ClientToolMap,\n FetchToken,\n FetchTokenArgs,\n FetchTokenResult,\n StartCallOptions,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type { NodeCall, NodeStartCallOptions, NodeVoiceClientFactory } from './NodeVoiceClient'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftedxp/voice-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "JS SDK for embedding a voice agent call in any JS environment — browser, Node.js, Electron. Zero framework dependencies. Drop-in companion to @craftedxp/voice-rn (React Native).",
|
|
5
5
|
"author": "Crafted XP",
|
|
6
6
|
"license": "MIT",
|