From 12385e2ed3d3b615fff8785b8dd61d073d44eed2 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 12 Oct 2026 16:24:49 -0400 Subject: [PATCH 1/4] feat: add hidden generate-docs command Prints a command's usage line, subcommands, and options as markdown in the format docs.livekit.io's CLI reference uses (e.g. `lk generate-docs agent deploy`). docs.livekit.io regenerates its CLI reference from this on every release. --- cmd/lk/generate_docs.go | 167 ++++++++++++++++++++++++++++++++++++++++ cmd/lk/main.go | 7 ++ 2 files changed, 174 insertions(+) create mode 100644 cmd/lk/generate_docs.go diff --git a/cmd/lk/generate_docs.go b/cmd/lk/generate_docs.go new file mode 100644 index 00000000..9f5c37a9 --- /dev/null +++ b/cmd/lk/generate_docs.go @@ -0,0 +1,167 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/urfave/cli/v3" +) + +// docs.livekit.io renders this command's output inline in its CLI reference +// (livekit/web docs-cli-help workflow), so the reference always matches the +// released help text. The output sits under a hand-written heading and +// description, so it carries only the usage line, subcommands, and options. +func generateDocs(ctx context.Context, cmd *cli.Command) error { + pruneHiddenCommands(cmd.Root()) + path := cmd.Args().Slice() + target := cmd.Root() + for _, name := range path { + if target = target.Command(name); target == nil { + return fmt.Errorf("unknown command: lk %s", strings.Join(path, " ")) + } + } + out.Result(commandDocs(target, path)) + return nil +} + +func commandDocs(cmd *cli.Command, path []string) string { + var b strings.Builder + usage := cmd.UsageText + if usage == "" { + usage = strings.Join(append([]string{"lk"}, path...), " ") + if len(cmd.VisibleCommands()) > 0 { + usage += " " + } + if len(docFlags(cmd)) > 0 { + usage += " [options]" + } + if cmd.ArgsUsage != "" { + usage += " " + cmd.ArgsUsage + } + } + fmt.Fprintf(&b, "```shell\n%s\n```\n", strings.TrimSpace(usage)) + + if subs := cmd.VisibleCommands(); len(subs) > 0 { + b.WriteString("\nSubcommands:\n\n") + for _, sub := range subs { + name := sub.Name + if sub.ArgsUsage != "" { + name += " " + sub.ArgsUsage + } + fmt.Fprintf(&b, "- `%s`: %s\n", name, sentence(sub.Usage)) + } + } + + if flags := docFlags(cmd); len(flags) > 0 { + // Each docs page covers one top-level command group, so options are + // labelled by the path below it: "Options for `deploy`". + label := strings.Join(path, " ") + if len(path) > 1 { + label = strings.Join(path[1:], " ") + } + fmt.Fprintf(&b, "\nOptions for `%s`:\n\n", label) + for _, f := range flags { + b.WriteString(flagDocs(f)) + } + } + return b.String() +} + +// docFlags is the command's own visible flags, without the --help flag urfave +// copies into every command. +func docFlags(cmd *cli.Command) []cli.Flag { + var flags []cli.Flag + for _, f := range cmd.VisibleFlags() { + if f.Names()[0] != cli.HelpFlag.Names()[0] { + flags = append(flags, f) + } + } + return flags +} + +func flagDocs(f cli.Flag) string { + doc, ok := f.(cli.DocGenerationFlag) + if !ok { + return "" + } + placeholder, usage := unquoteUsage(doc.GetUsage()) + if doc.TakesValue() && placeholder == "" { + placeholder = "VALUE" + } + + names := f.Names() + name := "--" + names[0] + if doc.TakesValue() { + name += " " + placeholder + } + line := fmt.Sprintf("- `%s`", name) + if len(names) > 1 { + var aliases []string + for _, a := range names[1:] { + aliases = append(aliases, "`"+flagPrefix(a)+a+"`") + } + line += fmt.Sprintf(" (alias %s)", strings.Join(aliases, ", ")) + } + line += ": " + sentence(usage) + if def := defaultText(doc); def != "" && !strings.Contains(usage, "Default") { + line += fmt.Sprintf(" Default: `%s`.", def) + } + return line + "\n" +} + +func defaultText(f cli.DocGenerationFlag) string { + if t := f.GetDefaultText(); t != "" { + return t + } + switch v := strings.Trim(f.GetValue(), `"`); v { + case "", "false", "0", "[]", "0s": + return "" + default: + return v + } +} + +func flagPrefix(name string) string { + if len(name) == 1 { + return "-" + } + return "--" +} + +// unquoteUsage matches urfave/cli's help: the first `quoted` word in a flag's +// usage is its value placeholder, and prints without the backticks. +func unquoteUsage(usage string) (string, string) { + i := strings.Index(usage, "`") + if i < 0 { + return "", usage + } + j := strings.Index(usage[i+1:], "`") + if j < 0 { + return "", usage + } + name := usage[i+1 : i+1+j] + return name, usage[:i] + name + usage[i+2+j:] +} + +func sentence(s string) string { + s = strings.TrimSpace(s) + if s == "" || strings.HasSuffix(s, ".") || strings.HasSuffix(s, ")") && strings.HasSuffix(strings.TrimSuffix(s, ")"), ".") { + return s + } + return s + "." +} diff --git a/cmd/lk/main.go b/cmd/lk/main.go index aed56094..a986a5c9 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -73,6 +73,13 @@ Docs: https://docs.livekit.io/intro/basics/cli/`, }, }, }, + { + Name: "generate-docs", + Usage: "Print markdown reference docs for a command", + ArgsUsage: "[command path...]", + Action: generateDocs, + Hidden: true, + }, }, Before: initLogger, After: func(context.Context, *cli.Command) error { From 6e50ef4a1b7609973181c2a3ecbec56ffe15adeb Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 12 Oct 2026 16:24:50 -0400 Subject: [PATCH 2/4] ci: notify docs when a release is published --- .github/workflows/notify-docs.yaml | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/notify-docs.yaml diff --git a/.github/workflows/notify-docs.yaml b/.github/workflows/notify-docs.yaml new file mode 100644 index 00000000..fc574451 --- /dev/null +++ b/.github/workflows/notify-docs.yaml @@ -0,0 +1,40 @@ +# Copyright 2026 LiveKit, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Notify docs + +# Tells livekit/internal-actions that a release went public. It forwards the +# event to livekit/web, which regenerates the CLI reference on docs.livekit.io +# from `lk generate-docs` at this tag and opens a PR. +on: + release: + types: [published] + +# The dispatch authenticates with SYNC_DISPATCH_TOKEN; GITHUB_TOKEN isn't used. +permissions: {} + +jobs: + dispatch: + if: ${{ !github.event.release.prerelease }} + runs-on: ubuntu-latest + steps: + - name: Dispatch to internal actions repo + env: + GH_TOKEN: ${{ secrets.SYNC_DISPATCH_TOKEN }} + TAG: ${{ github.event.release.tag_name }} + run: | + gh api -X POST repos/livekit/internal-actions/dispatches \ + -f event_type=livekit-cli-released \ + -f client_payload[source_repo]=livekit/livekit-cli \ + -f client_payload[tag]="$TAG" From 3c9fe7231b5f1548cc6fc643ec1c9ec7c782d5e2 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 12 Oct 2026 16:29:24 -0400 Subject: [PATCH 3/4] docs: align help text with the docs.livekit.io CLI reference Ports the CLI reference's descriptions for the agent, simulate, debugger, private-link, cloud auth, project, and docs commands into their flag and subcommand help, and gives every value flag a placeholder, so the generated reference reads like the current hand-written one. --- cmd/lk/agent.go | 30 ++++++++++++------------- cmd/lk/agent_private_link.go | 14 ++++++------ cmd/lk/agent_run.go | 6 ++--- cmd/lk/cloud.go | 5 +++-- cmd/lk/console.go | 14 ++++++------ cmd/lk/debugger.go | 43 ++++++++++++++++++++---------------- cmd/lk/docs.go | 43 ++++++++++++++++++------------------ cmd/lk/project.go | 14 ++++++------ cmd/lk/simulate.go | 30 ++++++++++++------------- cmd/lk/simulate_list.go | 2 +- cmd/lk/utils.go | 10 ++++----- 11 files changed, 109 insertions(+), 102 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index 923170dd..78b29a9b 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -128,14 +128,14 @@ var ( idFlag = func(required bool) *cli.StringFlag { return &cli.StringFlag{ Name: "id", - Usage: fmt.Sprintf("`ID` of the agent. If unset, and the %s file is present, will use the id found there.", config.LiveKitTOMLFile), + Usage: fmt.Sprintf("Agent `ID`. If unset and %s is present, uses the ID found there.", config.LiveKitTOMLFile), Required: required, } } idSliceFlag = &cli.StringSliceFlag{ Name: "id", - Usage: "`IDs` of agent(s)", + Usage: "Filter to one or more agent `IDs`. Repeatable.", Required: false, } @@ -158,20 +158,20 @@ var ( secretsFileFlag = &cli.StringFlag{ Name: "secrets-file", - Usage: "`FILE` containing secret KEY=VALUE pairs, one per line. These will be injected as environment variables into the agent.", + Usage: "`FILE` containing secret KEY=VALUE pairs, one per line. Injected as environment variables into the agent.", TakesFile: true, Required: false, } secretsFlag = &cli.StringSliceFlag{ Name: "secrets", - Usage: "KEY=VALUE comma separated secrets. These will be injected as environment variables into the agent. These take precedence over secrets-file.", + Usage: "One or more `KEY=VALUE` secrets. Repeat the flag for each secret. Injected as environment variables into the agent. Takes precedence over --secrets-file.", Required: false, } secretsMountFlag = &cli.StringSliceFlag{ Name: "secret-mount", - Usage: "Local path to a secret file to be mounted on agent environment.", + Usage: "Path to a `FILE` to load as a file-mounted secret in the agent container.", Required: false, } @@ -184,32 +184,32 @@ var ( logTypeFlag = &cli.StringFlag{ Name: "log-type", - Usage: "Type of logs to retrieve. Valid values are 'deploy' and 'build'.", + Usage: "Log `TYPE` to retrieve. Valid values: deploy, build.", Value: "deploy", Required: false, } regionFlag = &cli.StringFlag{ Name: "region", - Usage: "Region to deploy the agent to. If unset, will deploy to the nearest region.", + Usage: "`REGION` code to deploy the agent to. If unset, the CLI prompts you to select a region.", Required: false, } deploymentFlag = &cli.StringFlag{ Name: "deployment", - Usage: "Agent deployment", + Usage: "`NAME` of the deployment. Defaults to production.", Required: false, Aliases: []string{"d"}, } agentPrebuiltImageFlag = &cli.StringFlag{ Name: "image", - Usage: "Pre-built image from the local Docker daemon (e.g. myimage:latest). Requires Docker.", + Usage: "Pre-built `IMAGE` from the local Docker daemon (e.g. myimage:latest). Requires Docker.", } agentPrebuiltImageTarFlag = &cli.StringFlag{ Name: "image-tar", - Usage: "Pre-built image from an OCI tar file (e.g. ./image.tar). No Docker daemon required.", + Usage: "Pre-built image from an OCI tar `FILE` (e.g. ./image.tar). No Docker daemon required.", } skipSDKCheckFlag = &cli.BoolFlag{ @@ -244,7 +244,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Flags: [][]cli.Flag{{ &cli.StringFlag{ Name: "lang", - Usage: "`LANGUAGE` of the starter template, one of \"python\", \"node\".", + Usage: "`LANGUAGE` of the starter template. One of python, node.", Action: func(ctx context.Context, cmd *cli.Command, l string) error { if _, ok := starterTemplateURLs[l]; !ok { return fmt.Errorf("unsupported language: %s", l) @@ -254,7 +254,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", }, &cli.BoolFlag{ Name: "deploy", - Usage: "If set, automatically deploys the agent to LiveKit Cloud after initialization.", + Usage: "Deploy the agent to LiveKit Cloud after initialization.", Value: false, }, templateFlag, @@ -309,7 +309,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "overwrite", - Usage: "Overwrite existing Dockerfile and/or .dockerignore if they exist.", + Usage: "Overwrite existing Dockerfile and .dockerignore files.", Required: false, Value: false, }, @@ -407,7 +407,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Flags: []cli.Flag{ &cli.StringFlag{ Name: "version", - Usage: "Version to rollback to, defaults to most recent previous to current.", + Usage: "`VERSION` to roll back to. Defaults to the most recent version prior to the current.", Value: "latest", Required: true, }, @@ -487,7 +487,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", idFlag(false), &cli.BoolFlag{ Name: "overwrite", - Usage: "If set, will overwrite existing secrets.", + Usage: "Overwrite existing secrets.", Required: false, Value: false, }, diff --git a/cmd/lk/agent_private_link.go b/cmd/lk/agent_private_link.go index 6e74e45b..0159c3bf 100644 --- a/cmd/lk/agent_private_link.go +++ b/cmd/lk/agent_private_link.go @@ -28,26 +28,26 @@ var privateLinkCommands = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "name", - Usage: "Private link name", + Usage: "`NAME` for the private link. Required.", Required: true, }, &cli.StringFlag{ Name: "region", - Usage: "LiveKit region. If unset in interactive mode, a picker of available regions is shown.", + Usage: "LiveKit agent `REGION` for the link. This is the LiveKit region, not the cloud provider region. If unset in interactive mode, the CLI shows a picker of available regions.", }, &cli.UintFlag{ Name: "port", - Usage: "Destination port", + Usage: "TCP `PORT` to reach on the target service. Required. Must match the port your endpoint service listens on.", Required: true, }, &cli.StringFlag{ Name: "endpoint", - Usage: "Customer-provided endpoint identifier", + Usage: "The `ENDPOINT` to connect to: the AWS endpoint service name or the Azure Resource ID. Required.", Required: true, }, &cli.StringFlag{ Name: "cloud-region", - Usage: "Cloud provider region (e.g. eastus, us-east-2). Required when --endpoint is an Azure Resource ID", + Usage: "Cloud provider `REGION` that hosts the resource. Required when --endpoint is an Azure Resource ID. For AWS, LiveKit derives the region from the endpoint service name, so you can omit it.", }, jsonFlag, }, @@ -69,7 +69,7 @@ var privateLinkCommands = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", - Usage: "Private link ID", + Usage: "Private link `ID`. Required.", Required: true, }, jsonFlag, @@ -83,7 +83,7 @@ var privateLinkCommands = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", - Usage: "Private link ID", + Usage: "Private link `ID`. Required.", Required: true, }, jsonFlag, diff --git a/cmd/lk/agent_run.go b/cmd/lk/agent_run.go index 16376c3d..97875590 100644 --- a/cmd/lk/agent_run.go +++ b/cmd/lk/agent_run.go @@ -99,7 +99,7 @@ func init() { var agentRunFlags = []cli.Flag{ &cli.StringFlag{ Name: "log-level", - Usage: "Log level (TRACE, DEBUG, INFO, WARN, ERROR)", + Usage: "Log `LEVEL`. One of TRACE, DEBUG, INFO, WARN, ERROR.", }, } @@ -122,12 +122,12 @@ var devCommand = &cli.Command{ Flags: append(agentRunFlags, &cli.BoolFlag{ Name: "no-reload", - Usage: "Disable auto-reload on file changes", + Usage: "Disable auto-reload on file changes.", }, &cli.StringFlag{ Name: "deployment", Sources: cli.EnvVars(agentDeploymentEnv), - Usage: "Deployment to register the agent under (default: a random `ID` so dev traffic is isolated from production; pass \"production\" to explicitly serve production traffic)", + Usage: "Deployment `NAME` to register the agent under. Defaults to a random ID so dev traffic is isolated from production; pass production to explicitly serve production traffic.", }, ), Action: runAgentDev, diff --git a/cmd/lk/cloud.go b/cmd/lk/cloud.go index 8398e924..47bb3ad6 100644 --- a/cmd/lk/cloud.go +++ b/cmd/lk/cloud.go @@ -88,19 +88,20 @@ var ( &cli.BoolFlag{ Name: "revoke", Aliases: []string{"R"}, + Usage: "Revoke an authorization for an existing project, selected with --project or the default project. This revokes the API keys that were issued with lk cloud auth, and then removes the project from the CLI.", Destination: &revoke, }, &cli.IntFlag{ Name: "timeout", Aliases: []string{"t"}, - Usage: "Number of `SECONDS` to attempt authentication before giving up", + Usage: "Number of `SECONDS` to attempt authentication before giving up.", Destination: &timeout, Value: 60 * 15, }, &cli.IntFlag{ Name: "poll-interval", Aliases: []string{"i"}, - Usage: "Number of `SECONDS` between poll requests to verify authentication", + Usage: "Number of `SECONDS` between poll requests while waiting.", Destination: &interval, Value: 4, }, diff --git a/cmd/lk/console.go b/cmd/lk/console.go index 551d7f6a..a606db0c 100644 --- a/cmd/lk/console.go +++ b/cmd/lk/console.go @@ -74,33 +74,33 @@ conversation one command at a time, use "lk agent debugger" instead.`, &cli.IntFlag{ Name: "port", Aliases: []string{"p"}, - Usage: "TCP port for agent communication", + Usage: "TCP `PORT` for agent communication. Defaults to 0 (auto-select).", Value: 0, }, &cli.StringFlag{ Name: "input-device", - Usage: "Input device index or name substring", + Usage: "Input device index or `NAME` substring.", }, &cli.StringFlag{ Name: "output-device", - Usage: "Output device index or name substring", + Usage: "Output device index or `NAME` substring.", }, &cli.BoolFlag{ Name: "list-devices", - Usage: "List available audio devices and exit", + Usage: "List available audio devices and exit.", }, &cli.BoolFlag{ Name: "no-aec", - Usage: "Disable acoustic echo cancellation", + Usage: "Disable acoustic echo cancellation.", }, &cli.BoolFlag{ Name: "text", Aliases: []string{"t"}, - Usage: "Start in text mode instead of voice (no STT/TTS); ctrl+t toggles later", + Usage: "Start in text mode instead of audio mode.", }, &cli.BoolFlag{ Name: "record", - Usage: "Record audio and session report to console-recordings/", + Usage: "Record audio and a session report to console-recordings/.", }, }, Action: runConsole, diff --git a/cmd/lk/debugger.go b/cmd/lk/debugger.go index 30626903..c0d1149d 100644 --- a/cmd/lk/debugger.go +++ b/cmd/lk/debugger.go @@ -71,23 +71,24 @@ var sessionPortFlag = &cli.IntFlag{ Name: "port", Sources: cli.EnvVars(envSessionPort), Value: defaultSessionPort, - Usage: "Loopback port the session listens on (one session per port)", + Usage: "Local `PORT` for the session.", } var sessionIdleFlag = &cli.DurationFlag{ - Name: "idle-timeout", - Value: defaultIdleTimeout, - Usage: "Stop the session after `DURATION` without any command, such as 30m, 2h, or 90s (0 keeps it running until stop)", + Name: "idle-timeout", + Value: defaultIdleTimeout, + DefaultText: "30m", + Usage: "Stop the session after `DURATION` without any command. Takes a duration with a unit, such as 30m, 2h, or 90s. Use 0 to keep it running until stop.", } var sessionAudioFlag = &cli.BoolFlag{ Name: "audio", - Usage: "Speak each turn into the agent's microphone input instead of sending text, running its full audio pipeline (uses LiveKit Inference TTS on your connected LiveKit Cloud project)", + Usage: "Speak each turn into the agent's microphone input instead of sending text.", } var sessionMetricsFlag = &cli.BoolFlag{ Name: "metrics", - Usage: "Show latency metrics: time to first token per reply and how long each turn took", + Usage: "Show time to first token for each reply and how long the turn took.", } // renderFlags reads the shared rendering flags off a command. @@ -162,7 +163,7 @@ running. Use --port to run several agents side by side (one session per port). { Name: "start", Usage: "Start the agent in the background and wait until it is ready", - ArgsUsage: "[entrypoint] [-- node/python-args...]", + ArgsUsage: "[entrypoint] [-- runtime-args]", Description: `Launches the agent as a detached process and returns once it is connected and ready for turns, spoken ones with --audio. If the agent speaks first (a greeting from on_enter), that is printed here. The summary line names the active agent and its tools. @@ -184,7 +185,7 @@ without any command, so a forgotten session does not linger.`, { Name: "say", Usage: "Send one user turn and print the agent's tool calls and reply", - ArgsUsage: " (or pipe the text on stdin)", + ArgsUsage: "", Description: `Sends the text as the user's turn and streams what the agent does until the turn completes: tool calls with their arguments and results, handoffs to other agents, errors, and the reply. Anything the agent said since the previous turn @@ -207,13 +208,14 @@ be piped on stdin: jsonFlag, sessionMetricsFlag, &cli.DurationFlag{ - Name: "timeout", - Value: defaultSayTimeout, - Usage: "Give up waiting for the agent's reply after `DURATION`, such as 2m or 90s", + Name: "timeout", + Value: defaultSayTimeout, + DefaultText: "2m", + Usage: "Stop waiting for the agent's reply after `DURATION`. Takes a duration with a unit, such as 2m or 90s.", }, &cli.BoolFlag{ Name: "logs", - Usage: "Interleave agent log lines emitted during the turn", + Usage: "Show the agent's log lines emitted during the turn, beneath the step they belong to.", }, }, Action: runSessionSay, @@ -244,8 +246,8 @@ another program in real time.`, Flags: []cli.Flag{ sessionPortFlag, jsonFlag, - &cli.IntFlag{Name: "last", Aliases: []string{"n"}, Value: 50, Usage: "How many recent events to replay before streaming (0 for all kept, up to 500)"}, - &cli.BoolFlag{Name: "logs", Usage: "Include the agent's log lines in the stream"}, + &cli.IntFlag{Name: "last", Aliases: []string{"n"}, Value: 50, Usage: "Print the last `N` events before streaming new ones. Use 0 for every event kept, up to 500."}, + &cli.BoolFlag{Name: "logs", Usage: "Include the agent's log lines in the stream."}, }, Action: runSessionEvents, }, @@ -268,8 +270,8 @@ stripped). The whole log is also kept in a file; "status" shows its path. Use "say --logs" to see the lines emitted during a single turn instead.`, Flags: []cli.Flag{ sessionPortFlag, - &cli.IntFlag{Name: "last", Aliases: []string{"n"}, Value: 50, Usage: "How many of the most recent lines to print (0 for the whole log)"}, - &cli.BoolFlag{Name: "follow", Aliases: []string{"f"}, Usage: "Keep streaming new lines until interrupted"}, + &cli.IntFlag{Name: "last", Aliases: []string{"n"}, Value: 50, Usage: "Print the last `N` lines. Use 0 for the whole log."}, + &cli.BoolFlag{Name: "follow", Aliases: []string{"f"}, Usage: "Keep streaming new lines until interrupted."}, }, Action: runSessionLogs, }, @@ -292,14 +294,17 @@ entire log before the summary; --json returns everything in one document.`, Flags: []cli.Flag{ sessionPortFlag, jsonFlag, - sessionMetricsFlag, + &cli.BoolFlag{ + Name: "metrics", + Usage: "Show latency metrics with the conversation.", + }, &cli.BoolFlag{ Name: "chat-history", - Usage: "Also print the full conversation", + Usage: "Also print the full conversation.", }, &cli.BoolFlag{ Name: "logs", - Usage: "Also print the agent process's entire log", + Usage: "Also print the agent process's entire log.", }, }, Action: runSessionStop, diff --git a/cmd/lk/docs.go b/cmd/lk/docs.go index 5d9511ec..5d1462ff 100644 --- a/cmd/lk/docs.go +++ b/cmd/lk/docs.go @@ -71,7 +71,7 @@ All output is rendered as markdown.`, &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, - Usage: "Output as JSON instead of markdown", + Usage: "Output as JSON instead of markdown.", }, &cli.StringFlag{ Name: "server-url", @@ -93,7 +93,7 @@ docs rather than relying wholly on search.`, }, { Name: "search", - Usage: "Search the LiveKit documentation", + Usage: "Search the documentation by keyword", ArgsUsage: "[QUERY]", Description: `Search the docs for a given query. Returns paged results showing page titles, hierarchical placement, and (sometimes) a content snippet. @@ -107,16 +107,17 @@ directly (via "lk docs overview"), not a replacement.`, &cli.StringFlag{ Name: "query", Aliases: []string{"q"}, - Usage: "Search `QUERY` text", + Usage: "Search `QUERY` text. Can also be passed as a positional argument.", }, &cli.IntFlag{ Name: "page", Aliases: []string{"p"}, - Usage: "Page number (starts at 0)", + Usage: "`PAGE` number for pagination, starting at 0.", }, &cli.IntFlag{ - Name: "hits-per-page", - Usage: "Results per page (1-50, default 20)", + Name: "hits-per-page", + Usage: "Return `N` results per page (1-50).", + DefaultText: "20", }, }, }, @@ -155,38 +156,38 @@ may not appear.`, &cli.StringFlag{ Name: "query", Aliases: []string{"q"}, - Usage: "Search term (use code identifiers, not natural language)", + Usage: "`QUERY` to search for. Use code identifiers, not natural language.", }, &cli.StringFlag{ Name: "repo", Aliases: []string{"r"}, - Usage: "Target `REPO` (e.g. livekit/agents) or ALL", + Usage: "Target `REPO` (for example, livekit/agents) or ALL to search everywhere.", Value: "ALL", }, &cli.StringFlag{ Name: "language", Aliases: []string{"l"}, - Usage: "Language filter (e.g. Python, TypeScript)", + Usage: "`LANGUAGE` filter (for example, Python, TypeScript).", }, &cli.StringFlag{ Name: "scope", - Usage: "Search scope: content, filename, or both", + Usage: "Search `SCOPE`: content, filename, or both.", Value: "content", }, &cli.IntFlag{ Name: "limit", - Usage: "Max results to return (1-50)", + Usage: "Return at most `N` results (1-50).", Value: 20, }, &cli.BoolFlag{ Name: "full-file", - Usage: "Return full file content instead of snippets", + Usage: "Return full file contents instead of snippets.", }, }, }, { Name: "changelog", - Usage: "Get recent releases and changelog for a LiveKit SDK or package", + Usage: "Get recent releases and changelog entries for a LiveKit SDK or package", ArgsUsage: "IDENTIFIER", Description: `Get recent releases for a LiveKit repository or package. Supports repository IDs (e.g. "livekit/agents") and package identifiers @@ -201,18 +202,18 @@ Examples: Flags: []cli.Flag{ &cli.IntFlag{ Name: "releases", - Usage: "Number of releases to fetch (1-20)", + Usage: "Fetch `N` releases (1-20).", Value: 2, }, &cli.IntFlag{ Name: "skip", - Usage: "Number of releases to skip for pagination", + Usage: "Skip `N` releases, for pagination.", }, }, }, { Name: "list-sdks", - Usage: "List all LiveKit SDK repositories and package names", + Usage: "List all LiveKit SDK repositories with their package names", Description: `Returns a list of all LiveKit SDK repositories (client SDKs, server SDKs, and agent frameworks) with their package names for each platform. Useful for cross-referencing dependencies and finding the right SDK.`, @@ -228,7 +229,7 @@ Use this when you need details about pricing, costs, plans, or billing.`, }, { Name: "submit-feedback", - Usage: "Submit feedback on the LiveKit documentation", + Usage: "Submit constructive feedback on the LiveKit documentation", ArgsUsage: "[FEEDBACK]", Description: `Submit constructive feedback on the LiveKit docs. This feedback is read by the LiveKit team and used to improve the documentation. @@ -241,20 +242,20 @@ Examples: Flags: []cli.Flag{ &cli.StringFlag{ Name: "page", - Usage: "The docs `PAGE` the feedback is about (e.g. /agents/build/tools)", + Usage: "The docs `PAGE` the feedback is about (for example, /agents/build/tools).", }, &cli.StringFlag{ Name: "feedback", Aliases: []string{"f"}, - Usage: "Feedback text (max 1024 characters)", + Usage: "Feedback `TEXT` (max 1024 characters). Can also be passed as a positional argument.", }, &cli.StringFlag{ Name: "agent", - Usage: "Identity of the agent submitting feedback (e.g. \"Cursor\", \"Claude Code\")", + Usage: "Identity of the `AGENT` submitting feedback (for example, \"Cursor\", \"Claude Code\").", }, &cli.StringFlag{ Name: "model", - Usage: "Model `ID` used by the agent (e.g. \"gpt-5\", \"claude-4.5-sonnet\")", + Usage: "Model `ID` used by the agent (for example, \"gpt-5\", \"claude-4.5-sonnet\").", }, }, }, diff --git a/cmd/lk/project.go b/cmd/lk/project.go index fbad331a..f4bb06bd 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -43,32 +43,32 @@ var ( { Name: "add", Usage: "Add a new project (for LiveKit Cloud projects, also see `lk cloud auth`)", - UsageText: "lk project add PROJECT_NAME", + UsageText: "lk project add PROJECT_NAME --url URL --api-key KEY --api-secret SECRET [--default]", ArgsUsage: "PROJECT_NAME", Action: addProject, Flags: []cli.Flag{ &cli.StringFlag{ Name: "url", - Usage: "`URL` of the LiveKit server", + Usage: "WebSocket `URL` of the LiveKit server.", }, &cli.StringFlag{ Name: "api-key", - Usage: "Project `KEY`", + Usage: "Project API `KEY`.", }, &cli.StringFlag{ Name: "api-secret", - Usage: "Project `SECRET`", + Usage: "Project API `SECRET`.", }, &cli.BoolFlag{ Name: "default", - Usage: "Set this project as the default", + Usage: "Set this project as the default.", }, }, }, { Name: "list", Usage: "List all configured projects", - UsageText: "lk project list", + UsageText: "lk project list [options]", Action: listProjects, Flags: []cli.Flag{jsonFlag}, }, @@ -100,7 +100,7 @@ var ( Flags: []cli.Flag{ &cli.StringFlag{ Name: "name", - Usage: "New project `NAME`", + Usage: "New project `NAME`.", Required: true, }, jsonFlag, diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index 37e52cbe..567439a2 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -78,34 +78,34 @@ var simulateCommand = &cli.Command{ &cli.IntFlag{ Name: "num-simulations", Aliases: []string{"n"}, - Usage: "Number of scenarios to generate", + Usage: "Number `N` of scenarios to generate from the agent's source. Has no effect when a scenario file is in use.", }, &cli.IntFlag{ Name: "concurrency", - Usage: "Max simulations running in parallel (default: server-side limit)", + Usage: "Maximum number `N` of simulations to run in parallel within a run. Defaults to 15 when unset. An explicit value can't exceed 20; a higher value is rejected. Separately, a project-wide limit of 30 running simulations applies across all runs.", }, &cli.IntFlag{ Name: "samples", Aliases: []string{"k"}, - Usage: "Run every scenario `K` times and report pass@k (passed at least once) and pass^k (passed every time). Requires --scenarios", + Usage: "Run every scenario `K` times in one run, and report pass@k (passed at least once) and pass^k (passed every time). Requires --scenarios. LiveKit Cloud accepts only 3. Each sample counts as a separate simulation, and the number of scenarios multiplied by K can't exceed the limit of 250 simulations per run.", }, &cli.FloatFlag{ Name: "pass-rate", Value: defaultPassRate, - Usage: "Share of a scenario's samples that must pass for it to pass, as a `RATE` from 0 to 1. Rounds to whole samples, so 0.75 of 3 needs 2. Requires --samples", + Usage: "Share of samples that must pass for a scenario to pass, as a `RATE` greater than 0 and at most 1. Rounds to whole samples with a minimum of one; for example, 0.75 of 3 samples needs 2 passes. Requires --samples.", }, &cli.StringFlag{ Name: "scenarios", - Usage: "Path to a scenarios `FILE` (yaml). If omitted, scenarios are generated from the agent's source", + Usage: "Path to a scenario `FILE`. If you omit it, the CLI generates scenarios from the agent's source. The file and every scenario in it need an id, and the CLI offers to add any that are missing.", }, &cli.BoolFlag{ Name: "yes", Aliases: []string{"y"}, - Usage: "Skip confirmation prompts: source upload, and adding missing ids to the --scenarios file (required for non-interactive runs)", + Usage: "Skip the confirmation prompts for the source upload and for adding missing IDs to the scenario file. Required for non-interactive runs that generate scenarios from source or run a file with missing IDs.", }, &cli.StringFlag{ Name: "agent-name", - Usage: "Run against an already-running agent instead of spawning one locally. Pass the registered `NAME`, or \"\" to target the project's default agent (the one that auto-joins every room). Requires --scenarios.", + Usage: "Run against an already-running agent instead of spawning one locally. Pass the registered `NAME`, or an empty string (\"\") to target the project's default agent that auto-joins every room. Requires --scenarios.", }, }, } @@ -139,7 +139,7 @@ func loadSimulateProject(ctx context.Context, cmd *cli.Command) (context.Context var simulateTextCommand = &cli.Command{ Name: "text", - Usage: "Simulate text-only interactions", + Usage: "Run scenarios in text mode", Description: "Options on lk agent simulate apply here too, e.g. --scenarios and --agent-name.", ArgsUsage: "[entrypoint]", HideHelpCommand: true, @@ -154,7 +154,7 @@ var simulateTextCommand = &cli.Command{ // side of the subcommand name. var simulateAudioCommand = &cli.Command{ Name: "audio", - Usage: "Simulate speech-to-speech interactions using the agent's full audio pipeline", + Usage: "Run scenarios through the full STT-LLM-TTS pipeline", Description: "Options on lk agent simulate apply here too, e.g. --scenarios and --agent-name.", ArgsUsage: "[entrypoint]", HideHelpCommand: true, @@ -182,8 +182,8 @@ var simulateAudioCommand = &cli.Command{ // they take a run ID instead and ignore the rest. var simulateViewCommand = &cli.Command{ Name: "view", - Usage: "Open a pre-existing simulation run", - ArgsUsage: "", + Usage: "Open a previous run instead of starting a new one. No agent is started and nothing is dispatched", + ArgsUsage: "RUN_ID", HideHelpCommand: true, Before: loadSimulateProject, Action: runSimulateView, @@ -191,8 +191,8 @@ var simulateViewCommand = &cli.Command{ var simulateExportCommand = &cli.Command{ Name: "export", - Usage: "Print a finished run and its exact per-job chat contexts as JSON. Nothing is run or polled", - ArgsUsage: "", + Usage: "Print a finished run and its per-job chat contexts to stdout as JSON. The run must already be complete", + ArgsUsage: "RUN_ID", HideHelpCommand: true, Before: loadSimulateProject, Action: func(ctx context.Context, cmd *cli.Command) error { @@ -220,8 +220,8 @@ var ( simulateCancelCommand = &cli.Command{ Name: "cancel", - Usage: "Cancel an in-progress simulation run", - ArgsUsage: "", + Usage: "Cancel a run that's still in progress", + ArgsUsage: "RUN_ID", HideHelpCommand: true, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { if experimentalAuthEnabled(cmd) { diff --git a/cmd/lk/simulate_list.go b/cmd/lk/simulate_list.go index eee286bb..572d93a7 100644 --- a/cmd/lk/simulate_list.go +++ b/cmd/lk/simulate_list.go @@ -44,7 +44,7 @@ var simulateListCommand = &cli.Command{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "limit", - Usage: "maximum number of runs to return. If unset, defaults to API page size", + Usage: "Number `N` of runs to return. If unset, defaults to the API page size.", }, // experimental-auth only: the Public API filters by status and is // cursor-paginated. diff --git a/cmd/lk/utils.go b/cmd/lk/utils.go index 6c2778e8..e12756df 100644 --- a/cmd/lk/utils.go +++ b/cmd/lk/utils.go @@ -67,7 +67,7 @@ var ( jsonFlag = &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, - Usage: "Output as JSON", + Usage: "Output as JSON.", } // quietFlag is global. "silent" is kept as an alias for backwards compatibility with // the former per-command --silent flag; both resolve to the same value and feed the @@ -92,12 +92,12 @@ var ( } templateFlag = &cli.StringFlag{ Name: "template", - Usage: "`TEMPLATE` to instantiate, see " + bootstrap.TemplateBaseURL, + Usage: "`TEMPLATE` to instantiate. For example, agent-starter-python or agent-starter-node. See " + bootstrap.TemplateBaseURL + " for the full list.", Destination: &templateName, } templateURLFlag = &cli.StringFlag{ Name: "template-url", - Usage: "`URL` to instantiate, must contain a taskfile.yaml", + Usage: "Custom `URL` to a repository or archive to instantiate (for example, a Git repo URL). The project at that URL must include a taskfile.yaml file at its root.", Destination: &templateURL, } sandboxFlag = &cli.StringFlag{ @@ -108,13 +108,13 @@ var ( } installFlag = &cli.BoolFlag{ Name: "install", - Usage: "Run installation after creating the application", + Usage: "Run installation after creating the application.", } // skillsSetupFlag controls whether lk agent init installs LiveKit's coding // agent skills; unset, lk asks (or installs, without a terminal). skillsSetupFlag = &cli.BoolFlag{ Name: "skills", - Usage: "Install LiveKit skills and the Docs MCP server for your coding agents (see lk skills)", + Usage: "Install LiveKit skills and the Docs MCP server for your coding agents (see lk skills).", } // roleFlag selects a member/invite access level for the Public API commands. roleFlag = &cli.StringFlag{ From 8d9fd62223d26cc196eecb6687f22d72f63cb2ad Mon Sep 17 00:00:00 2001 From: u9g Date: Thu, 15 Oct 2026 10:40:41 -0400 Subject: [PATCH 4/4] fix: accurate --deployment help, aliases, and --list for generate-docs (#1040) * fix: describe --deployment for agent delete and promote agent delete deletes the whole agent when --deployment is unset, and agent promote rejects an unset --deployment, so the shared flag's "Defaults to production" was wrong for both. They now have their own flag, and promote requires it. * feat: list command aliases in generate-docs output Each alias gets its own usage line and joins the options label, the way the docs.livekit.io CLI reference wrote them by hand (lk agent logs / lk agent tail). * feat: add generate-docs --list Prints every command under a path that the reference documents: commands that are run, and groups with their own options. docs.livekit.io generates a partial for each, so a new command can't go missing from the reference. --- cmd/lk/agent.go | 17 +++++++++++-- cmd/lk/generate_docs.go | 56 ++++++++++++++++++++++++++++++++--------- cmd/lk/main.go | 6 +++++ 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index 78b29a9b..f0acc8d2 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -202,6 +202,19 @@ var ( Aliases: []string{"d"}, } + deleteDeploymentFlag = &cli.StringFlag{ + Name: "deployment", + Usage: "`NAME` of the deployment to delete. If unset, deletes the entire agent, including production.", + Aliases: []string{"d"}, + } + + promoteDeploymentFlag = &cli.StringFlag{ + Name: "deployment", + Usage: "`NAME` of the non-production deployment whose image is promoted to production.", + Required: true, + Aliases: []string{"d"}, + } + agentPrebuiltImageFlag = &cli.StringFlag{ Name: "image", Usage: "Pre-built `IMAGE` from the local Docker daemon (e.g. myimage:latest). Requires Docker.", @@ -358,7 +371,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Action: promoteAgent, Flags: []cli.Flag{ idFlag(false), - deploymentFlag, + promoteDeploymentFlag, }, ArgsUsage: "[working-dir]", }, @@ -436,7 +449,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Aliases: []string{"destroy"}, Flags: []cli.Flag{ idFlag(false), - deploymentFlag, + deleteDeploymentFlag, }, ArgsUsage: "[working-dir]", }, diff --git a/cmd/lk/generate_docs.go b/cmd/lk/generate_docs.go index 9f5c37a9..4a86d383 100644 --- a/cmd/lk/generate_docs.go +++ b/cmd/lk/generate_docs.go @@ -35,24 +35,37 @@ func generateDocs(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("unknown command: lk %s", strings.Join(path, " ")) } } + if cmd.Bool("list") { + out.Result(strings.Join(documentedPaths(target, path), "\n")) + return nil + } out.Result(commandDocs(target, path)) return nil } +// documentedPaths is every command in cmd's subtree that the reference documents: +// commands that are run (no subcommands), and groups that take their own options. +func documentedPaths(cmd *cli.Command, path []string) []string { + subs := cmd.VisibleCommands() + var paths []string + if len(subs) == 0 || len(docFlags(cmd)) > 0 { + paths = append(paths, strings.Join(path, " ")) + } + for _, sub := range subs { + paths = append(paths, documentedPaths(sub, append(path[:len(path):len(path)], sub.Name))...) + } + return paths +} + func commandDocs(cmd *cli.Command, path []string) string { var b strings.Builder usage := cmd.UsageText if usage == "" { - usage = strings.Join(append([]string{"lk"}, path...), " ") - if len(cmd.VisibleCommands()) > 0 { - usage += " " - } - if len(docFlags(cmd)) > 0 { - usage += " [options]" - } - if cmd.ArgsUsage != "" { - usage += " " + cmd.ArgsUsage + var lines []string + for _, name := range append([]string{cmd.Name}, cmd.Aliases...) { + lines = append(lines, usageLine(cmd, append(path[:len(path)-1:len(path)-1], name))) } + usage = strings.Join(lines, "\n# or\n") } fmt.Fprintf(&b, "```shell\n%s\n```\n", strings.TrimSpace(usage)) @@ -70,11 +83,16 @@ func commandDocs(cmd *cli.Command, path []string) string { if flags := docFlags(cmd); len(flags) > 0 { // Each docs page covers one top-level command group, so options are // labelled by the path below it: "Options for `deploy`". - label := strings.Join(path, " ") + // Aliases are joined into one label: "Options for `logs`/`tail`". + parent := strings.Join(path[:len(path)-1], " ") if len(path) > 1 { - label = strings.Join(path[1:], " ") + parent = strings.Join(path[1:len(path)-1], " ") + } + var labels []string + for _, name := range append([]string{cmd.Name}, cmd.Aliases...) { + labels = append(labels, "`"+strings.TrimSpace(parent+" "+name)+"`") } - fmt.Fprintf(&b, "\nOptions for `%s`:\n\n", label) + fmt.Fprintf(&b, "\nOptions for %s:\n\n", strings.Join(labels, "/")) for _, f := range flags { b.WriteString(flagDocs(f)) } @@ -82,6 +100,20 @@ func commandDocs(cmd *cli.Command, path []string) string { return b.String() } +func usageLine(cmd *cli.Command, path []string) string { + usage := strings.Join(append([]string{"lk"}, path...), " ") + if len(cmd.VisibleCommands()) > 0 { + usage += " " + } + if len(docFlags(cmd)) > 0 { + usage += " [options]" + } + if cmd.ArgsUsage != "" { + usage += " " + cmd.ArgsUsage + } + return usage +} + // docFlags is the command's own visible flags, without the --help flag urfave // copies into every command. func docFlags(cmd *cli.Command) []cli.Flag { diff --git a/cmd/lk/main.go b/cmd/lk/main.go index a986a5c9..5afd8c21 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -79,6 +79,12 @@ Docs: https://docs.livekit.io/intro/basics/cli/`, ArgsUsage: "[command path...]", Action: generateDocs, Hidden: true, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "Print the path of every command below the given one that has a reference entry, instead of its docs", + }, + }, }, }, Before: initLogger,