@mindstudio-ai/interface 0.1.17 → 0.1.19
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/README.md +60 -0
- package/dist/chunk-XA6W4LYT.js +4 -0
- package/dist/chunk-XA6W4LYT.js.map +1 -0
- package/dist/index.d.ts +147 -11
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/voice.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FFFGB4U3.js +0 -3
- package/dist/chunk-FFFGB4U3.js.map +0 -1
package/README.md
CHANGED
|
@@ -301,6 +301,34 @@ chat.sendMessage(threadId, "What's in this document?", {
|
|
|
301
301
|
|
|
302
302
|
Attachments are preserved in thread history — when you load a thread via `getThread()`, user messages include their original `attachments` array.
|
|
303
303
|
|
|
304
|
+
#### Client tools
|
|
305
|
+
|
|
306
|
+
A tool declared in `agent.md` with `target: "client"` runs in this browser instead of the app's backend. Register a handler and its return value goes back to the agent as the tool result, so the agent knows what happened instead of assuming:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const unregister = chat.registerClientTool('pickFile', async ({ prompt }) => {
|
|
310
|
+
const file = await openFilePicker(prompt);
|
|
311
|
+
return file ? { path: file.path } : { cancelled: true };
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The agent holds its turn while your handler runs, for up to 15 minutes — long enough that a handler can resolve from a dialog's Save button rather than returning immediately, which is what makes confirm-before-acting possible:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
chat.registerClientTool('confirmDeploy', ({ summary }) =>
|
|
319
|
+
new Promise((resolve) => {
|
|
320
|
+
showApprovalDialog(summary, {
|
|
321
|
+
onApprove: (note) => resolve({ approved: true, note }),
|
|
322
|
+
onReject: (reason) => resolve({ approved: false, reason }),
|
|
323
|
+
});
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The SDK always answers, so the agent is never left hanging: your return value, `{ error: <message> }` if the handler throws, `result_too_large` past ~32KB serialized, and `unhandled_client_tool` immediately when no handler is registered for the name. If nothing answers within the window the agent gets `client_timeout`; closing the page gets it `client_disconnected`.
|
|
329
|
+
|
|
330
|
+
Handlers are keyed by tool name, live for the lifetime of the client rather than one message, and the returned function unregisters. For a one-off, the `onClientToolCall` callback on `sendMessage` works the same way — whatever it returns becomes the result — and is consulted only when no handler is registered for that name.
|
|
331
|
+
|
|
304
332
|
#### SSE event types
|
|
305
333
|
|
|
306
334
|
All events are available via the `onEvent` catch-all as the `AgentChatEvent` discriminated union:
|
|
@@ -312,6 +340,7 @@ All events are available via the `onEvent` catch-all as the `AgentChatEvent` dis
|
|
|
312
340
|
| `thinking_complete` | `thinking`, `signature` | `onThinkingComplete` |
|
|
313
341
|
| `tool_call_start` | `id`, `name` | `onToolCallStart` |
|
|
314
342
|
| `tool_call_result` | `id`, `output` | `onToolCallResult` |
|
|
343
|
+
| `client_tool_call` | `id`, `name`, `input`, `timeoutMs` | answered by `registerClientTool` |
|
|
315
344
|
| `error` | `error` | `onError` |
|
|
316
345
|
| `tool_use` | `id`, `name`, `input` | `onEvent` only |
|
|
317
346
|
| `tool_input_delta` | `id`, `name`, `delta` | `onEvent` only |
|
|
@@ -373,6 +402,37 @@ const { sessions, nextCursor } = await voice.listSessions();
|
|
|
373
402
|
const detail = await voice.getSession(sessions[0].id); // includes transcript
|
|
374
403
|
```
|
|
375
404
|
|
|
405
|
+
### `events`
|
|
406
|
+
|
|
407
|
+
Server→client realtime. Backend code publishes to named channels (the agent SDK's `events.publish`); this client receives those publishes live over a platform-held SSE — no polling, no WebSockets. Authorization is a **grant** minted by one of the app's own backend methods, which is why `connect` takes a token *provider*: grants expire on purpose (expiry is the revocation window), and the SDK re-mints through your method on every expiry, re-running your auth checks.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { createClient, events } from '@mindstudio-ai/interface';
|
|
411
|
+
const api = createClient();
|
|
412
|
+
|
|
413
|
+
const sub = events.connect({
|
|
414
|
+
// Your backend method does its auth checks, then mints the grant:
|
|
415
|
+
getToken: () => api.watchJobs().then((r) => r.token),
|
|
416
|
+
|
|
417
|
+
// A publish on one of the grant's channels:
|
|
418
|
+
onEvent: (e) => {
|
|
419
|
+
if (e.channel.startsWith('jobs:')) refreshJob(e.data);
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// Fires on EVERY (re)connect — refetch current state here.
|
|
423
|
+
onConnect: () => refetchJobs(),
|
|
424
|
+
|
|
425
|
+
// Terminal only: your getToken threw (logged out, role revoked).
|
|
426
|
+
onError: (err) => showBanner(err.message),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
sub.close(); // on unmount
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Reconcile on connect.** Events are at-most-once nudges: nothing is buffered while you're disconnected, nothing is replayed when you return. `onConnect` is where you refetch — a subscriber without it will silently miss whatever happened while it was away. Subscribe for speed, reconcile for truth.
|
|
433
|
+
|
|
434
|
+
Reconnects (network drops, grant expiry, deploys) are handled internally with backoff; the app sees them only as `onConnect` firing again. A `getToken` failure is deliberately terminal — it means your own method refused, and retrying an authorization refusal in a loop is wrong; call `connect` again when your auth state changes.
|
|
435
|
+
|
|
376
436
|
## Error handling
|
|
377
437
|
|
|
378
438
|
```ts
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var _=class extends Error{code;status;constructor(e,n,r){super(e),this.name="MindStudioInterfaceError",this.code=n,this.status=r}};var it="/_/telemetry/";var w=[];function h(t){w.push(t),w.length>20&&w.shift()}function A(){return w.slice()}function x(t){return t.includes(it)}function st(t){return typeof t=="string"?t:t instanceof URL?t.href:t.url||String(t)}var E="",M=new Set;function N(t){return M.add(t),()=>{M.delete(t)}}function b(t){t!==E&&(h({type:"navigation",from:E,to:t,timestamp:Date.now()}),E=t,M.forEach(e=>{try{e(t)}catch{}}))}function at(){let t=window;if(t.__ms_history_patched)return;t.__ms_history_patched=!0,E=location.href;let e=history.pushState.bind(history);history.pushState=function(r,o,i){e(r,o,i);try{b(location.href)}catch{}};let n=history.replaceState.bind(history);history.replaceState=function(r,o,i){n(r,o,i);try{b(location.href)}catch{}},window.addEventListener("popstate",()=>{try{b(location.href)}catch{}}),window.addEventListener("hashchange",()=>{try{b(location.href)}catch{}})}function ct(t){if(!window.fetch||window.fetch.__ms_patched)return;let n=window.fetch.bind(window),r=function(o,i){let u=st(o);if(x(u))return n(o,i);let y=(i?.method||(typeof o!="string"&&!(o instanceof URL)?o.method:"")||"GET").toUpperCase(),f=Date.now();return n(o,i).then(s=>{let p={type:"fetch",method:y,url:u,status:s.status,ok:s.ok,duration:Date.now()-f,timestamp:f};if(!s.ok&&t)try{s.clone().text().then(d=>{p.body=d.slice(0,1024),h(p)}).catch(()=>h(p))}catch{h(p)}else h(p);return s},s=>{throw h({type:"fetch",method:y,url:u,ok:!1,duration:Date.now()-f,timestamp:f,error:s instanceof Error?s.message:String(s??"fetch failed")}),s})};r.__ms_patched=!0,window.fetch=r}function ut(){if(XMLHttpRequest.prototype.open.__ms_patched)return;let e=XMLHttpRequest.prototype.open,n=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(r,o,...i){return this.__ms_method=(r||"GET").toUpperCase(),this.__ms_url=String(o),e.apply(this,[r,o,...i])},XMLHttpRequest.prototype.send=function(r){let o=this.__ms_method||"GET",i=this.__ms_url||"";if(x(i))return n.apply(this,[r]);let u=Date.now();return this.addEventListener("loadend",function(){h({type:"xhr",method:o,url:i,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 P=!1;function U(t){if(!P){P=!0;try{at()}catch{}try{ct(t.captureResponseBodies)}catch{}try{ut()}catch{}}}var q="/_/telemetry/errors",lt=1e3,dt=50,ft=6e4,O=!1,c=[],L=null,B=0,X=!1;function pt(t,e){let n=e.split(`
|
|
2
|
+
`)[1]??"";return`${t}|${n.trim()}`}function F(t){if(X)return;let e=pt(t.message,t.stack);for(let n of c)if(n._fp===e){n.count=(n.count??1)+1;return}c.length>=dt||(t._fp=e,c.push(t),mt())}function mt(){L||c.length===0||(L=setTimeout(()=>{L=null,ht()},lt))}function j(){let t=c;return c=[],t.map(({_fp:e,...n})=>n)}async function ht(){if(c.length===0)return;if(Date.now()<B){c=[];return}let t=(()=>{try{return a()}catch{return null}})();if(!t){c=[];return}let e=j();try{let n=await fetch(g(q),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.token}`},body:JSON.stringify({events:e})});if(n.status===429){let r=n.headers.get("Retry-After"),o=r?parseInt(r,10):NaN,i=Number.isFinite(o)&&o>0?o*1e3:ft;B=Date.now()+i}}catch{}}function H(){if(c.length===0)return;if(Date.now()<B){c=[];return}let t=null;try{t=a()}catch{c=[];return}let e=j();try{fetch(g(q),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.token}`},body:JSON.stringify({events:e}),keepalive:!0})}catch{}}function gt(t){try{let e=a();F({releaseId:e.releaseId,url:location.href,userAgent:navigator.userAgent,timestamp:Date.now(),type:"error",message:t.message||"Unknown error",stack:t.error?.stack??"",source:t.filename||void 0,line:typeof t.lineno=="number"?t.lineno:void 0,column:typeof t.colno=="number"?t.colno:void 0,breadcrumbs:A()})}catch{}}function yt(t){try{let e=a(),n=t.reason,r,o="";n&&typeof n=="object"?(r=n.message||String(n),o=n.stack??""):r=String(n??"Unhandled rejection"),F({releaseId:e.releaseId,url:location.href,userAgent:navigator.userAgent,timestamp:Date.now(),type:"unhandledrejection",message:r,stack:o,breadcrumbs:A()})}catch{}}function vt(){if(O||typeof window>"u")return;let t;try{t=a()}catch{return}if(t.telemetry?.errors===!1)return;O=!0,U({captureResponseBodies:!!t.telemetryCaptureResponseBodies}),window.addEventListener("error",gt),window.addEventListener("unhandledrejection",yt);let e=()=>{X=!0,H()};window.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&H()})}function $(){try{vt()}catch{}}var R;function a(){if(R)return R;let t=globalThis.__MINDSTUDIO__;if(!t)throw new _("@mindstudio-ai/interface requires the MindStudio platform context. This page must be loaded inside MindStudio (window.__MINDSTUDIO__ is missing).","not_initialized");if(!t.token||!t.releaseId||!t.methods){let e=["token","releaseId","methods"].filter(n=>!t[n]).join(", ");throw new _(`MindStudio platform context is incomplete. Missing: ${e}`,"invalid_config")}return R=t,$(),z(),R}function _t(){let t=globalThis.__MINDSTUDIO__?.basePath;if(typeof t!="string")return"";let e=t.replace(/\/+$/,"");return e?e.startsWith("/")?e:`/${e}`:""}function g(t){return`${_t()}${t}`}function Ht(t){let e=a();t.token!==void 0&&(e.token=t.token),t.user!==void 0&&(e.user=t.user),t.methods!==void 0&&(e.methods=t.methods),t.visitorId!==void 0&&(e.visitorId=t.visitorId)}var bt={initialBackoffMs:1e3,maxBackoffMs:1e4,stableMs:3e4,retry503DefaultMs:3e4};function Y(t,e=bt){let n=!1,r="idle",o=null,i=null,u=e.initialBackoffMs,y=()=>{let d=u;return u=Math.min(u*2,e.maxBackoffMs),d+Math.random()*250},f=d=>{i&&clearTimeout(i),i=setTimeout(()=>{i=null,n&&p()},d)},s=()=>{if(!(!n&&r==="stopped")){if(n=!1,r="stopped",i&&(clearTimeout(i),i=null),o){try{o.abort()}catch{}o=null}t.onStop?.()}};async function p(){if(!n)return;r="connecting";let d;try{d=await t.prepare()}catch{s();return}if(d==="stop"||!n){s();return}o=new AbortController;let et=o.signal;try{let m=await fetch(d.url,{method:"GET",headers:{...d.headers,Accept:"text/event-stream"},signal:et});if(!n)return;if(m.status===503){let v=parseInt(m.headers.get("Retry-After")??"",10),k=Number.isFinite(v)&&v>0?v*1e3:e.retry503DefaultMs;f(k);return}if(!m.ok||!m.body){(t.onStatus?.(m.status)??"retry")==="stop"?s():f(y());return}let S=Date.now();r="open",t.onOpen?.();let nt=m.body.getReader(),rt=new TextDecoder,T="";for(;n;){let{done:v,value:k}=await nt.read();if(v)break;if(!t.onLine)continue;T+=rt.decode(k,{stream:!0});let C=T.split(`
|
|
3
|
+
`);T=C.pop()??"";for(let ot of C)t.onLine(ot.replace(/\r$/,""))}n&&(Date.now()-S>=e.stableMs&&(u=e.initialBackoffMs),r="connecting",f(y()))}catch(m){let S=m?.name;n&&S!=="AbortError"&&(r="connecting",f(y()))}}return{start(){n||(n=!0,p())},stop:s,get state(){return r}}}var W="/_/telemetry/events",wt=1e3,Et=100,Rt=6e4,l=[],I=null,D=0,Z=!1;function K(t){Z||l.length>=Et||(l.push(t),St())}function St(){I||l.length===0||(I=setTimeout(()=>{I=null,Tt()},wt))}function Q(){let t=l;return l=[],t}async function Tt(){if(l.length===0)return;if(Date.now()<D){l=[];return}let t;try{t=a()}catch{l=[];return}let e=Q();try{let n=await fetch(g(W),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.token}`},body:JSON.stringify({events:e})});if(n.status===429){let r=n.headers.get("Retry-After"),o=r?parseInt(r,10):NaN,i=Number.isFinite(o)&&o>0?o*1e3:Rt;D=Date.now()+i}}catch{}}function G(){if(l.length===0)return;if(Date.now()<D){l=[];return}let t;try{t=a()}catch{l=[];return}let e=Q();try{fetch(g(W),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.token}`},body:JSON.stringify({events:e}),keepalive:!0})}catch{}}function kt(t){if(!t)return;let e={};for(let[n,r]of Object.entries(t))(typeof r=="string"||typeof r=="number"||typeof r=="boolean")&&(e[n]=r);return Object.keys(e).length>0?e:void 0}function J(t){try{let e=a();K({type:"pageview",releaseId:e.releaseId,url:t,referrer:document.referrer,userAgent:navigator.userAgent,language:navigator.language,screen:{w:window.innerWidth,h:window.innerHeight},timestamp:Date.now()})}catch{}}function Mt(t,e){try{let n=a();K({type:"event",releaseId:n.releaseId,name:t,url:location.href,timestamp:Date.now(),props:kt(e)})}catch{}}var At="/_/telemetry/presence",tt=Y({prepare:()=>{let t;try{t=a()}catch{return"stop"}return{url:g(At),headers:{Authorization:`Bearer ${t.token}`}}},onStatus:t=>t===401||t===204?"stop":"retry"});function Lt(){tt.stop()}var zt={track:Mt},V=!1;function Bt(){if(V||typeof window>"u")return;let t;try{t=a()}catch{return}if(t.telemetry?.analytics===!1)return;V=!0;let e=()=>{Z=!0,G(),Lt()};window.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&G()}),N(n=>{J(n)}),J(location.href),tt.start()}function z(){try{Bt()}catch{}}export{_ as a,Y as b,zt as c,a as d,_t as e,g as f,Ht as g};
|
|
4
|
+
//# sourceMappingURL=chunk-XA6W4LYT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 * 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 — auto-capture of uncaught errors and unhandled\n * promise rejections, batched and shipped to `/_/telemetry/errors`.\n *\n * ## How it works\n *\n * Installed by `maybeInstallTelemetry()`, 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 installation happens at page load without app\n * code needing to invoke anything. Falls back to lazy install on first\n * SDK use if eager init was skipped (SSR / tests / no bootstrap).\n *\n * Once installed:\n *\n * 1. `window.error` and `unhandledrejection` listeners enqueue events\n * into a pending batch (max 50 per batch — 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` to\n * `/_/telemetry/errors`.\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 * No public API — the only knob is opt-out 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\ninterface ErrorEvent {\n releaseId: string;\n url: string;\n userAgent: string;\n timestamp: number;\n type: 'error' | 'unhandledrejection';\n message: string;\n stack: 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;\nlet unloaded = false;\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 if (unloaded) {\n return;\n }\n\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\nfunction captureError(e: globalThis.ErrorEvent): void {\n try {\n const config = getConfig();\n enqueue({\n releaseId: config.releaseId,\n url: location.href,\n userAgent: navigator.userAgent,\n timestamp: Date.now(),\n type: 'error',\n message: e.message || 'Unknown error',\n stack: e.error?.stack ?? '',\n source: e.filename || undefined,\n line: typeof e.lineno === 'number' ? e.lineno : undefined,\n column: typeof e.colno === 'number' ? e.colno : undefined,\n breadcrumbs: getBreadcrumbs(),\n });\n } catch {\n // never crash on capture\n }\n}\n\nfunction captureRejection(e: PromiseRejectionEvent): void {\n try {\n const config = getConfig();\n const reason = e.reason as\n | { message?: string; stack?: string }\n | string\n | undefined;\n\n let message: string;\n let stack = '';\n\n if (reason && typeof reason === 'object') {\n message = reason.message || String(reason);\n stack = reason.stack ?? '';\n } else {\n message = String(reason ?? 'Unhandled rejection');\n }\n\n enqueue({\n releaseId: config.releaseId,\n url: location.href,\n userAgent: navigator.userAgent,\n timestamp: Date.now(),\n type: 'unhandledrejection',\n message,\n stack,\n breadcrumbs: getBreadcrumbs(),\n });\n } catch {\n // never crash on capture\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 const handleUnload = (): void => {\n unloaded = true;\n drainOnUnload();\n };\n window.addEventListener('pagehide', handleUnload);\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 * 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 maybeInstallTelemetry();\n maybeInstallAnalytics();\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","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","unloaded","fingerprint","message","stack","firstStackLine","enqueue","event","fp","existing","scheduleFlush","flush","takeBatch","batch","_fp","rest","config","getConfig","events","res","withBase","retryAfterHeader","retryAfterSec","waitMs","drainOnUnload","captureError","e","getBreadcrumbs","captureRejection","reason","installErrorMonitoring","installBreadcrumbs","handleUnload","maybeInstallTelemetry","_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"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -224,11 +224,26 @@ declare function createClient<T = DefaultMethodClient>(): T;
|
|
|
224
224
|
* └─ Promise resolves with { stopReason, usage }
|
|
225
225
|
* ```
|
|
226
226
|
*
|
|
227
|
+
* ## Client tools
|
|
228
|
+
*
|
|
229
|
+
* A tool declared in `agent.md` with `target: "client"` runs in this browser
|
|
230
|
+
* instead of the app's backend. Register a handler and its return value goes
|
|
231
|
+
* back to the agent as the tool result:
|
|
232
|
+
*
|
|
233
|
+
* ```
|
|
234
|
+
* ├─ data: {"type":"client_tool_call","id":"call_x","name":"pickFile","input":{...}}
|
|
235
|
+
* │
|
|
236
|
+
* ├─ your handler runs (the agent is waiting)
|
|
237
|
+
* │
|
|
238
|
+
* └─ POST /_/agent/threads/{threadId}/tool-results
|
|
239
|
+
* Body: { callId: "call_x", result: { path: "/invoices/q3.pdf" } }
|
|
240
|
+
* ```
|
|
241
|
+
*
|
|
227
242
|
* ## Stateless design
|
|
228
243
|
*
|
|
229
|
-
* This client is a thin wrapper over HTTP endpoints.
|
|
230
|
-
* local state — your React/framework layer owns
|
|
231
|
-
* array, and UI state.
|
|
244
|
+
* This client is a thin wrapper over HTTP endpoints. Apart from client-tool
|
|
245
|
+
* handlers, it does not manage local state — your React/framework layer owns
|
|
246
|
+
* the thread list, message array, and UI state.
|
|
232
247
|
*
|
|
233
248
|
* ## Streaming callbacks
|
|
234
249
|
*
|
|
@@ -376,15 +391,18 @@ interface AgentToolCallResultEvent {
|
|
|
376
391
|
}
|
|
377
392
|
/**
|
|
378
393
|
* The agent invoked a client tool (agent.md `target: "client"`) — an action
|
|
379
|
-
* that happens in this browser (open a sheet,
|
|
380
|
-
*
|
|
381
|
-
* with an
|
|
394
|
+
* that happens in this browser (open a sheet, pick a file, confirm something).
|
|
395
|
+
* The agent is holding its turn until the SDK answers, which it always does:
|
|
396
|
+
* with your handler's return value, with an error if it threw, or with
|
|
397
|
+
* `unhandled_client_tool` if nothing is registered for the name.
|
|
382
398
|
*/
|
|
383
399
|
interface AgentClientToolCallEvent {
|
|
384
400
|
type: 'client_tool_call';
|
|
385
401
|
id: string;
|
|
386
402
|
name: string;
|
|
387
403
|
input: unknown;
|
|
404
|
+
/** How long the agent will wait for an answer before giving up on it. */
|
|
405
|
+
timeoutMs?: number;
|
|
388
406
|
ts: number;
|
|
389
407
|
}
|
|
390
408
|
/** Stream complete. */
|
|
@@ -435,11 +453,16 @@ interface SendMessageCallbacks {
|
|
|
435
453
|
/** Called when a tool call produces a result. */
|
|
436
454
|
onToolCallResult?: (id: string, output: unknown) => void;
|
|
437
455
|
/**
|
|
438
|
-
* Called when the agent invokes a client tool (agent.md `target:
|
|
439
|
-
*
|
|
440
|
-
*
|
|
456
|
+
* Called when the agent invokes a client tool (agent.md `target: "client"`)
|
|
457
|
+
* and no handler is registered for its name — run the on-screen action here.
|
|
458
|
+
* Whatever you return becomes the tool result; return nothing and the agent
|
|
459
|
+
* is told the action ran.
|
|
460
|
+
*
|
|
461
|
+
* {@link AgentChatClient.registerClientTool} is the better home for anything
|
|
462
|
+
* beyond a one-off, since handlers there survive across messages and are
|
|
463
|
+
* keyed by tool name.
|
|
441
464
|
*/
|
|
442
|
-
onClientToolCall?: (name: string, input: unknown, id: string) =>
|
|
465
|
+
onClientToolCall?: (name: string, input: unknown, id: string) => unknown | Promise<unknown>;
|
|
443
466
|
/** Called on a stream-level error event. */
|
|
444
467
|
onError?: (error: string) => void;
|
|
445
468
|
/** Called for every SSE event (including ones with named callbacks). */
|
|
@@ -512,6 +535,31 @@ interface AgentChatClient {
|
|
|
512
535
|
claimThread(threadId: string, options?: {
|
|
513
536
|
previousToken?: string;
|
|
514
537
|
}): Promise<void>;
|
|
538
|
+
/**
|
|
539
|
+
* Register a handler for a client tool — a tool declared in `agent.md` with
|
|
540
|
+
* `target: "client"`, whose effect lives in this browser (open a sheet, pick
|
|
541
|
+
* a file, confirm an action). When the agent invokes it, the handler runs and
|
|
542
|
+
* its return value goes back as the tool result, so the agent knows what
|
|
543
|
+
* happened rather than assuming.
|
|
544
|
+
*
|
|
545
|
+
* The agent waits while your handler runs — long enough for a person to read
|
|
546
|
+
* something and decide — so a handler may resolve from a dialog's Save button
|
|
547
|
+
* as readily as it can return immediately. Throwing returns an error the
|
|
548
|
+
* agent reasons about; a tool with no handler answers instantly with
|
|
549
|
+
* `unhandled_client_tool` rather than leaving the agent hanging.
|
|
550
|
+
*
|
|
551
|
+
* One handler per tool name (a later registration replaces the earlier one);
|
|
552
|
+
* returns an unregister function.
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* ```ts
|
|
556
|
+
* chat.registerClientTool('pickFile', async ({ prompt }) => {
|
|
557
|
+
* const file = await openFilePicker(prompt);
|
|
558
|
+
* return file ? { path: file.path } : { cancelled: true };
|
|
559
|
+
* });
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
registerClientTool(name: string, handler: (input: unknown) => unknown | Promise<unknown>): () => void;
|
|
515
563
|
/**
|
|
516
564
|
* Send a message and stream the agent's response.
|
|
517
565
|
*
|
|
@@ -1161,6 +1209,94 @@ interface AnalyticsClient {
|
|
|
1161
1209
|
}
|
|
1162
1210
|
declare const analytics: AnalyticsClient;
|
|
1163
1211
|
|
|
1212
|
+
/**
|
|
1213
|
+
* App events client — server→client realtime.
|
|
1214
|
+
*
|
|
1215
|
+
* Backend code publishes to named channels (`events.publish` in
|
|
1216
|
+
* `@mindstudio-ai/agent`); this client holds a platform-served SSE and
|
|
1217
|
+
* delivers matching publishes as they happen. Authorization is a **grant**
|
|
1218
|
+
* minted by one of the app's own backend methods — which is why `connect`
|
|
1219
|
+
* takes a token *provider*, not a token: grants expire on purpose (expiry is
|
|
1220
|
+
* the revocation window), and the SDK re-mints through your method on every
|
|
1221
|
+
* expiry, re-running your auth checks.
|
|
1222
|
+
*
|
|
1223
|
+
* ```ts
|
|
1224
|
+
* import { createClient, events } from '@mindstudio-ai/interface';
|
|
1225
|
+
* const api = createClient();
|
|
1226
|
+
*
|
|
1227
|
+
* const sub = events.connect({
|
|
1228
|
+
* getToken: () => api.watchJobs().then((r) => r.token),
|
|
1229
|
+
* onEvent: (e) => {
|
|
1230
|
+
* if (e.channel.startsWith('jobs:')) refreshJob(e.data);
|
|
1231
|
+
* },
|
|
1232
|
+
* onConnect: () => refetchJobs(), // reconcile — see below
|
|
1233
|
+
* });
|
|
1234
|
+
*
|
|
1235
|
+
* // later (component unmount, page change):
|
|
1236
|
+
* sub.close();
|
|
1237
|
+
* ```
|
|
1238
|
+
*
|
|
1239
|
+
* ## Reconcile on connect
|
|
1240
|
+
*
|
|
1241
|
+
* Events are at-most-once nudges: nothing is buffered while you're
|
|
1242
|
+
* disconnected and nothing is replayed when you reconnect. `onConnect` fires
|
|
1243
|
+
* on EVERY (re)connect — network drop, grant expiry, server deploy — and is
|
|
1244
|
+
* where you refetch current state. Subscribe for speed, reconcile for truth;
|
|
1245
|
+
* a subscriber without an `onConnect` refetch will silently miss whatever
|
|
1246
|
+
* happened while it was away.
|
|
1247
|
+
*
|
|
1248
|
+
* ## Lifecycle
|
|
1249
|
+
*
|
|
1250
|
+
* - Grant expiry (`grant_expired` frame, every TTL window) → the SDK calls
|
|
1251
|
+
* `getToken` again and reconnects. Invisible to the app beyond `onConnect`.
|
|
1252
|
+
* - Network drop → reconnect with backoff, reusing the still-valid token (no
|
|
1253
|
+
* method invocation wasted).
|
|
1254
|
+
* - `getToken` throwing → terminal: your own method refused (logged out, role
|
|
1255
|
+
* revoked), so the SDK calls `onError` and closes rather than retrying an
|
|
1256
|
+
* authorization refusal in a loop. Call `connect` again when your app's
|
|
1257
|
+
* auth state changes.
|
|
1258
|
+
*/
|
|
1259
|
+
/** One published event, delivered on a channel the grant names. */
|
|
1260
|
+
interface AppEvent {
|
|
1261
|
+
channel: string;
|
|
1262
|
+
data: unknown;
|
|
1263
|
+
/** Publish time, milliseconds since epoch. */
|
|
1264
|
+
ts: number;
|
|
1265
|
+
}
|
|
1266
|
+
interface EventsConnectOptions {
|
|
1267
|
+
/**
|
|
1268
|
+
* Return a subscribe token — typically by invoking the backend method that
|
|
1269
|
+
* mints the grant (`events.grant` in the agent SDK) after its own auth
|
|
1270
|
+
* checks. Called on first connect and again after every grant expiry.
|
|
1271
|
+
* Throwing is terminal: `onError` fires and the subscription closes.
|
|
1272
|
+
*/
|
|
1273
|
+
getToken: () => Promise<string> | string;
|
|
1274
|
+
/** A published event on one of the grant's channels. */
|
|
1275
|
+
onEvent: (event: AppEvent) => void;
|
|
1276
|
+
/**
|
|
1277
|
+
* Fires on every (re)connect. Refetch current state here — events are
|
|
1278
|
+
* at-most-once, so this is the correctness half of the contract.
|
|
1279
|
+
*/
|
|
1280
|
+
onConnect?: () => void;
|
|
1281
|
+
/** Terminal failures only (a `getToken` refusal). Reconnects are silent. */
|
|
1282
|
+
onError?: (error: Error) => void;
|
|
1283
|
+
}
|
|
1284
|
+
interface EventsSubscription {
|
|
1285
|
+
/** Stop the stream and all reconnection. Idempotent. */
|
|
1286
|
+
close(): void;
|
|
1287
|
+
/** `connecting` | `open` | `closed` */
|
|
1288
|
+
readonly state: 'connecting' | 'open' | 'closed';
|
|
1289
|
+
}
|
|
1290
|
+
declare function connect(options: EventsConnectOptions): EventsSubscription;
|
|
1291
|
+
/**
|
|
1292
|
+
* The app-events client. `connect` opens a managed subscription — reconnects,
|
|
1293
|
+
* grant re-minting and backoff are handled internally; the app supplies the
|
|
1294
|
+
* token source and the handlers.
|
|
1295
|
+
*/
|
|
1296
|
+
declare const events: {
|
|
1297
|
+
connect: typeof connect;
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1164
1300
|
/**
|
|
1165
1301
|
* Error class for the @mindstudio-ai/interface SDK.
|
|
1166
1302
|
*
|
|
@@ -1223,4 +1359,4 @@ type Breadcrumb = {
|
|
|
1223
1359
|
timestamp: number;
|
|
1224
1360
|
};
|
|
1225
1361
|
|
|
1226
|
-
export { type AbortablePromise, type AgentChatClient, type AgentChatEvent, type AnalyticsClient, type AppUser, type Auth, type AuthStatus, type BootstrapConfig, type Breadcrumb, type Country, type CustomEventProps, type InvokeOptions, type Message, MindStudioInterfaceError, type SendMessageCallbacks, type SendMessageOptions, type SendMessageResult, type SignInWithRemyOptions, type Thread, type ThreadListPage, type ThreadSummary, type UploadFileOptions, type UploadToken, analytics, auth, createAgentChatClient, createClient, platform };
|
|
1362
|
+
export { type AbortablePromise, type AgentChatClient, type AgentChatEvent, type AnalyticsClient, type AppEvent, type AppUser, type Auth, type AuthStatus, type BootstrapConfig, type Breadcrumb, type Country, type CustomEventProps, type EventsConnectOptions, type EventsSubscription, type InvokeOptions, type Message, MindStudioInterfaceError, type SendMessageCallbacks, type SendMessageOptions, type SendMessageResult, type SignInWithRemyOptions, type Thread, type ThreadListPage, type ThreadSummary, type UploadFileOptions, type UploadToken, analytics, auth, createAgentChatClient, createClient, events, platform };
|