Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5d2a9d4
feat: setup installs ElevenLabs in your coding agents, and onboard ca…
bitpshr Nov 10, 2026
0301d97
fix: setup only runs a program called claude, stops what it starts, a…
bitpshr Nov 10, 2026
cbad495
ci: run setup on Windows with an agent in a folder with parentheses
bitpshr Nov 10, 2026
6846743
fix: setup finds your agents from PowerShell and cmd, and Ctrl-C stop…
bitpshr Nov 10, 2026
dc5d2aa
feat: onboard offers setup after every sign-in
bitpshr Nov 11, 2026
8b9dfc0
fix: setup never runs a program from a relative PATH entry, and fails…
bitpshr Nov 11, 2026
57a0e56
test: setup's agent detection, install rules, skills copy and child p…
bitpshr Nov 11, 2026
98b1ce5
test: setup, the onboarding offer and interrupts through the binary
bitpshr Nov 11, 2026
174828e
ci: run setup on Windows against stand-in Claude Code and Codex, incl…
bitpshr Nov 11, 2026
b4bb977
fix: a setup command stopped at its limit leaves nothing running, eve…
bitpshr Nov 11, 2026
715ceb9
test: setup and onboarding tests can run twice at once on one machine
bitpshr Nov 11, 2026
2b6fdd7
fix: setup shows a short, clean error however much a failed install p…
bitpshr Nov 11, 2026
f3b4017
fix: setup takes Claude Code from CLAUDE_CODE_EXECPATH only when it i…
bitpshr Nov 11, 2026
1662a28
fix: setup says the home folder is missing instead of blaming the agent
bitpshr Nov 11, 2026
16599ed
test: setup with no agent found prints what the skills copy did
bitpshr Nov 11, 2026
0329b28
test: the replies of a project that has not signed in say where they …
bitpshr Nov 11, 2026
7a68c2c
ci: the PowerShell setup step passes when its checks do, and runs for…
bitpshr Nov 11, 2026
c98d8e3
test: setup leaves nothing running when a program keeps starting othe…
bitpshr Nov 11, 2026
49ac660
fix: setup's error line drops every terminal escape, not only colours…
bitpshr Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ tests/e2e_smoke.rs
tests/say_test.rs
tests/onboard_test.rs
tests/flows_wait_test.rs
tests/fixtures/
.fern/replay.lock
.fern/replay.yml
.gitattributes
Expand Down
157 changes: 157 additions & 0 deletions .github/workflows/windows-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ on:
paths:
- cli/elevenlabs/workflow/onboard/**
- cli/elevenlabs/workflow/loopback.rs
- cli/elevenlabs/workflow/setup.rs
- cli/elevenlabs/workflow/setup/**
- .github/workflows/windows-smoke.yml

jobs:
Expand Down Expand Up @@ -50,6 +52,161 @@ jobs:
cargo build --bin elevenlabs
cygpath -w "$PWD/target/debug" >> "$GITHUB_PATH"

# npm installs agents as .cmd shims, often under a user folder such as
# `C:\Users\Ann (Work)`, whose parentheses break a hand-built `cmd /C`.
- name: Set up coding agents installed in a folder with parentheses
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
base_path="$PATH"
bin="$(mktemp -d)/agents (test) dir"
mkdir -p "$bin"
# Stand-ins for `claude` and `codex` that log their arguments, one
# line per run; the claude in $failing refuses `plugin install`.
stand_in() {
cat > "$1/$2.cmd" <<EOF
@echo off
echo %*>>"$(cygpath -w "$1/$2.log")"
$3
exit /b 0
EOF
}
stand_in "$bin" claude ""
stand_in "$bin" codex ""
failing="$(mktemp -d)/failing agent"
mkdir -p "$failing"
stand_in "$failing" claude 'echo %* | findstr /C:"plugin install" >nul && (>&2 echo install refused& exit /b 1)'
for var in USERPROFILE HOME CLAUDE_CONFIG_DIR CODEX_HOME; do
export "$var=$(cygpath -w "$(mktemp -d)")"
done
unset CLAUDE_CODE_EXECPATH
# Quotes std may add around an argument, and cmd's line endings, aside.
ran() { tr -d '"\r' < "$1" 2>/dev/null || true; }
expect() {
if [[ "$(ran "$1")" != "$2" ]]; then
echo "::error::$3, got:"
ran "$1"
exit 1
fi
}

export PATH="$bin:$base_path"
elevenlabs setup
expect "$bin/claude.log" "" "setup without --yes ran a Claude Code command"
expect "$bin/codex.log" "" "setup without --yes ran a Codex command"

elevenlabs setup --yes
expect "$bin/claude.log" "$(printf '%s\n' \
"plugin marketplace update elevenlabs" \
"plugin install elevenlabs --marketplace elevenlabs/plugin --scope user" \
"plugin update elevenlabs@elevenlabs")" "Expected the three Claude Code commands"
expect "$bin/codex.log" "$(printf '%s\n' \
"plugin marketplace add elevenlabs/plugin" \
"plugin marketplace upgrade elevenlabs" \
"plugin add elevenlabs@elevenlabs")" "Expected the three Codex commands"

export PATH="$failing:$base_path"
code=0
out=$(elevenlabs setup --yes) || code=$?
echo "$out"
if [[ "$code" != 1 ]]; then
echo "::error::setup --yes with a failing install exited with $code, not 1"
exit 1
fi
refused='Claude Code: failed: `claude plugin install elevenlabs --marketplace elevenlabs/plugin --scope user` exited with 1: install refused'
if [[ "$out" != *"$refused"* ]]; then
echo "::error::setup --yes didn't show the install's error"
exit 1
fi
expect "$failing/claude.log" "$(printf '%s\n' \
"plugin marketplace update elevenlabs" \
"plugin install elevenlabs --marketplace elevenlabs/plugin --scope user")" \
"Expected Claude Code to stop at the failed install"

# PowerShell and cmd pass the search path as `Path`; Git Bash passes `PATH`.
- name: Set up coding agents from PowerShell
if: github.event_name == 'pull_request'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
function New-TempFolder {
New-Item -ItemType Directory (Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid()))
}
# A stand-in that logs its arguments, one line per run, and runs $also first.
function New-StandIn($folder, $name, $also) {
Set-Content -Encoding ascii -Path (Join-Path $folder "$name.cmd") -Value @(
'@echo off'
"echo %*>>`"$(Join-Path $folder "$name.log")`""
$also
'exit /b 0'
)
}
# What a stand-in ran, quotes std may add around an argument aside.
function Get-Ran($folder, $name) {
$log = Join-Path $folder "$name.log"
if (-not (Test-Path $log)) { return '' }
@(Get-Content $log | ForEach-Object { $_.Replace('"', '').Trim() }) -join "`n"
}
function Assert-Ran($folder, $name, $expected, $what) {
$ran = Get-Ran $folder $name
if ($ran -cne ($expected -join "`n")) {
Write-Host "::error::$what, got:"
Write-Host $ran
exit 1
}
}
$bin = (New-TempFolder).FullName
New-StandIn $bin 'claude' ''
New-StandIn $bin 'codex' ''
$failing = (New-TempFolder).FullName
New-StandIn $failing 'claude' 'echo %* | findstr /C:"plugin install" >nul && (>&2 echo install refused& exit /b 1)'
$basePath = $env:Path
# Spelled `Path` whatever the runner passed.
function Set-SearchPath($first) {
Remove-Item env:Path
[Environment]::SetEnvironmentVariable('Path', "$first;$basePath")
}
foreach ($var in 'USERPROFILE', 'HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME') {
Set-Item "env:$var" (New-TempFolder).FullName
}
Remove-Item env:CLAUDE_CODE_EXECPATH -ErrorAction SilentlyContinue
Set-SearchPath $bin
$name = [Environment]::GetEnvironmentVariables().Keys | Where-Object { $_ -ieq 'path' }
Write-Host "The search path variable is spelled: $name"
if ($name -cne 'Path') { throw "Expected the search path variable spelled Path, got $name" }

elevenlabs setup
if ($LASTEXITCODE -ne 0) { throw "setup exited with $LASTEXITCODE" }
Assert-Ran $bin 'claude' @() 'setup without --yes ran a Claude Code command'
Assert-Ran $bin 'codex' @() 'setup without --yes ran a Codex command'

elevenlabs setup --yes
if ($LASTEXITCODE -ne 0) { throw "setup --yes exited with $LASTEXITCODE" }
Assert-Ran $bin 'claude' @(
'plugin marketplace update elevenlabs'
'plugin install elevenlabs --marketplace elevenlabs/plugin --scope user'
'plugin update elevenlabs@elevenlabs'
) 'Expected the three Claude Code commands'
Assert-Ran $bin 'codex' @(
'plugin marketplace add elevenlabs/plugin'
'plugin marketplace upgrade elevenlabs'
'plugin add elevenlabs@elevenlabs'
) 'Expected the three Codex commands'

Set-SearchPath $failing
$out = (elevenlabs setup --yes) -join "`n"
$code = $LASTEXITCODE
Write-Host $out
if ($code -ne 1) { throw "setup --yes with a failing install exited with $code, not 1" }
$refused = 'Claude Code: failed: `claude plugin install elevenlabs --marketplace elevenlabs/plugin --scope user` exited with 1: install refused'
if (-not $out.Contains($refused)) { throw "setup --yes didn't show the install's error" }
Assert-Ran $failing 'claude' @(
'plugin marketplace update elevenlabs'
'plugin install elevenlabs --marketplace elevenlabs/plugin --scope user'
) 'Expected Claude Code to stop at the failed install'
# The failed setup above exited 1 on purpose, and pwsh would end the step with it.
exit 0

- name: Set up Node.js
uses: actions/setup-node@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "elevenlabs-cli"
version = "1.10.0"
version = "1.11.0"
edition = "2021"
description = "CLI for the ElevenLabs API"
license = "MIT"
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The CLI does two things:
- [Agents as Code](#agents-as-code)
- [Data residency](#data-residency)
- [UI components](#ui-components)
- [Set up your coding agents](#set-up-your-coding-agents)
- [Usage](#usage)
- [Documentation](#documentation)
- [Advanced](#advanced)
Expand Down Expand Up @@ -443,6 +444,24 @@ elevenlabs components add # all components
elevenlabs components add conversation-bar
```

## Set up your coding agents

`elevenlabs setup` installs the [ElevenLabs plugin](https://github.com/elevenlabs/plugin) (the ElevenLabs skills and the hosted MCP server) into the coding agents it finds, through each agent's own plugin command:

```bash
elevenlabs setup # say what it would do, and change nothing (so does --dry-run)
elevenlabs setup --yes # do it
```

- **Claude Code** (when run inside it, the program in `CLAUDE_CODE_EXECPATH` if that is `claude`, else `claude` on `PATH`): `claude plugin install elevenlabs --marketplace elevenlabs/plugin --scope user`, then `claude plugin update elevenlabs@elevenlabs`.
- **Codex** (`codex` on `PATH`): `codex plugin marketplace add elevenlabs/plugin`, then `codex plugin add elevenlabs@elevenlabs`.
- **Cursor** (a `~/.cursor` folder, or run inside Cursor) has no install command: `setup` prints the link to [its marketplace page](https://cursor.com/marketplace/elevenlabs), where you select Install.
- **None of these**: `setup` copies the [ElevenLabs skills](https://github.com/elevenlabs/skills) into `~/.agents/skills`, one folder per skill. A folder there that `setup` didn't write is left as it is, and named in the output.

Running it again updates the plugin. Each agent command may take 30 seconds and the whole run 100; `setup` exits 1 when an agent's install fails, with that agent's error and the command to try again. Agent commands run in your home folder, never in the project, and pressing Ctrl-C stops the one running along with anything it started. To finish, restart the agent and sign in to the ElevenLabs MCP server: run `/mcp` in Claude Code, or `codex mcp login elevenlabs` for Codex.

When onboarding through a coding agent (`npx -y @elevenlabs/cli@latest onboard init`), the flow can end by offering to do this for you; saying yes runs `onboard setup`, which does what `setup --yes` does.

## Usage

Every API resource appears as a subcommand (e.g. `elevenlabs <resource> <method>`). Run `elevenlabs <resource> --help` to see available methods.
Expand Down Expand Up @@ -563,6 +582,11 @@ approval link, where `<outcome>` is a fixed word such as `timed-out`,
SHA-256 hash of that sign-in's `state`, so it can be matched to its approval
page; the state itself is never sent.

When you accept the offer to set up your coding agents at the end of
onboarding, `onboard setup` makes one account request (`GET /v1/user`, tagged
`cmd/onboard.setup`) with the project's key, so we can count how many people
accept it; with telemetry off it makes none.

To turn it off, pass `--telemetry off` to any command, or set
`ELEVENLABS_TELEMETRY=off` (or `DO_NOT_TRACK=1`) to turn it off everywhere.
`--telemetry on` turns it back on for one command. Turning it off does not
Expand Down
2 changes: 2 additions & 0 deletions cli/elevenlabs/workflow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod project;
mod residency;
mod say;
mod settings;
mod setup;
mod skills;
mod telemetry;
mod templates;
Expand Down Expand Up @@ -57,6 +58,7 @@ pub fn register(app: CliApp) -> CliApp {
let app = feedback::register(app);
let app = say::register(app);
let app = flows_wait::register(app);
let app = setup::register(app);
// Shadows the framework's built-in; see that module's docs.
let app = skills::register(app);
components::register(app)
Expand Down
6 changes: 2 additions & 4 deletions cli/elevenlabs/workflow/onboard/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde_json::Value;

use super::super::say::run_async as block_on;
use super::super::settings;
use super::super::setup::SKILLS_ARCHIVE;
use super::auth::{api_base, check_key, sign_in_config, KeyCheck};
use super::connect;
use super::env_file::{self, API_KEY_ENV};
Expand Down Expand Up @@ -77,7 +78,7 @@ pub(super) fn verify(ctx: &AppContext, project: &Project, answered: bool) -> Res
load_key(project, &ending, &name, say)
}
(true, _, None) => match own_agent(ctx, project, &key) {
Some(agent) => agent_ready(&ending, file, &agent, reads_key)?,
Some(agent) => super::or_offer_setup(project, agent_ready(&ending, file, &agent, reads_key)?),
None => offer_test(project, &ending, file),
},
(false, false, _) => ask_what_to_build(project, &ending, file, "Your key works."),
Expand Down Expand Up @@ -245,9 +246,6 @@ pub(super) fn app_origin_for(residency: &str, authorize_url: &str) -> Result<Str
pub(super) const WHAT_TO_BUILD: &str =
"What would you like to build with ElevenLabs, or should I just add the SDK for now?";

/// The ElevenLabs skills, the whole repo in one download.
const SKILLS_ARCHIVE: &str = "https://github.com/elevenlabs/skills/archive/refs/heads/main.tar.gz";

/// The question, from `connect` or `check`, whenever the code doesn't use the
/// SDK yet: `say` ends with [`WHAT_TO_BUILD`]. Its `run` passes `--answered`,
/// so the next reply, right after the answer, is how to write the code.
Expand Down
3 changes: 2 additions & 1 deletion cli/elevenlabs/workflow/onboard/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ pub(super) fn handle(
super::sign_in_tag(previous.as_ref()),
);
let reply = sign_in_and_store(matches, ctx, &project, previous.as_ref(), || {
session.signed_in(&lock)
session.signed_in(&lock);
session.offer_setup();
})
.unwrap_or_else(|e| failed(&project, &e.to_string(), replace));
session.finish(&lock, &reply.to_value())?;
Expand Down
Loading
Loading