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 +52 -4
- package/agent-kit/README.md +97 -0
- package/agent-kit/instructions.md +472 -0
- package/bin/svelte-elevenlabs.js +528 -0
- package/dist/audio/audioManager.js +25 -10
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# svelte-elevenlabs
|
|
2
2
|
|
|
3
|
+
[](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
|
-
###
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
692
|
-
|
|
693
|
-
|
|
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() +
|
|
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 &&
|
|
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) +
|
|
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.
|
|
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
|
],
|