diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b327da..8c8236f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,3 +67,21 @@ jobs: with: files: dist/* fail_on_unmatched_files: true + + install-smoke: + # Post-release verification: the tagged release must already exist before the + # tarballs are downloadable, so this runs after `release` publishes and is + # informational (not a merge gate). Exercises install.sh's full path: + # asset resolution → download → SHA256 verify → extract → install → --verify. + name: Install smoke (linux) + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install via install.sh against the tagged release + run: | + TAG="${GITHUB_REF#refs/tags/}" + bash install.sh --ref "${TAG}" --verify --no-mcp --quiet + export PATH="$HOME/.local/bin:$PATH" + "$HOME/.local/bin/lksr" --version + "$HOME/.local/bin/lksr" guide | head -5 diff --git a/README.md b/README.md index ff5e83b..9e94f0a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@
-![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-blue.svg) -![Python](https://img.shields.io/badge/Python-3.10%E2%80%933.13-blue.svg) +![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS-blue.svg) +![Rust](https://img.shields.io/badge/Rust-1.97-blue.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![CI](https://github.com/quangdang46/livekit_agent_simulator/actions/workflows/ci.yml/badge.svg)](https://github.com/quangdang46/livekit_agent_simulator/actions/workflows/ci.yml) [![GitHub release](https://img.shields.io/github/v/release/quangdang46/livekit_agent_simulator)](https://github.com/quangdang46/livekit_agent_simulator/releases) @@ -15,7 +15,7 @@
**Dial any LiveKit voice agent with an AI simulated caller — WebRTC room, inbound SIP, or outbound SIP — and keep a full forensic log.** -Standalone MCP server + CLI (`lks`). Black-box testing: no imports from the agent under test, no edits to its code or `.env`. +Standalone MCP server + CLI (`lksr`, the Rust build). Black-box testing: no imports from the agent under test, no edits to its code or `.env`.

Quick Install

@@ -32,10 +32,10 @@ curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit_agent_simulato Paste into Claude Code, Cursor, Codex, AmpCode, Windsurf, or any coding agent **from the repo you want to test**: ```text -Install and configure livekit-agent-simulator (CLI: lks) for this project by following the instructions here: +Install and configure livekit-agent-simulator (CLI: lksr) for this project by following the instructions here: https://raw.githubusercontent.com/quangdang46/livekit_agent_simulator/main/docs/guide/installation.md -Target project root is this workspace. Use absolute --root paths. Install the portable CLI if missing, run lks init, help fill .agent-sim/config.yaml from my local env or ask me for LiveKit + active caller provider key (Gemini Live or OpenAI Realtime) + agent_name, ensure .agent-sim is gitignored, run preflight, and stop before execute if the voice agent worker is not running. Do not edit agent application source outside .agent-sim/. +Target project root is this workspace. Use absolute --root paths. Install the CLI if missing, run lksr init, help fill .agent-sim/config.yaml from my local env or ask me for LiveKit + active caller provider key (Gemini Live or OpenAI Realtime) + agent_name, ensure .agent-sim is gitignored, run preflight, and stop before execute if the voice agent worker is not running. Do not edit agent application source outside .agent-sim/. ``` Same idea, one line: @@ -65,7 +65,7 @@ Voice agents fail in ways unit tests never see: | Surface | What you get | |---------|--------------| -| `lks` CLI | init → preflight → execute → report → web | +| `lksr` CLI | init → preflight → execute → report → web | | MCP server | Same ops for Claude Code, Cursor, Codex, … | | Transport modes | `webrtc_sim` · `inbound_sip` · `outbound_human_pickup` · `outbound_sim_callee` (optional `agent_dials`) | | Reports | `events.jsonl`, `timeline.md`, `summary.json`, optional stereo WAV | @@ -93,13 +93,13 @@ curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit_agent_simulato | bash -s -- --verify # In the repo you want to test (agent worker must already be running) -lks init --root /path/to/target +lksr init --root /path/to/target # edit /path/to/target/.agent-sim/config.yaml (LiveKit + active provider keys, agent_name) -lks preflight --root /path/to/target -lks execute smoke-hello --root /path/to/target -lks report --root /path/to/target -lks web --root /path/to/target # Ctrl+C to stop +lksr preflight --root /path/to/target +lksr execute smoke-hello --root /path/to/target +lksr report --root /path/to/target +lksr web --root /path/to/target # Ctrl+C to stop ``` --- @@ -183,7 +183,7 @@ Mode details and config: [docs/telephony.md](docs/telephony.md). Templates: `inb ### Quick install (recommended) -**Download only — no uv/pip/build on your machine.** CI ships a portable pack (embedded Python + deps + report player). +**Download only — no uv/pip/build on your machine.** CI ships a single static `lksr` binary (SHA256-verified). ```bash # macOS / Linux @@ -192,26 +192,20 @@ curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit_agent_simulato ``` ```powershell -# Windows PowerShell +# Windows PowerShell (installs the Python build — a Rust Windows binary is not yet shipped) irm "https://raw.githubusercontent.com/quangdang46/livekit_agent_simulator/main/install.ps1" -OutFile "$env:TEMP\lks-install.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "$env:TEMP\lks-install.ps1" -Verify ``` -Also available from a release asset: - -```bash -curl -fsSL "https://github.com/quangdang46/livekit_agent_simulator/releases/download/v0.1.0/install.sh" \ - | bash -s -- --verify -``` - | Flag | Purpose | |------|---------| -| `--verify` | Checksum verification | -| `--ref v0.1.0` | Pin release tag | +| `--verify` | Post-install check (`lksr --version` + `lksr guide`) | +| `--ref v0.1.0-rust` | Pin release tag | | `--no-mcp` | Skip MCP registration into coding tools | +| `--easy-mode` | Append install dir to shell `PATH` in rc files | | `--uninstall` | Remove install | -By default the installer registers the MCP server `livekit-agent-simulator` (`lks mcp`) into detected tools: Claude Code, Cursor, Cline, Windsurf, VS Code Copilot, Gemini CLI, Amazon Q, OpenCode, Codex, Warp. +By default the installer registers the MCP server `livekit-agent-simulator` (`lksr mcp`) into detected tools: Claude Code, Cursor, Cline, Windsurf, VS Code Copilot, Gemini CLI, Amazon Q, OpenCode, Codex, Warp. **Agent-oriented install playbook (long form):** [docs/guide/installation.md](docs/guide/installation.md) Raw URL for paste into agents: @@ -235,7 +229,7 @@ Users never build this — CI packs `web/dist` into the wheel as `web_static`. S ```bash pnpm --dir web install pnpm --dir web build # → web/dist/ (attached by Hatch on uv build) -pnpm --dir web dev # HMR; proxy /api + /runs → lks web :8765 +pnpm --dir web dev # HMR; proxy /api + /runs → lksr web :8765 ``` See `web/README.md`. @@ -246,17 +240,17 @@ See `web/README.md`. ```bash # Agent worker must be running and registered with LiveKit -lks guide -lks init --root /path/to/target +lksr guide +lksr init --root /path/to/target # fill .agent-sim/config.yaml -lks preflight --root /path/to/target -lks scenario-init smoke-hello --root /path/to/target # if needed -lks validate smoke-hello --root /path/to/target -lks execute smoke-hello --root /path/to/target -lks runs --root /path/to/target -lks report --root /path/to/target -lks web --root /path/to/target +lksr preflight --root /path/to/target +lksr scenario-init smoke-hello --root /path/to/target # if needed +lksr validate smoke-hello --root /path/to/target +lksr execute smoke-hello --root /path/to/target +lksr runs --root /path/to/target +lksr report --root /path/to/target +lksr web --root /path/to/target ``` ### Minimal scenario (`smoke-hello`) @@ -285,7 +279,7 @@ pass_criteria: - The agent responded in the caller's language ``` -Optional multi-judge PassCriteria: `judges[]` + `mode` (`all` \| `majority` \| `any`). Assert highlights (`tool_order`, `constraint_respected`, recovery/latency): `lks guide`. +Optional multi-judge PassCriteria: `judges[]` + `mode` (`all` \| `majority` \| `any`). Assert highlights (`tool_order`, `constraint_respected`, recovery/latency): `lksr guide`. Full-line `#` comments in scaffolded YAML are guides — runtime ignores them. Legacy `*.jsonl` scenarios are still read. @@ -341,9 +335,9 @@ simulator: ``` ```bash -lks execute smoke-hello # `gemini` (marked default: true) -lks execute smoke-hello --profile gemini # Gemini Live caller -lks execute smoke-hello --profile openai # OpenAI Realtime caller +lksr execute smoke-hello # `gemini` (marked default: true) +lksr execute smoke-hello --profile gemini # Gemini Live caller +lksr execute smoke-hello --profile openai # OpenAI Realtime caller ``` **Selection** (`--profile` absent): if **exactly one** profile has @@ -374,7 +368,7 @@ block (backward compatible). ## Commands -CLI and MCP share the same public ops (`ops.py`). Prefer `execute` (validate then run). +CLI and MCP share the same public ops. Prefer `execute` (validate then run). All commands below run as `lksr `. MCP tool names are identical (accessed via the `livekit-agent-simulator` server). | CLI | MCP tool | Purpose | |-----|----------|---------| @@ -401,15 +395,15 @@ CLI and MCP share the same public ops (`ops.py`). Prefer `execute` (validate the | `mcp` | — | Start MCP server (stdio) | ```bash -lks execute smoke-hello --root /path/to/target -lks execute-all --tag smoke --root /path/to/target -lks serve --root /path/to/target # REST API on :8787 (same ops as CLI/MCP) -lks log --root /path/to/target -lks compare --root /path/to/target -lks compare --baseline --root /path/to/target -lks optimize scen-a,scen-b --held-out scen-c --root /path/to/target # → optimized// -lks execute scen-a --optimized --root /path/to/target # apply the winner -lks web --port 8765 --root /path/to/target +lksr execute smoke-hello --root /path/to/target +lksr execute-all --tag smoke --root /path/to/target +lksr serve --root /path/to/target # REST API on :8787 (same ops as CLI/MCP) +lksr log --root /path/to/target +lksr compare --root /path/to/target +lksr compare --baseline --root /path/to/target +lksr optimize scen-a,scen-b --held-out scen-c --root /path/to/target # → optimized// +lksr execute scen-a --optimized --root /path/to/target # apply the winner +lksr web --port 8765 --root /path/to/target ``` Every MCP tool needs `project_root` **except** `guide`. @@ -424,9 +418,9 @@ the MCP tools return. Single-dict commands (`init`, `export`, `convert`, `scenario-init`, `scenario-from-run`, `guide`, `web`) always print JSON. ```bash -lks scenarios # human table -lks scenarios --json # raw JSON for scripts / CI / agents -lks execute-all --json | jq '.suite' # pipe JSON to jq +lksr scenarios # human table +lksr scenarios --json # raw JSON for scripts / CI / agents +lksr execute-all --json | jq '.suite' # pipe JSON to jq ``` **Agents & CI:** use `--json` — the default table is for humans. @@ -439,7 +433,7 @@ Installer writes this when tools are detected. Manual Cursor: { "mcpServers": { "livekit-agent-simulator": { - "command": "lks", + "command": "lksr", "args": ["mcp"], "env": {} } @@ -447,20 +441,20 @@ Installer writes this when tools are detected. Manual Cursor: } ``` -Dev checkout (package not installed globally): +Dev checkout (Rust build from source, not installed globally): ```json { "mcpServers": { "livekit-agent-simulator": { - "command": "uv", - "args": ["run", "--directory", "/abs/path/livekit-agent-simulator", "lks", "mcp"] + "command": "cargo", + "args": ["run", "--manifest-path", "/abs/path/livekit-agent-simulator/src/livekit_agent_simulator_rust/crates/lks/Cargo.toml", "--", "mcp"] } } } ``` -Equivalent one-shot entry: `lks-mcp` (same process as `lks mcp`). +Equivalent one-shot entry: `lksr mcp`. --- @@ -494,8 +488,8 @@ src/livekit_agent_simulator/ | Workflow | Trigger | What it does | |----------|---------|--------------| -| [CI](.github/workflows/ci.yml) | PR / push → `main` | web UI build, `pytest` (3.10 + 3.12), `lks --help` | -| [Release](.github/workflows/release.yml) | tag `v*` | pytest → wheel → portable packs (win/linux/mac) → GitHub Release | +| [Rust CI](.github/workflows/rust-ci.yml) | PR / push → `main` | `cargo fmt`/`clippy`/`test` on the Rust workspace (offline; Windows `cargo check` only) | +| [Release](.github/workflows/release.yml) | tag `v*` | Build `lksr` release binaries (macOS aarch64 + Linux x86_64), tarballs + `SHA256SUMS.txt` → GitHub Release; post-release install smoke | ```bash # Local check @@ -515,10 +509,10 @@ git push origin v0.1.0 ### `preflight` fails connectivity ```bash -lks preflight --root /path/to/target +lksr preflight --root /path/to/target # Confirm livekit.url / api_key / api_secret and that the project is reachable. # Skip API check while editing config: -lks preflight --no-connectivity --root /path/to/target +lksr preflight --no-connectivity --root /path/to/target ``` ### Agent never joins the room @@ -536,13 +530,13 @@ Set `simulator.api_key` in `.agent-sim/config.yaml` for the active `simulator.pr With `observe.record_audio` enabled (default `true`): `reports//conversation.wav` ```bash -lks web --root /path/to/target +lksr web --root /path/to/target ``` ### MCP tools not listed ```bash -lks mcp # must be what the host launches +lksr mcp # must be what the host launches # or reinstall without --no-mcp curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit_agent_simulator/main/install.sh?$(date +%s)" \ | bash -s -- --verify @@ -551,8 +545,8 @@ curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit_agent_simulato ### Scenario validation errors ```bash -lks validate my-case --root /path/to/target -lks scenario-init my-case --root /path/to/target # fresh scaffold with // guides +lksr validate my-case --root /path/to/target +lksr scenario-init my-case --root /path/to/target # fresh scaffold with // guides ``` --- @@ -593,7 +587,7 @@ Same ops. Use CLI in terminals/CI; MCP inside coding agents. Prefer `execute_*` ### Can I assert on tool calls? -Yes — `Assert.spec.tools`, **`tool_order`** (required `tool.start` subsequence), `observe.tool_event_patterns`, Script/assert plugins, and/or PassCriteria + judge. See [`docs/plugins.md`](docs/plugins.md) and `lks guide`. +Yes — `Assert.spec.tools`, **`tool_order`** (required `tool.start` subsequence), `observe.tool_event_patterns`, Script/assert plugins, and/or PassCriteria + judge. See [`docs/plugins.md`](docs/plugins.md) and `lksr guide`. ### Where are reports stored? @@ -601,7 +595,7 @@ Yes — `Assert.spec.tools`, **`tool_order`** (required `tool.start` subsequence ### Is the report player separate? -No — `lks web` serves the prebuilt player from the install pack. Maintainers build from `web/`. +No — `lksr web` serves the prebuilt player from the install pack. Maintainers build from `web/`. --- @@ -615,7 +609,7 @@ No — `lks web` serves the prebuilt player from the install pack. Maintainers b | [docs/plugins.md](docs/plugins.md) | Verify plugins + Python API | | [docs/telephony.md](docs/telephony.md) | SIP modes + outbound_sim_callee preflight | | [docs/interrupt-scenario-matrix.md](docs/interrupt-scenario-matrix.md) | Barge / backchannel / noise authoring | -| `lks guide` | On-demand setup/ops guide (Assert, compare --baseline, PassCriteria) | +| `lksr guide` | On-demand setup/ops guide (Assert, compare --baseline, PassCriteria) | --- diff --git a/docs/guide/installation.md b/docs/guide/installation.md index ebe99dc..58880e5 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -1,8 +1,8 @@ -# Agent install guide — livekit-agent-simulator (`lks`) +# Agent install guide — livekit-agent-simulator (`lksr`) **Audience:** coding agents (Claude Code, Cursor, Codex, AmpCode, Windsurf, …) and humans who paste this URL into an agent. -**Goal:** install the `lks` CLI on the user’s machine, scaffold `.agent-sim/` inside **the user’s project repo** (the LiveKit voice agent under test), fill config safely, register MCP if useful, and prove the setup with `preflight` (and optionally a smoke run). +**Goal:** install the `lksr` CLI on the user’s machine, scaffold `.agent-sim/` inside **the user’s project repo** (the LiveKit voice agent under test), fill config safely, register MCP if useful, and prove the setup with `preflight` (and optionally a smoke run). On Windows the installed CLI is still `lks` (the Python build). **Hard rules for the agent:** @@ -12,7 +12,7 @@ 4. **Discover before `AskQuestion`.** Read target docs and existing `.agent-sim/` (read-only). Do not assume consumer-specific file paths or metadata keys (e.g. one repo’s `job-metadata.ts` or `yourProjectKey` is not universal). 5. Prefer **non-interactive** commands. Use `--root ` always. 6. Prefer the **portable installer** (no uv/pip on the user machine) unless the user is developing the simulator package itself. -7. **CLI is `lks`**. Runtime IDs: sim participant `lks-caller` / room prefix `lks-`. +7. **CLI is `lksr`** on macOS/Linux (Rust build); **`lks`** on Windows (Python build). Runtime IDs: sim participant `lks-caller` / room prefix `lks-`. --- @@ -20,8 +20,8 @@ | Piece | Purpose | |-------|---------| -| CLI `lks` | Black-box LiveKit room tester + report player | -| MCP server `livekit-agent-simulator` | Same ops for coding agents (`lks mcp`) | +| CLI `lksr` (macOS/Linux) / `lks` (Windows) | Black-box LiveKit room tester + report player | +| MCP server `livekit-agent-simulator` | Same ops for coding agents (`lksr mcp` / `lks mcp`) | | Target folder `.agent-sim/` | Config, scenarios, reports, local cues/plugins (gitignored) | The simulator joins LiveKit as `lks-caller`, talks to the user’s already-running agent under test via Gemini Live, and writes forensic reports. @@ -30,7 +30,8 @@ The simulator joins LiveKit as `lks-caller`, talks to the user’s already-runni | Need | Why | |------|-----| -| macOS / Linux / Windows | Portable packs ship for these | +| macOS / Linux | Install the Rust `lksr` binary via `install.sh` | +| Windows | Installs the Python `lks` build via `install.ps1` (a Rust Windows binary is not yet shipped) | | Network access | Download release + LiveKit + Gemini | | A **running** LiveKit agent under test | Registered with a known `agent_name` | | LiveKit Cloud (or self-host) URL + API key/secret | Room create + dispatch | @@ -48,8 +49,8 @@ macOS / Linux (bash): ```bash uname -s -which lks || true -lks --help 2>/dev/null | head -5 || true +which lksr || true +lksr --version 2>/dev/null || true pwd # If the user said "this project", use the current workspace root as TARGET_ROOT ``` @@ -77,13 +78,13 @@ $TARGET_ROOT = (Get-Location).Path # or the path the user named # Optional pin: $env:LK_SIM_REF = "v0.1.0" ``` -If `lks --help` already works, skip §2 install and go to §3 init. +If `lksr --version` (macOS/Linux) or `lks --help` (Windows) already works, skip §2 install and go to §3 init. --- -## 2. Install `lks` (portable) +## 2. Install `lksr` / `lks` -### macOS / Linux +### macOS / Linux (Rust binary) ```bash curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.sh?$(date +%s)" \ @@ -94,30 +95,30 @@ Pin a release (recommended for CI / reproducible agent setups): ```bash curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.sh?$(date +%s)" \ - | bash -s -- --ref v0.1.0 --verify + | bash -s -- --ref v0.1.0-rust --verify ``` Flags: | Flag | Meaning | |------|---------| -| `--verify` | Run post-install check (`lks --help`) | +| `--verify` | Run post-install check (`lksr --version` + `lksr guide`) | | `--ref vX.Y.Z` / `--version` | Pin release tag (default: latest) | | `--no-mcp` | Skip auto MCP registration | | `--easy-mode` | Append install dir to shell PATH in rc files | | `--uninstall` | Remove install | -Default binary location: `$HOME/.local/bin/lks` -If `lks` is not found after install, ensure `~/.local/bin` is on `PATH` for this shell: +Default binary location: `$HOME/.local/bin/lksr` +If `lksr` is not found after install, ensure `~/.local/bin` is on `PATH` for this shell: ```bash export PATH="$HOME/.local/bin:$PATH" hash -r 2>/dev/null || true -command -v lks -lks --help | head -20 +command -v lksr +lksr --version ``` -### Windows PowerShell +### Windows PowerShell (Python build) ```powershell irm "https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.ps1" -OutFile "$env:TEMP\lks-install.ps1" @@ -147,10 +148,12 @@ Get-Command lks, lks lks --help | Select-Object -First 20 ``` +> **Windows note:** `install.ps1` still installs the **Python build** (`lks`). A Rust Windows binary is not yet shipped (tracked in the Rust-port plan §1.6). The Python build downloads a portable zip, so it needs no `uv`/`pip` on the user machine. + ### Installer success criteria -- `command -v lks` / `Get-Command lks` resolves -- `lks --help` exits 0 and lists: `init`, `preflight`, `execute`, `scenario-from-run`, `web`, `mcp`, … +- macOS/Linux: `command -v lksr` resolves; `lksr --version` prints a version +- Windows: `Get-Command lks` resolves; `lks --help` exits 0 and lists: `init`, `preflight`, `execute`, `scenario-from-run`, `web`, `mcp`, … - Prefer not to use `uv run` / `pip install` for end users ### From source (only if user is developing the simulator package) @@ -169,7 +172,7 @@ uv run lks init --root /abs/path/to/target ## 3. Init project scaffold in the user’s repo ```bash -lks init --root "$TARGET_ROOT" +lksr init --root "$TARGET_ROOT" ``` This creates (if missing): @@ -183,7 +186,7 @@ $TARGET_ROOT/.agent-sim/ cues/README.md ``` -Plugin API (verify + lifecycle): [plugins.md](../plugins.md). Ops detail: `lks guide`. +Plugin API (verify + lifecycle): [plugins.md](../plugins.md). Ops detail: `lksr guide`. Re-running `init` also ensures `.agent-sim/` is listed in `$TARGET_ROOT/.gitignore`. **Idempotent:** re-running `init` does **not** overwrite existing `config.yaml`, scenarios, or reports — only creates missing scaffold files. @@ -194,7 +197,7 @@ Re-running `init` also ensures `.agent-sim/` is listed in `$TARGET_ROOT/.gitigno test -f "$TARGET_ROOT/.agent-sim/config.yaml" test -d "$TARGET_ROOT/.agent-sim/scenarios" grep -q '\.agent-sim/' "$TARGET_ROOT/.gitignore" || true -lks guide | head -5 +lksr guide | head -5 ``` --- @@ -228,11 +231,11 @@ simulator: api_key: "AQ.Ab8..." # Gemini Live key profiles: # optional named caller profiles - gemini: # lks execute --profile gemini + gemini: # lksr execute --profile gemini default: true # auto-selected when no --profile flag provider: google api_key: "AQ.Ab8..." - openai: # lks execute --profile openai + openai: # lksr execute --profile openai provider: openai api_key: "sk-..." voice: @@ -385,7 +388,7 @@ Rules: }, { "id": "record_audio", - "prompt": "Enable local call recording for lks web replay?", + "prompt": "Enable local call recording for lksr web replay?", "options": [ {"id": "yes", "label": "Yes — observe.record_audio: true (Recommended)"}, {"id": "no", "label": "No — skip conversation.wav"} @@ -468,35 +471,35 @@ observe: timezone: "Asia/Ho_Chi_Minh" ``` -More on dispatch / data topics: [portability.md](../portability.md). Voice / cues / plugins after setup: `lks guide`. +More on dispatch / data topics: [portability.md](../portability.md). Voice / cues / plugins after setup: `lksr guide`. --- ## 5. MCP registration (coding agents) -Default installer registers MCP server name **`livekit-agent-simulator`** → command `lks mcp` into detected tools (Claude Code, Cursor, Cline, Windsurf, VS Code Copilot, Gemini CLI, Amazon Q, OpenCode, Codex, Warp). +Default installer registers MCP server name **`livekit-agent-simulator`** → command `lksr mcp` (macOS/Linux) or `lks mcp` (Windows) into detected tools (Claude Code, Cursor, Cline, Windsurf, VS Code Copilot, Gemini CLI, Amazon Q, OpenCode, Codex, Warp). -If the user skipped MCP or tools were installed later, manual MCP config example: +If the user skipped MCP or tools were installed later, manual MCP config example (macOS/Linux): ```json { "mcpServers": { "livekit-agent-simulator": { - "command": "lks", + "command": "lksr", "args": ["mcp"] } } } ``` -Dev checkout (package not on PATH): +Dev checkout (Rust build from source, not on PATH): ```json { "mcpServers": { "livekit-agent-simulator": { - "command": "uv", - "args": ["run", "--directory", "/abs/path/livekit-agent-simulator", "lks", "mcp"] + "command": "cargo", + "args": ["run", "--manifest-path", "/abs/path/livekit-agent-simulator/src/livekit_agent_simulator_rust/crates/lks/Cargo.toml", "--", "mcp"] } } } @@ -518,9 +521,9 @@ Typical MCP flow: ## 6. Preflight (must pass before promising a full call) ```bash -lks preflight --root "$TARGET_ROOT" +lksr preflight --root "$TARGET_ROOT" # offline config-only: -lks preflight --no-connectivity --root "$TARGET_ROOT" +lksr preflight --no-connectivity --root "$TARGET_ROOT" ``` **Success:** JSON `ok: true` with checks for config, livekit.url, folders, active provider key, and (if connectivity on) `livekit.api` list_rooms. @@ -529,7 +532,7 @@ Common failures: | Symptom | Fix | |---------|-----| -| config missing | `lks init --root …` first | +| config missing | `lksr init --root …` first | | livekit.api 401 | Wrong URL / api_key / api_secret | | agent timeout later | Agent not running or `agent_name` mismatch | | Windows: `No module named 'encodings'` / `Could not find platform independent libraries` | Broken portable layout from older installer — run `install.ps1 -Repair -Verify` or reinstall with latest `install.ps1` | @@ -541,9 +544,9 @@ Common failures: List / scaffold: ```bash -lks scenarios --root "$TARGET_ROOT" -lks scenario-init my-case --root "$TARGET_ROOT" # scaffolds a .yaml with # guide comments -lks validate smoke-hello --root "$TARGET_ROOT" +lksr scenarios --root "$TARGET_ROOT" +lksr scenario-init my-case --root "$TARGET_ROOT" # scaffolds a .yaml with # guide comments +lksr validate smoke-hello --root "$TARGET_ROOT" ``` ### Scenario knobs after setup (STT / dead-air / noise / authoring) @@ -560,11 +563,11 @@ These are **not** required for install — use when writing scenarios under `.ag | Script `action: wait` + `silence_after_cue_ms` | Intentional caller silence hold (suppresses freestyle). On `action: speak`, `silence_after_cue_ms` does **not** long-mute freestyle after the line. | | Soft metrics `user_words_natural_*` | Freestyle word stats excluding Script-matched finals; prefer over overall `user_words_p50` for naturalness on Script-heavy packs | | `noise_when: "background"` / Script `"loop": true` | Continuous ambient noise under the call | -| `lks validate` → `authoring.tier` / `warning_codes` | Soft authoring quality gate (no LLM; does not flip `valid`) | +| `lksr validate` → `authoring.tier` / `warning_codes` | Soft authoring quality gate (no LLM; does not flip `valid`) | ### Example scenario pack (`templates/examples/`) -Copy the ones you need to `/.agent-sim/scenarios/`, then `lks validate --root `. Each pair (`.yaml` canonical + `.jsonl` legacy twin where present) is smoke-validated. +Copy the ones you need to `/.agent-sim/scenarios/`, then `lksr validate --root `. Each pair (`.yaml` canonical + `.jsonl` legacy twin where present) is smoke-validated. **Dialogue (freestyle caller):** - `dialogue-signup-basic` — first_speaker=user, open goals @@ -603,27 +606,27 @@ persona `traits` (length band + interaction style), and `speech_conditions.verbo audio cues. Pair each locale scenario with a `pass_criteria` that checks the agent responds in that language. -Ops detail: **`lks guide`**. +Ops detail: **`lksr guide`**. **Agent under test must be running** and registered with the same `livekit.agent_name` before execute. ```bash -lks execute smoke-hello --root "$TARGET_ROOT" +lksr execute smoke-hello --root "$TARGET_ROOT" # → .agent-sim/reports/001-smoke-hello-YYYYMMDD-HHMMSS-xxxx/ (NNN + UTC stamp; unique vs SQLite) -lks execute smoke-hello --name demo --root "$TARGET_ROOT" +lksr execute smoke-hello --name demo --root "$TARGET_ROOT" # → .agent-sim/reports/002-demo-YYYYMMDD-HHMMSS-xxxx/ (--name overrides the slug after the seq prefix) # flake control (each iteration gets its own NNN folder): -lks execute smoke-hello --root "$TARGET_ROOT" --repeat 3 --pass-at-k 2 +lksr execute smoke-hello --root "$TARGET_ROOT" --repeat 3 --pass-at-k 2 # named caller profile (provider/key from simulator.profiles.): -lks execute smoke-hello --profile openai --root "$TARGET_ROOT" +lksr execute smoke-hello --profile openai --root "$TARGET_ROOT" # suite: -lks execute-all --tag smoke --root "$TARGET_ROOT" -# lks execute-all --tag smoke --parallel 2 --root "$TARGET_ROOT" -# lks execute-all --tag smoke --profile openai --root "$TARGET_ROOT" +lksr execute-all --tag smoke --root "$TARGET_ROOT" +# lksr execute-all --tag smoke --parallel 2 --root "$TARGET_ROOT" +# lksr execute-all --tag smoke --profile openai --root "$TARGET_ROOT" ``` #### Risk-tier recipe (skip exploratory in CI) @@ -634,17 +637,17 @@ want, so exploratory scenarios don't gate merges: ```bash # CI merge gate: blocking + regression only -lks execute-all --tag blocking --root "$TARGET_ROOT" -lks execute-all --tag regression --root "$TARGET_ROOT" +lksr execute-all --tag blocking --root "$TARGET_ROOT" +lksr execute-all --tag regression --root "$TARGET_ROOT" # nightly: include scheduled + exploratory -lks execute-all --tag scheduled --root "$TARGET_ROOT" -lks execute-all --tag exploratory --root "$TARGET_ROOT" +lksr execute-all --tag scheduled --root "$TARGET_ROOT" +lksr execute-all --tag exploratory --root "$TARGET_ROOT" ``` -`authoring.tier` from `lks validate --json` and the `risk_tags` score also surface -these tiers; `lks execute-all --tag ` is the execution-side filter. A -scenario with **no** risk tag is reported by `lks validate` (`no_risk_tag`). +`authoring.tier` from `lksr validate --json` and the `risk_tags` score also surface +these tiers; `lksr execute-all --tag ` is the execution-side filter. A +scenario with **no** risk tag is reported by `lksr validate` (`no_risk_tag`). `run_id` format: `{NNN}-{slug}-{YYYYMMDD}-{HHMMSS}-{xxxx}` — default slug is the scenario id; `--name` / MCP `run_name` replaces the slug only (`scenario_id` remains in `meta.json`). @@ -653,24 +656,24 @@ Timestamp + hex keep ids unique when a report folder was deleted but SQLite stil Inspect: ```bash -lks runs --root "$TARGET_ROOT" -lks report 001-smoke-hello-20260716-144623-a1b2 --root "$TARGET_ROOT" -lks log 001-smoke-hello-20260716-144623-a1b2 --kind "transcript.*" --root "$TARGET_ROOT" +lksr runs --root "$TARGET_ROOT" +lksr report 001-smoke-hello-20260716-144623-a1b2 --root "$TARGET_ROOT" +lksr log 001-smoke-hello-20260716-144623-a1b2 --kind "transcript.*" --root "$TARGET_ROOT" # --kind accepts one kind or one prefix (e.g. sim.script*); not a comma list -lks web --root "$TARGET_ROOT" # http://127.0.0.1:8765 — list auto-updates ~3s; Ctrl+C to stop +lksr web --root "$TARGET_ROOT" # http://127.0.0.1:8765 — list auto-updates ~3s; Ctrl+C to stop # CI golden gate (exit 1 on regression): -# lks compare --baseline --root "$TARGET_ROOT" +# lksr compare --baseline --root "$TARGET_ROOT" ``` Promote a failure to a draft regression case: ```bash -lks scenario-from-run 001-smoke-hello-20260716-144623-a1b2 --root "$TARGET_ROOT" # dry-run -lks scenario-from-run 001-smoke-hello-20260716-144623-a1b2 --root "$TARGET_ROOT" --write # write .yaml draft +lksr scenario-from-run 001-smoke-hello-20260716-144623-a1b2 --root "$TARGET_ROOT" # dry-run +lksr scenario-from-run 001-smoke-hello-20260716-144623-a1b2 --root "$TARGET_ROOT" --write # write .yaml draft # then human/agent reviews Persona + Assert before treating as golden ``` -Draft extract (see `lks guide` → promote section): goals/constraints (not transcript paste into brief), +Draft extract (see `lksr guide` → promote section): goals/constraints (not transcript paste into brief), one Behavior barge stub from run markers when present, Script open when `first_speaker=user`, transcript sample in `Context.notes` only. @@ -701,7 +704,7 @@ assert: tool_order: [lookup, book] ``` -PassCriteria can use flat `criteria[]` or multi-judge `judges[]` + `mode` (`all` \| `majority` \| `any`). Full recipes: `lks guide`. +PassCriteria can use flat `criteria[]` or multi-judge `judges[]` + `mode` (`all` \| `majority` \| `any`). Full recipes: `lksr guide`. #### Audio-onset latency (perceived vs transcript) @@ -777,7 +780,7 @@ pass_criteria: criteria: [builtin:conversation_naturalness] ``` -Available presets (package `src/livekit_agent_simulator/evals/presets.py`, `lks plugins`/`list_presets`): +Available presets (package `src/livekit_agent_simulator/evals/presets.py`, `lksr plugins`/`list_presets`): | builtin key | What it grades | |---|---| @@ -817,13 +820,13 @@ Package templates (copy into `.agent-sim/scenarios/`): ```bash # After telephony: block in config.yaml (trunk + dial_in / sim_inbound_number): -lks validate inbound-caller-sim --root "$TARGET_ROOT" -# lks execute inbound-caller-sim --root "$TARGET_ROOT" # needs real trunk + DID routing +lksr validate inbound-caller-sim --root "$TARGET_ROOT" +# lksr execute inbound-caller-sim --root "$TARGET_ROOT" # needs real trunk + DID routing ``` Mode is **only** in scenario `Caller` — never in `config.yaml`. Guide: https://github.com/quangdang46/livekit-agent-simulator/blob/main/docs/telephony.md -Ops detail: `lks guide` (templates/GUIDE.md). +Ops detail: `lksr guide` (templates/GUIDE.md). SIP asserts: `Assert.spec.sip.participant_present` / `dial_answered` / `call_status_any`. @@ -833,17 +836,17 @@ SIP asserts: `Assert.spec.sip.participant_present` / `dial_answered` / `call_sta Mark setup complete only when **all** of these are true: -- [ ] `lks --help` works on PATH +- [ ] `lksr --version` works on PATH (macOS/Linux) - [ ] `$TARGET_ROOT/.agent-sim/config.yaml` exists with LiveKit + `agent_name` + active provider key (Gemini Live / OpenAI Realtime) set - [ ] `.agent-sim/` is gitignored -- [ ] `lks preflight --root "$TARGET_ROOT"` → `ok: true` +- [ ] `lksr preflight --root "$TARGET_ROOT"` → `ok: true` - [ ] User knows the agent under test must be running before `execute` - [ ] Consumer dispatch metadata / `data_topics` set when discovery (§4.0) shows they are required - [ ] (Tool scenarios) report contains `tool.*` and `session.chat_history`, with no `tool_events` observe gap - [ ] (Optional) MCP `livekit-agent-simulator` registered if they use a coding agent -- [ ] (Optional) `lks execute smoke-hello --root "$TARGET_ROOT"` → `status: done` or a clear next fix (agent timeout / Gemini quota) +- [ ] (Optional) `lksr execute smoke-hello --root "$TARGET_ROOT"` → `status: done` or a clear next fix (agent timeout / Gemini quota) - [ ] (Optional SIP) `telephony:` trunk/DID filled when testing `inbound_sip` / `outbound_human_pickup` / `outbound_sim_callee`; scenarios validated -- [ ] (Optional) new surfaces known: `lks serve` (REST API on :8787), `lks optimize` (persona-prompt optimizer), `speech_conditions.effects` (audio degradation), `handoff` / `no_unplanned_handoff` asserts — see `lks guide` +- [ ] (Optional) new surfaces known: `lksr serve` (REST API on :8787), `lksr optimize` (persona-prompt optimizer), `speech_conditions.effects` (audio degradation), `handoff` / `no_unplanned_handoff` asserts — see `lksr guide` **Do not claim “fully working E2E”** if preflight failed or the agent is not registered. @@ -868,16 +871,16 @@ export PATH="$HOME/.local/bin:$PATH" TARGET_ROOT="$(pwd)" # change if needed # 1) Install CLI (skip if already present) -if ! command -v lks >/dev/null 2>&1; then +if ! command -v lksr >/dev/null 2>&1; then curl -fsSL "https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.sh?$(date +%s)" \ | bash -s -- --verify export PATH="$HOME/.local/bin:$PATH" hash -r 2>/dev/null || true fi -lks --help >/dev/null +lksr --version >/dev/null # 2) Scaffold target project -lks init --root "$TARGET_ROOT" +lksr init --root "$TARGET_ROOT" # 3) STOP: discover TARGET_ROOT (§4.0), then fill $TARGET_ROOT/.agent-sim/config.yaml # livekit.url / api_key / api_secret / agent_name @@ -885,10 +888,10 @@ lks init --root "$TARGET_ROOT" # optional: livekit.dispatch_metadata, observe.data_topics (from consumer docs/search) # Then continue: -lks preflight --root "$TARGET_ROOT" +lksr preflight --root "$TARGET_ROOT" # 4) Ensure agent under test is running with matching agent_name -# lks execute smoke-hello --root "$TARGET_ROOT" -# lks web --root "$TARGET_ROOT" +# lksr execute smoke-hello --root "$TARGET_ROOT" +# lksr web --root "$TARGET_ROOT" ``` ### Windows (PowerShell) @@ -928,14 +931,14 @@ lks preflight --root $TARGET_ROOT | Resource | URL | |----------|-----| | Repo | https://github.com/quangdang46/livekit-agent-simulator | -| Installer (macOS/Linux) | https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.sh | -| Installer (Windows) | https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.ps1 | +| Installer (macOS/Linux, Rust binary) | https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.sh | +| Installer (Windows, Python build) | https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/install.ps1 | | This guide (raw) | https://raw.githubusercontent.com/quangdang46/livekit-agent-simulator/main/docs/guide/installation.md | -| Ops guide | package `lks guide` or `templates/GUIDE.md` (voice, cues, plugins) | +| Ops guide | package `lksr guide` or `templates/GUIDE.md` (voice, cues, plugins) | | Plugins (verify + before_run / after_run) | https://github.com/quangdang46/livekit-agent-simulator/blob/main/docs/plugins.md | | Portability | https://github.com/quangdang46/livekit-agent-simulator/blob/main/docs/portability.md | | Telephony (SIP modes) | https://github.com/quangdang46/livekit-agent-simulator/blob/main/docs/telephony.md | | Smoke notes | https://github.com/quangdang46/livekit-agent-simulator/blob/main/docs/smoke-test.md | | L3 observer design | `docs/plans/PLAN-20260713-lk-agent-session-observer.md` | -When instructions conflict: **this file + `lks guide`** beat outdated blog snippets. Prefer latest release unless the user pins a tag. +When instructions conflict: **this file + `lksr guide`** beat outdated blog snippets. Prefer latest release unless the user pins a tag. diff --git a/install-rust.sh b/install-rust.sh deleted file mode 100755 index d64f564..0000000 --- a/install-rust.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# Install lksr (the Rust build of livekit-agent-simulator) from GitHub Releases. -# Single static-ish binary — no uv/pip/build on the user machine. -# -# curl -fsSL "https://github.com/quangdang46/livekit-agent-simulator/releases/download/v0.1.0-rust/install-rust.sh" | bash -# curl -fsSL "…/install-rust.sh" | bash -s -- --verify -# -set -euo pipefail -umask 022 - -BINARY_NAME="lksr" -PKG_NAME="livekit-agent-simulator" -OWNER="quangdang46" -REPO="livekit-agent-simulator" -DEST="${DEST:-$HOME/.local/bin}" -GIT_REF="${LK_SIM_REF:-}" -QUIET=0 -VERIFY=0 -UNINSTALL=0 - -log_info() { [ "$QUIET" -eq 1 ] && return; echo "[${BINARY_NAME}] $*" >&2; } -log_success() { [ "$QUIET" -eq 1 ] && return; echo "OK $*" >&2; } -die() { echo "ERROR: $*" >&2; exit 1; } - -usage() { - cat </dev/null \ - | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 -} - -asset_name() { - local os arch - case "$(uname -s)" in - Darwin) os="macos" ;; - Linux) os="linux" ;; - *) die "unsupported OS: $(uname -s)" ;; - esac - case "$(uname -m)" in - x86_64|amd64) arch="x86_64" ;; - arm64|aarch64) arch="aarch64" ;; - *) die "unsupported arch: $(uname -m)" ;; - esac - echo "lksr-${os}-${arch}.tar.gz" -} - -install_binary() { - local ref="$1" tag asset url work tarball - tag="$(release_tag_from_ref "$ref")" - [ -n "$tag" ] || die "No GitRef and no GitHub releases. Pass --ref v0.1.0" - - asset="$(asset_name)" - log_info "Looking for ${BINARY_NAME} release ${tag}: ${asset}" - - url="$(curl -fsSL "https://api.github.com/repos/${OWNER}/${REPO}/releases/tags/${tag}" \ - | sed -n "s/.*\"browser_download_url\":[[:space:]]*\"\([^\"]*${asset}\)\".*/\1/p" \ - | head -1)" - [ -n "$url" ] || die "Release ${tag} missing asset ${asset}" - - work="$(mktemp -d "${TMPDIR:-/tmp}/lksr-install.XXXXXX")" - tarball="${work}/${asset}" - log_info "Downloading ${url}" - curl -fsSL "$url" -o "$tarball" - [ -s "$tarball" ] || die "empty download" - - log_info "Extracting..." - tar -xzf "$tarball" -C "$work" - local bin - bin="$(find "$work" -mindepth 1 -maxdepth 2 -type f -name "${BINARY_NAME}" | head -1)" - [ -n "$bin" ] || die "lksr binary not found in tarball" - - mkdir -p "$DEST" - install -m 0755 "$bin" "$DEST/${BINARY_NAME}" - log_success "Installed -> $DEST/${BINARY_NAME}" - - case ":$PATH:" in *":$DEST:"*) ;; *) - log_info "Ensure CLI on PATH: export PATH=\"$DEST:\$PATH\"" - ;; esac - - rm -rf "$work" -} - -[ "$UNINSTALL" -eq 1 ] && { rm -f "$DEST/${BINARY_NAME}"; log_success "Uninstalled $DEST/${BINARY_NAME}"; exit 0; } - -install_binary "$GIT_REF" - -if [ "$VERIFY" -eq 1 ]; then - "$DEST/${BINARY_NAME}" --version - log_success "${BINARY_NAME} verify OK" -fi diff --git a/install.sh b/install.sh index d8d710b..6788666 100755 --- a/install.sh +++ b/install.sh @@ -1,20 +1,22 @@ #!/usr/bin/env bash -# Install lks from GitHub Releases (CI portable pack). -# No uv/pip/build on the user machine - download zip + PATH. +# Install lksr (the Rust build of livekit-agent-simulator) from GitHub Releases. +# Single static binary, SHA256-verified — no uv/pip/build on the user machine. # -# curl -fsSL "https://github.com/quangdang46/livekit-agent-simulator/releases/download/v0.1.0/install.sh" | bash +# curl -fsSL "https://github.com/quangdang46/livekit-agent-simulator/releases/download/v0.1.0-rust/install.sh" | bash # set -euo pipefail umask 022 -BINARY_NAME="lks" +BINARY_NAME="lksr" MCP_SERVER_NAME="livekit-agent-simulator" PKG_NAME="livekit-agent-simulator" OWNER="quangdang46" REPO="livekit-agent-simulator" DEST="${DEST:-$HOME/.local/bin}" -INSTALL_ROOT="${INSTALL_ROOT:-$HOME/.local/share/lks}" -CURRENT_DIR="$INSTALL_ROOT/current" +# Optional override root. When set, the binary lands under $INSTALL_ROOT/current/ +# with a symlink at $DEST/lksr; when unset (default) it installs directly to $DEST. +INSTALL_ROOT="${INSTALL_ROOT:-}" +CURRENT_DIR="${INSTALL_ROOT:+$INSTALL_ROOT/current}" GIT_REF="${LK_SIM_REF:-}" QUIET=0 EASY=0 @@ -41,18 +43,19 @@ acquire_lock() { usage() { cat </dev/null 2>&1; then command -v "$BINARY_NAME" return 0 @@ -162,7 +165,7 @@ resolve_lk_sim() { configure_all_mcp_providers() { local lk_bin lk_bin=$(resolve_lk_sim) || { - log_warn "lks not found - skip MCP config" + log_warn "lksr not found - skip MCP config" return 0 } log_info "MCP providers -> ${lk_bin} mcp" @@ -186,8 +189,8 @@ EOF uninstall_all() { log_info "Uninstalling ${PKG_NAME}..." - rm -rf "$INSTALL_ROOT" 2>/dev/null || true - rm -f "$DEST/lks" "$DEST/lks-mcp" 2>/dev/null || true + rm -f "$DEST/lksr" "$DEST/lks" "$DEST/lks-mcp" 2>/dev/null || true + [ -n "$INSTALL_ROOT" ] && rm -rf "$INSTALL_ROOT" 2>/dev/null || true _remove_mcp_from_file "$HOME/.claude.json" "$MCP_SERVER_NAME" _remove_mcp_from_file "$HOME/.cursor/mcp.json" "$MCP_SERVER_NAME" _remove_mcp_from_file "$HOME/.vscode/mcp.json" "$MCP_SERVER_NAME" "servers" @@ -207,7 +210,7 @@ resolve_install_ref() { fi local latest latest="$(latest_release_tag || true)" - [ -n "$latest" ] || die "No GitRef and no GitHub releases. Pass --ref v0.1.0" + [ -n "$latest" ] || die "No GitRef and no GitHub releases. Pass --ref v0.1.0-rust" log_info "Default ref -> latest release ${latest}" echo "$latest" } @@ -221,78 +224,74 @@ release_tag_from_ref() { esac } -portable_asset_name() { +asset_name() { local os arch - os="$(uname -s | tr 'A-Z' 'a-z')" - arch="$(uname -m)" - case "$os" in - darwin) os="macos" ;; - linux) os="linux" ;; - msys*|cygwin*|mingw*) os="windows" ;; + case "$(uname -s)" in + Darwin) os="macos" ;; + Linux) os="linux" ;; + *) die "unsupported OS: $(uname -s). On Windows use install.ps1 (Python build)." ;; esac - case "$arch" in - x86_64|amd64) arch="x64" ;; - arm64|aarch64) arch="arm64" ;; + case "$(uname -m)" in + x86_64|amd64) arch="x86_64" ;; + arm64|aarch64) arch="aarch64" ;; + *) die "unsupported arch: $(uname -m)" ;; esac - echo "lks-${os}-${arch}.zip" + echo "lksr-${os}-${arch}.tar.gz" } -install_portable() { - local ref="$1" tag asset url work zip payload +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + die "no sha256sum/shasum available for checksum verification" + fi +} + +install_binary() { + local ref="$1" tag asset url work tarball bin expected actual tag="$(release_tag_from_ref "$ref")" - [ -n "$tag" ] || die "Portable packs require a version tag (e.g. v0.1.0), not '$ref'" + [ -n "$tag" ] || die "Release binary install requires a version tag (e.g. v0.1.0-rust), not '$ref'" - asset="$(portable_asset_name)" - log_info "Looking for CI portable pack on release ${tag}: ${asset}" + asset="$(asset_name)" + log_info "Looking for ${BINARY_NAME} release ${tag}: ${asset}" url="$(curl -fsSL "https://api.github.com/repos/${OWNER}/${REPO}/releases/tags/${tag}" \ | sed -n "s/.*\"browser_download_url\":[[:space:]]*\"\\([^\"]*${asset}\\)\".*/\\1/p" \ | head -1)" [ -n "$url" ] || die "Release ${tag} missing asset ${asset}" - work="$(mktemp -d "${TMPDIR:-/tmp}/lks-portable.XXXXXX")" - zip="${work}/${asset}" + work="$(mktemp -d "${TMPDIR:-/tmp}/lksr-install.XXXXXX")" + tarball="${work}/${asset}" log_info "Downloading ${url}" - curl -fsSL "$url" -o "$zip" - [ -s "$zip" ] || die "empty download" - - log_info "Extracting portable pack..." - mkdir -p "$work/out" - unzip -q "$zip" -d "$work/out" - payload="$(find "$work/out" -mindepth 1 -maxdepth 1 -type d -name 'lks-*' | head -1)" - [ -n "$payload" ] || die "portable folder not found in zip" - - rm -rf "$INSTALL_ROOT" - mkdir -p "$CURRENT_DIR" - # Copy lks-linux-x64/* into current/, not current/lks-linux-x64/ - cp -a "$payload"/. "$CURRENT_DIR/" - repair_nested_portable_layout() { - local dir="$1" - if [ -f "$dir/python/Lib/encodings/__init__.py" ] || [ -f "$dir/python/lib/python3.12/encodings/__init__.py" ]; then - return 0 - fi - local nested - nested="$(find "$dir" -mindepth 1 -maxdepth 1 -type d -name 'lks-*' | head -1)" - [ -n "$nested" ] || return 1 - if [ ! -f "$nested/python/Lib/encodings/__init__.py" ] && [ ! -f "$nested/python/lib/python3.12/encodings/__init__.py" ]; then - return 1 - fi - log_info "Repairing nested portable layout ($(basename "$nested") -> $dir)" - if [ -d "$dir/python" ]; then - rm -rf "$dir/python" - fi - cp -a "$nested"/. "$dir/" - rm -rf "$nested" - [ -f "$dir/python/Lib/encodings/__init__.py" ] || [ -f "$dir/python/lib/python3.12/encodings/__init__.py" ] - } - repair_nested_portable_layout "$CURRENT_DIR" || die "portable pack invalid: python missing under $CURRENT_DIR/python" - chmod +x "$CURRENT_DIR/lks" "$CURRENT_DIR/lks-mcp" 2>/dev/null || true - - mkdir -p "$DEST" - # Primary - ln -sfn "$CURRENT_DIR/lks" "$DEST/lks" - ln -sfn "$CURRENT_DIR/lks-mcp" "$DEST/lks-mcp" - log_info "Installed -> $CURRENT_DIR (shims in $DEST: lks)" + curl -fsSL "$url" -o "$tarball" + [ -s "$tarball" ] || die "empty download" + + log_info "Verifying SHA256..." + curl -fsSL "https://github.com/${OWNER}/${REPO}/releases/download/${tag}/SHA256SUMS.txt" -o "$work/SHA256SUMS.txt" \ + || die "release ${tag} has no SHA256SUMS.txt" + expected="$(awk -v a="$asset" '$2==a {print $1; exit}' "$work/SHA256SUMS.txt")" + [ -n "$expected" ] || die "SHA256SUMS.txt has no entry for ${asset}" + actual="$(sha256_of "$tarball")" + [ "$expected" = "$actual" ] || die "SHA256 mismatch for ${asset} (expected ${expected}, got ${actual})" + log_info "SHA256 OK (${expected})" + + log_info "Extracting..." + tar -xzf "$tarball" -C "$work" + bin="$(find "$work" -mindepth 1 -maxdepth 2 -type f -name "${BINARY_NAME}" | head -1)" + [ -n "$bin" ] || die "lksr binary not found in tarball" + + if [ -n "$CURRENT_DIR" ]; then + mkdir -p "$CURRENT_DIR" "$DEST" + install -m 0755 "$bin" "$CURRENT_DIR/lksr" + ln -sfn "$CURRENT_DIR/lksr" "$DEST/lksr" + log_success "Installed -> $CURRENT_DIR/lksr (symlink in $DEST/lksr)" + else + mkdir -p "$DEST" + install -m 0755 "$bin" "$DEST/lksr" + log_success "Installed -> $DEST/lksr" + fi rm -rf "$work" } @@ -306,9 +305,9 @@ main() { local ref ref="$(resolve_install_ref)" - log_info "Installing ${PKG_NAME} portable pack (ref ${ref})" - log_info "No uv/pip/build on this machine - CI already built everything" - install_portable "$ref" + log_info "Installing ${PKG_NAME} Rust binary (ref ${ref})" + log_info "Single static binary - no uv/pip/build on this machine" + install_binary "$ref" export PATH="${DEST}:${PATH}" maybe_add_path @@ -323,8 +322,8 @@ main() { local lk lk="$(resolve_lk_sim 2>/dev/null || true)" [ -n "$lk" ] || die "${BINARY_NAME} not on PATH after install" - "$lk" --help >/dev/null - log_success "Verified ${BINARY_NAME} --help" + "$lk" --version >/dev/null && "$lk" guide >/dev/null + log_success "Verified ${BINARY_NAME} --version + guide" fi echo "" @@ -333,7 +332,7 @@ main() { echo " CLI: $lkb" echo " MCP: $lkb mcp" fi - echo " Pack: $CURRENT_DIR" + echo " Bin: $DEST/lksr" echo "" echo " Quick start:" echo " ${BINARY_NAME} guide"