@sable-ai/sdk-core 0.1.4 → 0.1.5

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 CHANGED
@@ -1,88 +1,332 @@
1
- # @sable-ai/sdk-core
1
+ # Adding a Sable agent to your website
2
2
 
3
- Browser runtime for Sable: joins a LiveKit room and lets users talk to an
4
- agent worker with optional vision + browser-bridge tools. Ships as a
5
- Stripe.js-style split bundle:
3
+ > **Beta.** The platform UI, the SDK API, and the error codes described below
4
+ > may still change before general availability. If you're integrating now,
5
+ > pin the SDK to an exact version.
6
6
 
7
- | File | Size (gzipped) | When it loads |
8
- | --- | --- | --- |
9
- | `sable.js` (loader) | **~530 B** | Page load |
10
- | `sable-core.mjs` (full SDK, livekit inlined) | ~150 KB | First `Sable.start()` call |
7
+ Sable is a voice + vision agent that lives inside your users' browsers. Drop
8
+ a single `<script>` tag on the pages where you want it, call `Sable.start()`,
9
+ and an agent you configured on the Sable platform can talk to your user and
10
+ see what they see no iframes, no overlays you don't control, no changes to
11
+ your backend.
12
+
13
+ This guide walks you through the end-to-end integration as a web engineer at
14
+ a company that already has a Sable account.
15
+
16
+ ---
17
+
18
+ ## 1. Create an agent on the Sable platform
19
+
20
+ 1. Sign in to <https://platform.withsable.com>.
21
+ 2. **Agents → New agent**. Give it a name and pick a template (voice-only,
22
+ voice + vision, etc).
23
+ 3. Write the agent's system prompt and configure its tools. This is the same
24
+ agent config you'd use in any Sable channel — the SDK just exposes it in
25
+ your user's browser instead of a Sable-hosted call page.
26
+ 4. Publish the agent. You'll land on the agent detail page.
27
+
28
+ ## 2. Add your domain to the allowlist
29
+
30
+ Still on the agent detail page:
31
+
32
+ 1. Open the **Web SDK** tab.
33
+ 2. Under **Allowed domains**, add every origin where you intend to load the
34
+ SDK. Exact hostnames only — `example.com`, `www.example.com`,
35
+ `app.example.com`. Wildcards are supported as a leading `*.`
36
+ (e.g. `*.example.com` covers every subdomain).
37
+ 3. Save.
38
+
39
+ Why this matters: the SDK is loaded inside the user's browser and talks
40
+ directly to the Sable API from the page. Sable rejects requests from any
41
+ origin that isn't on this list, so adding your domain here is the gate
42
+ that lets the script tag on your site actually connect. **If you forget
43
+ this step, you'll see a CORS error in the devtools console and nothing
44
+ else — the call never starts.**
45
+
46
+ Allowed domains are scoped to the agent. An agent for your marketing site
47
+ and an agent for your product dashboard can have different allowlists.
48
+
49
+ ## 3. Copy your public key
50
+
51
+ On the same **Web SDK** tab, under **Public key**, you'll see a value like:
52
+
53
+ ```
54
+ pk_live_8f3a9c2d4e5b6a7c
55
+ ```
11
56
 
12
- Pages that never open a session pay only the loader cost.
57
+ This is the key you'll paste into your page. It's a **publishable** key —
58
+ safe to ship in client-side code, visible in devtools, not a secret. It
59
+ identifies the agent and is validated by Sable against the allowed-domains
60
+ list above, so even if someone copies it onto their own site it won't work
61
+ unless their origin is on your allowlist.
13
62
 
14
- ## Install
63
+ If you ever need to rotate it (e.g. the key shows up somewhere you don't
64
+ want it), click **Rotate key** and update the snippet on your site. The old
65
+ key is immediately invalidated.
15
66
 
16
- ### Script tag (recommended for no-build sites + extensions)
67
+ ## 4. Load the SDK
68
+
69
+ You have two options. Both expose the same API; pick whichever fits your
70
+ stack.
71
+
72
+ ### Option A: Script tag (works everywhere)
73
+
74
+ Add this to every page you want the agent to be available on:
17
75
 
18
76
  ```html
19
- <script src="https://sdk.withsable.com/v0.1.4/sable.js"></script>
77
+ <script src="https://sdk.withsable.com/v1/sable.js" async></script>
20
78
  ```
21
79
 
22
- The loader installs `window.Sable` synchronously. On the first
23
- `Sable.start()` call it lazy-imports `sable-core.mjs` from the same CDN
24
- path and forwards the call.
80
+ That's a **~530 B gzipped loader stub** no dependencies, no
81
+ stylesheet, no livekit, no vision runtime. The loader installs a single
82
+ global (`window.Sable`) and does nothing else until you call `start()`:
83
+ no network requests, no microphone prompt, no DOM mutations. Idle cost
84
+ to your site is effectively zero.
85
+
86
+ On the first `Sable.start()` call, the loader dynamic-imports the full
87
+ SDK (`sable-core.mjs`, ~150 KB gzipped with livekit-client inlined) from
88
+ the **same CDN path** it was served from — so loading
89
+ `https://sdk.withsable.com/v0.1.5/sable.js` always pulls
90
+ `https://sdk.withsable.com/v0.1.5/sable-core.mjs`, and version cohesion
91
+ is automatic. Pages that never start a session pay only the loader cost;
92
+ pages that do start a session pay the core download exactly once, cached
93
+ aggressively for the lifetime of the version pin.
25
94
 
26
- Available CDN pins:
95
+ The script is served from Sable's CDN (`sdk.withsable.com`, Cloudflare
96
+ Pages), cached at the edge, and versioned. Three path conventions:
27
97
 
28
- | Path | Cache | Use case |
98
+ | URL | Cache | Use case |
29
99
  | --- | --- | --- |
30
- | `https://sdk.withsable.com/v0.1.4/sable.js` | 1 year, immutable | **Recommended for production** — exact version pin |
31
- | `https://sdk.withsable.com/v1/sable.js` | 1 hour | Latest 0.x (accepts patch releases automatically) |
100
+ | `https://sdk.withsable.com/v0.1.5/sable.js` | 1 year, immutable | **Recommended for production** — exact version pin |
101
+ | `https://sdk.withsable.com/v1/sable.js` | 1 hour | Latest `0.x` accepts patch releases automatically |
32
102
  | `https://sdk.withsable.com/latest/sable.js` | 5 minutes | Demos and smoke tests only |
33
103
 
34
- ### npm (ESM)
104
+ `v1` is a stable major line — the API won't change without a major
105
+ bump. Pin to an exact version (`v0.1.5`) if you want bit-for-bit
106
+ reproducibility.
107
+
108
+ ### Option B: npm package (React, Vue, Svelte, Next, etc.)
35
109
 
36
110
  ```bash
37
- bun add @sable-ai/sdk-core
111
+ npm install @sable-ai/sdk-core
38
112
  ```
39
113
 
40
114
  ```js
41
115
  import Sable from "@sable-ai/sdk-core";
42
- // Importing also installs window.Sable (first-write-wins), so mixed
43
- // script-tag + npm usage on the same page stays coherent.
116
+
117
+ await Sable.start({ publicKey: "pk_live_..." });
44
118
  ```
45
119
 
46
- The npm build keeps `livekit-client` as an external peer so your bundler
47
- can dedupe it.
120
+ The npm package is a standalone ESM bundle (livekit-client declared as a
121
+ regular dependency so your bundler can dedupe it) — it does **not**
122
+ fetch from the CDN. Importing the module also installs `window.Sable`
123
+ with first-write-wins semantics, so mixing the script tag and the npm
124
+ package on the same page is safe: whichever loads first installs the
125
+ global and the other becomes a no-op. You get TypeScript types and
126
+ bundler-integrated imports. Use this path if you want typed autocomplete
127
+ and a bundler-controlled dependency graph.
128
+
129
+ ## 5. Start a session from your app code
48
130
 
49
- ## Usage
131
+ When you want the agent to actually connect — on a button click, on page
132
+ load, when the user opens a help menu, whatever — call:
50
133
 
51
134
  ```js
52
135
  await window.Sable.start({
53
- agentPublicId: "agt_...",
54
- apiUrl: "https://sable-api-gateway-9dfmhij9.wl.gateway.dev", // optional, default
55
- nickelRegion: "us-east1", // optional
136
+ publicKey: "pk_live_8f3a9c2d4e5b6a7c",
137
+
138
+ // Optional what the agent can see.
139
+ vision: {
140
+ enabled: true,
141
+ // How frames are produced. Defaults to the built-in wireframe renderer.
142
+ // Discriminated on `type`:
143
+ // { type: "wireframe", features: { includeImages?: boolean } }
144
+ // { type: "fn", captureFn: () => HTMLCanvasElement | ImageBitmap }
145
+ frameSource: {
146
+ type: "wireframe",
147
+ rate: 2, // frames per second; default 2
148
+ features: {
149
+ includeImages: true, // include rendered images, not just layout boxes
150
+ },
151
+ },
152
+ },
153
+
154
+ // Optional — implementations for methods the agent can RPC into your page.
155
+ // The SDK defines a small set of "UI stub" methods (e.g. showMessage,
156
+ // highlightElement). You can override any of them here, and add your own
157
+ // for agent tools specific to your app. Anything you pass becomes callable
158
+ // by the agent.
159
+ runtime: {
160
+ showMessage: (text) => myToast.show(text),
161
+ highlightElement: (selector) => { /* ... */ },
162
+ openDocument: (docId) => router.push(`/docs/${docId}`),
163
+ },
164
+
165
+ // Optional — arbitrary context forwarded to the agent at session start.
166
+ // Surfaces verbatim in the agent's initial prompt.
167
+ context: {
168
+ userId: currentUser.id,
169
+ userName: currentUser.name,
170
+ currentPage: "dashboard",
171
+ },
56
172
  });
173
+ ```
57
174
 
58
- // ... user talks to the agent ...
175
+ `Sable.start()` returns a promise that resolves when the mic is live and
176
+ the agent has greeted the user. It rejects if the public key is invalid,
177
+ the origin isn't on the allowlist, or the user denies microphone access.
59
178
 
179
+ To end the session:
180
+
181
+ ```js
60
182
  await window.Sable.stop();
61
183
  ```
62
184
 
63
- `Sable.on(event, handler)` subscriptions registered before `start()`
64
- resolves are buffered by the loader and flushed onto the real session
65
- once `sable-core.mjs` finishes loading.
185
+ You can call `start()` and `stop()` as many times as you like during a page
186
+ lifetime. Only one session can be active at a time.
187
+
188
+ ## 6. React to session events (optional)
189
+
190
+ If you want to, say, show your own UI when the agent is talking, subscribe
191
+ to events:
192
+
193
+ ```js
194
+ window.Sable.on("session:started", () => { ... });
195
+ window.Sable.on("session:ended", (reason) => { ... });
196
+ window.Sable.on("agent:speaking", (speaking) => { ... });
197
+ window.Sable.on("user:speaking", (speaking) => { ... });
198
+ window.Sable.on("error", (err) => { ... });
199
+ ```
200
+
201
+ All events are fire-and-forget; the SDK does not care whether you subscribe.
202
+
203
+ ---
204
+
205
+ ## Reference: `Sable.start()` options
206
+
207
+ | Option | Type | Default | Description |
208
+ | --- | --- | --- | --- |
209
+ | `publicKey` | `string` | **required** | The `pk_live_*` key from the platform. |
210
+ | `vision.enabled` | `boolean` | `false` | Whether to publish a video track of the page to the agent. |
211
+ | `vision.frameSource` | `FrameSource` | `{ type: "wireframe" }` | Where frames come from. Either the built-in wireframe renderer or a custom function (see below). |
212
+ | `vision.frameSource.rate` | `number` | `2` | Capture rate in frames per second. Applies to both `wireframe` and `fn`. Higher = more responsive agent vision, more bandwidth. |
213
+ | `vision.frameSource.features.includeImages` | `boolean` | `true` | (Wireframe only.) Include rendered images, not just layout boxes. Set to `false` to ship only layout boxes (lower bandwidth). |
214
+ | `vision.frameSource.captureFn` | `() => HTMLCanvasElement \| ImageBitmap` | — | (Required when `type: "fn"`.) Called at `rate` Hz. Useful for feeding the agent a custom canvas (e.g. a 3D scene, a video element, a WebGL surface). |
215
+ | `runtime` | `Record<string, Function>` | `{}` | Implementations for UI-stub methods the agent can call, plus any additional methods you want to expose as agent tools. |
216
+ | `context` | `Record<string, unknown>` | `{}` | Forwarded verbatim to the agent at session start. Appears in the agent's initial prompt. |
217
+
218
+ ## Reference: errors
219
+
220
+ | Code | Cause | Fix |
221
+ | --- | --- | --- |
222
+ | `SABLE_INVALID_KEY` | Public key doesn't exist or was rotated. | Copy the current key from the platform. |
223
+ | `SABLE_ORIGIN_NOT_ALLOWED` | Page's origin isn't on the agent's allowlist. | Add the exact origin in the platform's Web SDK → Allowed domains. |
224
+ | `SABLE_MIC_DENIED` | User denied microphone permission. | Prompt them again, or show a message explaining why voice is needed. |
225
+ | `SABLE_RATE_LIMITED` | Too many sessions started from this origin in a short window. | Back off and retry. |
226
+ | `SABLE_NETWORK` | Couldn't reach the Sable API. | Usually transient — retry with backoff. |
227
+
228
+ ---
229
+
230
+ ## FAQ
231
+
232
+ **Do I need a backend integration?**
233
+ No. The SDK talks directly to the Sable API from the page. There's no
234
+ webhook to install, no server-to-server auth, no token exchange you have to
235
+ implement. The public key + allowed-domains check is the entire trust model.
236
+
237
+ **Is the public key a secret?**
238
+ No — it's designed to be shipped in client-side code. The security boundary
239
+ is the allowed-domains list, not the key itself. Someone who copies your
240
+ key to their own site can't use it unless their origin is also on your
241
+ allowlist.
66
242
 
67
- ## Architecture
243
+ **Script tag or npm package — which should I use?**
244
+ Either. Use the script tag for static HTML or if you want to avoid
245
+ bundler configuration — you get the Stripe.js-style split bundle
246
+ (~530 B on page load, core lazy-loaded from the CDN on first
247
+ `Sable.start()`). Use the npm package if you want TypeScript types and a
248
+ bundler-integrated, self-contained dependency graph — the package is a
249
+ standalone ESM build that does not fetch from the CDN. Mixing on the
250
+ same page is safe: both install `window.Sable` with first-write-wins
251
+ semantics, so whichever runs first wins and the other becomes a no-op.
68
252
 
69
- The loader is a tiny IIFE proxy (`src/loader.ts`) that:
253
+ **Why a global (`window.Sable`) instead of an import-only API?**
254
+ To keep the drop-in story honest. A single `<script>` tag works in every
255
+ web framework — React, Vue, Svelte, Next, Rails, plain HTML — without
256
+ bundler setup. The npm package also installs the same global on import,
257
+ so mixed usage stays coherent.
70
258
 
71
- 1. Captures `document.currentScript.src` synchronously at top-level
72
- (the only time `currentScript` is defined).
73
- 2. Installs `window.Sable` with stub `start/stop/on` methods.
74
- 3. On first `start()`, resolves `new URL("./sable-core.mjs", scriptSrc)`
75
- and dynamically imports it so `/v0.1.4/sable.js` always pulls
76
- `/v0.1.4/sable-core.mjs`. Version cohesion is automatic.
77
- 4. Memoises the import and flushes any queued `on()` subscriptions.
259
+ **Does the SDK render anything by default?**
260
+ The SDK itself is headless — it handles voice, vision, and agent
261
+ communication but renders nothing on its own. A default UI (mic button,
262
+ avatar, agent-driven overlays) can be enabled via a separate package, or
263
+ you can build your own using the event API and your own component library.
78
264
 
79
- See `docs/superpowers/specs/` and `infra/cdn/README.md` for more.
265
+ **Does the agent see my users' data?**
266
+ Only what's on the page at the moment the session is active, and only if
267
+ `vision.enabled` is `true`. The wireframe is generated client-side and
268
+ streamed as a video track; Sable never scrapes, indexes, or stores page
269
+ content server-side. Microphone audio is end-to-end within the voice
270
+ session.
80
271
 
81
- ## Local development
272
+ **What happens if the user navigates away?**
273
+ The session ends. The SDK tears down the connection, stops the microphone,
274
+ and unmounts anything it mounted. A fresh `Sable.start()` on the next page
275
+ creates a new session.
276
+
277
+ **How do I test this locally?**
278
+ Add `http://localhost:3000` (or whatever port you use) to the allowed
279
+ domains list alongside your production origin. The SDK treats localhost
280
+ the same as any other origin — there's no special dev bypass.
281
+
282
+ **Can I self-host the script?**
283
+ Not recommended. The CDN copy is versioned, cached globally, and updated
284
+ automatically. If you really need to — e.g. an air-gapped customer —
285
+ you need to host **both** files side-by-side under the same path:
286
+ `sable.js` (the loader) and `sable-core.mjs` (the full SDK). The loader
287
+ resolves the core via `new URL("./sable-core.mjs", document.currentScript.src)`,
288
+ so they must live in the same directory. Contact support for details.
289
+
290
+ **Does the SDK work with strict CSP (`script-src` nonces, etc.)?**
291
+ Yes. Apply your nonce to the `<script>` tag as usual. The loader
292
+ dynamic-imports `sable-core.mjs` from the **same origin** it was served
293
+ from (`sdk.withsable.com` by default), so your CSP needs to allow that
294
+ origin in `script-src` — typically `script-src 'self' https://sdk.withsable.com`
295
+ if you load from the public CDN. No third-party origins are contacted
296
+ at runtime beyond `sdk.withsable.com` (for the core bundle) and
297
+ `sable-api-gateway-9dfmhij9.wl.gateway.dev` (for the session — plus
298
+ the LiveKit WebSocket origin returned by the session endpoint). If
299
+ you self-host `sable.js` + `sable-core.mjs` under your own origin,
300
+ `'self'` covers the script side; the API + LiveKit origins still
301
+ need to be in `connect-src`.
302
+
303
+ ---
304
+
305
+ ## Development
306
+
307
+ > This section is for contributors to `@sable-ai/sdk-core` itself. If
308
+ > you're integrating Sable into your site, stop here.
309
+
310
+ The package source lives at
311
+ [`sable-inc/js-sdk`](https://github.com/sable-inc/js-sdk) under
312
+ `packages/sdk-core`. Build:
82
313
 
83
314
  ```bash
84
315
  bun install
85
316
  bun run --filter @sable-ai/sdk-core build
317
+ ```
318
+
319
+ This produces three artifacts in `dist/`:
320
+
321
+ | Artifact | Entry | Consumer |
322
+ | --- | --- | --- |
323
+ | `dist/sable.iife.js` | `src/loader.ts` (minified IIFE) | CDN script tag |
324
+ | `dist/sable-core.mjs` | `src/index.ts` (minified ESM, livekit inlined) | CDN lazy-import from the loader |
325
+ | `dist/esm/index.js` + `dist/types/` | `src/index.ts` (ESM, livekit external) | `npm install @sable-ai/sdk-core` |
326
+
327
+ ### Local smoke test
328
+
329
+ ```bash
86
330
  python3 -m http.server 5173 --directory packages/sdk-core
87
331
  ```
88
332
 
@@ -92,14 +336,14 @@ ID, click Start. Grant mic permission when prompted.
92
336
  **Only `http://localhost:*` origins work against the hosted API** —
93
337
  `sable-api`'s CORS policy does not yet allow arbitrary origins.
94
338
 
95
- ## Releasing
339
+ ### Releasing
96
340
 
97
- Tagging `sdk-core-v<x.y.z>` triggers `.github/workflows/release-sdk-core.yml`
98
- which:
341
+ Tagging `sdk-core-v<x.y.z>` triggers
342
+ `.github/workflows/release-sdk-core.yml`, which:
99
343
 
100
- 1. Runs typecheck + tests + build.
344
+ 1. Runs typecheck + tests + `bun run build`.
101
345
  2. Publishes to npm with OIDC trusted-publisher provenance.
102
- 3. Stages `dist/sable.iife.js` + `dist/sable-core.mjs` under
103
- `/v<version>/`, `/v1/`, and `/latest/` via `infra/cdn/stage.sh`.
346
+ 3. Stages `dist/sable.iife.js` + `dist/sable-core.mjs` under `/v<version>/`,
347
+ `/v1/`, and `/latest/` via `infra/cdn/stage.sh`.
104
348
  4. Deploys the staged directory to Cloudflare Pages
105
349
  (`sable-sdk-cdn` project, custom domain `sdk.withsable.com`).
package/dist/esm/index.js CHANGED
@@ -2001,7 +2001,7 @@ async function publishCanvasAsVideoTrack(room, lib, canvas, fps) {
2001
2001
  var DEFAULT_FRAME_SOURCE = {
2002
2002
  type: "wireframe",
2003
2003
  rate: 2,
2004
- features: { includeImages: false }
2004
+ features: { includeImages: true }
2005
2005
  };
2006
2006
  async function startVision(args) {
2007
2007
  const source = args.options.frameSource ?? DEFAULT_FRAME_SOURCE;
@@ -1483,4 +1483,4 @@ console.log(' await new Wireframe().toDataURL() — base64 for LLM');
1483
1483
  });
1484
1484
  ${is};
1485
1485
  return Wireframe;
1486
- })()`);return _n}var vn=null;function ru(){if(!vn)vn=(0,eval)(`(${ss})`);return vn}async function cs(){let t=ru()();return{screenshot_jpeg_b64:(await new(Dl())(document.body,{}).toDataURL()).replace(/^data:[^,]+,/,""),elements:t,viewport:{width:window.innerWidth,height:window.innerHeight},url:window.location.href}}async function hs(){let t=()=>new Promise((e)=>requestAnimationFrame(()=>requestAnimationFrame(()=>e())));await t(),await new Promise((e)=>{let r=performance.now(),o=performance.now(),s=new MutationObserver(()=>{o=performance.now()});s.observe(document.documentElement,{subtree:!0,childList:!0,attributes:!0,characterData:!0});let c=()=>{let i=performance.now();if(i-o>=30){s.disconnect(),e();return}if(i-r>=30){s.disconnect(),e();return}requestAnimationFrame(c)};requestAnimationFrame(c)}),await t()}function Qe(t,e){return async(l)=>{let n={};try{n=l.payload?JSON.parse(l.payload):{}}catch(r){console.warn(`[Sable] ${t}: bad JSON payload`,r)}try{let r=await e(n);return JSON.stringify(r??{})}catch(r){let o=r instanceof Error?r.message:String(r);return console.warn(`[Sable] ${t}: handler error`,o),JSON.stringify({error:o})}}}function fs(t){t.registerRpcMethod("browser.execute_action",Qe("browser.execute_action",async(e)=>{let l=e.action;if(!l||typeof l!=="object")throw Error("execute_action: missing action");return await os(l),{}})),t.registerRpcMethod("browser.get_dom_state",Qe("browser.get_dom_state",async()=>cs())),t.registerRpcMethod("browser.get_url",Qe("browser.get_url",async()=>({url:window.location.href}))),t.registerRpcMethod("browser.get_viewport",Qe("browser.get_viewport",async()=>({width:window.innerWidth,height:window.innerHeight}))),t.registerRpcMethod("browser.verify_selector",Qe("browser.verify_selector",async(e)=>{let l=typeof e.selector==="string"?e.selector:"",n=!1;try{n=!!document.querySelector(l)}catch{n=!1}return{matches:n}})),t.registerRpcMethod("browser.settle",Qe("browser.settle",async()=>{return await hs(),{}})),console.log("[Sable] browser bridge RPCs registered")}var ou=2;function su(t){let e=typeof t==="number"&&t>0?t:ou;return Math.max(1,Math.round(1000/e))}function iu(t){let e=Math.max(1,window.innerWidth),l=Math.max(1,window.innerHeight);if(t.width!==e||t.height!==l)t.width=e,t.height=l}function us(t,e){let l=e.getContext("2d",{alpha:!1});if(!l)return console.warn("[Sable] frame source: 2d context unavailable"),()=>{};let n=su(t.rate),r=!1,o,s=!1,c=async()=>{if(r)return;if(!s){s=!0;try{if(iu(e),t.type==="wireframe"){let i=t.features?.includeImages===!0,f=new(Dl())(document.body,{images:i}),{canvas:u}=await f.capture();l.fillStyle="#ffffff",l.fillRect(0,0,e.width,e.height),l.drawImage(u,0,0,e.width,e.height)}else if(t.type==="fn"){let i=t.captureFn();l.fillStyle="#ffffff",l.fillRect(0,0,e.width,e.height),l.drawImage(i,0,0,e.width,e.height)}}catch(i){console.warn("[Sable] frame source tick failed",i)}finally{s=!1}}if(!r)o=setTimeout(c,n)};return c(),()=>{if(r=!0,o!==void 0)clearTimeout(o)}}async function ds(t,e,l,n){let o=l.captureStream(n).getVideoTracks();if(o.length===0)throw Error("canvas.captureStream produced no video tracks");let s=o[0],c=new e.LocalVideoTrack(s,void 0,!0),i=await t.localParticipant.publishTrack(c,{source:e.Track.Source.ScreenShare,name:"browser"});return console.log("[Sable] vision track published",{trackSid:i.trackSid,fps:n}),async()=>{try{await t.localParticipant.unpublishTrack(c,!0)}catch(h){console.warn("[Sable] vision unpublishTrack failed",h)}try{s.stop()}catch{}console.log("[Sable] vision track stopped")}}var cu={type:"wireframe",rate:2,features:{includeImages:!1}};async function as(t){let e=t.options.frameSource??cu,l=typeof e.rate==="number"&&e.rate>0?e.rate:2,n=document.createElement("canvas");n.width=Math.max(1,window.innerWidth),n.height=Math.max(1,window.innerHeight);let r=us(e,n),o;try{o=await ds(t.room,t.lib,n,l)}catch(s){throw r(),s}return{canvas:n,stop:async()=>{r(),await o()}}}var tr="0.1.4";function er(){try{let t=window.localStorage?.getItem("sable:debug:panel");if(!t)return{};let e=JSON.parse(t);return typeof e==="object"&&e!==null?e:{}}catch{return{}}}function bs(t){try{window.localStorage?.setItem("sable:debug:panel",JSON.stringify(t))}catch{}}function xs(t){if(t)return!0;try{if(new URL(window.location.href).searchParams.get("sable-debug")==="1")return!0}catch{}try{if(window.localStorage?.getItem("sable:debug")==="1")return!0}catch{}return!1}function gs(t){let e=er(),l=document.createElement("div");if(l.setAttribute("data-sable-debug","vision"),Object.assign(l.style,{position:"fixed",width:"240px",zIndex:"2147483647",background:"#111",color:"#ddd",border:"1px solid #444",borderRadius:"8px",font:"11px/1.3 system-ui, sans-serif",boxShadow:"0 8px 24px rgba(0,0,0,.4)",pointerEvents:"none",userSelect:"none",overflow:"hidden"}),typeof e.left==="number"&&typeof e.top==="number")l.style.left=`${e.left}px`,l.style.top=`${e.top}px`;else l.style.right="12px",l.style.top="12px";let n=document.createElement("div");Object.assign(n.style,{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"6px",padding:"6px 8px",background:"#1a1a1a",borderBottom:"1px solid #333",cursor:"move",pointerEvents:"auto"});let r=document.createElement("div");r.textContent="sable: agent vision",Object.assign(r.style,{opacity:"0.75",fontWeight:"600",flex:"1",pointerEvents:"none"}),n.appendChild(r);let o=document.createElement("button");o.setAttribute("aria-label","Minimize vision panel"),Object.assign(o.style,{background:"transparent",color:"#ddd",border:"1px solid #444",borderRadius:"4px",width:"20px",height:"20px",cursor:"pointer",fontSize:"12px",lineHeight:"1",padding:"0",pointerEvents:"auto"}),n.appendChild(o),l.appendChild(n);let s=document.createElement("div");Object.assign(s.style,{padding:"6px",background:"#111",pointerEvents:"none"}),t.style.width="100%",t.style.height="auto",t.style.display="block",t.style.background="#fff",t.style.borderRadius="4px",t.style.pointerEvents="none",s.appendChild(t),l.appendChild(s);let c=!!e.minimized,i=()=>{s.style.display=c?"none":"block",o.textContent=c?"▢":"–",o.setAttribute("aria-label",c?"Restore vision panel":"Minimize vision panel")};i(),o.addEventListener("click",(x)=>{x.stopPropagation(),c=!c,i(),bs({...er(),minimized:c})});let h=null,f=(x)=>{if(x.target instanceof HTMLElement&&x.target.closest("button"))return;let g=l.getBoundingClientRect();h={offsetX:x.clientX-g.left,offsetY:x.clientY-g.top},l.style.left=`${g.left}px`,l.style.top=`${g.top}px`,l.style.right="auto",l.style.bottom="auto",n.setPointerCapture(x.pointerId),x.preventDefault()},u=(x)=>{if(!h)return;let{innerWidth:g,innerHeight:w}=window,p=l.offsetWidth,y=x.clientX-h.offsetX,$=x.clientY-h.offsetY;y=Math.min(Math.max(y,-p+48),g-48),$=Math.min(Math.max($,0),w-24),l.style.left=`${y}px`,l.style.top=`${$}px`},d=(x)=>{if(!h)return;h=null;try{n.releasePointerCapture(x.pointerId)}catch{}let g=parseFloat(l.style.left)||0,w=parseFloat(l.style.top)||0;bs({...er(),left:g,top:w})};return n.addEventListener("pointerdown",f),n.addEventListener("pointermove",u),n.addEventListener("pointerup",d),n.addEventListener("pointercancel",d),document.body.appendChild(l),console.log("[Sable] debug vision panel mounted",{minimized:c,restoredPosition:typeof e.left==="number"&&typeof e.top==="number"}),()=>{try{n.removeEventListener("pointerdown",f),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",d),n.removeEventListener("pointercancel",d),l.remove()}catch{}}}var Nx=5,Tf=500;function Mx(t){let e=t.remoteParticipants?Array.from(t.remoteParticipants.values()):[];return e.find((n)=>typeof n.identity==="string"&&n.identity.startsWith("agent"))?.identity??e[0]?.identity??null}async function Hx(t){for(let e=1;e<=Nx;e++){let l=Mx(t);if(!l){console.warn("[Sable] sendUiReady: no agent participant yet",{attempt:e}),await new Promise((n)=>setTimeout(n,Tf));continue}try{await t.localParticipant.performRpc({destinationIdentity:l,method:"uiReady",payload:JSON.stringify({timestamp:Date.now()})}),console.log("[Sable] uiReady sent",{identity:l,attempt:e});return}catch(n){console.warn("[Sable] uiReady RPC failed",{attempt:e,err:n}),await new Promise((r)=>setTimeout(r,Tf))}}console.error("[Sable] uiReady: exhausted retries — agent will not greet")}class _o{version=tr;emitter=new kn;activeRoom=null;visionHandle=null;unmountDebugPanel=null;on(t,e){return this.emitter.on(t,e)}async start(t){if(this.activeRoom)throw Error("Sable already started; call stop() first");let e=t.publicKey??t.agentPublicId;if(!e)throw Error("Sable.start: `publicKey` is required");let l=t.apiUrl??ts;console.log("[Sable] fetching connection details",{apiUrl:l});let n=await es({apiUrl:l,publicKey:e});console.log("[Sable] connection details received",{roomName:n.roomName,participantName:n.participantName});let r=await Promise.resolve().then(() => (qf(),jf)),{Room:o,RoomEvent:s,LocalVideoTrack:c,Track:i}=r,h=new o,f={LocalVideoTrack:c,Track:i};if(h.registerRpcMethod("agentReady",async()=>{return console.log("[Sable] RPC agentReady received"),Hx(h),JSON.stringify({success:!0})}),fs(h),rs(h,t.runtime),this.wireRoomEvents(h,s),await h.connect(n.serverUrl,n.participantToken),await h.localParticipant.setMicrophoneEnabled(!0),this.activeRoom=h,t.vision?.enabled)try{if(this.visionHandle=await as({room:h,lib:f,options:t.vision}),xs(t.debug))this.unmountDebugPanel=gs(this.visionHandle.canvas)}catch(u){console.warn("[Sable] failed to start vision",u)}console.log("[Sable] session live",{roomName:n.roomName,participantName:n.participantName}),this.emitter.emit("session:started",{roomName:n.roomName,participantName:n.participantName}),setTimeout(()=>{if(this.activeRoom!==h)return;let u=h,x=(u.remoteParticipants?Array.from(u.remoteParticipants.values()):[]).map((w)=>({identity:w.identity,tracks:w.trackPublications?Array.from(w.trackPublications.values()).map((p)=>({kind:p.kind,subscribed:p.isSubscribed})):[]}));if(!x.some((w)=>w.tracks.some((p)=>p.kind==="audio")))console.warn("[Sable] no remote audio track after 10s — agent worker probably failed to publish. Remote participants:",x)},1e4)}async stop(){let t=this.activeRoom;if(!t)return;if(this.activeRoom=null,this.unmountDebugPanel){try{this.unmountDebugPanel()}catch(e){console.warn("[Sable] debug panel unmount failed",e)}this.unmountDebugPanel=null}if(this.visionHandle){try{await this.visionHandle.stop()}catch(e){console.warn("[Sable] vision stop failed",e)}this.visionHandle=null}try{await t.localParticipant.setMicrophoneEnabled(!1)}catch(e){console.warn("[Sable] setMicrophoneEnabled(false) failed",e)}await t.disconnect(),console.log("[Sable] session ended"),this.emitter.emit("session:ended",{})}wireRoomEvents(t,e){t.on(e.ConnectionStateChanged,(l)=>{console.log("[Sable] ConnectionStateChanged",l)}),t.on(e.Disconnected,(l)=>{if(console.log("[Sable] Disconnected",l),this.activeRoom===t)this.stop().catch((n)=>console.warn("[Sable] stop on disconnect failed",n))}),t.on(e.ParticipantConnected,(l)=>{let n=l;console.log("[Sable] ParticipantConnected",{identity:n.identity,sid:n.sid})}),t.on(e.ParticipantDisconnected,(l)=>{console.warn("[Sable] ParticipantDisconnected",{identity:l.identity})}),t.on(e.TrackSubscribed,(l,n,r)=>{let o=l,s=r;if(console.log("[Sable] TrackSubscribed",{kind:o.kind,participant:s.identity}),o.kind==="audio"&&typeof o.attach==="function"){let c=o.attach();c.setAttribute("data-sable","1"),c.setAttribute("playsinline",""),c.autoplay=!0,document.body.appendChild(c),console.log("[Sable] attached remote audio element")}}),t.on(e.TrackUnsubscribed,(l)=>{let n=l;if(typeof n.detach==="function")for(let r of n.detach())r.remove()}),t.on(e.ActiveSpeakersChanged,(l)=>{let n=l??[],r=n.some((s)=>typeof s.identity==="string"&&s.identity.startsWith("agent")),o=n.some((s)=>typeof s.identity==="string"&&!s.identity.startsWith("agent"));this.emitter.emit("agent:speaking",r),this.emitter.emit("user:speaking",o)})}}var Sn=new _o;function Ff(){if(typeof window>"u")return;if(window.Sable)return;window.Sable=Sn,console.log("[Sable] SDK loaded",Sn.version)}Ff();var Ag=Sn;export{Ag as default,tr as VERSION};
1486
+ })()`);return _n}var vn=null;function ru(){if(!vn)vn=(0,eval)(`(${ss})`);return vn}async function cs(){let t=ru()();return{screenshot_jpeg_b64:(await new(Dl())(document.body,{}).toDataURL()).replace(/^data:[^,]+,/,""),elements:t,viewport:{width:window.innerWidth,height:window.innerHeight},url:window.location.href}}async function hs(){let t=()=>new Promise((e)=>requestAnimationFrame(()=>requestAnimationFrame(()=>e())));await t(),await new Promise((e)=>{let r=performance.now(),o=performance.now(),s=new MutationObserver(()=>{o=performance.now()});s.observe(document.documentElement,{subtree:!0,childList:!0,attributes:!0,characterData:!0});let c=()=>{let i=performance.now();if(i-o>=30){s.disconnect(),e();return}if(i-r>=30){s.disconnect(),e();return}requestAnimationFrame(c)};requestAnimationFrame(c)}),await t()}function Qe(t,e){return async(l)=>{let n={};try{n=l.payload?JSON.parse(l.payload):{}}catch(r){console.warn(`[Sable] ${t}: bad JSON payload`,r)}try{let r=await e(n);return JSON.stringify(r??{})}catch(r){let o=r instanceof Error?r.message:String(r);return console.warn(`[Sable] ${t}: handler error`,o),JSON.stringify({error:o})}}}function fs(t){t.registerRpcMethod("browser.execute_action",Qe("browser.execute_action",async(e)=>{let l=e.action;if(!l||typeof l!=="object")throw Error("execute_action: missing action");return await os(l),{}})),t.registerRpcMethod("browser.get_dom_state",Qe("browser.get_dom_state",async()=>cs())),t.registerRpcMethod("browser.get_url",Qe("browser.get_url",async()=>({url:window.location.href}))),t.registerRpcMethod("browser.get_viewport",Qe("browser.get_viewport",async()=>({width:window.innerWidth,height:window.innerHeight}))),t.registerRpcMethod("browser.verify_selector",Qe("browser.verify_selector",async(e)=>{let l=typeof e.selector==="string"?e.selector:"",n=!1;try{n=!!document.querySelector(l)}catch{n=!1}return{matches:n}})),t.registerRpcMethod("browser.settle",Qe("browser.settle",async()=>{return await hs(),{}})),console.log("[Sable] browser bridge RPCs registered")}var ou=2;function su(t){let e=typeof t==="number"&&t>0?t:ou;return Math.max(1,Math.round(1000/e))}function iu(t){let e=Math.max(1,window.innerWidth),l=Math.max(1,window.innerHeight);if(t.width!==e||t.height!==l)t.width=e,t.height=l}function us(t,e){let l=e.getContext("2d",{alpha:!1});if(!l)return console.warn("[Sable] frame source: 2d context unavailable"),()=>{};let n=su(t.rate),r=!1,o,s=!1,c=async()=>{if(r)return;if(!s){s=!0;try{if(iu(e),t.type==="wireframe"){let i=t.features?.includeImages===!0,f=new(Dl())(document.body,{images:i}),{canvas:u}=await f.capture();l.fillStyle="#ffffff",l.fillRect(0,0,e.width,e.height),l.drawImage(u,0,0,e.width,e.height)}else if(t.type==="fn"){let i=t.captureFn();l.fillStyle="#ffffff",l.fillRect(0,0,e.width,e.height),l.drawImage(i,0,0,e.width,e.height)}}catch(i){console.warn("[Sable] frame source tick failed",i)}finally{s=!1}}if(!r)o=setTimeout(c,n)};return c(),()=>{if(r=!0,o!==void 0)clearTimeout(o)}}async function ds(t,e,l,n){let o=l.captureStream(n).getVideoTracks();if(o.length===0)throw Error("canvas.captureStream produced no video tracks");let s=o[0],c=new e.LocalVideoTrack(s,void 0,!0),i=await t.localParticipant.publishTrack(c,{source:e.Track.Source.ScreenShare,name:"browser"});return console.log("[Sable] vision track published",{trackSid:i.trackSid,fps:n}),async()=>{try{await t.localParticipant.unpublishTrack(c,!0)}catch(h){console.warn("[Sable] vision unpublishTrack failed",h)}try{s.stop()}catch{}console.log("[Sable] vision track stopped")}}var cu={type:"wireframe",rate:2,features:{includeImages:!0}};async function as(t){let e=t.options.frameSource??cu,l=typeof e.rate==="number"&&e.rate>0?e.rate:2,n=document.createElement("canvas");n.width=Math.max(1,window.innerWidth),n.height=Math.max(1,window.innerHeight);let r=us(e,n),o;try{o=await ds(t.room,t.lib,n,l)}catch(s){throw r(),s}return{canvas:n,stop:async()=>{r(),await o()}}}var tr="0.1.4";function er(){try{let t=window.localStorage?.getItem("sable:debug:panel");if(!t)return{};let e=JSON.parse(t);return typeof e==="object"&&e!==null?e:{}}catch{return{}}}function bs(t){try{window.localStorage?.setItem("sable:debug:panel",JSON.stringify(t))}catch{}}function xs(t){if(t)return!0;try{if(new URL(window.location.href).searchParams.get("sable-debug")==="1")return!0}catch{}try{if(window.localStorage?.getItem("sable:debug")==="1")return!0}catch{}return!1}function gs(t){let e=er(),l=document.createElement("div");if(l.setAttribute("data-sable-debug","vision"),Object.assign(l.style,{position:"fixed",width:"240px",zIndex:"2147483647",background:"#111",color:"#ddd",border:"1px solid #444",borderRadius:"8px",font:"11px/1.3 system-ui, sans-serif",boxShadow:"0 8px 24px rgba(0,0,0,.4)",pointerEvents:"none",userSelect:"none",overflow:"hidden"}),typeof e.left==="number"&&typeof e.top==="number")l.style.left=`${e.left}px`,l.style.top=`${e.top}px`;else l.style.right="12px",l.style.top="12px";let n=document.createElement("div");Object.assign(n.style,{display:"flex",alignItems:"center",justifyContent:"space-between",gap:"6px",padding:"6px 8px",background:"#1a1a1a",borderBottom:"1px solid #333",cursor:"move",pointerEvents:"auto"});let r=document.createElement("div");r.textContent="sable: agent vision",Object.assign(r.style,{opacity:"0.75",fontWeight:"600",flex:"1",pointerEvents:"none"}),n.appendChild(r);let o=document.createElement("button");o.setAttribute("aria-label","Minimize vision panel"),Object.assign(o.style,{background:"transparent",color:"#ddd",border:"1px solid #444",borderRadius:"4px",width:"20px",height:"20px",cursor:"pointer",fontSize:"12px",lineHeight:"1",padding:"0",pointerEvents:"auto"}),n.appendChild(o),l.appendChild(n);let s=document.createElement("div");Object.assign(s.style,{padding:"6px",background:"#111",pointerEvents:"none"}),t.style.width="100%",t.style.height="auto",t.style.display="block",t.style.background="#fff",t.style.borderRadius="4px",t.style.pointerEvents="none",s.appendChild(t),l.appendChild(s);let c=!!e.minimized,i=()=>{s.style.display=c?"none":"block",o.textContent=c?"▢":"–",o.setAttribute("aria-label",c?"Restore vision panel":"Minimize vision panel")};i(),o.addEventListener("click",(x)=>{x.stopPropagation(),c=!c,i(),bs({...er(),minimized:c})});let h=null,f=(x)=>{if(x.target instanceof HTMLElement&&x.target.closest("button"))return;let g=l.getBoundingClientRect();h={offsetX:x.clientX-g.left,offsetY:x.clientY-g.top},l.style.left=`${g.left}px`,l.style.top=`${g.top}px`,l.style.right="auto",l.style.bottom="auto",n.setPointerCapture(x.pointerId),x.preventDefault()},u=(x)=>{if(!h)return;let{innerWidth:g,innerHeight:w}=window,p=l.offsetWidth,y=x.clientX-h.offsetX,$=x.clientY-h.offsetY;y=Math.min(Math.max(y,-p+48),g-48),$=Math.min(Math.max($,0),w-24),l.style.left=`${y}px`,l.style.top=`${$}px`},d=(x)=>{if(!h)return;h=null;try{n.releasePointerCapture(x.pointerId)}catch{}let g=parseFloat(l.style.left)||0,w=parseFloat(l.style.top)||0;bs({...er(),left:g,top:w})};return n.addEventListener("pointerdown",f),n.addEventListener("pointermove",u),n.addEventListener("pointerup",d),n.addEventListener("pointercancel",d),document.body.appendChild(l),console.log("[Sable] debug vision panel mounted",{minimized:c,restoredPosition:typeof e.left==="number"&&typeof e.top==="number"}),()=>{try{n.removeEventListener("pointerdown",f),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",d),n.removeEventListener("pointercancel",d),l.remove()}catch{}}}var Nx=5,Tf=500;function Mx(t){let e=t.remoteParticipants?Array.from(t.remoteParticipants.values()):[];return e.find((n)=>typeof n.identity==="string"&&n.identity.startsWith("agent"))?.identity??e[0]?.identity??null}async function Hx(t){for(let e=1;e<=Nx;e++){let l=Mx(t);if(!l){console.warn("[Sable] sendUiReady: no agent participant yet",{attempt:e}),await new Promise((n)=>setTimeout(n,Tf));continue}try{await t.localParticipant.performRpc({destinationIdentity:l,method:"uiReady",payload:JSON.stringify({timestamp:Date.now()})}),console.log("[Sable] uiReady sent",{identity:l,attempt:e});return}catch(n){console.warn("[Sable] uiReady RPC failed",{attempt:e,err:n}),await new Promise((r)=>setTimeout(r,Tf))}}console.error("[Sable] uiReady: exhausted retries — agent will not greet")}class _o{version=tr;emitter=new kn;activeRoom=null;visionHandle=null;unmountDebugPanel=null;on(t,e){return this.emitter.on(t,e)}async start(t){if(this.activeRoom)throw Error("Sable already started; call stop() first");let e=t.publicKey??t.agentPublicId;if(!e)throw Error("Sable.start: `publicKey` is required");let l=t.apiUrl??ts;console.log("[Sable] fetching connection details",{apiUrl:l});let n=await es({apiUrl:l,publicKey:e});console.log("[Sable] connection details received",{roomName:n.roomName,participantName:n.participantName});let r=await Promise.resolve().then(() => (qf(),jf)),{Room:o,RoomEvent:s,LocalVideoTrack:c,Track:i}=r,h=new o,f={LocalVideoTrack:c,Track:i};if(h.registerRpcMethod("agentReady",async()=>{return console.log("[Sable] RPC agentReady received"),Hx(h),JSON.stringify({success:!0})}),fs(h),rs(h,t.runtime),this.wireRoomEvents(h,s),await h.connect(n.serverUrl,n.participantToken),await h.localParticipant.setMicrophoneEnabled(!0),this.activeRoom=h,t.vision?.enabled)try{if(this.visionHandle=await as({room:h,lib:f,options:t.vision}),xs(t.debug))this.unmountDebugPanel=gs(this.visionHandle.canvas)}catch(u){console.warn("[Sable] failed to start vision",u)}console.log("[Sable] session live",{roomName:n.roomName,participantName:n.participantName}),this.emitter.emit("session:started",{roomName:n.roomName,participantName:n.participantName}),setTimeout(()=>{if(this.activeRoom!==h)return;let u=h,x=(u.remoteParticipants?Array.from(u.remoteParticipants.values()):[]).map((w)=>({identity:w.identity,tracks:w.trackPublications?Array.from(w.trackPublications.values()).map((p)=>({kind:p.kind,subscribed:p.isSubscribed})):[]}));if(!x.some((w)=>w.tracks.some((p)=>p.kind==="audio")))console.warn("[Sable] no remote audio track after 10s — agent worker probably failed to publish. Remote participants:",x)},1e4)}async stop(){let t=this.activeRoom;if(!t)return;if(this.activeRoom=null,this.unmountDebugPanel){try{this.unmountDebugPanel()}catch(e){console.warn("[Sable] debug panel unmount failed",e)}this.unmountDebugPanel=null}if(this.visionHandle){try{await this.visionHandle.stop()}catch(e){console.warn("[Sable] vision stop failed",e)}this.visionHandle=null}try{await t.localParticipant.setMicrophoneEnabled(!1)}catch(e){console.warn("[Sable] setMicrophoneEnabled(false) failed",e)}await t.disconnect(),console.log("[Sable] session ended"),this.emitter.emit("session:ended",{})}wireRoomEvents(t,e){t.on(e.ConnectionStateChanged,(l)=>{console.log("[Sable] ConnectionStateChanged",l)}),t.on(e.Disconnected,(l)=>{if(console.log("[Sable] Disconnected",l),this.activeRoom===t)this.stop().catch((n)=>console.warn("[Sable] stop on disconnect failed",n))}),t.on(e.ParticipantConnected,(l)=>{let n=l;console.log("[Sable] ParticipantConnected",{identity:n.identity,sid:n.sid})}),t.on(e.ParticipantDisconnected,(l)=>{console.warn("[Sable] ParticipantDisconnected",{identity:l.identity})}),t.on(e.TrackSubscribed,(l,n,r)=>{let o=l,s=r;if(console.log("[Sable] TrackSubscribed",{kind:o.kind,participant:s.identity}),o.kind==="audio"&&typeof o.attach==="function"){let c=o.attach();c.setAttribute("data-sable","1"),c.setAttribute("playsinline",""),c.autoplay=!0,document.body.appendChild(c),console.log("[Sable] attached remote audio element")}}),t.on(e.TrackUnsubscribed,(l)=>{let n=l;if(typeof n.detach==="function")for(let r of n.detach())r.remove()}),t.on(e.ActiveSpeakersChanged,(l)=>{let n=l??[],r=n.some((s)=>typeof s.identity==="string"&&s.identity.startsWith("agent")),o=n.some((s)=>typeof s.identity==="string"&&!s.identity.startsWith("agent"));this.emitter.emit("agent:speaking",r),this.emitter.emit("user:speaking",o)})}}var Sn=new _o;function Ff(){if(typeof window>"u")return;if(window.Sable)return;window.Sable=Sn,console.log("[Sable] SDK loaded",Sn.version)}Ff();var Ag=Sn;export{Ag as default,tr as VERSION};
@@ -12,7 +12,7 @@ export interface WireframeFrameSource {
12
12
  features?: {
13
13
  /**
14
14
  * Include rendered images in the wireframe (instead of placeholder boxes).
15
- * Slightly higher CPU + bandwidth. Default: false.
15
+ * Slightly higher CPU + bandwidth. Default: true.
16
16
  */
17
17
  includeImages?: boolean;
18
18
  };
@@ -39,7 +39,7 @@ export interface VisionOptions {
39
39
  enabled?: boolean;
40
40
  /**
41
41
  * Where video frames come from. Defaults to the built-in wireframe renderer
42
- * at 2 fps with images disabled.
42
+ * at 2 fps with images enabled.
43
43
  */
44
44
  frameSource?: FrameSource;
45
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sable-ai/sdk-core",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Sable SDK core — headless runtime (voice + vision + agent RPC) that runs in the user's browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@ export interface WireframeFrameSource {
19
19
  features?: {
20
20
  /**
21
21
  * Include rendered images in the wireframe (instead of placeholder boxes).
22
- * Slightly higher CPU + bandwidth. Default: false.
22
+ * Slightly higher CPU + bandwidth. Default: true.
23
23
  */
24
24
  includeImages?: boolean;
25
25
  };
@@ -51,7 +51,7 @@ export interface VisionOptions {
51
51
  enabled?: boolean;
52
52
  /**
53
53
  * Where video frames come from. Defaults to the built-in wireframe renderer
54
- * at 2 fps with images disabled.
54
+ * at 2 fps with images enabled.
55
55
  */
56
56
  frameSource?: FrameSource;
57
57
  }
@@ -26,7 +26,7 @@ export type { LiveKitPublishLib, PublishCapableRoom } from "./publisher";
26
26
  const DEFAULT_FRAME_SOURCE: FrameSource = {
27
27
  type: "wireframe",
28
28
  rate: 2,
29
- features: { includeImages: false },
29
+ features: { includeImages: true },
30
30
  };
31
31
 
32
32
  export interface StartVisionArgs {