@mindstudio-ai/interface 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/voice.js CHANGED
@@ -1,2 +1,2 @@
1
- import{a as k,b,f as V,h as A}from"./chunk-DJDMBBRF.js";var O="/_/voice",R="lk.transcription",L="lk.chat",N="ms.voice.tool",M="ms.voice.client-tool",H="ms.voice.client-tool-result",$=32e3,D="lk.transcription_final",K="lk.segment_id",G="lk.agent.state",q=200,U=100;async function T(i,f,a){let o={Authorization:`Bearer ${V().token}`};a!==void 0&&(o["Content-Type"]="application/json");let r=await fetch(A(`${O}${i}`),{method:f,headers:o,...a!==void 0&&{body:JSON.stringify(a)}});if(!r.ok)throw await b(r,`Voice request failed: ${r.status} ${r.statusText}`,"voice_error");return await r.json()}function J(){return{stateChange:new Set,transcript:new Set,toolCall:new Set,error:new Set}}function Q(i){switch(i){case"listening":case"idle":return"listening";case"thinking":return"thinking";case"speaking":return"speaking";default:return"connecting"}}function X(){return{async startSession(i){i?.signal?.throwIfAborted();let f=V().token,a=await T("/sessions","POST"),l=await import("livekit-client");i?.signal?.throwIfAborted();let o=new l.Room,r=J(),w=new Map,m="connecting",_=!1,u=null,S=null,v=e=>{if(!(e===m||m==="ended")){m=e;for(let t of r.stateChange)t(e)}},P=e=>{for(let t of r.error)t(e)},E=()=>{u&&(u.remove(),u=null),S&&window.__MS_VOICE__===S&&delete window.__MS_VOICE__,S=null,v("ended")},x=e=>o.remoteParticipants.get(e)?.kind===l.ParticipantKind.AGENT;o.on(l.RoomEvent.TrackSubscribed,(e,t,n)=>{e.kind===l.Track.Kind.Audio&&n.kind===l.ParticipantKind.AGENT&&(u=e.attach(),u.style.display="none",document.body.appendChild(u))}),o.on(l.RoomEvent.ParticipantAttributesChanged,(e,t)=>{if(t.kind===l.ParticipantKind.AGENT){let n=t.attributes[G];n&&v(Q(n))}}),o.on(l.RoomEvent.Disconnected,()=>{E()}),o.registerTextStreamHandler(R,(e,t)=>{let n=t.identity===o.localParticipant.identity?"user":"agent",d=e.info.attributes??{},s=d[K]??e.info.id;(async()=>{try{if(n==="user"){let c=await e.readAll(),p=d[D]==="true";for(let C of r.transcript)C({role:n,segmentId:s,text:c,final:p})}else{let c="";for await(let p of e){c+=p;for(let C of r.transcript)C({role:n,segmentId:s,text:c,final:!1})}for(let p of r.transcript)p({role:n,segmentId:s,text:c,final:!0})}}catch{}})()}),o.registerTextStreamHandler(M,e=>{(async()=>{let t;try{t=JSON.parse(await e.readAll())}catch{return}if(!t.id||!t.name)return;let n=async s=>{try{await o.localParticipant.sendText(JSON.stringify({id:t.id,...s}),{topic:H})}catch(c){P(c)}},d=w.get(t.name);if(!d){console.warn(`[voice] client tool "${t.name}" invoked but no handler is registered \u2014 call session.registerClientTool()`),await n({error:"unhandled_client_tool"});return}try{let s=await d(t.args),c=s===void 0?void 0:JSON.stringify(s);if(c!==void 0&&c.length>$){await n({error:"result_too_large"});return}await n(s===void 0?{}:{result:s})}catch(s){await n({error:s instanceof Error?s.message:String(s)})}})()}),o.registerTextStreamHandler(N,(e,t)=>{x(t.identity)&&(async()=>{try{let n=JSON.parse(await e.readAll());if(!n.method)return;let d=n.status==="done"||n.status==="failed"?n.status:"running";for(let s of r.toolCall)s({method:n.method,status:d,at:n.at??Date.now(),...n.result!==void 0?{result:n.result}:{},...n.resultTruncated===!0?{resultTruncated:!0}:{}})}catch{}})()});let I=()=>{o.disconnect()};i?.signal?.addEventListener("abort",I,{once:!0});try{await o.connect(a.url,a.token),i?.signal?.throwIfAborted();try{await o.localParticipant.setMicrophoneEnabled(!0)}catch(e){let t=e?.name;throw t==="NotAllowedError"||t==="PermissionDeniedError"?new k("Microphone access was denied. Allow microphone access for this site and try again.","microphone_denied"):t==="NotFoundError"?new k("No microphone was found on this device.","microphone_not_found"):e}}catch(e){throw await o.disconnect().catch(()=>{}),E(),i?.signal?.aborted?new DOMException("Voice session aborted","AbortError"):e}finally{i?.signal?.removeEventListener("abort",I)}let y={sessionId:a.sessionId,room:a.room,get state(){return m},get isMuted(){return _},on(e,t){let n=r[e];return n.add(t),()=>{n.delete(t)}},async mute(){await o.localParticipant.setMicrophoneEnabled(!1),_=!0},async unmute(){await o.localParticipant.setMicrophoneEnabled(!0),_=!1},async sendText(e){try{await o.localParticipant.sendText(e,{topic:L})}catch(t){throw P(t),t}},registerClientTool(e,t){return w.set(e,t),()=>{w.get(e)===t&&w.delete(e)}},async refreshIdentity(){await T(`/sessions/${encodeURIComponent(a.sessionId)}/refresh-identity`,"POST",{previousToken:f})},async end(){await o.disconnect(),E()}},g=[];r.transcript.add(e=>{let t=g.findIndex(n=>n.segmentId===e.segmentId&&n.role===e.role);t>=0?g[t]=e:(g.push(e),g.length>q&&g.shift())});let h=[];return r.toolCall.add(e=>{h.push(e),h.length>U&&h.shift()}),S={sessionId:a.sessionId,get state(){return m},transcript:g,toolCalls:h,sendText:e=>y.sendText(e),end:()=>y.end()},window.__MS_VOICE__=S,y},async listSessions(i){let f=i?`?cursor=${encodeURIComponent(i)}`:"";return T(`/sessions${f}`,"GET")},async getSession(i){return T(`/sessions/${encodeURIComponent(i)}`,"GET")},async deleteSession(i){await T(`/sessions/${encodeURIComponent(i)}/delete`,"POST")}}}export{X as createVoiceClient};
1
+ import{a as k,b,f as V,h as A}from"./chunk-5E7SM65A.js";var O="/_/voice",R="lk.transcription",L="lk.chat",N="ms.voice.tool",M="ms.voice.client-tool",H="ms.voice.client-tool-result",$=32e3,D="lk.transcription_final",K="lk.segment_id",G="lk.agent.state",q=200,U=100;async function T(i,f,a){let o={Authorization:`Bearer ${V().token}`};a!==void 0&&(o["Content-Type"]="application/json");let r=await fetch(A(`${O}${i}`),{method:f,headers:o,...a!==void 0&&{body:JSON.stringify(a)}});if(!r.ok)throw await b(r,`Voice request failed: ${r.status} ${r.statusText}`,"voice_error");return await r.json()}function J(){return{stateChange:new Set,transcript:new Set,toolCall:new Set,error:new Set}}function Q(i){switch(i){case"listening":case"idle":return"listening";case"thinking":return"thinking";case"speaking":return"speaking";default:return"connecting"}}function X(){return{async startSession(i){i?.signal?.throwIfAborted();let f=V().token,a=await T("/sessions","POST"),l=await import("livekit-client");i?.signal?.throwIfAborted();let o=new l.Room,r=J(),w=new Map,m="connecting",_=!1,u=null,S=null,v=e=>{if(!(e===m||m==="ended")){m=e;for(let t of r.stateChange)t(e)}},P=e=>{for(let t of r.error)t(e)},E=()=>{u&&(u.remove(),u=null),S&&window.__MS_VOICE__===S&&delete window.__MS_VOICE__,S=null,v("ended")},x=e=>o.remoteParticipants.get(e)?.kind===l.ParticipantKind.AGENT;o.on(l.RoomEvent.TrackSubscribed,(e,t,n)=>{e.kind===l.Track.Kind.Audio&&n.kind===l.ParticipantKind.AGENT&&(u=e.attach(),u.style.display="none",document.body.appendChild(u))}),o.on(l.RoomEvent.ParticipantAttributesChanged,(e,t)=>{if(t.kind===l.ParticipantKind.AGENT){let n=t.attributes[G];n&&v(Q(n))}}),o.on(l.RoomEvent.Disconnected,()=>{E()}),o.registerTextStreamHandler(R,(e,t)=>{let n=t.identity===o.localParticipant.identity?"user":"agent",d=e.info.attributes??{},s=d[K]??e.info.id;(async()=>{try{if(n==="user"){let c=await e.readAll(),p=d[D]==="true";for(let C of r.transcript)C({role:n,segmentId:s,text:c,final:p})}else{let c="";for await(let p of e){c+=p;for(let C of r.transcript)C({role:n,segmentId:s,text:c,final:!1})}for(let p of r.transcript)p({role:n,segmentId:s,text:c,final:!0})}}catch{}})()}),o.registerTextStreamHandler(M,e=>{(async()=>{let t;try{t=JSON.parse(await e.readAll())}catch{return}if(!t.id||!t.name)return;let n=async s=>{try{await o.localParticipant.sendText(JSON.stringify({id:t.id,...s}),{topic:H})}catch(c){P(c)}},d=w.get(t.name);if(!d){console.warn(`[voice] client tool "${t.name}" invoked but no handler is registered \u2014 call session.registerClientTool()`),await n({error:"unhandled_client_tool"});return}try{let s=await d(t.args),c=s===void 0?void 0:JSON.stringify(s);if(c!==void 0&&c.length>$){await n({error:"result_too_large"});return}await n(s===void 0?{}:{result:s})}catch(s){await n({error:s instanceof Error?s.message:String(s)})}})()}),o.registerTextStreamHandler(N,(e,t)=>{x(t.identity)&&(async()=>{try{let n=JSON.parse(await e.readAll());if(!n.method)return;let d=n.status==="done"||n.status==="failed"?n.status:"running";for(let s of r.toolCall)s({method:n.method,status:d,at:n.at??Date.now(),...n.result!==void 0?{result:n.result}:{},...n.resultTruncated===!0?{resultTruncated:!0}:{}})}catch{}})()});let I=()=>{o.disconnect()};i?.signal?.addEventListener("abort",I,{once:!0});try{await o.connect(a.url,a.token),i?.signal?.throwIfAborted();try{await o.localParticipant.setMicrophoneEnabled(!0)}catch(e){let t=e?.name;throw t==="NotAllowedError"||t==="PermissionDeniedError"?new k("Microphone access was denied. Allow microphone access for this site and try again.","microphone_denied"):t==="NotFoundError"?new k("No microphone was found on this device.","microphone_not_found"):e}}catch(e){throw await o.disconnect().catch(()=>{}),E(),i?.signal?.aborted?new DOMException("Voice session aborted","AbortError"):e}finally{i?.signal?.removeEventListener("abort",I)}let y={sessionId:a.sessionId,room:a.room,get state(){return m},get isMuted(){return _},on(e,t){let n=r[e];return n.add(t),()=>{n.delete(t)}},async mute(){await o.localParticipant.setMicrophoneEnabled(!1),_=!0},async unmute(){await o.localParticipant.setMicrophoneEnabled(!0),_=!1},async sendText(e){try{await o.localParticipant.sendText(e,{topic:L})}catch(t){throw P(t),t}},registerClientTool(e,t){return w.set(e,t),()=>{w.get(e)===t&&w.delete(e)}},async refreshIdentity(){await T(`/sessions/${encodeURIComponent(a.sessionId)}/refresh-identity`,"POST",{previousToken:f})},async end(){await o.disconnect(),E()}},g=[];r.transcript.add(e=>{let t=g.findIndex(n=>n.segmentId===e.segmentId&&n.role===e.role);t>=0?g[t]=e:(g.push(e),g.length>q&&g.shift())});let h=[];return r.toolCall.add(e=>{h.push(e),h.length>U&&h.shift()}),S={sessionId:a.sessionId,get state(){return m},transcript:g,toolCalls:h,sendText:e=>y.sendText(e),end:()=>y.end()},window.__MS_VOICE__=S,y},async listSessions(i){let f=i?`?cursor=${encodeURIComponent(i)}`:"";return T(`/sessions${f}`,"GET")},async getSession(i){return T(`/sessions/${encodeURIComponent(i)}`,"GET")},async deleteSession(i){await T(`/sessions/${encodeURIComponent(i)}/delete`,"POST")}}}export{X as createVoiceClient};
2
2
  //# sourceMappingURL=voice.js.map
package/dist/voice.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/voice.ts"],"sourcesContent":["/**\n * Voice client for apps with a voice interface.\n *\n * Imported from the `./voice` subpath so apps that never use voice ship none\n * of it — and `livekit-client` (the one dependency, for the realtime media\n * transport) is loaded dynamically on the first `startSession()` call:\n *\n * ```\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n * ```\n *\n * Flow: `startSession()` mints a session from the platform\n * (`POST /_/voice/sessions`), connects to the media room, publishes the\n * microphone, and returns a {@link VoiceSession} that surfaces the\n * conversation as events — agent state, live captions for both sides, tool\n * activity — plus mute / text-injection / end controls. Agent audio playback\n * is handled internally (a hidden autoplaying element); the app never touches\n * audio elements.\n */\n\nimport { getConfig, withBase } from './config.js';\nimport { MindStudioInterfaceError, errorFromResponse } from './errors.js';\nimport type { Room, RemoteTrack, TextStreamReader } from 'livekit-client';\n\n// ---------------------------------------------------------------------------\n// Wire constants (the voice worker's room contract)\n// ---------------------------------------------------------------------------\n\nconst VOICE_BASE = '/_/voice';\n\n/** Live captions, published by the agent framework. */\nconst TOPIC_TRANSCRIPTION = 'lk.transcription';\n/** Text input into the live conversation (interrupts + replies). */\nconst TOPIC_CHAT = 'lk.chat';\n/** Tool activity, published by the MindStudio voice worker. */\nconst TOPIC_TOOL = 'ms.voice.tool';\nconst TOPIC_CLIENT_TOOL = 'ms.voice.client-tool';\nconst TOPIC_CLIENT_TOOL_RESULT = 'ms.voice.client-tool-result';\n// Client-tool handler returns must fit the same forwarding cap as tool\n// results; oversize returns become an error the agent can speak around.\nconst CLIENT_TOOL_RESULT_MAX_CHARS = 32_000;\n\nconst ATTR_TRANSCRIPTION_FINAL = 'lk.transcription_final';\nconst ATTR_SEGMENT_ID = 'lk.segment_id';\nconst ATTR_AGENT_STATE = 'lk.agent.state';\n\n// ---------------------------------------------------------------------------\n// Session types\n// ---------------------------------------------------------------------------\n\n/** Lifecycle of a live voice session, driven by the agent's published state. */\nexport type VoiceSessionState =\n | 'connecting'\n | 'listening'\n | 'thinking'\n | 'speaking'\n | 'ended';\n\n/** One live-caption update. */\nexport interface VoiceTranscriptEvent {\n /** Whose speech this is. */\n role: 'user' | 'agent';\n /** Stable id for one utterance — updates with the same id replace, not append. */\n segmentId: string;\n /** The utterance text so far (always the full text, never a delta). */\n text: string;\n /** True once this segment won't change again. */\n final: boolean;\n}\n\n/** Tool activity from the agent, for inline status UI. */\nexport interface VoiceToolCallEvent {\n /** The method id being executed. */\n method: string;\n status: 'running' | 'done' | 'failed';\n /** Milliseconds since epoch. */\n at: number;\n /**\n * The tool's return value — present on every 'done' (unless truncated), so\n * the UI can render what the agent just did (a citation, a record, a\n * confirmation) in lockstep with speech. Delivered only to this session's\n * room — the same security context as the invocation. Failed calls carry\n * no result.\n */\n result?: unknown;\n /** Set instead of `result` when the payload exceeded the forwarding size\n * cap (~32KB serialized) — fetch the data yourself in that case. */\n resultTruncated?: boolean;\n}\n\nexport interface VoiceSessionCallbacks {\n stateChange: (state: VoiceSessionState) => void;\n transcript: (event: VoiceTranscriptEvent) => void;\n toolCall: (event: VoiceToolCallEvent) => void;\n error: (error: Error) => void;\n}\n\nexport interface StartSessionOptions {\n /** Abort the connection attempt (the promise rejects with `AbortError`). */\n signal?: AbortSignal;\n}\n\n/** A live voice conversation. */\nexport interface VoiceSession {\n /** The platform session id (matches the call record in session history). */\n readonly sessionId: string;\n /** The media room name. */\n readonly room: string;\n /** Current session state. */\n readonly state: VoiceSessionState;\n /** Whether the microphone is currently muted. */\n readonly isMuted: boolean;\n\n /** Subscribe to a session event. Returns an unsubscribe function. */\n on<K extends keyof VoiceSessionCallbacks>(\n event: K,\n callback: VoiceSessionCallbacks[K],\n ): () => void;\n\n /** Mute the microphone (the session stays live). */\n mute(): Promise<void>;\n /** Unmute the microphone. */\n unmute(): Promise<void>;\n /**\n * Inject text into the live conversation — the agent treats it like user\n * speech (interrupts what it's saying and responds). Use for exact strings\n * that are painful aloud: addresses, codes, emails.\n */\n sendText(text: string): Promise<void>;\n /**\n * Register a handler for a client tool — a tool declared in voice.md with\n * `target: \"client\"`, whose effect lives in this browser (open a sheet,\n * navigate, highlight). When the agent invokes it, the handler runs and its\n * return value goes back to the agent as the tool result — a real\n * request/response, so the agent knows the action happened. Throwing (or an\n * unregistered tool) returns an error the agent speaks around; the agent\n * waits up to ~30s. One handler per tool name (later registrations\n * replace); returns an unregister function.\n */\n registerClientTool(\n name: string,\n handler: (args: unknown) => unknown | Promise<unknown>,\n ): () => void;\n\n /**\n * Upgrade this live session from anonymous to the now-signed-in user —\n * call it right after your in-app verification succeeds (progressive\n * auth). Subsequent tool calls run as the verified user with their roles,\n * and the agent's Current User context refreshes, WITHOUT tearing down\n * the conversation. Requires the session to have been started anonymously\n * (an already-identified session rejects with `already_identified`);\n * the SDK proves ownership with the token the session was minted under.\n * On failure, ending and restarting the session is the fallback.\n */\n refreshIdentity(): Promise<void>;\n /** End the session and release the microphone. */\n end(): Promise<void>;\n}\n\n/**\n * The active session's QA/debug handle, published on `window.__MS_VOICE__`\n * while a session is live (cleared when it ends; last session wins).\n *\n * Exists so automation that can't produce audio — the platform's browser QA\n * agent driving the app in headless Chrome — can converse with the voice\n * agent by injected text and assert on what it said and which tools ran,\n * while the app's real UI (client-tool cards, state orb, captions) renders\n * normally. It grants nothing the page's own scripts don't already have:\n * it's the same session object, in the same execution context, made\n * reachable instead of trapped in the app's closure.\n */\nexport interface VoiceQaHandle {\n sessionId: string;\n /** Live session state (reads through to the session). */\n readonly state: VoiceSessionState;\n /** Inject a user turn — same as `session.sendText`. */\n sendText(text: string): Promise<void>;\n /**\n * Caption history so far, one entry per segment (entries update in place as\n * a segment grows; `final` marks settled ones). Read before `end()` — the\n * handle is removed when the session ends.\n */\n transcript: VoiceTranscriptEvent[];\n /** Tool activity so far, in arrival order (`done` entries carry `result`). */\n toolCalls: VoiceToolCallEvent[];\n /** End the session — same as `session.end`. */\n end(): Promise<void>;\n}\n\ndeclare global {\n interface Window {\n /** Active voice session's QA/debug handle — see {@link VoiceQaHandle}. */\n __MS_VOICE__?: VoiceQaHandle;\n }\n}\n\n// Buffer bounds for the QA handle. Segments update in place, so the\n// transcript cap is a count of utterances, not events; both caps exist only\n// so a long-running session can't grow the page's memory unboundedly.\nconst QA_TRANSCRIPT_MAX_SEGMENTS = 200;\nconst QA_TOOL_CALLS_MAX = 100;\n\n// ---------------------------------------------------------------------------\n// Call-record types (session history)\n// ---------------------------------------------------------------------------\n\n/** A past voice session (call record), transcript excluded. */\nexport interface VoiceSessionSummary {\n id: string;\n status: 'active' | 'ended' | 'failed';\n startedAt: string;\n endedAt: string | null;\n durationSecs: number | null;\n endedReason: string | null;\n}\n\n/** One persisted transcript entry on a call record. */\nexport interface VoiceTranscriptEntry {\n role: 'user' | 'assistant';\n text: string;\n interrupted?: boolean;\n /** Milliseconds since epoch. */\n at: number;\n}\n\n/** A past voice session with its transcript. */\nexport interface VoiceSessionDetail extends VoiceSessionSummary {\n transcript: VoiceTranscriptEntry[];\n}\n\nexport interface VoiceSessionListPage {\n sessions: VoiceSessionSummary[];\n nextCursor: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Client interface\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClient {\n /**\n * Start a voice session: mints it from the platform, connects to the media\n * room, and publishes the microphone.\n *\n * @throws {MindStudioInterfaceError} `microphone_denied` when the user\n * refuses microphone access, `voice_concurrency_limit` /\n * `voice_visitor_limit` when the app's session limits are reached,\n * `insufficient_credits/*` (402) when the app's workspace can't pay for a\n * call, `auth_required` (401) / `role_required` (403) when the interface's\n * auth block denies the caller (route to the app's login flow),\n * `no_voice_config` when the app has no voice interface.\n */\n startSession(options?: StartSessionOptions): Promise<VoiceSession>;\n\n /** List the current user's (or visitor's) past sessions, newest first. */\n listSessions(cursor?: string): Promise<VoiceSessionListPage>;\n\n /** Fetch one past session, transcript included. */\n getSession(sessionId: string): Promise<VoiceSessionDetail>;\n\n /**\n * Delete one of the current user's (or visitor's) finished sessions,\n * transcript included.\n *\n * @throws {MindStudioInterfaceError} `session_not_found` when it isn't\n * theirs or doesn't exist, `session_not_settled` while it is still in\n * progress or being billed (retry shortly).\n */\n deleteSession(sessionId: string): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\nasync function request<T>(\n path: string,\n method: string,\n body?: unknown,\n): Promise<T> {\n const config = getConfig();\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n };\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const res = await fetch(withBase(`${VOICE_BASE}${path}`), {\n method,\n headers,\n ...(body !== undefined && { body: JSON.stringify(body) }),\n });\n\n if (!res.ok) {\n throw await errorFromResponse(\n res,\n `Voice request failed: ${res.status} ${res.statusText}`,\n 'voice_error',\n );\n }\n\n return (await res.json()) as T;\n}\n\ntype Listeners = {\n [K in keyof VoiceSessionCallbacks]: Set<VoiceSessionCallbacks[K]>;\n};\n\nfunction createListeners(): Listeners {\n return {\n stateChange: new Set(),\n transcript: new Set(),\n toolCall: new Set(),\n error: new Set(),\n };\n}\n\n/** Map the agent's published state onto the session's client-facing states. */\nfunction mapAgentState(value: string): VoiceSessionState {\n switch (value) {\n case 'listening':\n case 'idle':\n return 'listening';\n case 'thinking':\n return 'thinking';\n case 'speaking':\n return 'speaking';\n default:\n return 'connecting';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a voice client.\n *\n * @returns A {@link VoiceClient} for starting live sessions and reading past\n * call records.\n *\n * @example\n * ```ts\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n *\n * const voice = createVoiceClient();\n * const session = await voice.startSession();\n *\n * session.on('stateChange', (state) => setOrbState(state));\n * session.on('transcript', ({ role, segmentId, text, final }) =>\n * upsertCaption(segmentId, role, text, final),\n * );\n * session.on('toolCall', ({ method, status }) => showToolStatus(method, status));\n *\n * // ... later\n * await session.end();\n * ```\n */\nexport function createVoiceClient(): VoiceClient {\n return {\n async startSession(options?: StartSessionOptions): Promise<VoiceSession> {\n options?.signal?.throwIfAborted();\n\n // Snapshot the bearer this session is minted under: after an in-app\n // login the app's config token is REPLACED (a fresh interface session),\n // and refreshIdentity() must prove ownership with the original one.\n const mintTimeToken = getConfig().token;\n\n const minted = await request<{\n sessionId: string;\n url: string;\n token: string;\n room: string;\n }>('/sessions', 'POST');\n\n // The media transport loads on first use — apps that never start a\n // voice session never fetch it.\n const lk = await import('livekit-client');\n\n options?.signal?.throwIfAborted();\n\n const room: Room = new lk.Room();\n const listeners = createListeners();\n const clientToolHandlers = new Map<\n string,\n (args: unknown) => unknown | Promise<unknown>\n >();\n let state: VoiceSessionState = 'connecting';\n let muted = false;\n let audioEl: HTMLMediaElement | null = null;\n let qaHandle: VoiceQaHandle | null = null;\n\n const setState = (next: VoiceSessionState) => {\n if (next === state || state === 'ended') {\n return;\n }\n state = next;\n for (const cb of listeners.stateChange) {\n cb(next);\n }\n };\n\n const emitError = (error: Error) => {\n for (const cb of listeners.error) {\n cb(error);\n }\n };\n\n const cleanup = () => {\n if (audioEl) {\n audioEl.remove();\n audioEl = null;\n }\n // Only remove the QA handle if it's still ours — a newer session may\n // have replaced it already (last session wins).\n if (qaHandle && window.__MS_VOICE__ === qaHandle) {\n delete window.__MS_VOICE__;\n }\n qaHandle = null;\n setState('ended');\n };\n\n const isAgent = (identity: string) => {\n const participant = room.remoteParticipants.get(identity);\n return participant?.kind === lk.ParticipantKind.AGENT;\n };\n\n // Agent audio: attach to an SDK-managed hidden element. The agent\n // waits for this subscription before speaking.\n room.on(\n lk.RoomEvent.TrackSubscribed,\n (track: RemoteTrack, _pub, participant) => {\n if (\n track.kind === lk.Track.Kind.Audio &&\n participant.kind === lk.ParticipantKind.AGENT\n ) {\n audioEl = track.attach();\n audioEl.style.display = 'none';\n document.body.appendChild(audioEl);\n }\n },\n );\n\n // Session state from the agent participant's published attribute.\n room.on(\n lk.RoomEvent.ParticipantAttributesChanged,\n (_changed, participant) => {\n if (participant.kind === lk.ParticipantKind.AGENT) {\n const value = participant.attributes[ATTR_AGENT_STATE];\n if (value) {\n setState(mapAgentState(value));\n }\n }\n },\n );\n\n room.on(lk.RoomEvent.Disconnected, () => {\n cleanup();\n });\n\n // Live captions. User transcripts arrive attributed as the local\n // participant (full-replacement streams per segment, final flagged in\n // the attributes); agent transcripts are one delta stream per segment,\n // final when the stream ends.\n room.registerTextStreamHandler(\n TOPIC_TRANSCRIPTION,\n (reader: TextStreamReader, participantInfo) => {\n const role =\n participantInfo.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[ATTR_SEGMENT_ID] ?? reader.info.id;\n\n void (async () => {\n try {\n if (role === 'user') {\n const text = await reader.readAll();\n const final = attributes[ATTR_TRANSCRIPTION_FINAL] === 'true';\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final });\n }\n } else {\n let text = '';\n for await (const chunk of reader) {\n text += chunk;\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: false });\n }\n }\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: true });\n }\n }\n } catch {\n // A transcript stream dropping mid-read is cosmetic — skip it.\n }\n })();\n },\n );\n\n // Tool activity from the voice worker.\n // Client-tool invocations from the agent: run the app's handler and\n // stream the outcome back. Every path answers (unhandled/thrown/oversize\n // included) so the agent never waits out the timeout unnecessarily.\n room.registerTextStreamHandler(TOPIC_CLIENT_TOOL, (reader) => {\n void (async () => {\n let invocation: { id?: string; name?: string; args?: unknown };\n try {\n invocation = JSON.parse(await reader.readAll());\n } catch {\n return; // Malformed invocation — nothing to correlate a reply to.\n }\n if (!invocation.id || !invocation.name) {\n return;\n }\n const respond = async (response: Record<string, unknown>) => {\n try {\n await room.localParticipant.sendText(\n JSON.stringify({ id: invocation.id, ...response }),\n { topic: TOPIC_CLIENT_TOOL_RESULT },\n );\n } catch (err) {\n emitError(err as Error);\n }\n };\n\n const handler = clientToolHandlers.get(invocation.name);\n if (!handler) {\n console.warn(\n `[voice] client tool \"${invocation.name}\" invoked but no handler is registered — call session.registerClientTool()`,\n );\n await respond({ error: 'unhandled_client_tool' });\n return;\n }\n try {\n const result = await handler(invocation.args);\n const serialized =\n result === undefined ? undefined : JSON.stringify(result);\n if (\n serialized !== undefined &&\n serialized.length > CLIENT_TOOL_RESULT_MAX_CHARS\n ) {\n await respond({ error: 'result_too_large' });\n return;\n }\n await respond(result === undefined ? {} : { result });\n } catch (err) {\n await respond({\n error: err instanceof Error ? err.message : String(err),\n });\n }\n })();\n });\n\n room.registerTextStreamHandler(\n TOPIC_TOOL,\n (reader: TextStreamReader, participantInfo) => {\n if (!isAgent(participantInfo.identity)) {\n return;\n }\n void (async () => {\n try {\n const parsed = JSON.parse(await reader.readAll()) as {\n method?: string;\n status?: string;\n at?: number;\n result?: unknown;\n resultTruncated?: boolean;\n };\n if (!parsed.method) {\n return;\n }\n const status =\n parsed.status === 'done' || parsed.status === 'failed'\n ? parsed.status\n : 'running';\n for (const cb of listeners.toolCall) {\n cb({\n method: parsed.method,\n status,\n at: parsed.at ?? Date.now(),\n ...(parsed.result !== undefined\n ? { result: parsed.result }\n : {}),\n ...(parsed.resultTruncated === true\n ? { resultTruncated: true }\n : {}),\n });\n }\n } catch {\n // Malformed tool event — skip it.\n }\n })();\n },\n );\n\n const abortHandler = () => {\n void room.disconnect();\n };\n options?.signal?.addEventListener('abort', abortHandler, { once: true });\n\n try {\n await room.connect(minted.url, minted.token);\n options?.signal?.throwIfAborted();\n\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n } catch (err) {\n const name = (err as Error)?.name;\n if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {\n throw new MindStudioInterfaceError(\n 'Microphone access was denied. Allow microphone access for this site and try again.',\n 'microphone_denied',\n );\n }\n if (name === 'NotFoundError') {\n throw new MindStudioInterfaceError(\n 'No microphone was found on this device.',\n 'microphone_not_found',\n );\n }\n throw err;\n }\n } catch (err) {\n await room.disconnect().catch(() => undefined);\n cleanup();\n if (options?.signal?.aborted) {\n throw new DOMException('Voice session aborted', 'AbortError');\n }\n throw err;\n } finally {\n options?.signal?.removeEventListener('abort', abortHandler);\n }\n\n const session: VoiceSession = {\n sessionId: minted.sessionId,\n room: minted.room,\n get state() {\n return state;\n },\n get isMuted() {\n return muted;\n },\n on(event, callback) {\n const set = listeners[event] as Set<typeof callback>;\n set.add(callback);\n return () => {\n set.delete(callback);\n };\n },\n async mute() {\n await room.localParticipant.setMicrophoneEnabled(false);\n muted = true;\n },\n async unmute() {\n await room.localParticipant.setMicrophoneEnabled(true);\n muted = false;\n },\n async sendText(text: string) {\n try {\n await room.localParticipant.sendText(text, { topic: TOPIC_CHAT });\n } catch (err) {\n emitError(err as Error);\n throw err;\n }\n },\n registerClientTool(\n name: string,\n handler: (args: unknown) => unknown | Promise<unknown>,\n ) {\n clientToolHandlers.set(name, handler);\n return () => {\n if (clientToolHandlers.get(name) === handler) {\n clientToolHandlers.delete(name);\n }\n };\n },\n async refreshIdentity() {\n await request(\n `/sessions/${encodeURIComponent(minted.sessionId)}/refresh-identity`,\n 'POST',\n { previousToken: mintTimeToken },\n );\n },\n async end() {\n await room.disconnect();\n cleanup();\n },\n };\n\n // Publish the QA/debug handle (see VoiceQaHandle). Its buffers\n // subscribe through the same listener sets as app callbacks. Transcript\n // entries upsert by segment: events carry the full text so far, and the\n // agent side emits cumulative partials per chunk, so appending would\n // flood the buffer with near-duplicates of one utterance.\n const qaTranscript: VoiceTranscriptEvent[] = [];\n listeners.transcript.add((event) => {\n const idx = qaTranscript.findIndex(\n (e) => e.segmentId === event.segmentId && e.role === event.role,\n );\n if (idx >= 0) {\n qaTranscript[idx] = event;\n } else {\n qaTranscript.push(event);\n if (qaTranscript.length > QA_TRANSCRIPT_MAX_SEGMENTS) {\n qaTranscript.shift();\n }\n }\n });\n const qaToolCalls: VoiceToolCallEvent[] = [];\n listeners.toolCall.add((event) => {\n qaToolCalls.push(event);\n if (qaToolCalls.length > QA_TOOL_CALLS_MAX) {\n qaToolCalls.shift();\n }\n });\n qaHandle = {\n sessionId: minted.sessionId,\n get state() {\n return state;\n },\n transcript: qaTranscript,\n toolCalls: qaToolCalls,\n sendText: (text) => session.sendText(text),\n end: () => session.end(),\n };\n window.__MS_VOICE__ = qaHandle;\n\n return session;\n },\n\n async listSessions(cursor?: string): Promise<VoiceSessionListPage> {\n const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';\n return request<VoiceSessionListPage>(`/sessions${query}`, 'GET');\n },\n\n async getSession(sessionId: string): Promise<VoiceSessionDetail> {\n return request<VoiceSessionDetail>(\n `/sessions/${encodeURIComponent(sessionId)}`,\n 'GET',\n );\n },\n\n async deleteSession(sessionId: string): Promise<void> {\n await request(\n `/sessions/${encodeURIComponent(sessionId)}/delete`,\n 'POST',\n );\n },\n };\n}\n"],"mappings":"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","names":["VOICE_BASE","TOPIC_TRANSCRIPTION","TOPIC_CHAT","TOPIC_TOOL","TOPIC_CLIENT_TOOL","TOPIC_CLIENT_TOOL_RESULT","CLIENT_TOOL_RESULT_MAX_CHARS","ATTR_TRANSCRIPTION_FINAL","ATTR_SEGMENT_ID","ATTR_AGENT_STATE","QA_TRANSCRIPT_MAX_SEGMENTS","QA_TOOL_CALLS_MAX","request","path","method","body","headers","getConfig","res","withBase","errorFromResponse","createListeners","mapAgentState","value","createVoiceClient","options","mintTimeToken","minted","lk","room","listeners","clientToolHandlers","state","muted","audioEl","qaHandle","setState","next","cb","emitError","error","cleanup","isAgent","identity","track","_pub","participant","_changed","reader","participantInfo","role","attributes","segmentId","text","final","chunk","invocation","respond","response","err","handler","result","serialized","parsed","status","abortHandler","name","MindStudioInterfaceError","session","event","callback","set","qaTranscript","idx","e","qaToolCalls","cursor","query","sessionId"]}
1
+ {"version":3,"sources":["../src/voice.ts"],"sourcesContent":["/**\n * Voice client for apps with a voice interface.\n *\n * Imported from the `./voice` subpath so apps that never use voice ship none\n * of it — and `livekit-client` (the one dependency, for the realtime media\n * transport) is loaded dynamically on the first `startSession()` call:\n *\n * ```\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n * ```\n *\n * Flow: `startSession()` mints a session from the platform\n * (`POST /_/voice/sessions`), connects to the media room, publishes the\n * microphone, and returns a {@link VoiceSession} that surfaces the\n * conversation as events — agent state, live captions for both sides, tool\n * activity — plus mute / text-injection / end controls. Agent audio playback\n * is handled internally (a hidden autoplaying element); the app never touches\n * audio elements.\n */\n\nimport { getConfig, withBase } from './config.js';\nimport { MindStudioInterfaceError, errorFromResponse } from './errors.js';\nimport type { Room, RemoteTrack, TextStreamReader } from 'livekit-client';\n\n// ---------------------------------------------------------------------------\n// Wire constants (the voice worker's room contract)\n// ---------------------------------------------------------------------------\n\nconst VOICE_BASE = '/_/voice';\n\n/** Live captions, published by the agent framework. */\nconst TOPIC_TRANSCRIPTION = 'lk.transcription';\n/** Text input into the live conversation (interrupts + replies). */\nconst TOPIC_CHAT = 'lk.chat';\n/** Tool activity, published by the MindStudio voice worker. */\nconst TOPIC_TOOL = 'ms.voice.tool';\nconst TOPIC_CLIENT_TOOL = 'ms.voice.client-tool';\nconst TOPIC_CLIENT_TOOL_RESULT = 'ms.voice.client-tool-result';\n// Client-tool handler returns must fit the same forwarding cap as tool\n// results; oversize returns become an error the agent can speak around.\nconst CLIENT_TOOL_RESULT_MAX_CHARS = 32_000;\n\nconst ATTR_TRANSCRIPTION_FINAL = 'lk.transcription_final';\nconst ATTR_SEGMENT_ID = 'lk.segment_id';\nconst ATTR_AGENT_STATE = 'lk.agent.state';\n\n// ---------------------------------------------------------------------------\n// Session types\n// ---------------------------------------------------------------------------\n\n/** Lifecycle of a live voice session, driven by the agent's published state. */\nexport type VoiceSessionState =\n 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended';\n\n/** One live-caption update. */\nexport interface VoiceTranscriptEvent {\n /** Whose speech this is. */\n role: 'user' | 'agent';\n /** Stable id for one utterance — updates with the same id replace, not append. */\n segmentId: string;\n /** The utterance text so far (always the full text, never a delta). */\n text: string;\n /** True once this segment won't change again. */\n final: boolean;\n}\n\n/** Tool activity from the agent, for inline status UI. */\nexport interface VoiceToolCallEvent {\n /** The method id being executed. */\n method: string;\n status: 'running' | 'done' | 'failed';\n /** Milliseconds since epoch. */\n at: number;\n /**\n * The tool's return value — present on every 'done' (unless truncated), so\n * the UI can render what the agent just did (a citation, a record, a\n * confirmation) in lockstep with speech. Delivered only to this session's\n * room — the same security context as the invocation. Failed calls carry\n * no result.\n */\n result?: unknown;\n /** Set instead of `result` when the payload exceeded the forwarding size\n * cap (~32KB serialized) — fetch the data yourself in that case. */\n resultTruncated?: boolean;\n}\n\nexport interface VoiceSessionCallbacks {\n stateChange: (state: VoiceSessionState) => void;\n transcript: (event: VoiceTranscriptEvent) => void;\n toolCall: (event: VoiceToolCallEvent) => void;\n error: (error: Error) => void;\n}\n\nexport interface StartSessionOptions {\n /** Abort the connection attempt (the promise rejects with `AbortError`). */\n signal?: AbortSignal;\n}\n\n/** A live voice conversation. */\nexport interface VoiceSession {\n /** The platform session id (matches the call record in session history). */\n readonly sessionId: string;\n /** The media room name. */\n readonly room: string;\n /** Current session state. */\n readonly state: VoiceSessionState;\n /** Whether the microphone is currently muted. */\n readonly isMuted: boolean;\n\n /** Subscribe to a session event. Returns an unsubscribe function. */\n on<K extends keyof VoiceSessionCallbacks>(\n event: K,\n callback: VoiceSessionCallbacks[K],\n ): () => void;\n\n /** Mute the microphone (the session stays live). */\n mute(): Promise<void>;\n /** Unmute the microphone. */\n unmute(): Promise<void>;\n /**\n * Inject text into the live conversation — the agent treats it like user\n * speech (interrupts what it's saying and responds). Use for exact strings\n * that are painful aloud: addresses, codes, emails.\n */\n sendText(text: string): Promise<void>;\n /**\n * Register a handler for a client tool — a tool declared in voice.md with\n * `target: \"client\"`, whose effect lives in this browser (open a sheet,\n * navigate, highlight). When the agent invokes it, the handler runs and its\n * return value goes back to the agent as the tool result — a real\n * request/response, so the agent knows the action happened. Throwing (or an\n * unregistered tool) returns an error the agent speaks around; the agent\n * waits up to ~30s. One handler per tool name (later registrations\n * replace); returns an unregister function.\n */\n registerClientTool(\n name: string,\n handler: (args: unknown) => unknown | Promise<unknown>,\n ): () => void;\n\n /**\n * Upgrade this live session from anonymous to the now-signed-in user —\n * call it right after your in-app verification succeeds (progressive\n * auth). Subsequent tool calls run as the verified user with their roles,\n * and the agent's Current User context refreshes, WITHOUT tearing down\n * the conversation. Requires the session to have been started anonymously\n * (an already-identified session rejects with `already_identified`);\n * the SDK proves ownership with the token the session was minted under.\n * On failure, ending and restarting the session is the fallback.\n */\n refreshIdentity(): Promise<void>;\n /** End the session and release the microphone. */\n end(): Promise<void>;\n}\n\n/**\n * The active session's QA/debug handle, published on `window.__MS_VOICE__`\n * while a session is live (cleared when it ends; last session wins).\n *\n * Exists so automation that can't produce audio — the platform's browser QA\n * agent driving the app in headless Chrome — can converse with the voice\n * agent by injected text and assert on what it said and which tools ran,\n * while the app's real UI (client-tool cards, state orb, captions) renders\n * normally. It grants nothing the page's own scripts don't already have:\n * it's the same session object, in the same execution context, made\n * reachable instead of trapped in the app's closure.\n */\nexport interface VoiceQaHandle {\n sessionId: string;\n /** Live session state (reads through to the session). */\n readonly state: VoiceSessionState;\n /** Inject a user turn — same as `session.sendText`. */\n sendText(text: string): Promise<void>;\n /**\n * Caption history so far, one entry per segment (entries update in place as\n * a segment grows; `final` marks settled ones). Read before `end()` — the\n * handle is removed when the session ends.\n */\n transcript: VoiceTranscriptEvent[];\n /** Tool activity so far, in arrival order (`done` entries carry `result`). */\n toolCalls: VoiceToolCallEvent[];\n /** End the session — same as `session.end`. */\n end(): Promise<void>;\n}\n\ndeclare global {\n interface Window {\n /** Active voice session's QA/debug handle — see {@link VoiceQaHandle}. */\n __MS_VOICE__?: VoiceQaHandle;\n }\n}\n\n// Buffer bounds for the QA handle. Segments update in place, so the\n// transcript cap is a count of utterances, not events; both caps exist only\n// so a long-running session can't grow the page's memory unboundedly.\nconst QA_TRANSCRIPT_MAX_SEGMENTS = 200;\nconst QA_TOOL_CALLS_MAX = 100;\n\n// ---------------------------------------------------------------------------\n// Call-record types (session history)\n// ---------------------------------------------------------------------------\n\n/** A past voice session (call record), transcript excluded. */\nexport interface VoiceSessionSummary {\n id: string;\n status: 'active' | 'ended' | 'failed';\n startedAt: string;\n endedAt: string | null;\n durationSecs: number | null;\n endedReason: string | null;\n}\n\n/** One persisted transcript entry on a call record. */\nexport interface VoiceTranscriptEntry {\n role: 'user' | 'assistant';\n text: string;\n interrupted?: boolean;\n /** Milliseconds since epoch. */\n at: number;\n}\n\n/** A past voice session with its transcript. */\nexport interface VoiceSessionDetail extends VoiceSessionSummary {\n transcript: VoiceTranscriptEntry[];\n}\n\nexport interface VoiceSessionListPage {\n sessions: VoiceSessionSummary[];\n nextCursor: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Client interface\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClient {\n /**\n * Start a voice session: mints it from the platform, connects to the media\n * room, and publishes the microphone.\n *\n * @throws {MindStudioInterfaceError} `microphone_denied` when the user\n * refuses microphone access, `voice_concurrency_limit` /\n * `voice_visitor_limit` when the app's session limits are reached,\n * `insufficient_credits/*` (402) when the app's workspace can't pay for a\n * call, `auth_required` (401) / `role_required` (403) when the interface's\n * auth block denies the caller (route to the app's login flow),\n * `no_voice_config` when the app has no voice interface.\n */\n startSession(options?: StartSessionOptions): Promise<VoiceSession>;\n\n /** List the current user's (or visitor's) past sessions, newest first. */\n listSessions(cursor?: string): Promise<VoiceSessionListPage>;\n\n /** Fetch one past session, transcript included. */\n getSession(sessionId: string): Promise<VoiceSessionDetail>;\n\n /**\n * Delete one of the current user's (or visitor's) finished sessions,\n * transcript included.\n *\n * @throws {MindStudioInterfaceError} `session_not_found` when it isn't\n * theirs or doesn't exist, `session_not_settled` while it is still in\n * progress or being billed (retry shortly).\n */\n deleteSession(sessionId: string): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\nasync function request<T>(\n path: string,\n method: string,\n body?: unknown,\n): Promise<T> {\n const config = getConfig();\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n };\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const res = await fetch(withBase(`${VOICE_BASE}${path}`), {\n method,\n headers,\n ...(body !== undefined && { body: JSON.stringify(body) }),\n });\n\n if (!res.ok) {\n throw await errorFromResponse(\n res,\n `Voice request failed: ${res.status} ${res.statusText}`,\n 'voice_error',\n );\n }\n\n return (await res.json()) as T;\n}\n\ntype Listeners = {\n [K in keyof VoiceSessionCallbacks]: Set<VoiceSessionCallbacks[K]>;\n};\n\nfunction createListeners(): Listeners {\n return {\n stateChange: new Set(),\n transcript: new Set(),\n toolCall: new Set(),\n error: new Set(),\n };\n}\n\n/** Map the agent's published state onto the session's client-facing states. */\nfunction mapAgentState(value: string): VoiceSessionState {\n switch (value) {\n case 'listening':\n case 'idle':\n return 'listening';\n case 'thinking':\n return 'thinking';\n case 'speaking':\n return 'speaking';\n default:\n return 'connecting';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a voice client.\n *\n * @returns A {@link VoiceClient} for starting live sessions and reading past\n * call records.\n *\n * @example\n * ```ts\n * import { createVoiceClient } from '@mindstudio-ai/interface/voice';\n *\n * const voice = createVoiceClient();\n * const session = await voice.startSession();\n *\n * session.on('stateChange', (state) => setOrbState(state));\n * session.on('transcript', ({ role, segmentId, text, final }) =>\n * upsertCaption(segmentId, role, text, final),\n * );\n * session.on('toolCall', ({ method, status }) => showToolStatus(method, status));\n *\n * // ... later\n * await session.end();\n * ```\n */\nexport function createVoiceClient(): VoiceClient {\n return {\n async startSession(options?: StartSessionOptions): Promise<VoiceSession> {\n options?.signal?.throwIfAborted();\n\n // Snapshot the bearer this session is minted under: after an in-app\n // login the app's config token is REPLACED (a fresh interface session),\n // and refreshIdentity() must prove ownership with the original one.\n const mintTimeToken = getConfig().token;\n\n const minted = await request<{\n sessionId: string;\n url: string;\n token: string;\n room: string;\n }>('/sessions', 'POST');\n\n // The media transport loads on first use — apps that never start a\n // voice session never fetch it.\n const lk = await import('livekit-client');\n\n options?.signal?.throwIfAborted();\n\n const room: Room = new lk.Room();\n const listeners = createListeners();\n const clientToolHandlers = new Map<\n string,\n (args: unknown) => unknown | Promise<unknown>\n >();\n let state: VoiceSessionState = 'connecting';\n let muted = false;\n let audioEl: HTMLMediaElement | null = null;\n let qaHandle: VoiceQaHandle | null = null;\n\n const setState = (next: VoiceSessionState) => {\n if (next === state || state === 'ended') {\n return;\n }\n state = next;\n for (const cb of listeners.stateChange) {\n cb(next);\n }\n };\n\n const emitError = (error: Error) => {\n for (const cb of listeners.error) {\n cb(error);\n }\n };\n\n const cleanup = () => {\n if (audioEl) {\n audioEl.remove();\n audioEl = null;\n }\n // Only remove the QA handle if it's still ours — a newer session may\n // have replaced it already (last session wins).\n if (qaHandle && window.__MS_VOICE__ === qaHandle) {\n delete window.__MS_VOICE__;\n }\n qaHandle = null;\n setState('ended');\n };\n\n const isAgent = (identity: string) => {\n const participant = room.remoteParticipants.get(identity);\n return participant?.kind === lk.ParticipantKind.AGENT;\n };\n\n // Agent audio: attach to an SDK-managed hidden element. The agent\n // waits for this subscription before speaking.\n room.on(\n lk.RoomEvent.TrackSubscribed,\n (track: RemoteTrack, _pub, participant) => {\n if (\n track.kind === lk.Track.Kind.Audio &&\n participant.kind === lk.ParticipantKind.AGENT\n ) {\n audioEl = track.attach();\n audioEl.style.display = 'none';\n document.body.appendChild(audioEl);\n }\n },\n );\n\n // Session state from the agent participant's published attribute.\n room.on(\n lk.RoomEvent.ParticipantAttributesChanged,\n (_changed, participant) => {\n if (participant.kind === lk.ParticipantKind.AGENT) {\n const value = participant.attributes[ATTR_AGENT_STATE];\n if (value) {\n setState(mapAgentState(value));\n }\n }\n },\n );\n\n room.on(lk.RoomEvent.Disconnected, () => {\n cleanup();\n });\n\n // Live captions. User transcripts arrive attributed as the local\n // participant (full-replacement streams per segment, final flagged in\n // the attributes); agent transcripts are one delta stream per segment,\n // final when the stream ends.\n room.registerTextStreamHandler(\n TOPIC_TRANSCRIPTION,\n (reader: TextStreamReader, participantInfo) => {\n const role =\n participantInfo.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[ATTR_SEGMENT_ID] ?? reader.info.id;\n\n void (async () => {\n try {\n if (role === 'user') {\n const text = await reader.readAll();\n const final = attributes[ATTR_TRANSCRIPTION_FINAL] === 'true';\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final });\n }\n } else {\n let text = '';\n for await (const chunk of reader) {\n text += chunk;\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: false });\n }\n }\n for (const cb of listeners.transcript) {\n cb({ role, segmentId, text, final: true });\n }\n }\n } catch {\n // A transcript stream dropping mid-read is cosmetic — skip it.\n }\n })();\n },\n );\n\n // Tool activity from the voice worker.\n // Client-tool invocations from the agent: run the app's handler and\n // stream the outcome back. Every path answers (unhandled/thrown/oversize\n // included) so the agent never waits out the timeout unnecessarily.\n room.registerTextStreamHandler(TOPIC_CLIENT_TOOL, (reader) => {\n void (async () => {\n let invocation: { id?: string; name?: string; args?: unknown };\n try {\n invocation = JSON.parse(await reader.readAll());\n } catch {\n return; // Malformed invocation — nothing to correlate a reply to.\n }\n if (!invocation.id || !invocation.name) {\n return;\n }\n const respond = async (response: Record<string, unknown>) => {\n try {\n await room.localParticipant.sendText(\n JSON.stringify({ id: invocation.id, ...response }),\n { topic: TOPIC_CLIENT_TOOL_RESULT },\n );\n } catch (err) {\n emitError(err as Error);\n }\n };\n\n const handler = clientToolHandlers.get(invocation.name);\n if (!handler) {\n console.warn(\n `[voice] client tool \"${invocation.name}\" invoked but no handler is registered — call session.registerClientTool()`,\n );\n await respond({ error: 'unhandled_client_tool' });\n return;\n }\n try {\n const result = await handler(invocation.args);\n const serialized =\n result === undefined ? undefined : JSON.stringify(result);\n if (\n serialized !== undefined &&\n serialized.length > CLIENT_TOOL_RESULT_MAX_CHARS\n ) {\n await respond({ error: 'result_too_large' });\n return;\n }\n await respond(result === undefined ? {} : { result });\n } catch (err) {\n await respond({\n error: err instanceof Error ? err.message : String(err),\n });\n }\n })();\n });\n\n room.registerTextStreamHandler(\n TOPIC_TOOL,\n (reader: TextStreamReader, participantInfo) => {\n if (!isAgent(participantInfo.identity)) {\n return;\n }\n void (async () => {\n try {\n const parsed = JSON.parse(await reader.readAll()) as {\n method?: string;\n status?: string;\n at?: number;\n result?: unknown;\n resultTruncated?: boolean;\n };\n if (!parsed.method) {\n return;\n }\n const status =\n parsed.status === 'done' || parsed.status === 'failed'\n ? parsed.status\n : 'running';\n for (const cb of listeners.toolCall) {\n cb({\n method: parsed.method,\n status,\n at: parsed.at ?? Date.now(),\n ...(parsed.result !== undefined\n ? { result: parsed.result }\n : {}),\n ...(parsed.resultTruncated === true\n ? { resultTruncated: true }\n : {}),\n });\n }\n } catch {\n // Malformed tool event — skip it.\n }\n })();\n },\n );\n\n const abortHandler = () => {\n void room.disconnect();\n };\n options?.signal?.addEventListener('abort', abortHandler, { once: true });\n\n try {\n await room.connect(minted.url, minted.token);\n options?.signal?.throwIfAborted();\n\n try {\n await room.localParticipant.setMicrophoneEnabled(true);\n } catch (err) {\n const name = (err as Error)?.name;\n if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {\n throw new MindStudioInterfaceError(\n 'Microphone access was denied. Allow microphone access for this site and try again.',\n 'microphone_denied',\n );\n }\n if (name === 'NotFoundError') {\n throw new MindStudioInterfaceError(\n 'No microphone was found on this device.',\n 'microphone_not_found',\n );\n }\n throw err;\n }\n } catch (err) {\n await room.disconnect().catch(() => undefined);\n cleanup();\n if (options?.signal?.aborted) {\n throw new DOMException('Voice session aborted', 'AbortError');\n }\n throw err;\n } finally {\n options?.signal?.removeEventListener('abort', abortHandler);\n }\n\n const session: VoiceSession = {\n sessionId: minted.sessionId,\n room: minted.room,\n get state() {\n return state;\n },\n get isMuted() {\n return muted;\n },\n on(event, callback) {\n const set = listeners[event] as Set<typeof callback>;\n set.add(callback);\n return () => {\n set.delete(callback);\n };\n },\n async mute() {\n await room.localParticipant.setMicrophoneEnabled(false);\n muted = true;\n },\n async unmute() {\n await room.localParticipant.setMicrophoneEnabled(true);\n muted = false;\n },\n async sendText(text: string) {\n try {\n await room.localParticipant.sendText(text, { topic: TOPIC_CHAT });\n } catch (err) {\n emitError(err as Error);\n throw err;\n }\n },\n registerClientTool(\n name: string,\n handler: (args: unknown) => unknown | Promise<unknown>,\n ) {\n clientToolHandlers.set(name, handler);\n return () => {\n if (clientToolHandlers.get(name) === handler) {\n clientToolHandlers.delete(name);\n }\n };\n },\n async refreshIdentity() {\n await request(\n `/sessions/${encodeURIComponent(minted.sessionId)}/refresh-identity`,\n 'POST',\n { previousToken: mintTimeToken },\n );\n },\n async end() {\n await room.disconnect();\n cleanup();\n },\n };\n\n // Publish the QA/debug handle (see VoiceQaHandle). Its buffers\n // subscribe through the same listener sets as app callbacks. Transcript\n // entries upsert by segment: events carry the full text so far, and the\n // agent side emits cumulative partials per chunk, so appending would\n // flood the buffer with near-duplicates of one utterance.\n const qaTranscript: VoiceTranscriptEvent[] = [];\n listeners.transcript.add((event) => {\n const idx = qaTranscript.findIndex(\n (e) => e.segmentId === event.segmentId && e.role === event.role,\n );\n if (idx >= 0) {\n qaTranscript[idx] = event;\n } else {\n qaTranscript.push(event);\n if (qaTranscript.length > QA_TRANSCRIPT_MAX_SEGMENTS) {\n qaTranscript.shift();\n }\n }\n });\n const qaToolCalls: VoiceToolCallEvent[] = [];\n listeners.toolCall.add((event) => {\n qaToolCalls.push(event);\n if (qaToolCalls.length > QA_TOOL_CALLS_MAX) {\n qaToolCalls.shift();\n }\n });\n qaHandle = {\n sessionId: minted.sessionId,\n get state() {\n return state;\n },\n transcript: qaTranscript,\n toolCalls: qaToolCalls,\n sendText: (text) => session.sendText(text),\n end: () => session.end(),\n };\n window.__MS_VOICE__ = qaHandle;\n\n return session;\n },\n\n async listSessions(cursor?: string): Promise<VoiceSessionListPage> {\n const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';\n return request<VoiceSessionListPage>(`/sessions${query}`, 'GET');\n },\n\n async getSession(sessionId: string): Promise<VoiceSessionDetail> {\n return request<VoiceSessionDetail>(\n `/sessions/${encodeURIComponent(sessionId)}`,\n 'GET',\n );\n },\n\n async deleteSession(sessionId: string): Promise<void> {\n await request(\n `/sessions/${encodeURIComponent(sessionId)}/delete`,\n 'POST',\n );\n },\n };\n}\n"],"mappings":"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","names":["VOICE_BASE","TOPIC_TRANSCRIPTION","TOPIC_CHAT","TOPIC_TOOL","TOPIC_CLIENT_TOOL","TOPIC_CLIENT_TOOL_RESULT","CLIENT_TOOL_RESULT_MAX_CHARS","ATTR_TRANSCRIPTION_FINAL","ATTR_SEGMENT_ID","ATTR_AGENT_STATE","QA_TRANSCRIPT_MAX_SEGMENTS","QA_TOOL_CALLS_MAX","request","path","method","body","headers","getConfig","res","withBase","errorFromResponse","createListeners","mapAgentState","value","createVoiceClient","options","mintTimeToken","minted","lk","room","listeners","clientToolHandlers","state","muted","audioEl","qaHandle","setState","next","cb","emitError","error","cleanup","isAgent","identity","track","_pub","participant","_changed","reader","participantInfo","role","attributes","segmentId","text","final","chunk","invocation","respond","response","err","handler","result","serialized","parsed","status","abortHandler","name","MindStudioInterfaceError","session","event","callback","set","qaTranscript","idx","e","qaToolCalls","cursor","query","sessionId"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/interface",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Frontend SDK for MindStudio v2 app interfaces",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,9 +24,7 @@
24
24
  "build": "tsup",
25
25
  "dev": "tsup --watch",
26
26
  "typecheck": "npm run tsc -- --noEmit",
27
- "prepare": "husky && tsup",
28
27
  "prepublishOnly": "tsup",
29
- "lint:fix": "prettier --write ./src",
30
28
  "tsc": "node node_modules/@typescript/native/bin/tsc",
31
29
  "//tsc": "The native TypeScript 7 compiler by explicit path. Both it (typescript@7 under the @typescript/native alias) and the JS compiler behind the `typescript` alias — which tsup needs, since its bundled rollup-plugin-dts uses the compiler API — declare a `tsc` bin, so node_modules/.bin/tsc is whichever npm linked last."
32
30
  },
@@ -54,17 +52,10 @@
54
52
  ],
55
53
  "devDependencies": {
56
54
  "@typescript/native": "npm:typescript@^7.0.2",
57
- "husky": "^9.1.7",
58
- "lint-staged": "^16.3.2",
59
- "prettier": "^3.8.1",
60
- "prettier-plugin-curly": "^0.4.1",
61
55
  "tsup": "^8",
62
56
  "typescript": "npm:@typescript/typescript6@^6.0.2"
63
57
  },
64
- "lint-staged": {
65
- "**/*": "prettier --write --ignore-unknown"
66
- },
67
58
  "dependencies": {
68
- "livekit-client": "^2.22.0"
59
+ "livekit-client": "^2.22.3"
69
60
  }
70
61
  }
@@ -1,4 +0,0 @@
1
- var b=class extends Error{code;status;constructor(t,n,r){super(t),this.name="MindStudioInterfaceError",this.code=n,this.status=r}};async function xe(e,t,n){let r=t,s=n;try{let o=await e.json();typeof o.error=="string"&&o.error?r=o.error:typeof o.errorMessage=="string"&&o.errorMessage&&(r=o.errorMessage),typeof o.errorString=="string"&&o.errorString?s=o.errorString:typeof o.code=="string"&&o.code&&(s=o.code)}catch{}return new b(r,s,e.status)}var oe="/_/telemetry/";var w=[];function g(e){w.push(e),w.length>20&&w.shift()}function P(){return w.slice()}function N(e){return e.includes(oe)}function se(e){return typeof e=="string"?e:e instanceof URL?e.href:e.url||String(e)}var E="",A=new Set;function O(e){return A.add(e),()=>{A.delete(e)}}function _(e){e!==E&&(g({type:"navigation",from:E,to:e,timestamp:Date.now()}),E=e,A.forEach(t=>{try{t(e)}catch{}}))}function ie(){let e=window;if(e.__ms_history_patched)return;e.__ms_history_patched=!0,E=location.href;let t=history.pushState.bind(history);history.pushState=function(r,s,o){t(r,s,o);try{_(location.href)}catch{}};let n=history.replaceState.bind(history);history.replaceState=function(r,s,o){n(r,s,o);try{_(location.href)}catch{}},window.addEventListener("popstate",()=>{try{_(location.href)}catch{}}),window.addEventListener("hashchange",()=>{try{_(location.href)}catch{}})}function ae(e){if(!window.fetch||window.fetch.__ms_patched)return;let n=window.fetch.bind(window),r=function(s,o){let u=se(s);if(N(u))return n(s,o);let y=(o?.method||(typeof s!="string"&&!(s instanceof URL)?s.method:"")||"GET").toUpperCase(),f=Date.now();return n(s,o).then(i=>{let p={type:"fetch",method:y,url:u,status:i.status,ok:i.ok,duration:Date.now()-f,timestamp:f};if(!i.ok&&e)try{i.clone().text().then(d=>{p.body=d.slice(0,1024),g(p)}).catch(()=>g(p))}catch{g(p)}else g(p);return i},i=>{throw g({type:"fetch",method:y,url:u,ok:!1,duration:Date.now()-f,timestamp:f,error:i instanceof Error?i.message:String(i??"fetch failed")}),i})};r.__ms_patched=!0,window.fetch=r}function ce(){if(XMLHttpRequest.prototype.open.__ms_patched)return;let t=XMLHttpRequest.prototype.open,n=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(r,s,...o){return this.__ms_method=(r||"GET").toUpperCase(),this.__ms_url=String(s),t.apply(this,[r,s,...o])},XMLHttpRequest.prototype.send=function(r){let s=this.__ms_method||"GET",o=this.__ms_url||"";if(N(o))return n.apply(this,[r]);let u=Date.now();return this.addEventListener("loadend",function(){g({type:"xhr",method:s,url:o,status:this.status,ok:this.status>=200&&this.status<300,duration:Date.now()-u,timestamp:u})},{once:!0}),n.apply(this,[r])},XMLHttpRequest.prototype.open.__ms_patched=!0}var D=!1;function U(e){if(!D){D=!0;try{ie()}catch{}try{ae(e.captureResponseBodies)}catch{}try{ce()}catch{}}}var X="/_/telemetry/errors",ue=1e3,le=50,de=6e4,H=!1,c=[],L=null,B=0;function fe(e,t){let n=t.split(`
2
- `)[1]??"";return`${e}|${n.trim()}`}function pe(e){let t=fe(e.message,e.stack);for(let n of c)if(n._fp===t){n.count=(n.count??1)+1;return}c.length>=le||(e._fp=t,c.push(e),me())}function me(){L||c.length===0||(L=setTimeout(()=>{L=null,ge()},ue))}function F(){let e=c;return c=[],e.map(({_fp:t,...n})=>n)}async function ge(){if(c.length===0)return;if(Date.now()<B){c=[];return}let e=(()=>{try{return a()}catch{return null}})();if(!e){c=[];return}let t=F();try{let n=await fetch(h(X),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({events:t})});if(n.status===429){let r=n.headers.get("Retry-After"),s=r?parseInt(r,10):NaN,o=Number.isFinite(s)&&s>0?s*1e3:de;B=Date.now()+o}}catch{}}function q(){if(c.length===0)return;if(Date.now()<B){c=[];return}let e=null;try{e=a()}catch{c=[];return}let t=F();try{fetch(h(X),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({events:t}),keepalive:!0})}catch{}}function he(e,t){if(e instanceof Error)return{message:e.message||t,stack:e.stack??""};if(e&&typeof e=="object"){let n=e;return{message:typeof n.message=="string"&&n.message?n.message:String(e),stack:typeof n.stack=="string"?n.stack:""}}return{message:String(e??t),stack:""}}function k(e,t){try{let n=a();if(n.telemetry?.errors===!1)return;let r=he(e,t.fallbackMessage??"Unknown error");pe({releaseId:n.releaseId,url:location.href,userAgent:navigator.userAgent,timestamp:Date.now(),mechanism:t.mechanism,handled:t.handled,message:t.message||r.message,stack:r.stack,...t.componentStack?{componentStack:t.componentStack}:{},...t.source?{source:t.source}:{},...typeof t.line=="number"?{line:t.line}:{},...typeof t.column=="number"?{column:t.column}:{},breadcrumbs:P()})}catch{}}function ye(e){k(e.error,{mechanism:"browser.onerror",handled:!1,message:e.message,source:e.filename||void 0,line:typeof e.lineno=="number"?e.lineno:void 0,column:typeof e.colno=="number"?e.colno:void 0})}function be(e){k(e.reason,{mechanism:"browser.onunhandledrejection",handled:!1,fallbackMessage:"Unhandled rejection"})}function ve(){if(H||typeof window>"u")return;let e;try{e=a()}catch{return}e.telemetry?.errors!==!1&&(H=!0,U({captureResponseBodies:!!e.telemetryCaptureResponseBodies}),window.addEventListener("error",ye),window.addEventListener("unhandledrejection",be),window.addEventListener("pagehide",q),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&q()}))}function j(){try{ve()}catch{}}var Oe={captureException(e,t){k(e,{mechanism:t?.mechanism??"manual",handled:t?.handled??!0,...t?.componentStack?{componentStack:t.componentStack}:{}})},reactErrorHandler(e){return(t,n)=>{k(t,{mechanism:"react.error_handler",handled:e?.handled??!1,...n?.componentStack?{componentStack:n.componentStack}:{}}),e?.onError?.(t,n)}}};var S;function a(){if(S)return S;let e=globalThis.__MINDSTUDIO__;if(!e)throw new b("@mindstudio-ai/interface requires the MindStudio platform context. This page must be loaded inside MindStudio (window.__MINDSTUDIO__ is missing).","not_initialized");if(!e.token||!e.releaseId||!e.methods){let t=["token","releaseId","methods"].filter(n=>!e[n]).join(", ");throw new b(`MindStudio platform context is incomplete. Missing: ${t}`,"invalid_config")}return S=e,j(),setTimeout(()=>{$()},0),S}function _e(){let e=globalThis.__MINDSTUDIO__?.basePath;if(typeof e!="string")return"";let t=e.replace(/\/+$/,"");return t?t.startsWith("/")?t:`/${t}`:""}function h(e){return`${_e()}${e}`}function Fe(e){let t=a();e.token!==void 0&&(t.token=e.token),e.user!==void 0&&(t.user=e.user),e.methods!==void 0&&(t.methods=e.methods),e.visitorId!==void 0&&(t.visitorId=e.visitorId)}var we={initialBackoffMs:1e3,maxBackoffMs:1e4,stableMs:3e4,retry503DefaultMs:3e4};function z(e,t=we){let n=!1,r="idle",s=null,o=null,u=t.initialBackoffMs,y=()=>{let d=u;return u=Math.min(u*2,t.maxBackoffMs),d+Math.random()*250},f=d=>{o&&clearTimeout(o),o=setTimeout(()=>{o=null,n&&p()},d)},i=()=>{if(!(!n&&r==="stopped")){if(n=!1,r="stopped",o&&(clearTimeout(o),o=null),s){try{s.abort()}catch{}s=null}e.onStop?.()}};async function p(){if(!n)return;r="connecting";let d;try{d=await e.prepare()}catch{i();return}if(d==="stop"||!n){i();return}s=new AbortController;let ee=s.signal;try{let m=await fetch(d.url,{method:"GET",headers:{...d.headers,Accept:"text/event-stream"},signal:ee});if(!n)return;if(m.status===503){let v=parseInt(m.headers.get("Retry-After")??"",10),M=Number.isFinite(v)&&v>0?v*1e3:t.retry503DefaultMs;f(M);return}if(!m.ok||!m.body){(e.onStatus?.(m.status)??"retry")==="stop"?i():f(y());return}let R=Date.now();r="open",e.onOpen?.();let te=m.body.getReader(),ne=new TextDecoder,T="";for(;n;){let{done:v,value:M}=await te.read();if(v)break;if(!e.onLine)continue;T+=ne.decode(M,{stream:!0});let C=T.split(`
3
- `);T=C.pop()??"";for(let re of C)e.onLine(re.replace(/\r$/,""))}n&&(Date.now()-R>=t.stableMs&&(u=t.initialBackoffMs),r="connecting",f(y()))}catch(m){let R=m?.name;n&&R!=="AbortError"&&(r="connecting",f(y()))}}return{start(){n||(n=!0,p())},stop:i,get state(){return r}}}var V="/_/telemetry/events",Ee=1e3,ke=100,Se=6e4,l=[],I=null,x=0,W=!1;function Z(e){W||l.length>=ke||(l.push(e),Re())}function Re(){I||l.length===0||(I=setTimeout(()=>{I=null,Te()},Ee))}function K(){let e=l;return l=[],e}async function Te(){if(l.length===0)return;if(Date.now()<x){l=[];return}let e;try{e=a()}catch{l=[];return}let t=K();try{let n=await fetch(h(V),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({events:t})});if(n.status===429){let r=n.headers.get("Retry-After"),s=r?parseInt(r,10):NaN,o=Number.isFinite(s)&&s>0?s*1e3:Se;x=Date.now()+o}}catch{}}function Y(){if(l.length===0)return;if(Date.now()<x){l=[];return}let e;try{e=a()}catch{l=[];return}let t=K();try{fetch(h(V),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.token}`},body:JSON.stringify({events:t}),keepalive:!0})}catch{}}function Me(e){if(!e)return;let t={};for(let[n,r]of Object.entries(e))(typeof r=="string"||typeof r=="number"||typeof r=="boolean")&&(t[n]=r);return Object.keys(t).length>0?t:void 0}function G(e){try{let t=a();Z({type:"pageview",releaseId:t.releaseId,url:e,referrer:document.referrer,userAgent:navigator.userAgent,language:navigator.language,screen:{w:window.innerWidth,h:window.innerHeight},timestamp:Date.now()})}catch{}}function Ae(e,t){try{let n=a();Z({type:"event",releaseId:n.releaseId,name:e,url:location.href,timestamp:Date.now(),props:Me(t)})}catch{}}var Le="/_/telemetry/presence",Q=z({prepare:()=>{let e;try{e=a()}catch{return"stop"}return{url:h(Le),headers:{Authorization:`Bearer ${e.token}`}}},onStatus:e=>e===401||e===204?"stop":"retry"});function Be(){Q.stop()}var Je={track:Ae},J=!1;function Ie(){if(J||typeof window>"u")return;let e;try{e=a()}catch{return}if(e.telemetry?.analytics===!1)return;J=!0;let t=()=>{W=!0,Y(),Be()};window.addEventListener("pagehide",t),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&Y()}),O(n=>{G(n)}),G(location.href),Q.start()}function $(){try{Ie()}catch{}}export{b as a,xe as b,z as c,Je as d,Oe as e,a as f,_e as g,h,Fe as i};
4
- //# sourceMappingURL=chunk-DJDMBBRF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/telemetry-breadcrumbs.ts","../src/telemetry-errors.ts","../src/config.ts","../src/sse-loop.ts","../src/telemetry-analytics.ts"],"sourcesContent":["/**\n * Error class for the @mindstudio-ai/interface SDK.\n *\n * Thrown by `createClient()` method calls on HTTP errors, by `platform`\n * actions on failures, and by the config reader when bootstrap globals\n * are missing.\n */\nexport class MindStudioInterfaceError extends Error {\n /** Machine-readable error code (e.g. 'method_not_found', 'not_initialized'). */\n readonly code: string;\n\n /** HTTP status code, when the error originated from an API response. */\n readonly status?: number;\n\n constructor(message: string, code: string, status?: number) {\n super(message);\n this.name = 'MindStudioInterfaceError';\n this.code = code;\n this.status = status;\n }\n}\n\n/**\n * The platform's error envelope, in one place.\n *\n * Every call site used to inline its own `{ error, code }` parse — a shape the\n * platform never sent. It answers `{ code: <numeric status>, errorString:\n * <slug>, errorMessage: <public text + request id>, error: <public text> }`, so\n * the old parse found no `error` (message stayed as the bare transport line,\n * \"Auth request failed: 400\") and read the NUMBER 400 into `code`, which made\n * every `err.code === 'invalid_code'` branch in app code unreachable.\n *\n * Message preference is `error` before `errorMessage`: they carry the same\n * public text, but `errorMessage` appends \"| Request ID: …\", which apps render\n * straight into the UI. Code preference is `errorString` before `code`, and\n * `code` only when it is a string — otherwise a numeric status would win again.\n * Older responses that really do send a string `{ error, code }` still parse.\n *\n * `fallbackMessage` is the transport line to keep when the body says nothing\n * useful; `fallbackCode` is the caller's domain slug (e.g. 'auth_error').\n */\nexport async function errorFromResponse(\n res: Response,\n fallbackMessage: string,\n fallbackCode: string,\n): Promise<MindStudioInterfaceError> {\n let message = fallbackMessage;\n let code = fallbackCode;\n\n try {\n const body = (await res.json()) as {\n error?: string;\n errorMessage?: string;\n errorString?: string;\n code?: string | number;\n };\n if (typeof body.error === 'string' && body.error) {\n message = body.error;\n } else if (typeof body.errorMessage === 'string' && body.errorMessage) {\n message = body.errorMessage;\n }\n if (typeof body.errorString === 'string' && body.errorString) {\n code = body.errorString;\n } else if (typeof body.code === 'string' && body.code) {\n code = body.code;\n }\n } catch {\n // Response wasn't JSON — keep the fallbacks.\n }\n\n return new MindStudioInterfaceError(message, code, res.status);\n}\n","/**\n * Breadcrumbs — a ring buffer of recent navigations + network calls\n * attached to outgoing error reports for debugging context.\n *\n * Installed by `installErrorMonitoring()` in `telemetry-errors.ts`.\n * Three monkeypatches feed the buffer:\n *\n * - `history.pushState`/`replaceState`/`popstate` → `navigation`\n * - `window.fetch` → `fetch`\n * - `XMLHttpRequest.prototype.open`/`send` → `xhr`\n *\n * Each patch is idempotent — guarded by a `__ms_*_patched` flag so\n * re-imports (HMR, double bundle inclusion, etc.) are no-ops.\n *\n * The SDK's own telemetry endpoint is excluded from capture so error\n * reports don't feed themselves through the breadcrumb pipeline.\n */\n\nconst TELEMETRY_ENDPOINT_FRAGMENT = '/_/telemetry/';\nconst MAX_BREADCRUMBS = 20;\nconst MAX_BODY_BYTES = 1024;\n\n/**\n * A single breadcrumb entry. Discriminated union — server validates the\n * `type` field; unknown types are dropped at ingest.\n */\nexport type Breadcrumb =\n | {\n type: 'navigation';\n from: string;\n to: string;\n timestamp: number;\n }\n | {\n type: 'fetch';\n method: string;\n url: string;\n status?: number;\n ok: boolean;\n duration: number;\n timestamp: number;\n error?: string;\n /** Truncated response body for failed requests, when capture is enabled. */\n body?: string;\n }\n | {\n type: 'xhr';\n method: string;\n url: string;\n status?: number;\n ok: boolean;\n duration: number;\n timestamp: number;\n };\n\nconst buffer: Breadcrumb[] = [];\n\nfunction push(entry: Breadcrumb): void {\n buffer.push(entry);\n if (buffer.length > MAX_BREADCRUMBS) {\n buffer.shift();\n }\n}\n\n/**\n * Return a shallow copy of the current breadcrumb buffer.\n * Called when an error is captured to snapshot context at that moment.\n */\nexport function getBreadcrumbs(): Breadcrumb[] {\n return buffer.slice();\n}\n\nfunction shouldSkipUrl(url: string): boolean {\n return url.includes(TELEMETRY_ENDPOINT_FRAGMENT);\n}\n\nfunction resolveUrl(input: RequestInfo | URL): string {\n if (typeof input === 'string') {\n return input;\n }\n if (input instanceof URL) {\n return input.href;\n }\n return input.url || String(input);\n}\n\nlet lastHref = '';\n\nconst navListeners = new Set<(url: string) => void>();\n\n/**\n * Subscribe to navigation events. Fires on every history change\n * (`pushState`, `replaceState`, `popstate`, `hashchange`) with the new URL.\n *\n * Used by the analytics module to emit pageview events without\n * installing a second set of history patches.\n *\n * @returns Unsubscribe function.\n */\nexport function onNavigation(cb: (url: string) => void): () => void {\n navListeners.add(cb);\n return () => {\n navListeners.delete(cb);\n };\n}\n\nfunction emitNavigation(to: string): void {\n if (to === lastHref) {\n return;\n }\n push({\n type: 'navigation',\n from: lastHref,\n to,\n timestamp: Date.now(),\n });\n lastHref = to;\n navListeners.forEach((cb) => {\n try {\n cb(to);\n } catch {\n // listener errors must not break navigation\n }\n });\n}\n\nfunction installHistory(): void {\n const w = window as unknown as Record<string, unknown>;\n if (w.__ms_history_patched) {\n return;\n }\n w.__ms_history_patched = true;\n\n lastHref = location.href;\n\n const originalPushState = history.pushState.bind(history);\n history.pushState = function (\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ): void {\n originalPushState(data, unused, url);\n try {\n emitNavigation(location.href);\n } catch {\n // never crash navigation\n }\n };\n\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = function (\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ): void {\n originalReplaceState(data, unused, url);\n try {\n emitNavigation(location.href);\n } catch {\n // never crash navigation\n }\n };\n\n window.addEventListener('popstate', () => {\n try {\n emitNavigation(location.href);\n } catch {\n // swallow\n }\n });\n\n window.addEventListener('hashchange', () => {\n try {\n emitNavigation(location.href);\n } catch {\n // swallow\n }\n });\n}\n\nfunction installFetch(captureResponseBodies: boolean): void {\n if (!window.fetch) {\n return;\n }\n const fetchFn = window.fetch as typeof fetch & { __ms_patched?: boolean };\n if (fetchFn.__ms_patched) {\n return;\n }\n\n const originalFetch = window.fetch.bind(window);\n\n const patched: typeof fetch = function (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> {\n const url = resolveUrl(input);\n\n if (shouldSkipUrl(url)) {\n return originalFetch(input, init);\n }\n\n const method = (\n init?.method ||\n (typeof input !== 'string' && !(input instanceof URL)\n ? input.method\n : '') ||\n 'GET'\n ).toUpperCase();\n const startTime = Date.now();\n\n return originalFetch(input, init).then(\n (response) => {\n const entry: Breadcrumb = {\n type: 'fetch',\n method,\n url,\n status: response.status,\n ok: response.ok,\n duration: Date.now() - startTime,\n timestamp: startTime,\n };\n\n if (!response.ok && captureResponseBodies) {\n try {\n response\n .clone()\n .text()\n .then((body) => {\n entry.body = body.slice(0, MAX_BODY_BYTES);\n push(entry);\n })\n .catch(() => push(entry));\n } catch {\n push(entry);\n }\n } else {\n push(entry);\n }\n\n return response;\n },\n (err: unknown) => {\n push({\n type: 'fetch',\n method,\n url,\n ok: false,\n duration: Date.now() - startTime,\n timestamp: startTime,\n error:\n err instanceof Error ? err.message : String(err ?? 'fetch failed'),\n });\n throw err;\n },\n );\n };\n\n (patched as typeof fetch & { __ms_patched: boolean }).__ms_patched = true;\n window.fetch = patched;\n}\n\nfunction installXhr(): void {\n const openProto = XMLHttpRequest.prototype.open as ((\n ...args: unknown[]\n ) => void) & { __ms_patched?: boolean };\n if (openProto.__ms_patched) {\n return;\n }\n\n const originalOpen = XMLHttpRequest.prototype.open;\n const originalSend = XMLHttpRequest.prototype.send;\n\n XMLHttpRequest.prototype.open = function (\n this: XMLHttpRequest & {\n __ms_method?: string;\n __ms_url?: string;\n },\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ): void {\n this.__ms_method = (method || 'GET').toUpperCase();\n this.__ms_url = String(url);\n return originalOpen.apply(this, [method, url, ...rest] as Parameters<\n typeof originalOpen\n >);\n } as typeof XMLHttpRequest.prototype.open;\n\n XMLHttpRequest.prototype.send = function (\n this: XMLHttpRequest & {\n __ms_method?: string;\n __ms_url?: string;\n },\n body?: Document | XMLHttpRequestBodyInit | null,\n ): void {\n const method = this.__ms_method || 'GET';\n const url = this.__ms_url || '';\n\n if (shouldSkipUrl(url)) {\n return originalSend.apply(this, [body] as Parameters<\n typeof originalSend\n >);\n }\n\n const startTime = Date.now();\n\n this.addEventListener(\n 'loadend',\n function (this: XMLHttpRequest) {\n push({\n type: 'xhr',\n method,\n url,\n status: this.status,\n ok: this.status >= 200 && this.status < 300,\n duration: Date.now() - startTime,\n timestamp: startTime,\n });\n },\n { once: true },\n );\n\n return originalSend.apply(this, [body] as Parameters<typeof originalSend>);\n } as typeof XMLHttpRequest.prototype.send;\n\n (\n XMLHttpRequest.prototype.open as typeof XMLHttpRequest.prototype.open & {\n __ms_patched: boolean;\n }\n ).__ms_patched = true;\n}\n\nlet _breadcrumbsInstalled = false;\n\n/**\n * Install all three breadcrumb sources. Idempotent.\n *\n * @param opts.captureResponseBodies — when `true`, failed `fetch`\n * responses have their bodies read and attached (truncated to ~1KB).\n * Backend strips this field unless the per-app setting is enabled.\n */\nexport function installBreadcrumbs(opts: {\n captureResponseBodies: boolean;\n}): void {\n if (_breadcrumbsInstalled) {\n return;\n }\n _breadcrumbsInstalled = true;\n\n try {\n installHistory();\n } catch {\n // history patches are best-effort\n }\n try {\n installFetch(opts.captureResponseBodies);\n } catch {\n // fetch patches are best-effort\n }\n try {\n installXhr();\n } catch {\n // xhr patches are best-effort\n }\n}\n","/**\n * Error reporting — crashes batched and shipped to `/_/telemetry/errors`.\n *\n * ## Producers\n *\n * The window is not the design, it is one producer. Everything enters\n * through {@link capture}, and every entrant declares a MECHANISM (which\n * producer reported it) and whether it was HANDLED (did the app degrade\n * gracefully, or did the user hit a wall):\n *\n * | mechanism | source | handled |\n * | ------------------------------ | ----------------------------- | ------- |\n * | `browser.onerror` | window `error` event | false |\n * | `browser.onunhandledrejection` | window rejection event | false |\n * | `react.error_handler` | {@link reactErrorHandler} | false |\n * | `manual` | {@link captureException} | true |\n *\n * This matters because React never tells the window about an error one of\n * its boundaries caught — `onCaughtError` defaults to `console.error` and\n * nothing else — so a render crash in an app with a boundary is invisible\n * to the window listeners. React 19 apps wire the root hooks instead:\n *\n * ```ts\n * createRoot(el, {\n * onUncaughtError: telemetry.reactErrorHandler(),\n * onCaughtError: telemetry.reactErrorHandler(),\n * });\n * ```\n *\n * ## Transport\n *\n * Installed by `maybeInstallTelemetry()`, which `getConfig()` calls on\n * first successful bootstrap read — synchronously at import when the\n * platform bootstrap is present (see the eager-init block in\n * `src/index.ts`), because React's first render task would otherwise run\n * before the listeners existed. Falls back to lazy install on first SDK\n * use if eager init was skipped (SSR / tests / no bootstrap).\n *\n * 1. Producers enqueue into a pending batch (max 50 — the server cap).\n * 2. Events with the same fingerprint within a batch collapse into a\n * single entry with `count: N` (bandwidth optimization).\n * 3. The batch flushes ~1s after the first push via `POST`.\n * 4. On `pagehide` / `visibilitychange='hidden'`, the buffer drains\n * via `fetch` with `keepalive: true` so the Bearer header survives.\n * 5. On `429`, the SDK pauses sends for the `Retry-After` window.\n * 6. Any other transport failure is swallowed — telemetry must never\n * crash the host app.\n *\n * Opt out of every producer, including the manual one, via\n * `window.__MINDSTUDIO__.telemetry = { errors: false }`.\n */\n\nimport { getConfig, withBase } from './config.js';\nimport {\n installBreadcrumbs,\n getBreadcrumbs,\n type Breadcrumb,\n} from './telemetry-breadcrumbs.js';\n\nconst ENDPOINT = '/_/telemetry/errors';\nconst FLUSH_INTERVAL_MS = 1000;\nconst MAX_BATCH_SIZE = 50;\nconst DEFAULT_RETRY_AFTER_MS = 60_000;\n\n/**\n * Which producer reported a crash. Server-side vocabulary; an unrecognized\n * string is stored as `unknown` rather than dropped.\n */\nexport type ErrorMechanism =\n | 'browser.onerror'\n | 'browser.onunhandledrejection'\n | 'react.error_handler'\n | 'manual';\n\ninterface ErrorEvent {\n releaseId: string;\n url: string;\n userAgent: string;\n timestamp: number;\n mechanism: string;\n handled: boolean;\n message: string;\n stack: string;\n componentStack?: string;\n source?: string;\n line?: number;\n column?: number;\n breadcrumbs: Breadcrumb[];\n count?: number;\n /** Internal: fingerprint for within-batch dedupe. Stripped before send. */\n _fp?: string;\n}\n\nlet _installed = false;\nlet pending: ErrorEvent[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | null = null;\nlet suppressUntil = 0;\n\nfunction fingerprint(message: string, stack: string): string {\n const firstStackLine = stack.split('\\n')[1] ?? '';\n return `${message}|${firstStackLine.trim()}`;\n}\n\nfunction enqueue(event: ErrorEvent): void {\n const fp = fingerprint(event.message, event.stack);\n\n for (const existing of pending) {\n if (existing._fp === fp) {\n existing.count = (existing.count ?? 1) + 1;\n return;\n }\n }\n\n if (pending.length >= MAX_BATCH_SIZE) {\n return;\n }\n\n event._fp = fp;\n pending.push(event);\n scheduleFlush();\n}\n\nfunction scheduleFlush(): void {\n if (flushTimer || pending.length === 0) {\n return;\n }\n flushTimer = setTimeout(() => {\n flushTimer = null;\n void flush();\n }, FLUSH_INTERVAL_MS);\n}\n\nfunction takeBatch(): ErrorEvent[] {\n const batch = pending;\n pending = [];\n return batch.map(({ _fp, ...rest }) => {\n void _fp;\n return rest;\n });\n}\n\nasync function flush(): Promise<void> {\n if (pending.length === 0) {\n return;\n }\n\n if (Date.now() < suppressUntil) {\n pending = [];\n return;\n }\n\n const config = (() => {\n try {\n return getConfig();\n } catch {\n return null;\n }\n })();\n if (!config) {\n pending = [];\n return;\n }\n\n const events = takeBatch();\n\n try {\n const res = await fetch(withBase(ENDPOINT), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.token}`,\n },\n body: JSON.stringify({ events }),\n });\n\n if (res.status === 429) {\n const retryAfterHeader = res.headers.get('Retry-After');\n const retryAfterSec = retryAfterHeader\n ? parseInt(retryAfterHeader, 10)\n : NaN;\n const waitMs =\n Number.isFinite(retryAfterSec) && retryAfterSec > 0\n ? retryAfterSec * 1000\n : DEFAULT_RETRY_AFTER_MS;\n suppressUntil = Date.now() + waitMs;\n }\n } catch {\n // network / serialization failure — drop silently\n }\n}\n\nfunction drainOnUnload(): void {\n if (pending.length === 0) {\n return;\n }\n if (Date.now() < suppressUntil) {\n pending = [];\n return;\n }\n\n let config: ReturnType<typeof getConfig> | null = null;\n try {\n config = getConfig();\n } catch {\n pending = [];\n return;\n }\n\n const events = takeBatch();\n\n try {\n void fetch(withBase(ENDPOINT), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.token}`,\n },\n body: JSON.stringify({ events }),\n keepalive: true,\n });\n } catch {\n // best effort\n }\n}\n\n/**\n * Read a message + stack off anything a producer might hand us: an Error,\n * a rejection reason, a string, a DOM exception, `undefined`.\n */\nfunction describe(\n error: unknown,\n fallbackMessage: string,\n): { message: string; stack: string } {\n if (error instanceof Error) {\n return {\n message: error.message || fallbackMessage,\n stack: error.stack ?? '',\n };\n }\n if (error && typeof error === 'object') {\n const shaped = error as { message?: unknown; stack?: unknown };\n return {\n message:\n typeof shaped.message === 'string' && shaped.message\n ? shaped.message\n : String(error),\n stack: typeof shaped.stack === 'string' ? shaped.stack : '',\n };\n }\n return { message: String(error ?? fallbackMessage), stack: '' };\n}\n\n/**\n * The one door into the queue. Every producer goes through here, so the\n * errors opt-out, the bootstrap read and the never-throw guarantee are all\n * enforced in one place.\n */\nfunction capture(\n error: unknown,\n options: {\n mechanism: string;\n handled: boolean;\n fallbackMessage?: string;\n /** Overrides for producers that know better than `error` does. */\n message?: string;\n componentStack?: string | null;\n source?: string;\n line?: number;\n column?: number;\n },\n): void {\n try {\n const config = getConfig();\n if (config.telemetry?.errors === false) {\n return;\n }\n\n const described = describe(\n error,\n options.fallbackMessage ?? 'Unknown error',\n );\n\n enqueue({\n releaseId: config.releaseId,\n url: location.href,\n userAgent: navigator.userAgent,\n timestamp: Date.now(),\n mechanism: options.mechanism,\n handled: options.handled,\n message: options.message || described.message,\n stack: described.stack,\n ...(options.componentStack\n ? { componentStack: options.componentStack }\n : {}),\n ...(options.source ? { source: options.source } : {}),\n ...(typeof options.line === 'number' ? { line: options.line } : {}),\n ...(typeof options.column === 'number' ? { column: options.column } : {}),\n breadcrumbs: getBreadcrumbs(),\n });\n } catch {\n // never crash on capture\n }\n}\n\nfunction captureError(e: globalThis.ErrorEvent): void {\n capture(e.error, {\n mechanism: 'browser.onerror',\n handled: false,\n // The event's own message survives a cross-origin stack strip; the\n // error object may not be there at all.\n message: e.message,\n source: e.filename || undefined,\n line: typeof e.lineno === 'number' ? e.lineno : undefined,\n column: typeof e.colno === 'number' ? e.colno : undefined,\n });\n}\n\nfunction captureRejection(e: PromiseRejectionEvent): void {\n capture(e.reason, {\n mechanism: 'browser.onunhandledrejection',\n handled: false,\n fallbackMessage: 'Unhandled rejection',\n });\n}\n\n/**\n * Install error capture + breadcrumb collection. Idempotent.\n *\n * Reads `window.__MINDSTUDIO__.telemetry?.errors` — if explicitly\n * `false`, no listeners are attached and the function is a no-op.\n *\n * Otherwise attaches:\n * - `window.error` + `unhandledrejection`\n * - `pagehide` + `visibilitychange === 'hidden'` (for buffer drain)\n * - Breadcrumb sources via {@link installBreadcrumbs}\n */\nexport function installErrorMonitoring(): void {\n if (_installed) {\n return;\n }\n if (typeof window === 'undefined') {\n return;\n }\n\n let config: ReturnType<typeof getConfig>;\n try {\n config = getConfig();\n } catch {\n return;\n }\n\n if (config.telemetry?.errors === false) {\n return;\n }\n\n _installed = true;\n\n installBreadcrumbs({\n captureResponseBodies: !!config.telemetryCaptureResponseBodies,\n });\n\n window.addEventListener('error', captureError);\n window.addEventListener('unhandledrejection', captureRejection);\n\n // Drain on the way out, but stay live: `pagehide` fires when a mobile\n // browser backgrounds the tab, and the page can come back from the\n // back/forward cache and run for hours more. Latching \"unloaded\" here\n // meant every crash after the first backgrounding was dropped.\n window.addEventListener('pagehide', drainOnUnload);\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n drainOnUnload();\n }\n });\n}\n\n/**\n * Wrapper that swallows any error from {@link installErrorMonitoring}\n * so a misbehaving telemetry layer can never break `getConfig()`.\n *\n * @internal Called from config.ts on every `getConfig()` — idempotency\n * is enforced inside `installErrorMonitoring`.\n */\nexport function maybeInstallTelemetry(): void {\n try {\n installErrorMonitoring();\n } catch {\n // telemetry must never crash the host app\n }\n}\n\n//////////////////////////////////////////////////////////////////////////////\n// Public API\n//////////////////////////////////////////////////////////////////////////////\n\n/** What React 19's `onCaughtError` / `onUncaughtError` pass as their 2nd arg. */\nexport interface ReactErrorInfo {\n componentStack?: string | null;\n}\n\nexport interface CaptureOptions {\n /**\n * Which producer this came from. Defaults to `manual`. Values outside\n * {@link ErrorMechanism} are accepted but stored as `unknown`.\n */\n mechanism?: ErrorMechanism | string;\n /**\n * Did the app degrade gracefully? Defaults to `true` for a manual\n * capture — you caught it, so something other than a dead page happened.\n */\n handled?: boolean;\n /** React component tree, when you have one. */\n componentStack?: string | null;\n}\n\nexport interface ReactErrorHandlerOptions {\n /**\n * Whether the boundary that caught this rendered a real fallback.\n * Defaults to `false`: a boundary existing is not evidence the app\n * recovered, and the common root boundary is a dead end.\n */\n handled?: boolean;\n /** Called after the report is queued, with React's own arguments. */\n onError?: (error: unknown, errorInfo?: ReactErrorInfo) => void;\n}\n\nexport interface Telemetry {\n captureException(error: unknown, options?: CaptureOptions): void;\n reactErrorHandler(\n options?: ReactErrorHandlerOptions,\n ): (error: unknown, errorInfo?: ReactErrorInfo) => void;\n}\n\n/**\n * Crash reporting for cases the window can't see.\n *\n * @example Report something you caught yourself\n * ```ts\n * try {\n * await api.submitOrder(order);\n * } catch (err) {\n * telemetry.captureException(err);\n * setError('Could not submit that order.');\n * }\n * ```\n *\n * @example Wire React 19's root hooks (required to see render crashes)\n * ```tsx\n * createRoot(document.getElementById('root')!, {\n * onUncaughtError: telemetry.reactErrorHandler(),\n * onCaughtError: telemetry.reactErrorHandler(),\n * }).render(<App />);\n * ```\n */\nexport const telemetry: Telemetry = {\n captureException(error, options) {\n capture(error, {\n mechanism: options?.mechanism ?? 'manual',\n handled: options?.handled ?? true,\n ...(options?.componentStack\n ? { componentStack: options.componentStack }\n : {}),\n });\n },\n\n reactErrorHandler(options) {\n return (error, errorInfo) => {\n capture(error, {\n mechanism: 'react.error_handler',\n handled: options?.handled ?? false,\n ...(errorInfo?.componentStack\n ? { componentStack: errorInfo.componentStack }\n : {}),\n });\n options?.onError?.(error, errorInfo);\n };\n },\n};\n","/**\n * Bootstrap configuration reader.\n *\n * Reads and validates `window.__MINDSTUDIO__` — the globals injected by\n * the platform before the app's JavaScript runs. The result is cached\n * after first read.\n *\n * All other modules in the SDK call `getConfig()` internally. App code\n * should never need to call this directly — use `createClient()`, `auth`,\n * and `platform` instead.\n */\n\nimport { MindStudioInterfaceError } from './errors.js';\nimport { maybeInstallAnalytics } from './telemetry-analytics.js';\nimport { maybeInstallTelemetry } from './telemetry-errors.js';\nimport type { AppUser, BootstrapConfig } from './types.js';\n\nlet _config: BootstrapConfig | undefined;\n\n/**\n * Read and validate the bootstrap configuration from `window.__MINDSTUDIO__`.\n *\n * Caches the result after first successful read. Throws if the globals\n * are missing or incomplete — this means the page isn't running inside\n * the MindStudio platform.\n *\n * @returns The validated bootstrap config\n * @throws {MindStudioInterfaceError} if `window.__MINDSTUDIO__` is missing or invalid\n */\nexport function getConfig(): BootstrapConfig {\n if (_config) {\n return _config;\n }\n\n const raw = (globalThis as Record<string, unknown>).__MINDSTUDIO__ as\n | Partial<BootstrapConfig>\n | undefined;\n\n if (!raw) {\n throw new MindStudioInterfaceError(\n '@mindstudio-ai/interface requires the MindStudio platform context. ' +\n 'This page must be loaded inside MindStudio (window.__MINDSTUDIO__ is missing).',\n 'not_initialized',\n );\n }\n\n // Validate required fields (user can be null for unauthenticated sessions)\n if (!raw.token || !raw.releaseId || !raw.methods) {\n const missing = ['token', 'releaseId', 'methods']\n .filter((k) => !raw[k as keyof BootstrapConfig])\n .join(', ');\n\n throw new MindStudioInterfaceError(\n `MindStudio platform context is incomplete. Missing: ${missing}`,\n 'invalid_config',\n );\n }\n\n _config = raw as BootstrapConfig;\n\n // Auto-install telemetry surfaces on first successful config read.\n // Both are idempotent + try/caught — telemetry must never break getConfig().\n //\n // Errors install synchronously: the listeners have to exist before React's\n // first render task, which is the whole reason index.ts calls this eagerly.\n // Analytics waits a tick — its first pageview reads the URL, and taking it\n // after the app's router has had a chance to normalize it is the more\n // accurate reading, quite apart from keeping a presence connection off the\n // critical path.\n maybeInstallTelemetry();\n setTimeout(() => {\n maybeInstallAnalytics();\n }, 0);\n\n return _config;\n}\n\n/**\n * The mount prefix this app is served under on the current host, normalized\n * to '' (served at the root — its own subdomain/custom host) or a\n * leading-slash, no-trailing-slash path like '/demos/vector-databases'\n * (served under a parent app's mount).\n *\n * Reads the raw bootstrap directly and never throws, so telemetry and\n * unload-time beacons can call it unconditionally. Use it as the router\n * basename: `createBrowserRouter(routes, { basename: platform.basePath || '/' })`.\n */\nexport function getBasePath(): string {\n const raw = (\n (globalThis as Record<string, unknown>).__MINDSTUDIO__ as\n | Partial<BootstrapConfig>\n | undefined\n )?.basePath;\n if (typeof raw !== 'string') {\n return '';\n }\n const trimmed = raw.replace(/\\/+$/, '');\n if (!trimmed) {\n return '';\n }\n return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;\n}\n\n/**\n * Prefix a root-relative `/_/...` path with the mount basePath. Identity when\n * the app serves at the root — every platform call in the SDK goes through\n * this so mounted apps talk to their own `/_/` namespace on the parent host.\n */\nexport function withBase(path: string): string {\n return `${getBasePath()}${path}`;\n}\n\n/**\n * Update the cached config in-place after an auth state transition.\n *\n * The cached config is a mutable reference — all SDK modules read it\n * on-demand via `getConfig()`, so mutations propagate immediately to\n * method invocation, agent chat, uploads, etc.\n *\n * @internal Not exported from the package — used by the auth module only.\n */\nexport function updateConfig(updates: {\n token?: string;\n user?: AppUser | null;\n methods?: Record<string, string>;\n visitorId?: string;\n}): void {\n const config = getConfig();\n if (updates.token !== undefined) {\n config.token = updates.token;\n }\n if (updates.user !== undefined) {\n config.user = updates.user;\n }\n if (updates.methods !== undefined) {\n config.methods = updates.methods;\n }\n if (updates.visitorId !== undefined) {\n config.visitorId = updates.visitorId;\n }\n}\n","/**\n * Long-lived SSE connection loop — the reconnect discipline shared by the\n * presence heartbeat (telemetry-analytics.ts) and the app-events client\n * (events.ts). Extracted from the presence client rather than copied, so the\n * hard-won rules can't drift between consumers:\n *\n * - Exponential backoff with jitter, capped, and a STABLE-CONNECTION\n * threshold before the backoff resets — resetting on a bare 200 lets a\n * stream that dies instantly reconnect every ~1s forever, so a connection\n * only earns the reset by staying open.\n * - `Retry-After` honored on 503 (the server's capacity signal).\n * - `fetch` + reader, never `EventSource`, so credentials ride the\n * `Authorization` header instead of a URL.\n *\n * Policy stays in the consumer: `prepare` builds each attempt's request (and\n * can stop the loop), `onStatus` decides what a non-OK response means, and\n * `onLine` interprets the stream. Presence discards lines and stops on\n * 401/204; events parses frames and re-mints its grant.\n */\n\nexport interface SseLoopOptions {\n /**\n * Build the next attempt's request. Called before EVERY connect, so a\n * consumer can refresh credentials per attempt. Return `'stop'` to end the\n * loop (e.g. config gone, credential refusal).\n */\n prepare: () =>\n | Promise<{ url: string; headers: Record<string, string> } | 'stop'>\n | { url: string; headers: Record<string, string> }\n | 'stop';\n /**\n * Policy for any unreadable response — an error status, or a bodyless\n * success like 204. `'retry'` schedules a backoff reconnect; `'stop'` ends\n * the loop. 503 never reaches this hook (the loop honors Retry-After\n * itself). Default: `'retry'`.\n */\n onStatus?: (status: number) => 'retry' | 'stop';\n /** One complete line from the stream, `\\n`-split, terminator stripped. */\n onLine?: (line: string) => void;\n /** The stream opened (2xx with a body). Fires on every (re)connect. */\n onOpen?: () => void;\n /** The loop ended for good — `stop()`, a `'stop'` policy, or `prepare` said so. */\n onStop?: () => void;\n}\n\nexport interface SseLoop {\n start(): void;\n stop(): void;\n readonly state: 'idle' | 'connecting' | 'open' | 'stopped';\n}\n\n/** @internal Timing knobs, injectable so tests don't wait wall-clock. */\nexport interface SseLoopTiming {\n initialBackoffMs: number;\n maxBackoffMs: number;\n /** Minimum open-stream lifetime that earns a backoff reset. */\n stableMs: number;\n /** Wait after a 503 with no usable Retry-After. */\n retry503DefaultMs: number;\n}\n\nconst DEFAULT_TIMING: SseLoopTiming = {\n initialBackoffMs: 1000,\n maxBackoffMs: 10_000,\n stableMs: 30_000,\n retry503DefaultMs: 30_000,\n};\n\nexport function createSseLoop(\n options: SseLoopOptions,\n timing: SseLoopTiming = DEFAULT_TIMING,\n): SseLoop {\n let active = false;\n let state: SseLoop['state'] = 'idle';\n let abort: AbortController | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let backoffMs = timing.initialBackoffMs;\n\n const nextBackoff = (): number => {\n const base = backoffMs;\n backoffMs = Math.min(backoffMs * 2, timing.maxBackoffMs);\n return base + Math.random() * 250;\n };\n\n const scheduleReconnect = (ms: number): void => {\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n }\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n if (active) {\n void connect();\n }\n }, ms);\n };\n\n const halt = (): void => {\n if (!active && state === 'stopped') {\n return;\n }\n active = false;\n state = 'stopped';\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n if (abort) {\n try {\n abort.abort();\n } catch {\n // swallow\n }\n abort = null;\n }\n options.onStop?.();\n };\n\n async function connect(): Promise<void> {\n if (!active) {\n return;\n }\n state = 'connecting';\n\n let request: { url: string; headers: Record<string, string> } | 'stop';\n try {\n request = await options.prepare();\n } catch {\n // prepare() throwing is a consumer bug or a deliberate refusal it\n // should have expressed as 'stop' — either way, looping on it is worse.\n halt();\n return;\n }\n if (request === 'stop' || !active) {\n halt();\n return;\n }\n\n abort = new AbortController();\n const signal = abort.signal;\n\n try {\n const res = await fetch(request.url, {\n method: 'GET',\n headers: { ...request.headers, Accept: 'text/event-stream' },\n signal,\n });\n\n if (!active) {\n return;\n }\n\n if (res.status === 503) {\n const retryAfterSec = parseInt(\n res.headers.get('Retry-After') ?? '',\n 10,\n );\n const waitMs =\n Number.isFinite(retryAfterSec) && retryAfterSec > 0\n ? retryAfterSec * 1000\n : timing.retry503DefaultMs;\n scheduleReconnect(waitMs);\n return;\n }\n\n // Anything unreadable — an error status, or a bodyless success like 204\n // (which the dev-tunnel presence mock uses to mean \"don't reconnect\") —\n // goes through the consumer's policy hook.\n if (!res.ok || !res.body) {\n if ((options.onStatus?.(res.status) ?? 'retry') === 'stop') {\n halt();\n } else {\n scheduleReconnect(nextBackoff());\n }\n return;\n }\n\n // Note the connect time — but don't reset the backoff yet. Only a\n // stream that stays open long enough to be healthy earns the reset.\n const connectedAt = Date.now();\n state = 'open';\n options.onOpen?.();\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (active) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!options.onLine) {\n continue; // drain-and-discard consumer (presence)\n }\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n options.onLine(line.replace(/\\r$/, ''));\n }\n }\n\n if (active) {\n if (Date.now() - connectedAt >= timing.stableMs) {\n backoffMs = timing.initialBackoffMs;\n }\n state = 'connecting';\n scheduleReconnect(nextBackoff());\n }\n } catch (err) {\n const name = (err as { name?: string } | null)?.name;\n if (active && name !== 'AbortError') {\n state = 'connecting';\n scheduleReconnect(nextBackoff());\n }\n }\n }\n\n return {\n start() {\n if (active) {\n return;\n }\n active = true;\n void connect();\n },\n stop: halt,\n get state() {\n return state;\n },\n };\n}\n","/**\n * Analytics — Plausible/Fathom-style visitor analytics.\n *\n * Auto-tracks pageviews via the existing history patches in\n * `telemetry-breadcrumbs.ts` and exposes a small public API:\n *\n * ```ts\n * import { analytics } from '@mindstudio-ai/interface';\n *\n * // Custom events (optional — pageviews track automatically)\n * analytics.track('vendor_submitted', { vendorType: 'restaurant' });\n * ```\n *\n * Server-side derivation handles geo (IP → country), device class\n * (UA → mobile/tablet/desktop), referrers, UTM parsing, and\n * sessionization. The SDK ships nothing for those — just raw\n * pageview events with enough context for backend enrichment.\n *\n * ## How it works\n *\n * Installed by `maybeInstallAnalytics()`, which is called by `getConfig()`\n * in `config.ts`. `getConfig()` runs automatically on the next tick after\n * SDK import when the platform bootstrap is present (see eager-init block\n * in `src/index.ts`), so pageview tracking + presence are live at page\n * load without app code needing to invoke anything. Falls back to lazy\n * install on first SDK use if eager init was skipped.\n *\n * Once installed:\n *\n * 1. Subscribes to navigation events from `telemetry-breadcrumbs.ts`\n * and emits a pageview on each change.\n * 2. Fires an initial pageview for the first-load URL.\n * 3. Batches events (~1s debounce, 100/batch cap) and POSTs to\n * `/_/telemetry/events`.\n * 4. On `pagehide` / `visibilitychange='hidden'`, drains via\n * `fetch` with `keepalive: true`.\n * 5. On `429`, pauses sends for the `Retry-After` window.\n *\n * ## Privacy posture\n *\n * Live visitor counts are intentionally **not** exposed to app code.\n * Aggregate visitor metrics (live count included) are surfaced only\n * through the platform dashboard to the app owner — visitors should\n * not be able to learn about other visitors' presence through the\n * SDK. If an app explicitly wants user-facing presence as a designed\n * feature (e.g. a multiplayer experience), it must build that on its\n * own application-level abstraction with its own consent semantics,\n * not on platform telemetry.\n *\n * Opt-out via `window.__MINDSTUDIO__.telemetry = { analytics: false }`.\n */\n\nimport { getConfig, withBase } from './config.js';\nimport { onNavigation } from './telemetry-breadcrumbs.js';\nimport { createSseLoop } from './sse-loop.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Flat-primitive prop values for custom events.\n *\n * Backend caps: max 10 keys, key length ~50 chars, value length ~500 chars.\n * Non-primitive values (objects, arrays, null) are stripped client-side\n * before send to save bandwidth — backend would silently drop them anyway.\n */\nexport type CustomEventProps = Record<string, string | number | boolean>;\n\n/**\n * The analytics namespace — pageview auto-tracking + custom events.\n *\n * Pageviews are auto-tracked from history changes; no public API for\n * them. Opt out entirely via `window.__MINDSTUDIO__.telemetry.analytics = false`.\n */\nexport interface AnalyticsClient {\n /**\n * Record a custom event with optional flat-primitive props.\n *\n * Non-primitive prop values (objects, arrays, null, undefined) are\n * stripped client-side before send. Server further caps name length\n * to 200 chars, props to 10 keys, key length to 50, value length to 500.\n *\n * Best-effort — never throws, never blocks. If the SDK isn't\n * initialized yet, the call is silently dropped.\n */\n track(event: string, props?: CustomEventProps): void;\n}\n\n// ---------------------------------------------------------------------------\n// Internal event types\n// ---------------------------------------------------------------------------\n\ntype PageviewEvent = {\n type: 'pageview';\n releaseId: string;\n url: string;\n referrer: string;\n userAgent: string;\n language: string;\n screen: { w: number; h: number };\n timestamp: number;\n};\n\ntype CustomEvent = {\n type: 'event';\n releaseId: string;\n name: string;\n url: string;\n timestamp: number;\n props?: CustomEventProps;\n};\n\ntype AnalyticsEvent = PageviewEvent | CustomEvent;\n\n// ---------------------------------------------------------------------------\n// Batched event transport\n// ---------------------------------------------------------------------------\n\nconst EVENTS_ENDPOINT = '/_/telemetry/events';\nconst FLUSH_INTERVAL_MS = 1000;\nconst MAX_BATCH_SIZE = 100;\nconst DEFAULT_RETRY_AFTER_MS = 60_000;\n\nlet pending: AnalyticsEvent[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | null = null;\nlet suppressUntil = 0;\nlet unloaded = false;\n\nfunction enqueue(event: AnalyticsEvent): void {\n if (unloaded) {\n return;\n }\n if (pending.length >= MAX_BATCH_SIZE) {\n return;\n }\n pending.push(event);\n scheduleFlush();\n}\n\nfunction scheduleFlush(): void {\n if (flushTimer || pending.length === 0) {\n return;\n }\n flushTimer = setTimeout(() => {\n flushTimer = null;\n void flush();\n }, FLUSH_INTERVAL_MS);\n}\n\nfunction takeBatch(): AnalyticsEvent[] {\n const batch = pending;\n pending = [];\n return batch;\n}\n\nasync function flush(): Promise<void> {\n if (pending.length === 0) {\n return;\n }\n\n if (Date.now() < suppressUntil) {\n pending = [];\n return;\n }\n\n let config;\n try {\n config = getConfig();\n } catch {\n pending = [];\n return;\n }\n\n const events = takeBatch();\n\n try {\n const res = await fetch(withBase(EVENTS_ENDPOINT), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.token}`,\n },\n body: JSON.stringify({ events }),\n });\n\n if (res.status === 429) {\n const retryAfterHeader = res.headers.get('Retry-After');\n const retryAfterSec = retryAfterHeader\n ? parseInt(retryAfterHeader, 10)\n : NaN;\n const waitMs =\n Number.isFinite(retryAfterSec) && retryAfterSec > 0\n ? retryAfterSec * 1000\n : DEFAULT_RETRY_AFTER_MS;\n suppressUntil = Date.now() + waitMs;\n }\n } catch {\n // network / serialization failure — drop silently\n }\n}\n\nfunction drainOnUnload(): void {\n if (pending.length === 0) {\n return;\n }\n if (Date.now() < suppressUntil) {\n pending = [];\n return;\n }\n\n let config;\n try {\n config = getConfig();\n } catch {\n pending = [];\n return;\n }\n\n const events = takeBatch();\n\n try {\n void fetch(withBase(EVENTS_ENDPOINT), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.token}`,\n },\n body: JSON.stringify({ events }),\n keepalive: true,\n });\n } catch {\n // best effort\n }\n}\n\n// ---------------------------------------------------------------------------\n// Custom event prop sanitization\n// ---------------------------------------------------------------------------\n\nfunction sanitizeProps(\n input?: Record<string, unknown>,\n): CustomEventProps | undefined {\n if (!input) {\n return undefined;\n }\n const out: CustomEventProps = {};\n for (const [k, v] of Object.entries(input)) {\n if (\n typeof v === 'string' ||\n typeof v === 'number' ||\n typeof v === 'boolean'\n ) {\n out[k] = v;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Pageview emit\n// ---------------------------------------------------------------------------\n\nfunction emitPageview(url: string): void {\n try {\n const config = getConfig();\n enqueue({\n type: 'pageview',\n releaseId: config.releaseId,\n url,\n referrer: document.referrer,\n userAgent: navigator.userAgent,\n language: navigator.language,\n screen: { w: window.innerWidth, h: window.innerHeight },\n timestamp: Date.now(),\n });\n } catch {\n // capture must never crash\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public `track`\n// ---------------------------------------------------------------------------\n\nfunction track(event: string, props?: CustomEventProps): void {\n try {\n const config = getConfig();\n enqueue({\n type: 'event',\n releaseId: config.releaseId,\n name: event,\n url: location.href,\n timestamp: Date.now(),\n props: sanitizeProps(props as Record<string, unknown> | undefined),\n });\n } catch {\n // never crash\n }\n}\n\n// ---------------------------------------------------------------------------\n// Silent presence connection\n//\n// The SDK opens a long-lived SSE connection to `/_/telemetry/presence`\n// while the tab is alive. The connection itself is the presence signal:\n// while it's open, the visitor is tracked as \"online\" in the server-side\n// Redis set; when the tab closes (or network drops), the connection\n// closes and the server immediately knows the visitor is gone.\n//\n// Any data the server pushes over this connection is silently discarded.\n// The visitor-facing live count is intentionally NOT exposed by the SDK.\n// The owner-facing dashboard reads counts via a separate authenticated\n// internal endpoint, never through the SDK.\n//\n// Reading-and-discarding the stream is required to keep the underlying\n// TCP buffer flowing — the open connection itself is what matters, not\n// the data flowing over it.\n// ---------------------------------------------------------------------------\n\nconst PRESENCE_ENDPOINT = '/_/telemetry/presence';\n\n// The connection machinery — backoff + jitter, the stable-connection rule,\n// 503 Retry-After, abort lifecycle — lives in sse-loop.ts, shared with the\n// app-events client (events.ts). Only presence's POLICY lives here: stop for\n// good on 401 (session bad — nothing to do until something refreshes config)\n// and on 204 (presence stubbed/disabled — the dev-tunnel mock relies on\n// EventSource semantics, where a 204 means \"abandon and do NOT reconnect\").\n// No onLine handler: lines are drained and discarded inside the loop, since\n// the open connection is the presence signal and the visitor-facing SDK\n// deliberately exposes no counts.\nconst presenceLoop = createSseLoop({\n prepare: () => {\n let config;\n try {\n config = getConfig();\n } catch {\n return 'stop';\n }\n return {\n url: withBase(PRESENCE_ENDPOINT),\n headers: { Authorization: `Bearer ${config.token}` },\n };\n },\n onStatus: (status) => (status === 401 || status === 204 ? 'stop' : 'retry'),\n});\n\nfunction closePresenceConnection(): void {\n presenceLoop.stop();\n}\n\n// ---------------------------------------------------------------------------\n// Public client\n// ---------------------------------------------------------------------------\n\nexport const analytics: AnalyticsClient = { track };\n\n// ---------------------------------------------------------------------------\n// Install + opt-out\n// ---------------------------------------------------------------------------\n\nlet _installed = false;\n\n/**\n * Install pageview auto-tracking + transport. Idempotent.\n *\n * Reads `window.__MINDSTUDIO__.telemetry?.analytics` — if explicitly\n * `false`, the SDK installs nothing and the function is a no-op.\n * `track()` calls also no-op in that case since enqueued events\n * have no flush path established.\n */\nexport function installAnalytics(): void {\n if (_installed) {\n return;\n }\n if (typeof window === 'undefined') {\n return;\n }\n\n let config;\n try {\n config = getConfig();\n } catch {\n return;\n }\n\n if (config.telemetry?.analytics === false) {\n return;\n }\n\n _installed = true;\n\n // Unload listeners (attached once)\n const handleUnload = (): void => {\n unloaded = true;\n drainOnUnload();\n closePresenceConnection();\n };\n window.addEventListener('pagehide', handleUnload);\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n drainOnUnload();\n }\n });\n\n // Auto-pageviews on every history change\n onNavigation((url) => {\n emitPageview(url);\n });\n\n // Initial pageview\n emitPageview(location.href);\n\n // Silent presence heartbeat — the open connection itself signals\n // \"this visitor is online\" to the server. No data is consumed.\n presenceLoop.start();\n}\n\n/**\n * Wrapper that swallows any error from {@link installAnalytics} so a\n * misbehaving telemetry layer can never break `getConfig()`.\n *\n * @internal Called from config.ts on every `getConfig()` — idempotency\n * is enforced inside `installAnalytics`.\n */\nexport function maybeInstallAnalytics(): void {\n try {\n installAnalytics();\n } catch {\n // telemetry must never crash the host app\n }\n}\n"],"mappings":"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","names":["MindStudioInterfaceError","message","code","status","errorFromResponse","res","fallbackMessage","fallbackCode","body","TELEMETRY_ENDPOINT_FRAGMENT","buffer","push","entry","getBreadcrumbs","shouldSkipUrl","url","TELEMETRY_ENDPOINT_FRAGMENT","resolveUrl","input","lastHref","navListeners","onNavigation","cb","emitNavigation","to","installHistory","w","originalPushState","data","unused","originalReplaceState","installFetch","captureResponseBodies","originalFetch","patched","init","method","startTime","response","body","err","installXhr","originalOpen","originalSend","rest","_breadcrumbsInstalled","installBreadcrumbs","opts","ENDPOINT","FLUSH_INTERVAL_MS","MAX_BATCH_SIZE","DEFAULT_RETRY_AFTER_MS","_installed","pending","flushTimer","suppressUntil","fingerprint","message","stack","firstStackLine","enqueue","event","fp","existing","scheduleFlush","flush","takeBatch","batch","_fp","rest","config","getConfig","events","res","withBase","retryAfterHeader","retryAfterSec","waitMs","drainOnUnload","describe","error","fallbackMessage","shaped","capture","options","described","getBreadcrumbs","captureError","captureRejection","installErrorMonitoring","installBreadcrumbs","maybeInstallTelemetry","telemetry","errorInfo","_config","getConfig","raw","MindStudioInterfaceError","missing","k","maybeInstallTelemetry","maybeInstallAnalytics","getBasePath","trimmed","withBase","path","updateConfig","updates","config","DEFAULT_TIMING","createSseLoop","options","timing","active","state","abort","reconnectTimer","backoffMs","nextBackoff","base","scheduleReconnect","ms","connect","halt","request","signal","res","retryAfterSec","waitMs","connectedAt","reader","decoder","buffer","done","value","lines","line","err","name","EVENTS_ENDPOINT","FLUSH_INTERVAL_MS","MAX_BATCH_SIZE","DEFAULT_RETRY_AFTER_MS","pending","flushTimer","suppressUntil","unloaded","enqueue","event","scheduleFlush","flush","takeBatch","batch","config","getConfig","events","res","withBase","retryAfterHeader","retryAfterSec","waitMs","drainOnUnload","sanitizeProps","input","out","k","v","emitPageview","url","track","props","PRESENCE_ENDPOINT","presenceLoop","createSseLoop","status","closePresenceConnection","analytics","_installed","installAnalytics","handleUnload","onNavigation","maybeInstallAnalytics"]}