svelte-elevenlabs 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # svelte-elevenlabs
2
2
 
3
+ [![ConversationWidget running a live ElevenLabs session](https://raw.githubusercontent.com/GABRYCA/svelte-elevenlabs/main/docs/media/conversation-widget-demo.gif)](https://github.com/GABRYCA/svelte-elevenlabs/blob/main/docs/media/conversation-widget-demo.mp4)
4
+
5
+ A live session recorded from the showcase at `/demo`. Click the preview for the MP4 with sound.
6
+
3
7
  > **This is an independent community project. Not affiliated with or endorsed by ElevenLabs Inc.**
4
8
 
5
9
  A Svelte 5 library and SvelteKit 3 showcase for ElevenLabs Conversational AI. One controller opens a low-latency voice or text session, exposes reactive call state, streams PCM audio over a WebSocket, and releases its sockets, media tracks, audio nodes, and timers on teardown.
@@ -31,7 +35,7 @@ The showcase server and package tooling run on **Node 22.17 or newer** and **Bun
31
35
  - **Ready-made UI.** `<AudioVisualizer>` and `<ConversationWidget>` consume the same controller. The widget supports voice, text, feedback, playback volume, device selection, transcripts, queue state, and theming through CSS custom properties.
32
36
  - **Safe signed URLs.** The included SvelteKit route validates `agent_` and `seng_` ids, sets `Cache-Control: no-store`, and keeps `ELEVENLABS_API_KEY` on the server. The static Pages build removes the route and disables signed mode in the UI.
33
37
 
34
- The repository also contains a runnable SvelteKit 3 showcase in `src/routes`. It demonstrates the controller, signed and public connection modes, voice and text sessions, device controls, feedback, and the diagnostic console.
38
+ The repository also contains a runnable SvelteKit 3 showcase in `src/routes`. It demonstrates the controller, signed and public connection modes, voice and text sessions, device controls, feedback, and the diagnostic console. The `/demo` page renders `<ConversationWidget>` on its own, with a focus view for recording, and the `svelte-elevenlabs agents` command installs a reviewed guide for coding agents.
35
39
 
36
40
  ## Documentation
37
41
 
@@ -39,6 +43,7 @@ The showcase ships a documentation section at [gabryca.github.io/svelte-elevenla
39
43
 
40
44
  - **Start here:** overview, install, and quickstart.
41
45
  - **Guides:** sessions, audio, client tools, components, and signed URLs.
46
+ - **Tooling:** the agent kit for coding agents.
42
47
  - **Reference:** API reference and troubleshooting.
43
48
 
44
49
  Run `bun run dev` and open `http://localhost:5173/docs` to read it locally. The static Pages build includes the same pages and respects `PAGES_BASE_PATH`, while the signed URL guide explains why that route only exists on the Node or Bun server.
@@ -55,6 +60,36 @@ The same material is published as plain Markdown for coding agents and other lan
55
60
 
56
61
  The files are prerendered, so they exist in the Node, Bun, and static Pages builds. Absolute links follow `PAGES_BASE_PATH`, and the reference version is read from `package.json`.
57
62
 
63
+ The agent kit is a shorter, decision-focused companion for project work. It installs into the consumer project instead of replacing these endpoints.
64
+
65
+ ## Agent kit
66
+
67
+ The package ships a canonical guide for coding agents and a small installer that writes it into the file each tool already reads.
68
+
69
+ ```sh
70
+ # Bun
71
+ bunx svelte-elevenlabs agents install
72
+
73
+ # npm
74
+ npx svelte-elevenlabs agents install
75
+ ```
76
+
77
+ With no target names, the command detects Claude Code, Cursor, OpenCode, GitHub Copilot, Windsurf, Cline, Gemini CLI, and `AGENTS.md` aware tools in the current project. When it cannot detect one, it adds a managed block to `AGENTS.md`. Choose targets explicitly when you want more:
78
+
79
+ ```sh
80
+ bunx svelte-elevenlabs agents install claude cursor opencode
81
+ bunx svelte-elevenlabs agents install --all
82
+ bunx svelte-elevenlabs agents install claude --dry-run
83
+ ```
84
+
85
+ Targets are `claude`, `cursor`, `opencode`, `copilot`, `windsurf`, `cline`, `gemini`, and `agents`. Shared files such as `AGENTS.md`, `GEMINI.md`, and `.github/copilot-instructions.md` keep their existing content. The installer updates only the block between `svelte-elevenlabs:agent-kit:start` and `svelte-elevenlabs:agent-kit:end`. Dedicated skill and rule files are skipped when they already exist without a managed block unless you pass `--force`.
86
+
87
+ Use `agents list` to print every destination path and `agents print` to send the guide to standard output. The canonical source is [`agent-kit/instructions.md`](https://github.com/GABRYCA/svelte-elevenlabs/blob/main/agent-kit/instructions.md), and the full guide is at [gabryca.github.io/svelte-elevenlabs/docs/agents](https://gabryca.github.io/svelte-elevenlabs/docs/agents).
88
+
89
+ ## Live demo
90
+
91
+ The showcase includes a focus view at [gabryca.github.io/svelte-elevenlabs/demo](https://gabryca.github.io/svelte-elevenlabs/demo). It renders `<ConversationWidget>` against a live agent with the same controller as the main console. The Node and Bun builds use signed URLs; the static Pages build uses a public agent id.
92
+
58
93
  ## Requirements
59
94
 
60
95
  | Requirement | Version or note |
@@ -640,9 +675,18 @@ The project uses SvelteKit 3, whose Vite configuration lives in `vite.config.ts`
640
675
 
641
676
  The release workflow runs on every push to `main`, tags new package versions, publishes GitHub releases, deploys Pages, and uploads build artifacts. Pull request CI runs the same checks without touching ElevenLabs quota. Both install with a frozen lockfile.
642
677
 
643
- ### Package layout
678
+ ### Repository layout
644
679
 
645
680
  ```text
681
+ agent-kit/
682
+ instructions.md # canonical guide for coding agents
683
+ README.md # install notes and target table
684
+ bin/
685
+ svelte-elevenlabs.js # dependency free agents CLI
686
+ docs/media/
687
+ conversation-widget-demo.gif # README preview
688
+ conversation-widget-demo.mp4 # clip with sound
689
+ conversation-widget-demo-poster.jpg
646
690
  src/
647
691
  env.ts # explicit public demo env for SvelteKit 3
648
692
  lib/
@@ -659,12 +703,16 @@ src/
659
703
  routes/
660
704
  +layout.ts # prerender only in Pages mode
661
705
  +page.svelte # live signal-console showcase
706
+ demo/+page.svelte # ConversationWidget demo and focus view
707
+ docs/agents/+page.svelte # agent kit documentation
662
708
  api/signed-url/+server.ts # server-only signed URL exchange
663
709
  ```
664
710
 
711
+ The published tarball contains `dist`, `agent-kit`, `bin`, `README.md`, and `LICENSE`. The showcase in `src` and the media in `docs` stay in the repository.
712
+
665
713
  ## Publishing
666
714
 
667
- The package version is `0.5.1`. `prepack` runs `svelte-kit sync`, `svelte-package`, and `publint`, so `dist` is rebuilt from source before every publish.
715
+ The package version is `0.6.0`. `prepack` runs `svelte-kit sync`, `svelte-package`, and `publint`, so `dist` is rebuilt from source before every publish.
668
716
 
669
717
  ```sh
670
718
  bun run prepack
@@ -672,7 +720,7 @@ npm pack --dry-run
672
720
  npm publish --access public
673
721
  ```
674
722
 
675
- `publishConfig.access` is already `public`. The published files are limited to `dist`, and the package exposes the root entry plus the `audio`, `types`, and `package.json` subpaths.
723
+ `publishConfig.access` is already `public`. The published files include `dist`, `agent-kit`, and `bin`. The package exposes the root entry plus the `audio`, `types`, and `package.json` subpaths, and the `svelte-elevenlabs` bin entry provides the agent kit command.
676
724
 
677
725
  When you push a version bump to `main`, the release workflow creates the `v<version>` tag, publishes the GitHub release, and attaches the same tarball that `bun pm pack` produces. You can still publish to npm manually from that tarball.
678
726
 
@@ -0,0 +1,97 @@
1
+ # Agent kit
2
+
3
+ Rules, snippets, and a one-command installer that teach coding agents how to use
4
+ `svelte-elevenlabs` correctly.
5
+
6
+ Coding agents often guess API names, forget that `useConversation` must run
7
+ during component initialisation, or move an API key into a client component.
8
+ This kit gives them one accurate source file and places it where Claude Code,
9
+ Cursor, OpenCode, GitHub Copilot, Windsurf, Cline, Gemini CLI, and generic
10
+ `AGENTS.md` aware tools already look.
11
+
12
+ ## Install
13
+
14
+ Run the command inside the SvelteKit project that uses the package:
15
+
16
+ ```sh
17
+ # Bun
18
+ bunx svelte-elevenlabs agents install
19
+
20
+ # npm
21
+ npx svelte-elevenlabs agents install
22
+ ```
23
+
24
+ With no target names the command detects the agents configured in the project.
25
+ When it cannot detect one, it adds a managed block to `AGENTS.md`.
26
+
27
+ Choose targets explicitly when you want more than detection finds:
28
+
29
+ ```sh
30
+ bunx svelte-elevenlabs agents install claude cursor opencode
31
+ bunx svelte-elevenlabs agents install --all
32
+ bunx svelte-elevenlabs agents install claude --dry-run
33
+ bunx svelte-elevenlabs agents install claude --force
34
+ ```
35
+
36
+ Use `agents list` to see every target and destination path, and `agents print`
37
+ to send the guide to standard output:
38
+
39
+ ```sh
40
+ bunx svelte-elevenlabs agents list
41
+ bunx svelte-elevenlabs agents print cursor
42
+ ```
43
+
44
+ ## Targets
45
+
46
+ | Name | Tool | Destination |
47
+ | ---------- | --------------------------------------------------- | --------------------------------------------- |
48
+ | `claude` | Claude Code | `.claude/skills/svelte-elevenlabs/SKILL.md` |
49
+ | `cursor` | Cursor | `.cursor/rules/svelte-elevenlabs.mdc` |
50
+ | `opencode` | OpenCode | `.opencode/skills/svelte-elevenlabs/SKILL.md` |
51
+ | `copilot` | GitHub Copilot | `.github/copilot-instructions.md` |
52
+ | `windsurf` | Windsurf | `.windsurf/rules/svelte-elevenlabs.md` |
53
+ | `cline` | Cline | `.clinerules/svelte-elevenlabs.md` |
54
+ | `gemini` | Gemini CLI | `GEMINI.md` |
55
+ | `agents` | Codex, OpenCode, Jules, and other `AGENTS.md` tools | `AGENTS.md` |
56
+
57
+ Shared files such as `AGENTS.md`, `GEMINI.md`, and the Copilot instructions keep
58
+ your existing content. The installer adds or updates a marked block between
59
+ `svelte-elevenlabs:agent-kit:start` and `svelte-elevenlabs:agent-kit:end`.
60
+ Running the command again replaces only that block.
61
+
62
+ Dedicated skill and rule files are only overwritten with `--force`.
63
+
64
+ ## Manual installation
65
+
66
+ If you cannot run the command, copy the contents of
67
+ [`instructions.md`](./instructions.md) into the file your tool reads. For a
68
+ Claude Code or OpenCode skill, add this frontmatter above it:
69
+
70
+ ```yaml
71
+ ---
72
+ name: svelte-elevenlabs
73
+ description: Build or review Svelte 5 ElevenLabs Conversational AI voice and text sessions with svelte-elevenlabs.
74
+ license: MIT
75
+ ---
76
+ ```
77
+
78
+ The canonical guide is also published at
79
+ [gabryca.github.io/svelte-elevenlabs/docs/agents](https://gabryca.github.io/svelte-elevenlabs/docs/agents),
80
+ and the full API reference is at
81
+ [llms-full.txt](https://gabryca.github.io/svelte-elevenlabs/llms-full.txt).
82
+
83
+ ## What the guide covers
84
+
85
+ - The non negotiable rules for secrets, lifecycle, and secure contexts.
86
+ - A minimal voice component and a signed URL server route.
87
+ - `ConversationWidget`, `AudioVisualizer`, and `ConversationProvider`.
88
+ - Text only sessions and typed turns.
89
+ - Client tools, MCP approvals, feedback, and device controls.
90
+ - The reactive controller surface and the options that change behavior.
91
+ - Error codes and a review checklist.
92
+
93
+ ## Safety
94
+
95
+ The agent kit never contains an API key and never asks an agent to create one in
96
+ the browser. Signed URL work stays on the server. The kit is plain Markdown, so
97
+ you can review every line before you commit it.
@@ -0,0 +1,472 @@
1
+ # svelte-elevenlabs agent guide
2
+
3
+ This guide is for coding agents that add, review, or repair an ElevenLabs
4
+ Conversational AI integration in a Svelte 5 or SvelteKit project. It is
5
+ installed by the `svelte-elevenlabs agents` command and is safe to keep under
6
+ version control.
7
+
8
+ Package: `svelte-elevenlabs`
9
+ Canonical documentation: https://gabryca.github.io/svelte-elevenlabs/docs
10
+ Complete machine reference: https://gabryca.github.io/svelte-elevenlabs/llms-full.txt
11
+ Live demo: https://gabryca.github.io/svelte-elevenlabs/demo
12
+ Source and issues: https://github.com/GABRYCA/svelte-elevenlabs
13
+
14
+ ## What the package does
15
+
16
+ `svelte-elevenlabs` is an independent, MIT licensed Svelte 5 library for
17
+ ElevenLabs Conversational AI. One controller owns the browser side of a voice
18
+ or text session:
19
+
20
+ - The WebSocket handshake with the Conversational AI endpoint.
21
+ - Microphone capture, low-pass filtering, resampling, PCM16 framing, and base64
22
+ transport.
23
+ - Agent playback on one audio clock, with immediate barge-in flushing.
24
+ - Transcripts, latency, queue state, device controls, client tools, feedback,
25
+ and typed turns.
26
+ - Teardown of sockets, media tracks, worklet URLs, timers, and audio contexts.
27
+
28
+ The package is not an official ElevenLabs SDK. It follows the public
29
+ Conversational AI WebSocket protocol and reports failures with stable error
30
+ codes.
31
+
32
+ ## Rules that must not be broken
33
+
34
+ 1. Never expose `ELEVENLABS_API_KEY` in a client component, a `PUBLIC_*`
35
+ variable, a route response, a log line, or a static build. Only a server
36
+ route may read `process.env.ELEVENLABS_API_KEY`.
37
+ 2. Call `useConversation` during component initialisation, directly inside
38
+ `<script>`. Do not call it inside `onMount`, an event handler, a loop, or a
39
+ conditional that can change later. It registers an `$effect` teardown.
40
+ 3. Call `startConversation` from a user gesture such as a click or tap. Do not
41
+ start a voice session during server-side rendering, module evaluation, or
42
+ page load.
43
+ 4. Use `createConversation` outside a component and call `destroy()` yourself
44
+ when the owner is disposed. This applies to stores, tests, workers, and
45
+ custom lifecycle code.
46
+ 5. Use a `.svelte.ts` or `.svelte.js` file when a module needs runes. A plain
47
+ `.ts` module cannot use `$state`, `$derived`, or `$effect`.
48
+ 6. In a reusable package, keep `$app/*` and `$env/*` imports out of the
49
+ library entry. The `svelte-elevenlabs` package itself follows this rule.
50
+ 7. Prefer signed URLs for private agents and production traffic. A public agent
51
+ id is acceptable for a demo only.
52
+ 8. Text only sessions still need a valid agent and an open WebSocket. They skip
53
+ microphone permission and Web Audio setup.
54
+ 9. Do not invent method names or options. Use the API in this guide or read the
55
+ installed TypeScript declarations before writing code.
56
+ 10. Keep compatibility with Svelte 5, Node 22.17 or newer, and Bun 1.4 or newer.
57
+ Do not add a dependency when a small local function will do.
58
+
59
+ ## Requirements
60
+
61
+ | Requirement | Version or note |
62
+ | ----------- | --------------------------------------------------------------------------- |
63
+ | Svelte | 5.x peer dependency |
64
+ | SvelteKit | Optional, needed for the signed URL route and server rendering |
65
+ | Node | 22.17 or newer for a server build and package tooling |
66
+ | Bun | 1.4 or newer for the included scripts |
67
+ | Browser | WebSocket. Voice also needs Web Audio, `getUserMedia`, and a secure context |
68
+ | ElevenLabs | An agent id. Signed mode also needs a workspace API key |
69
+
70
+ The microphone requires HTTPS or localhost. The controller reports an
71
+ `insecure-context` error when the page is not in a secure context.
72
+
73
+ ## Install
74
+
75
+ ```sh
76
+ # Bun
77
+ bun add svelte-elevenlabs
78
+
79
+ # npm
80
+ npm install svelte-elevenlabs
81
+ ```
82
+
83
+ The package is ESM only. The root entry contains Svelte components, so use a
84
+ Svelte aware bundler. Node scripts and tests can import the Node safe subpaths:
85
+
86
+ ```ts
87
+ import { parseAudioFormat, resampleLinear } from 'svelte-elevenlabs/audio';
88
+ import type { AudioFormat, ConversationOptions } from 'svelte-elevenlabs/types';
89
+ ```
90
+
91
+ ## Minimal voice component
92
+
93
+ ```svelte
94
+ <script lang="ts">
95
+ import { useConversation } from 'svelte-elevenlabs';
96
+
97
+ const conversation = useConversation({
98
+ agentId: 'agent_your_public_id',
99
+ onError: (error) => console.error('[voice]', error.code, error.message)
100
+ });
101
+
102
+ function toggle(): void {
103
+ if (conversation.status === 'disconnected') {
104
+ void conversation.startConversation();
105
+ return;
106
+ }
107
+
108
+ conversation.stopConversation();
109
+ }
110
+ </script>
111
+
112
+ <button type="button" onclick={toggle} aria-busy={conversation.status === 'connecting'}>
113
+ {conversation.status === 'connected' ? 'End conversation' : 'Start conversation'}
114
+ </button>
115
+
116
+ {#if conversation.error}
117
+ <p role="alert">{conversation.error}</p>
118
+ {/if}
119
+ ```
120
+
121
+ `startConversation` resolves after metadata negotiation or after a start
122
+ failure. Connection and audio failures are published through `error`,
123
+ `errorCode`, and `onError` instead of rejecting, so a `void` call is safe.
124
+
125
+ ## Signed URL server route
126
+
127
+ Create `src/routes/api/signed-url/+server.ts`. Keep the key on the server and
128
+ return only the short-lived URL.
129
+
130
+ ```ts
131
+ import { json } from '@sveltejs/kit';
132
+ import type { RequestHandler } from './$types';
133
+
134
+ const ORIGIN = 'https://api.elevenlabs.io';
135
+ const AGENT_ID = /^(agent|seng)_[A-Za-z0-9]+$/;
136
+
137
+ export const prerender = false;
138
+
139
+ export const GET: RequestHandler = async ({ url, fetch, request, setHeaders }) => {
140
+ setHeaders({ 'cache-control': 'no-store' });
141
+
142
+ const agentId = url.searchParams.get('agent_id')?.trim() ?? '';
143
+ if (!AGENT_ID.test(agentId)) {
144
+ return json(
145
+ { error: { message: 'Add a valid agent_id query parameter.' } },
146
+ { status: 400, headers: { 'cache-control': 'no-store' } }
147
+ );
148
+ }
149
+
150
+ const apiKey = process.env.ELEVENLABS_API_KEY?.trim() ?? '';
151
+ if (!apiKey) {
152
+ return json(
153
+ { error: { message: 'ELEVENLABS_API_KEY is missing on the server.' } },
154
+ { status: 500, headers: { 'cache-control': 'no-store' } }
155
+ );
156
+ }
157
+
158
+ const upstream = await fetch(
159
+ `${ORIGIN}/v1/convai/conversation/get-signed-url?agent_id=${agentId}`,
160
+ {
161
+ headers: { accept: 'application/json', 'xi-api-key': apiKey },
162
+ signal: request.signal
163
+ }
164
+ );
165
+
166
+ if (!upstream.ok) {
167
+ return json(
168
+ { error: { message: 'ElevenLabs could not create a signed URL.' } },
169
+ { status: 502, headers: { 'cache-control': 'no-store' } }
170
+ );
171
+ }
172
+
173
+ const body: unknown = await upstream.json().catch(() => null);
174
+ const signedUrl =
175
+ typeof body === 'object' && body !== null && 'signed_url' in body ? body.signed_url : null;
176
+
177
+ if (typeof signedUrl !== 'string' || !signedUrl.startsWith('wss://')) {
178
+ return json(
179
+ { error: { message: 'ElevenLabs returned an unexpected payload.' } },
180
+ { status: 502, headers: { 'cache-control': 'no-store' } }
181
+ );
182
+ }
183
+
184
+ return json({ signed_url: signedUrl }, { headers: { 'cache-control': 'no-store' } });
185
+ };
186
+ ```
187
+
188
+ Then start the session with signed mode:
189
+
190
+ ```ts
191
+ await conversation.startConversation('agent_your_private_id', { signed: true });
192
+ ```
193
+
194
+ The default endpoint is `/api/signed-url`. Set `signedUrlEndpoint` when your
195
+ route lives elsewhere. A signed URL is a credential. Do not log it or send it
196
+ to a third party.
197
+
198
+ For a static build there is no server route. Use a public agent id, or let the
199
+ visitor paste a signed URL they already have. Never put the API key in a static
200
+ artifact.
201
+
202
+ ## Ready-made UI
203
+
204
+ ```svelte
205
+ <script lang="ts">
206
+ import { ConversationWidget, useConversation } from 'svelte-elevenlabs';
207
+
208
+ const conversation = useConversation({ agentId: 'agent_your_public_id' });
209
+ </script>
210
+
211
+ <ConversationWidget
212
+ controller={conversation}
213
+ title="Support line"
214
+ subtitle="Voice or text"
215
+ signed={false}
216
+ />
217
+ ```
218
+
219
+ `ConversationWidget` owns its own controller when `controller` is omitted. When
220
+ you pass a controller, the widget mirrors it and shares its state with the rest
221
+ of the page. The visualizer is also available on its own:
222
+
223
+ ```svelte
224
+ <script lang="ts">
225
+ import { AudioVisualizer, useConversation } from 'svelte-elevenlabs';
226
+
227
+ const conversation = useConversation({ agentId: 'agent_your_public_id' });
228
+ </script>
229
+
230
+ <AudioVisualizer {conversation} height={212} />
231
+ ```
232
+
233
+ The components use scoped styles. In a Tailwind 4 app, add the package output to
234
+ the source scan so utility class names are discovered:
235
+
236
+ ```css
237
+ @import 'tailwindcss';
238
+ @source '../node_modules/svelte-elevenlabs/dist';
239
+ ```
240
+
241
+ ## Text only sessions
242
+
243
+ ```svelte
244
+ <script lang="ts">
245
+ import { useConversation } from 'svelte-elevenlabs';
246
+
247
+ const conversation = useConversation({
248
+ agentId: 'agent_your_public_id',
249
+ textOnly: true
250
+ });
251
+
252
+ let draft = $state('');
253
+
254
+ function send(event: SubmitEvent): void {
255
+ event.preventDefault();
256
+ if (conversation.sendUserMessage(draft)) draft = '';
257
+ }
258
+ </script>
259
+
260
+ <button type="button" onclick={() => conversation.startConversation()}> Open chat </button>
261
+
262
+ <form onsubmit={send}>
263
+ <input bind:value={draft} aria-label="Message" />
264
+ <button type="submit">Send</button>
265
+ </form>
266
+ ```
267
+
268
+ A per session override is available when one controller needs both modes:
269
+
270
+ ```ts
271
+ await conversation.startConversation('agent_your_public_id', { textOnly: true });
272
+ ```
273
+
274
+ ## Provider and client tools
275
+
276
+ Use the provider when several components need the same session. Use
277
+ `useConversationClientTool` for a tool that should disappear when its component
278
+ unmounts.
279
+
280
+ ```svelte
281
+ <!-- OrderTool.svelte -->
282
+ <script lang="ts">
283
+ import { useConversationClientTool } from 'svelte-elevenlabs';
284
+
285
+ useConversationClientTool('get_order', async ({ orderId }) => {
286
+ return { orderId, status: 'shipped' };
287
+ });
288
+ </script>
289
+
290
+ <span>Order tool ready</span>
291
+ ```
292
+
293
+ ```svelte
294
+ <!-- App.svelte -->
295
+ <script lang="ts">
296
+ import { ConversationProvider, ConversationWidget } from 'svelte-elevenlabs';
297
+ import OrderTool from './OrderTool.svelte';
298
+ </script>
299
+
300
+ <ConversationProvider options={{ agentId: 'agent_your_public_id' }}>
301
+ <OrderTool />
302
+ <ConversationWidget />
303
+ </ConversationProvider>
304
+ ```
305
+
306
+ Tool names must match the tool names configured on the ElevenLabs agent. A
307
+ named tool takes precedence over the `onClientToolCall` fallback. The handler
308
+ may return a string or a JSON serialisable value.
309
+
310
+ ## Lifecycle and cleanup
311
+
312
+ `useConversation` registers an `$effect` teardown. It closes the socket, stops
313
+ media tracks, disconnects the audio graph, revokes worklet URLs, clears timers,
314
+ aborts signed URL requests, and closes both audio contexts.
315
+
316
+ Use `createConversation` when you own the lifecycle:
317
+
318
+ ```ts
319
+ import { createConversation } from 'svelte-elevenlabs';
320
+
321
+ const conversation = createConversation({ agentId: 'agent_your_public_id' });
322
+
323
+ try {
324
+ await conversation.startConversation();
325
+ } finally {
326
+ conversation.stopConversation();
327
+ conversation.destroy();
328
+ }
329
+ ```
330
+
331
+ Do not reuse a destroyed controller. Create a new one.
332
+
333
+ ## Controller surface
334
+
335
+ Read these fields directly in Svelte markup. They are reactive state or derived
336
+ values.
337
+
338
+ | Field | Meaning |
339
+ | ------------------- | -------------------------------------------------------------------------------------------- |
340
+ | `status` | `'disconnected'`, `'connecting'`, or `'connected'` |
341
+ | `mode` | High level mode: `'disconnected'`, `'connecting'`, `'ready'`, `'listening'`, or `'speaking'` |
342
+ | `isSpeaking` | Agent playback is scheduled or audible |
343
+ | `isListening` | The microphone is live and the agent can hear it |
344
+ | `isUserSpeaking` | Local voice activity detection sees speech |
345
+ | `isMuted` | Outgoing microphone audio is muted |
346
+ | `isTextOnly` | The current or next session is text only |
347
+ | `volume` | Combined input and output level from 0 to 1 |
348
+ | `inputVolume` | Microphone level from 0 to 1 |
349
+ | `outputVolume` | Agent playback level from 0 to 1 |
350
+ | `transcripts` | Stable streaming transcript entries |
351
+ | `messageCount` | Number of retained transcript entries |
352
+ | `error` | Human readable message, safe to render |
353
+ | `errorCode` | Stable machine readable failure category |
354
+ | `latency` | Time to first audio in milliseconds, or `null` |
355
+ | `latencyHistory` | Recent first audio measurements |
356
+ | `conversationId` | ElevenLabs conversation id, or `null` |
357
+ | `inputAudioFormat` | Negotiated microphone format, or `null` |
358
+ | `outputAudioFormat` | Negotiated agent format, or `null` |
359
+ | `queueStatus` | Server queue state, or `null` |
360
+ | `interruptions` | Number of accepted barge-in events |
361
+ | `canSendFeedback` | A response id is available for feedback |
362
+ | `vadScore` | Server-side voice activity score |
363
+ | `contextUsage` | LLM context usage when the server reports it |
364
+ | `outputGain` | Playback gain from 0 to 1 |
365
+ | `inputDeviceId` | Selected microphone id, or `null` |
366
+ | `outputDeviceId` | Selected output device id, or `null` |
367
+
368
+ Common methods:
369
+
370
+ | Method | Purpose |
371
+ | ---------------------------------------------------- | --------------------------------------------------------------------- |
372
+ | `startConversation(target?, { signed?, textOnly? })` | Open a session |
373
+ | `stopConversation()` | End a session and keep transcripts |
374
+ | `endSession()` | Alias for `stopConversation()` |
375
+ | `destroy()` | Release everything and mark the controller unusable |
376
+ | `toggleMute()` | Toggle outgoing microphone audio |
377
+ | `setMuted(muted)` | Set mute explicitly |
378
+ | `setVolume(value)` | Set playback gain from 0 to 1 |
379
+ | `sendUserMessage(text)` | Add a typed user turn on the same socket |
380
+ | `sendContextualUpdate(text, contextId?)` | Give the agent context without interrupting speech |
381
+ | `sendFeedback(like, eventId?)` | Submit response feedback |
382
+ | `sendUserActivity()` | Tell the agent the user is still active, throttled to once per second |
383
+ | `changeInputDevice(id?)` | Switch the microphone during a voice session |
384
+ | `changeOutputDevice(id?)` | Route playback to another output device |
385
+ | `listAudioDevices()` | Read available input and output devices |
386
+ | `registerClientTool(name, handler)` | Register a named tool and receive an unregister function |
387
+ | `sendMCPToolApprovalResult(toolCallId, approved)` | Answer an MCP approval request manually |
388
+ | `clearTranscripts()` | Remove transcript entries |
389
+ | `clearError()` | Clear the current error message and code |
390
+
391
+ ## Options that matter
392
+
393
+ | Option | Default | Notes |
394
+ | -------------------------- | ------------------- | ---------------------------------------------------------------- |
395
+ | `agentId` | None | Default public agent id |
396
+ | `signedUrl` | None | Default signed WebSocket URL |
397
+ | `signedUrlEndpoint` | `'/api/signed-url'` | Same origin exchange route |
398
+ | `serverLocation` | US origin | `'eu-residency'`, `'in-residency'`, or `'global'` for public ids |
399
+ | `textOnly` | `false` | Skip microphone and audio contexts |
400
+ | `micMuted` | `false` | Start with microphone muted |
401
+ | `outputVolume` | `1` | Initial playback gain |
402
+ | `inputChunkDurationMs` | `100` | Clamped to 50 to 250 |
403
+ | `interruptOnUserSpeech` | `true` | Flush agent audio when local VAD detects speech |
404
+ | `vadThreshold` | `0.075` | Normalised RMS threshold |
405
+ | `maxTranscripts` | `250` | Minimum retained value is 20 |
406
+ | `autoCleanup` | `true` | Only used by `useConversation` |
407
+ | `debug` | `false` | Protocol logs through `console.debug` |
408
+ | `clientTools` | None | Named client tool handlers |
409
+ | `onClientToolCall` | None | Fallback handler |
410
+ | `onError` | None | Called with a `ConversationError` |
411
+ | `onTranscript` | None | Called on every transcript change |
412
+ | `onInterruption` | None | Called on server or local barge-in |
413
+ | `onStatusChange` | None | Called when the connection status changes |
414
+ | `onMCPToolApprovalRequest` | None | Decide whether an MCP tool may proceed |
415
+
416
+ ## Errors
417
+
418
+ `ConversationError` exposes a stable `code` and a human readable `message`. The
419
+ controller also publishes the same values as `errorCode` and `error` and calls
420
+ `onError`.
421
+
422
+ Common codes:
423
+
424
+ - `not-supported`: the browser or requested transport is not available.
425
+ - `insecure-context`: the page is not HTTPS or localhost.
426
+ - `permission-denied`: microphone permission was refused.
427
+ - `audio-failed`: an audio context, worklet, or playback operation failed.
428
+ - `invalid-target`: the agent id or URL is malformed.
429
+ - `signed-url-failed`: the server exchange failed.
430
+ - `connection-failed`: the socket could not open.
431
+ - `connection-timeout`: the handshake did not finish in time.
432
+ - `server-closed`: the server closed the socket.
433
+ - `protocol-error`: the server sent an event the client could not parse.
434
+ - `device-not-found`: the selected device disappeared.
435
+ - `device-busy`: the device could not be opened.
436
+ - `device-constraints`: the device rejected the requested constraints.
437
+ - `device-switch-failed`: a live device switch did not complete.
438
+
439
+ Render `error` for people. Use `errorCode` for branching and analytics. Never
440
+ show a raw signed URL or API key in an error surface.
441
+
442
+ ## Review checklist
443
+
444
+ Before finishing a change, verify:
445
+
446
+ - The API key appears only in server code.
447
+ - `useConversation` is called during component initialisation.
448
+ - The start action runs from a user gesture.
449
+ - A voice session has a visible stop action.
450
+ - `error` and `status` are rendered with accessible text.
451
+ - Buttons have `type="button"` unless they submit a form.
452
+ - Status text uses a polite live region.
453
+ - Text only mode works without a microphone.
454
+ - A custom controller created with `createConversation` is destroyed.
455
+ - No `PUBLIC_*` variable contains a secret.
456
+ - The Tailwind source path is present when the widget is used in a Tailwind 4 app.
457
+ - The build passes `bun run check` and `bun run test` when this is the package repository.
458
+
459
+ ## Documentation map
460
+
461
+ - Install: https://gabryca.github.io/svelte-elevenlabs/docs/install
462
+ - Quickstart: https://gabryca.github.io/svelte-elevenlabs/docs/quickstart
463
+ - Sessions: https://gabryca.github.io/svelte-elevenlabs/docs/sessions
464
+ - Audio: https://gabryca.github.io/svelte-elevenlabs/docs/audio
465
+ - Client tools: https://gabryca.github.io/svelte-elevenlabs/docs/client-tools
466
+ - Components: https://gabryca.github.io/svelte-elevenlabs/docs/components
467
+ - Signed URLs: https://gabryca.github.io/svelte-elevenlabs/docs/signed-urls
468
+ - Agent kit: https://gabryca.github.io/svelte-elevenlabs/docs/agents
469
+ - API reference: https://gabryca.github.io/svelte-elevenlabs/docs/reference
470
+ - Troubleshooting: https://gabryca.github.io/svelte-elevenlabs/docs/troubleshooting
471
+ - Live demo: https://gabryca.github.io/svelte-elevenlabs/demo
472
+ - Full machine reference: https://gabryca.github.io/svelte-elevenlabs/llms-full.txt
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install the svelte-elevenlabs agent guide into a consumer project.
4
+ *
5
+ * The command is intentionally dependency free so it runs with Node 22.17 or
6
+ * newer and Bun 1.4 or newer. It writes only local Markdown files, never calls
7
+ * the network, and never asks for an API key.
8
+ */
9
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
10
+ import { dirname, join, relative, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const HERE = dirname(fileURLToPath(import.meta.url));
14
+ const ROOT = resolve(HERE, '..');
15
+ const KIT_ROOT = join(ROOT, 'agent-kit');
16
+ const GUIDE_PATH = join(KIT_ROOT, 'instructions.md');
17
+ const PACKAGE_PATH = join(ROOT, 'package.json');
18
+
19
+ const START_MARKER = '<!-- svelte-elevenlabs:agent-kit:start -->';
20
+ const END_MARKER = '<!-- svelte-elevenlabs:agent-kit:end -->';
21
+
22
+ const CLAUDE_DESCRIPTION =
23
+ 'Build or review Svelte 5 ElevenLabs Conversational AI voice and text sessions with the svelte-elevenlabs package. Use for ConversationWidget, AudioVisualizer, signed URL routes, client tools, and audio cleanup.';
24
+
25
+ /**
26
+ * @typedef {'claude' | 'cursor' | 'opencode' | 'copilot' | 'windsurf' | 'cline' | 'gemini' | 'agents'} TargetName
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} AgentTarget
31
+ * @property {TargetName} name Stable name used on the command line.
32
+ * @property {string} label Human readable tool name.
33
+ * @property {string} path File path relative to the consumer project.
34
+ * @property {boolean} shared Whether the file is shared with the project and must keep its content.
35
+ * @property {string} [frontmatter] YAML frontmatter required by the tool.
36
+ */
37
+
38
+ /** @type {AgentTarget[]} */
39
+ const TARGETS = [
40
+ {
41
+ name: 'claude',
42
+ label: 'Claude Code',
43
+ path: '.claude/skills/svelte-elevenlabs/SKILL.md',
44
+ shared: false,
45
+ frontmatter: `---\nname: svelte-elevenlabs\ndescription: ${CLAUDE_DESCRIPTION}\nlicense: MIT\n---\n\n`
46
+ },
47
+ {
48
+ name: 'cursor',
49
+ label: 'Cursor',
50
+ path: '.cursor/rules/svelte-elevenlabs.mdc',
51
+ shared: false,
52
+ frontmatter: `---\ndescription: ${CLAUDE_DESCRIPTION}\nglobs: src/**/*.svelte, src/**/*.ts, src/**/*.js\nalwaysApply: false\n---\n\n`
53
+ },
54
+ {
55
+ name: 'opencode',
56
+ label: 'OpenCode',
57
+ path: '.opencode/skills/svelte-elevenlabs/SKILL.md',
58
+ shared: false,
59
+ frontmatter: `---\nname: svelte-elevenlabs\ndescription: ${CLAUDE_DESCRIPTION}\nlicense: MIT\n---\n\n`
60
+ },
61
+ {
62
+ name: 'copilot',
63
+ label: 'GitHub Copilot',
64
+ path: '.github/copilot-instructions.md',
65
+ shared: true
66
+ },
67
+ {
68
+ name: 'windsurf',
69
+ label: 'Windsurf',
70
+ path: '.windsurf/rules/svelte-elevenlabs.md',
71
+ shared: false,
72
+ frontmatter: `---\ntrigger: model_decision\ndescription: ${CLAUDE_DESCRIPTION}\nglobs: src/**/*.svelte, src/**/*.ts\n---\n\n`
73
+ },
74
+ {
75
+ name: 'cline',
76
+ label: 'Cline',
77
+ path: '.clinerules/svelte-elevenlabs.md',
78
+ shared: false
79
+ },
80
+ {
81
+ name: 'gemini',
82
+ label: 'Gemini CLI',
83
+ path: 'GEMINI.md',
84
+ shared: true
85
+ },
86
+ {
87
+ name: 'agents',
88
+ label: 'AGENTS.md aware tools',
89
+ path: 'AGENTS.md',
90
+ shared: true
91
+ }
92
+ ];
93
+
94
+ /** Aliases accepted on the command line. */
95
+ const ALIASES = new Map([
96
+ ['claude-code', 'claude'],
97
+ ['github-copilot', 'copilot'],
98
+ ['codex', 'agents'],
99
+ ['generic', 'agents']
100
+ ]);
101
+
102
+ /** Error type used for expected command line failures. */
103
+ class AgentKitError extends Error {}
104
+
105
+ /**
106
+ * Read the package version without assuming a JSON import attribute.
107
+ *
108
+ * @returns {string} Version from `package.json`.
109
+ */
110
+ function readVersion() {
111
+ try {
112
+ const pkg = JSON.parse(readFileSync(PACKAGE_PATH, 'utf8'));
113
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
114
+ } catch {
115
+ return '0.0.0';
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Read the canonical agent guide.
121
+ *
122
+ * @returns {string} Guide body without a trailing newline.
123
+ */
124
+ function readGuide() {
125
+ if (!existsSync(GUIDE_PATH)) {
126
+ throw new AgentKitError(
127
+ `The agent guide is missing at ${GUIDE_PATH}. Reinstall svelte-elevenlabs and try again.`
128
+ );
129
+ }
130
+
131
+ return readFileSync(GUIDE_PATH, 'utf8').trimEnd();
132
+ }
133
+
134
+ /**
135
+ * Find one target by name or alias.
136
+ *
137
+ * @param {string} name Raw target name.
138
+ * @returns {AgentTarget} Matching target.
139
+ */
140
+ function findTarget(name) {
141
+ const normalized = ALIASES.get(name) ?? name;
142
+ const target = TARGETS.find((entry) => entry.name === normalized);
143
+ if (!target) {
144
+ throw new AgentKitError(
145
+ `Unknown agent target "${name}". Use one of: ${TARGETS.map((entry) => entry.name).join(', ')}.`
146
+ );
147
+ }
148
+ return target;
149
+ }
150
+
151
+ /**
152
+ * Build the managed block for one target.
153
+ *
154
+ * @param {AgentTarget} target Target being rendered.
155
+ * @param {string} guide Canonical guide body.
156
+ * @param {string} version Package version.
157
+ * @returns {string} Markdown block with start and end markers.
158
+ */
159
+ function renderBlock(target, guide, version) {
160
+ return `${START_MARKER}\n<!-- Generated by svelte-elevenlabs v${version}. Re-run the agents install command to update this block. -->\n\n${guide}\n${END_MARKER}`;
161
+ }
162
+
163
+ /**
164
+ * Render the complete content for a target.
165
+ *
166
+ * @param {AgentTarget} target Target being rendered.
167
+ * @param {string} guide Canonical guide body.
168
+ * @param {string} version Package version.
169
+ * @returns {string} Complete file content with a trailing newline.
170
+ */
171
+ function renderTarget(target, guide, version) {
172
+ const block = renderBlock(target, guide, version);
173
+ const content = target.frontmatter ? `${target.frontmatter}${block}` : block;
174
+ return `${content}\n`;
175
+ }
176
+
177
+ /**
178
+ * Replace an existing managed block without touching the rest of a file.
179
+ *
180
+ * @param {string} existing Current file content.
181
+ * @param {string} block New managed block.
182
+ * @returns {string} Updated content.
183
+ */
184
+ function replaceBlock(existing, block) {
185
+ const start = existing.indexOf(START_MARKER);
186
+ const end = existing.indexOf(END_MARKER, start);
187
+
188
+ if (!existing.trim()) return `${block}\n`;
189
+
190
+ if (start === -1) return `${existing.replace(/\s+$/, '')}\n\n${block}\n`;
191
+
192
+ if (end === -1) {
193
+ throw new AgentKitError(
194
+ 'Found a start marker without an end marker. Remove the incomplete block and try again.'
195
+ );
196
+ }
197
+
198
+ return `${existing.slice(0, start)}${block}${existing.slice(end + END_MARKER.length)}`;
199
+ }
200
+
201
+ /**
202
+ * Write one target and report what happened.
203
+ *
204
+ * @param {AgentTarget} target Target to write.
205
+ * @param {object} options Install options.
206
+ * @param {string} options.cwd Consumer project root.
207
+ * @param {string} options.guide Canonical guide body.
208
+ * @param {string} options.version Package version.
209
+ * @param {boolean} options.force Overwrite dedicated files without markers.
210
+ * @param {boolean} options.dryRun Report without writing.
211
+ * @returns {{ target: AgentTarget, path: string, status: string, detail?: string }} Result for the summary.
212
+ */
213
+ function installTarget(target, { cwd, guide, version, force, dryRun }) {
214
+ const absolutePath = join(cwd, target.path);
215
+ const exists = existsSync(absolutePath);
216
+ const current = exists ? readFileSync(absolutePath, 'utf8') : '';
217
+ const rendered = renderTarget(target, guide, version);
218
+ const block = renderBlock(target, guide, version);
219
+ let next = rendered;
220
+ let status = exists ? 'updated' : 'created';
221
+ let detail = '';
222
+
223
+ if (target.shared) {
224
+ next = replaceBlock(current, block);
225
+ if (exists) status = current.includes(START_MARKER) ? 'updated' : 'appended';
226
+ } else if (exists && current.includes(START_MARKER)) {
227
+ next = replaceBlock(current, block);
228
+ } else if (exists && !current.includes(START_MARKER)) {
229
+ if (!force) {
230
+ return {
231
+ target,
232
+ path: target.path,
233
+ status: 'skipped',
234
+ detail: 'the file exists without a managed block. Re-run with --force to replace it.'
235
+ };
236
+ }
237
+ status = 'replaced';
238
+ }
239
+
240
+ if (dryRun) {
241
+ const labels = {
242
+ created: 'would create',
243
+ updated: 'would update',
244
+ appended: 'would append',
245
+ replaced: 'would replace',
246
+ unchanged: 'unchanged',
247
+ skipped: 'would skip'
248
+ };
249
+ return {
250
+ target,
251
+ path: target.path,
252
+ status: labels[status] ?? status,
253
+ detail
254
+ };
255
+ }
256
+
257
+ if (next !== current) {
258
+ mkdirSync(dirname(absolutePath), { recursive: true });
259
+ writeFileSync(absolutePath, next, 'utf8');
260
+ } else {
261
+ status = 'unchanged';
262
+ }
263
+
264
+ return { target, path: target.path, status, detail };
265
+ }
266
+
267
+ /**
268
+ * Detect agent configuration files in a project.
269
+ *
270
+ * @param {string} cwd Consumer project root.
271
+ * @returns {TargetName[]} Detected targets in display order.
272
+ */
273
+ function detectTargets(cwd) {
274
+ const detected = [];
275
+
276
+ if (existsSync(join(cwd, '.claude'))) detected.push('claude');
277
+ if (existsSync(join(cwd, '.cursor')) || existsSync(join(cwd, '.cursorrules'))) {
278
+ detected.push('cursor');
279
+ }
280
+ if (
281
+ existsSync(join(cwd, '.opencode')) ||
282
+ existsSync(join(cwd, 'opencode.json')) ||
283
+ existsSync(join(cwd, 'opencode.jsonc'))
284
+ ) {
285
+ detected.push('opencode');
286
+ }
287
+ if (existsSync(join(cwd, '.github', 'copilot-instructions.md'))) detected.push('copilot');
288
+ if (existsSync(join(cwd, '.windsurf'))) detected.push('windsurf');
289
+ if (existsSync(join(cwd, '.clinerules'))) detected.push('cline');
290
+ if (existsSync(join(cwd, 'GEMINI.md'))) detected.push('gemini');
291
+ if (existsSync(join(cwd, 'AGENTS.md'))) detected.push('agents');
292
+
293
+ return detected;
294
+ }
295
+
296
+ /**
297
+ * Parse command line arguments.
298
+ *
299
+ * @param {string[]} argv Arguments after the executable name.
300
+ * @returns {{ command: string, subcommand: string, targets: string[], all: boolean, force: boolean, dryRun: boolean, cwd: string, help: boolean }} Parsed options.
301
+ */
302
+ function parseArgs(argv) {
303
+ const options = {
304
+ command: argv[0] ?? 'help',
305
+ subcommand: argv[1] ?? '',
306
+ targets: [],
307
+ all: false,
308
+ force: false,
309
+ dryRun: false,
310
+ cwd: process.cwd(),
311
+ help: false
312
+ };
313
+
314
+ const start = options.command === 'agents' ? 2 : 1;
315
+
316
+ for (let index = start; index < argv.length; index += 1) {
317
+ const argument = argv[index];
318
+
319
+ if (argument === '--all') {
320
+ options.all = true;
321
+ continue;
322
+ }
323
+ if (argument === '--force') {
324
+ options.force = true;
325
+ continue;
326
+ }
327
+ if (argument === '--dry-run') {
328
+ options.dryRun = true;
329
+ continue;
330
+ }
331
+ if (argument === '--help' || argument === '-h') {
332
+ options.help = true;
333
+ continue;
334
+ }
335
+ if (argument === '--cwd') {
336
+ const value = argv[index + 1];
337
+ if (!value) throw new AgentKitError('--cwd needs a directory path.');
338
+ options.cwd = resolve(value);
339
+ index += 1;
340
+ continue;
341
+ }
342
+ if (argument.startsWith('--cwd=')) {
343
+ options.cwd = resolve(argument.slice('--cwd='.length));
344
+ continue;
345
+ }
346
+ if (argument === '--tool' || argument === '--target') {
347
+ const value = argv[index + 1];
348
+ if (!value) throw new AgentKitError('--tool needs a target name.');
349
+ options.targets.push(...value.split(','));
350
+ index += 1;
351
+ continue;
352
+ }
353
+ if (argument.startsWith('--tool=') || argument.startsWith('--target=')) {
354
+ options.targets.push(...argument.slice(argument.indexOf('=') + 1).split(','));
355
+ continue;
356
+ }
357
+ if (argument.startsWith('-')) {
358
+ throw new AgentKitError(`Unknown option "${argument}". Run svelte-elevenlabs agents --help.`);
359
+ }
360
+
361
+ options.targets.push(...argument.split(','));
362
+ }
363
+
364
+ options.targets = [...new Set(options.targets.map((entry) => entry.trim()).filter(Boolean))];
365
+ return options;
366
+ }
367
+
368
+ /**
369
+ * Print command help.
370
+ */
371
+ function printHelp() {
372
+ console.log(`svelte-elevenlabs agent kit
373
+
374
+ Usage:
375
+ svelte-elevenlabs agents list
376
+ svelte-elevenlabs agents install [targets] [--all] [--force] [--dry-run]
377
+ svelte-elevenlabs agents print [target]
378
+
379
+ Targets:
380
+ ${TARGETS.map((target) => target.name).join(', ')}
381
+
382
+ Examples:
383
+ svelte-elevenlabs agents install
384
+ svelte-elevenlabs agents install claude cursor opencode
385
+ svelte-elevenlabs agents install --all
386
+ svelte-elevenlabs agents install claude --dry-run
387
+ svelte-elevenlabs agents print cursor
388
+
389
+ With no target names, the command detects configured agents and falls back to
390
+ AGENTS.md. Shared files keep their existing content. Re-running the command
391
+ updates only the managed block between the svelte-elevenlabs markers.`);
392
+ }
393
+
394
+ /**
395
+ * Print every target and its destination.
396
+ */
397
+ function listTargets() {
398
+ console.log('Available agent targets:\n');
399
+ for (const target of TARGETS) {
400
+ console.log(` ${target.name.padEnd(9)} ${target.label.padEnd(28)} ${target.path}`);
401
+ }
402
+ console.log('\nUse "svelte-elevenlabs agents install --all" to install every target.');
403
+ }
404
+
405
+ /**
406
+ * Run the agents command.
407
+ *
408
+ * @param {object} options Parsed options.
409
+ */
410
+ function runAgents(options) {
411
+ if (
412
+ !options.subcommand ||
413
+ options.subcommand === 'help' ||
414
+ options.subcommand === '--help' ||
415
+ options.subcommand === '-h' ||
416
+ options.help
417
+ ) {
418
+ printHelp();
419
+ return;
420
+ }
421
+
422
+ const version = readVersion();
423
+
424
+ if (options.subcommand === 'list') {
425
+ listTargets();
426
+ return;
427
+ }
428
+
429
+ const guide = readGuide();
430
+
431
+ if (options.subcommand === 'print') {
432
+ const requested = options.targets[0];
433
+ if (!requested) {
434
+ process.stdout.write(`${guide}\n`);
435
+ return;
436
+ }
437
+ const target = findTarget(requested);
438
+ process.stdout.write(renderTarget(target, guide, version));
439
+ return;
440
+ }
441
+
442
+ if (options.subcommand !== 'install') {
443
+ throw new AgentKitError(
444
+ `Unknown agents command "${options.subcommand}". Use list, install, or print.`
445
+ );
446
+ }
447
+
448
+ if (!options.cwd || !existsSync(options.cwd) || !statSync(options.cwd).isDirectory()) {
449
+ throw new AgentKitError(`The project directory does not exist: ${options.cwd}`);
450
+ }
451
+
452
+ const detected = detectTargets(options.cwd);
453
+ const requested = options.all
454
+ ? TARGETS.map((target) => target.name)
455
+ : options.targets.length > 0
456
+ ? options.targets.map((name) => findTarget(name).name)
457
+ : detected;
458
+
459
+ const selected = requested.length > 0 ? requested : ['agents'];
460
+ const unique = [...new Set(selected)];
461
+ const results = unique.map((name) =>
462
+ installTarget(findTarget(name), {
463
+ cwd: options.cwd,
464
+ guide,
465
+ version,
466
+ force: options.force,
467
+ dryRun: options.dryRun
468
+ })
469
+ );
470
+
471
+ console.log(
472
+ `${options.dryRun ? 'Agent kit dry run' : 'Agent kit installed'} for ${results.length} ${
473
+ results.length === 1 ? 'target' : 'targets'
474
+ }:\n`
475
+ );
476
+
477
+ for (const result of results) {
478
+ const displayPath = relative(options.cwd, join(options.cwd, result.path)).replaceAll('\\', '/');
479
+ const suffix = result.detail ? ` (${result.detail})` : '';
480
+ console.log(` ${result.status.padEnd(13)} ${displayPath}${suffix}`);
481
+ }
482
+
483
+ if (requested.length === 0) {
484
+ console.log(
485
+ '\nNo agent configuration was detected, so the guide was added to AGENTS.md. Use --all or name a tool to install more targets.'
486
+ );
487
+ }
488
+
489
+ if (!options.dryRun) {
490
+ console.log('\nRestart your agent session, then ask it to use svelte-elevenlabs.');
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Entry point with human readable failure handling.
496
+ */
497
+ function main() {
498
+ const options = parseArgs(process.argv.slice(2));
499
+
500
+ if (options.command === 'help' || options.command === '--help' || options.command === '-h') {
501
+ printHelp();
502
+ return;
503
+ }
504
+
505
+ if (options.command !== 'agents') {
506
+ throw new AgentKitError(
507
+ `Unknown command "${options.command}". Use "svelte-elevenlabs agents --help".`
508
+ );
509
+ }
510
+
511
+ runAgents(options);
512
+ }
513
+
514
+ process.stdout.on('error', (error) => {
515
+ if (error.code === 'EPIPE') process.exit(0);
516
+ throw error;
517
+ });
518
+
519
+ try {
520
+ main();
521
+ } catch (error) {
522
+ if (error instanceof AgentKitError) {
523
+ console.error(`[svelte-elevenlabs] ${error.message}`);
524
+ } else {
525
+ console.error('[svelte-elevenlabs] The agent kit command failed.', error);
526
+ }
527
+ process.exitCode = 1;
528
+ }
@@ -27,6 +27,14 @@ const DEFAULT_SAMPLE_RATE = 16_000;
27
27
  const DEFAULT_CHUNK_MS = 100;
28
28
  const VAD_ATTACK_FRAMES = 2;
29
29
  const VAD_RELEASE_FRAMES = 8;
30
+ /** Keep dropping agent chunks for a moment after a barge-in or user speech. */
31
+ const PLAYBACK_SUPPRESSION_MS = 300;
32
+ /**
33
+ * Wait this long after the last scheduled chunk before reporting idle.
34
+ * TTS streams sentence by sentence, and the gap between sentences can be
35
+ * close to a second, so a short gap does not mean the response is finished.
36
+ */
37
+ const PLAYBACK_IDLE_GRACE_MS = 1_000;
30
38
  /** Stop every track on a media stream, even after the manager was closed. */
31
39
  function stopStream(stream) {
32
40
  if (!stream)
@@ -661,10 +669,12 @@ export class AudioManager {
661
669
  if (!ctx || !master)
662
670
  return false;
663
671
  const parsed = typeof format === 'string' ? parseAudioFormat(format) : format;
664
- // Drop late chunks while the user is actively barging in.
672
+ // Drop late chunks while the user is barging in, and for a short
673
+ // grace period after the interruption. Checking only the timestamp
674
+ // let agent audio resume after 300 ms even while the user was still
675
+ // speaking.
665
676
  if (this.#options.interruptOnUserSpeech !== false &&
666
- this.#userSpeaking &&
667
- performance.now() < this.#suppressPlaybackUntil) {
677
+ (this.#userSpeaking || performance.now() < this.#suppressPlaybackUntil)) {
668
678
  return false;
669
679
  }
670
680
  try {
@@ -688,10 +698,11 @@ export class AudioManager {
688
698
  if (this.#nextStartTime < now) {
689
699
  this.#nextStartTime = now + 0.02;
690
700
  }
691
- // Recover from a long stall instead of scheduling seconds into the future.
692
- if (this.#nextStartTime - now > 1.5) {
693
- this.#nextStartTime = now + 0.02;
694
- }
701
+ // Fast TTS streams can deliver many seconds of audio in a few
702
+ // seconds. Keep every chunk in order; resetting the cursor here
703
+ // collapsed the backlog into overlapping audio and made responses
704
+ // sound truncated. A real stall is handled by the branch above
705
+ // when the context clock catches up with the scheduled cursor.
695
706
  const startAt = this.#nextStartTime;
696
707
  source.start(startAt);
697
708
  this.#nextStartTime = startAt + buffer.duration;
@@ -747,7 +758,7 @@ export class AudioManager {
747
758
  }
748
759
  this.#playback.clear();
749
760
  this.#nextStartTime = 0;
750
- this.#suppressPlaybackUntil = performance.now() + 300;
761
+ this.#suppressPlaybackUntil = performance.now() + PLAYBACK_SUPPRESSION_MS;
751
762
  this.#setOutputActive(false);
752
763
  }
753
764
  /**
@@ -1100,7 +1111,8 @@ export class AudioManager {
1100
1111
  this.#speechFrames += 1;
1101
1112
  if (!this.#userSpeaking && this.#speechFrames >= VAD_ATTACK_FRAMES) {
1102
1113
  this.#setUserSpeaking(true);
1103
- if (this.#options.interruptOnUserSpeech !== false && this.#outputActive) {
1114
+ if (this.#options.interruptOnUserSpeech !== false &&
1115
+ (this.#outputActive || this.#playback.size > 0)) {
1104
1116
  this.flushPlayback();
1105
1117
  this.#options.onLocalInterruption?.();
1106
1118
  }
@@ -1123,6 +1135,9 @@ export class AudioManager {
1123
1135
  if (!speaking) {
1124
1136
  this.#speechFrames = 0;
1125
1137
  this.#silenceFrames = 0;
1138
+ // Drop stale chunks from the interrupted response that may still
1139
+ // be in flight after the user stops speaking.
1140
+ this.#suppressPlaybackUntil = performance.now() + PLAYBACK_SUPPRESSION_MS;
1126
1141
  }
1127
1142
  this.#options.onUserSpeakingChange?.(speaking);
1128
1143
  }
@@ -1141,7 +1156,7 @@ export class AudioManager {
1141
1156
  if (this.#idleTimer)
1142
1157
  clearTimeout(this.#idleTimer);
1143
1158
  const generation = this.#playbackGeneration;
1144
- const delay = Math.max(0, (this.#nextStartTime - ctx.currentTime) * 1000) + 140;
1159
+ const delay = Math.max(0, (this.#nextStartTime - ctx.currentTime) * 1000) + PLAYBACK_IDLE_GRACE_MS;
1145
1160
  this.#idleTimer = setTimeout(() => {
1146
1161
  if (this.#closed || generation !== this.#playbackGeneration)
1147
1162
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-elevenlabs",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A community-driven Svelte 5 library and starter kit for ElevenLabs Conversational AI and low-latency audio streaming.",
5
5
  "license": "MIT",
6
6
  "author": "AnonymousGCA (https://github.com/GABRYCA)",
@@ -20,6 +20,9 @@
20
20
  },
21
21
  "type": "module",
22
22
  "packageManager": "bun@1.4.3",
23
+ "bin": {
24
+ "svelte-elevenlabs": "./bin/svelte-elevenlabs.js"
25
+ },
23
26
  "engines": {
24
27
  "node": ">=22.17.0",
25
28
  "bun": ">=1.4.0"
@@ -60,6 +63,8 @@
60
63
  },
61
64
  "files": [
62
65
  "dist",
66
+ "agent-kit",
67
+ "bin",
63
68
  "!dist/**/*.test.*",
64
69
  "!dist/**/*.spec.*"
65
70
  ],