Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
pkg/provider/resources/*.h264 filter=lfs diff=lfs merge=lfs -text
pkg/provider/resources/*.ivf filter=lfs diff=lfs merge=lfs -text
pkg/provider/resources/*.ogg filter=lfs diff=lfs merge=lfs -text

# Vendored C/C++ source (WebRTC APM, PortAudio, abseil, pffft, rnnoise)
pkg/apm/webrtc/**/*.cc linguist-vendored
pkg/apm/webrtc/**/*.c linguist-vendored
pkg/apm/webrtc/**/*.h linguist-vendored
pkg/apm/webrtc/**/*.m linguist-vendored
pkg/apm/webrtc/**/*.inc linguist-vendored
pkg/portaudio/pa_src/** linguist-vendored
Binary file modified .github/banner_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 147 additions & 15 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,163 @@ on:
pull_request:
branches: [ main ]

permissions:
contents: read

jobs:
build:
build-no-console:
name: Build (no console, no CGO)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/go/pkg/mod
~/go/bin
~/.cache
key: livekit-cli

- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.25"
go-version-file: go.mod

- name: Build without console tag
env:
CGO_ENABLED: "0"
run: go build -o bin/lk ./cmd/lk

- name: Verify binary
run: bin/lk --help > /dev/null

lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: true

- name: Download Go modules
run: go mod download
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod

- name: Lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9
- name: Static Check
uses: dominikh/staticcheck-action@288b4e28bae83c59f35f73651aeb5cab746a06fc # v1.4.0
with:
version: v2.11.4
version: "latest"
install-go: false

- name: Run Go tests
- name: Test
run: go test -v ./...

build:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
suffix: darwin_arm64
- os: ubuntu-latest
suffix: linux_amd64
zig_target: x86_64-linux-gnu.2.28
alsa_arch: amd64
alsa_triple: x86_64-linux-gnu
- os: ubuntu-latest
suffix: linux_arm64
zig_target: aarch64-linux-gnu.2.28
alsa_arch: arm64
alsa_triple: aarch64-linux-gnu
goarch: arm64
- os: ubuntu-latest
suffix: linux_arm
zig_target: arm-linux-gnueabihf.2.28
alsa_arch: armhf
alsa_triple: arm-linux-gnueabihf
goarch: arm
goarm: "7"
- os: ubuntu-latest
suffix: windows_amd64
zig_target: x86_64-windows-gnu
goos: windows
goarch: amd64
- os: ubuntu-latest
suffix: windows_arm64
zig_target: aarch64-windows-gnu
goos: windows
goarch: arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: true

- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod

- name: Install Zig
if: matrix.zig_target
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
with:
version: 0.14.1

- name: Install ALSA headers
if: matrix.alsa_arch
run: |
sudo dpkg --add-architecture ${{ matrix.alsa_arch }}
if [ "${{ matrix.alsa_arch }}" != "amd64" ]; then
CODENAME=$(lsb_release -cs)
# Restrict existing sources to amd64 to avoid 404s for foreign arch
for f in /etc/apt/sources.list.d/*.sources; do
grep -q '^Architectures:' "$f" || sudo sed -i '/^Types:/a Architectures: amd64 i386' "$f"
done
# Add ports.ubuntu.com for the foreign architecture
printf 'Types: deb\nURIs: http://ports.ubuntu.com/ubuntu-ports\nSuites: %s %s-updates\nComponents: main universe\nArchitectures: %s\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n' \
"$CODENAME" "$CODENAME" "${{ matrix.alsa_arch }}" | sudo tee /etc/apt/sources.list.d/ports.sources
fi
sudo apt-get update
sudo apt-get install -y libasound2-dev:${{ matrix.alsa_arch }}

- name: Generate Windows import libraries
if: matrix.goos == 'windows' && matrix.zig_target
run: |
ZIG_LIB=$(zig env | jq -r '.lib_dir')
echo "ZIG_LIB=${ZIG_LIB}" >> "$GITHUB_ENV"
LIB_DIR="${ZIG_LIB}/libc/mingw/lib-common"
# Zig bundles MinGW .def files but lld needs .a import libraries.
# Go's compiled objects embed COFF /DEFAULTLIB directives (e.g. dbghelp,
# bcrypt) that lld resolves directly, bypassing Zig's lazy .def→.a
# generation. Pre-generate all import libraries so lld can find them.
MACHINE=${{ matrix.goarch == 'amd64' && 'i386:x86-64' || 'arm64' }}
for def in "${LIB_DIR}"/*.def; do
lib=$(basename "$def" .def)
[ -f "${LIB_DIR}/lib${lib}.a" ] && continue
zig dlltool -d "$def" -l "${LIB_DIR}/lib${lib}.a" -m "$MACHINE" 2>/dev/null || true
done

- name: Build
env:
CGO_ENABLED: ${{ (matrix.goos && !matrix.zig_target) && '0' || '1' }}
CC: ${{ matrix.zig_target && format('zig cc -target {0}', matrix.zig_target) || '' }}
CXX: ${{ matrix.zig_target && format('zig c++ -target {0}', matrix.zig_target) || '' }}
# Zig uses its own sysroot; point it at the system ALSA headers and libraries
CGO_CFLAGS: ${{ matrix.alsa_triple && format('-isystem /usr/include -isystem /usr/include/{0}', matrix.alsa_triple) || '' }}
CGO_LDFLAGS: ${{ matrix.alsa_triple && format('-L/usr/lib/{0}', matrix.alsa_triple) || '' }}
# -fms-extensions: enable __try/__except (SEH) used by WebRTC
# -DNTDDI_VERSION: target Windows 10 base to skip WinRT includes absent from MinGW
CGO_CXXFLAGS: ${{ matrix.goos == 'windows' && '-fms-extensions -DNTDDI_VERSION=0x0A000000' || '' }}
GOOS: ${{ matrix.goos || '' }}
GOARCH: ${{ matrix.goarch || '' }}
GOARM: ${{ matrix.goarm || '' }}
shell: bash
run: |
EXT=""; if [ "${GOOS:-}" = "windows" ]; then EXT=".exe"; fi
TAGS=""
if [ "$CGO_ENABLED" = "1" ]; then TAGS="-tags console"; fi
# Force external linking for Windows so Go uses zig cc (CC) as the linker,
# and add Zig's MinGW lib path so lld can find the generated import libraries.
EXTLD=""
if [ "${GOOS:-}" = "windows" ] && [ "$CGO_ENABLED" = "1" ]; then
EXTLD="-linkmode=external -extldflags '-L${ZIG_LIB}/libc/mingw/lib-common'"
fi
go build $TAGS -ldflags "-w -s $EXTLD" -o "bin/lk${EXT}" ./cmd/lk

- name: Verify binary
if: "!matrix.goos && !matrix.goarch"
run: bin/lk --help > /dev/null
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.25"
go-version-file: go.mod

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.25"
go-version-file: go.mod
cache: true

- name: Download Go modules
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ dist/
.task/

.DS_Store
lk
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "pkg/portaudio/pa_src"]
path = pkg/portaudio/pa_src
url = https://github.com/PortAudio/portaudio.git
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ cli: check_lfs
GOOS=windows GOARCH=amd64 go build -ldflags "-w -s" -o bin/lk.exe ./cmd/lk


console:
CGO_ENABLED=1 go build -tags console -ldflags "-w -s" -o bin/lk ./cmd/lk

install: cli
ifeq ($(DETECTED_OS),Windows)
cp bin/lk.exe $(GOBIN)/lk.exe
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ git clone https://github.com/livekit/livekit-cli && cd livekit-cli
make install
```


# Usage

See `lk --help` for a complete list of subcommands. The `--help` flag can also be used on any subcommand for more information.
Expand Down
13 changes: 10 additions & 3 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,13 @@ var (
}
)

func noAgentError() error {
return fmt.Errorf("no agent project detected in the current directory\n\n" +
"Make sure you are running this command from an agent project directory\n" +
"containing one of: pyproject.toml, requirements.txt, uv.lock, package.json, or lock files.\n\n" +
"To get started, see: https://docs.livekit.io/agents/quickstart")
}

func createAgentClient(ctx context.Context, cmd *cli.Command) (context.Context, error) {
return createAgentClientWithOpts(ctx, cmd)
}
Expand Down Expand Up @@ -605,7 +612,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {

projectType, err := agentfs.DetectProjectType(os.DirFS(workingDir))
if err != nil {
return fmt.Errorf("unable to determine agent language: %w, please navigate to a directory containing an agent written in a supported language", err)
return noAgentError()
}
fmt.Printf("Detected agent language [%s]\n", util.Accented(string(projectType)))

Expand Down Expand Up @@ -773,7 +780,7 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {

projectType, err := agentfs.DetectProjectType(os.DirFS(workingDir))
if err != nil {
return fmt.Errorf("unable to determine agent language: %w, please make sure you are inside a directory containing an agent written in a supported language", err)
return noAgentError()
}
fmt.Printf("Detected agent language [%s]\n", util.Accented(string(projectType)))

Expand Down Expand Up @@ -1611,7 +1618,7 @@ func generateAgentDockerfile(ctx context.Context, cmd *cli.Command) error {

projectType, err := agentfs.DetectProjectType(os.DirFS(workingDir))
if err != nil {
return fmt.Errorf("unable to determine agent language: %w, please make sure you are inside a directory containing an agent written in a supported language", err)
return noAgentError()
}
fmt.Printf("Detected agent language [%s]\n", util.Accented(string(projectType)))

Expand Down
101 changes: 101 additions & 0 deletions cmd/lk/agent_reload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//go:build console

package main

import (
"fmt"
"net"
"sync"
"time"

agent "github.com/livekit/protocol/livekit/agent"

"github.com/livekit/livekit-cli/v2/pkg/ipc"
)

// reloadServer manages the dev-mode reload protocol between Go and Python processes.
// Flow:
// 1. Go → old Python: GetRunningJobsRequest → receives GetRunningJobsResponse (capture)
// 2. New Python → Go: GetRunningJobsRequest → Go replies with saved GetRunningJobsResponse (restore)
type reloadServer struct {
listener *ipc.Listener
mu sync.Mutex
savedJobs *agent.GetRunningAgentJobsResponse
}

func newReloadServer() (*reloadServer, error) {
ln, err := ipc.Listen("127.0.0.1:0")
if err != nil {
return nil, fmt.Errorf("reload server: %w", err)
}
return &reloadServer{listener: ln}, nil
}

func (rs *reloadServer) addr() string {
return rs.listener.Addr().String()
}

// captureJobs sends GetRunningJobsRequest to the old Python process and stores the response.
func (rs *reloadServer) captureJobs(conn net.Conn) {
conn.SetDeadline(time.Now().Add(1500 * time.Millisecond))
defer conn.SetDeadline(time.Time{})

req := &agent.AgentDevMessage{
Message: &agent.AgentDevMessage_GetRunningJobsRequest{
GetRunningJobsRequest: &agent.GetRunningAgentJobsRequest{},
},
}
if err := ipc.WriteProto(conn, req); err != nil {
fmt.Printf("reload: failed to send capture request: %v\n", err)
return
}

resp := &agent.AgentDevMessage{}
if err := ipc.ReadProto(conn, resp); err != nil {
fmt.Printf("reload: failed to read capture response: %v\n", err)
return
}

if jobs := resp.GetGetRunningJobsResponse(); jobs != nil {
rs.mu.Lock()
rs.savedJobs = jobs
rs.mu.Unlock()
fmt.Printf("reload: captured %d running job(s)\n", len(jobs.Jobs))
}
}

// serveNewProcess handles a GetRunningJobsRequest from the new Python process,
// replying with the previously captured jobs.
func (rs *reloadServer) serveNewProcess(conn net.Conn) {
req := &agent.AgentDevMessage{}
if err := ipc.ReadProto(conn, req); err != nil {
return
}
if req.GetGetRunningJobsRequest() == nil {
return
}

rs.mu.Lock()
saved := rs.savedJobs
rs.savedJobs = nil
rs.mu.Unlock()

if saved == nil {
saved = &agent.GetRunningAgentJobsResponse{}
}

resp := &agent.AgentDevMessage{
Message: &agent.AgentDevMessage_GetRunningJobsResponse{
GetRunningJobsResponse: saved,
},
}
if err := ipc.WriteProto(conn, resp); err != nil {
fmt.Printf("reload: failed to send restore response: %v\n", err)
} else if len(saved.Jobs) > 0 {
fmt.Printf("reload: restored %d job(s) to new process\n", len(saved.Jobs))
}
}

func (rs *reloadServer) close() error {
return rs.listener.Close()
}
Loading
Loading