From 6fabb05ac5af53d4543f01b4486716a28791c0eb Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 12 Oct 2026 16:24:49 -0400 Subject: [PATCH 1/9] 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 bfa44cc7..9ff88eab 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -77,6 +77,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, } From 19ca6fe28115e3e0d6c414271d35e7e80f708382 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 12 Oct 2026 16:24:50 -0400 Subject: [PATCH 2/9] 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 cc3d3222059fa6b9786e8fe04e34a90031892d4a Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Mon, 12 Oct 2026 16:29:24 -0400 Subject: [PATCH 3/9] 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 4dd56943..c9fdc0a4 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -127,14 +127,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, } @@ -157,20 +157,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, } @@ -183,32 +183,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{ @@ -242,7 +242,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) @@ -252,7 +252,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, @@ -296,7 +296,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, }, @@ -394,7 +394,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, }, @@ -474,7 +474,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 c4db3934..9535eb48 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 0d05754e..8484d894 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 bd3ec197..47b02566 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}, }, @@ -103,7 +103,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 c0fd6e49..5fae6409 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 { @@ -221,8 +221,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 userAuthEnabled(cmd) { diff --git a/cmd/lk/simulate_list.go b/cmd/lk/simulate_list.go index 01afd749..6c684e55 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.", }, // user-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 9323b41d..2dd4e0c7 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 @@ -93,23 +93,23 @@ 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, } 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 8111e49709bf540c25109b025511635be4026948 Mon Sep 17 00:00:00 2001 From: u9g Date: Thu, 15 Oct 2026 10:40:41 -0400 Subject: [PATCH 4/9] 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 c9fdc0a4..fa23a54c 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -201,6 +201,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.", @@ -345,7 +358,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Action: promoteAgent, Flags: []cli.Flag{ idFlag(false), - deploymentFlag, + promoteDeploymentFlag, }, ArgsUsage: "[working-dir]", }, @@ -423,7 +436,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 9ff88eab..3a8644b1 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -83,6 +83,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, From 02aabf3d525957162810aa33ab791b9ecb4ecee6 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 15 Oct 2026 16:13:54 -0400 Subject: [PATCH 5/9] feat(generate-docs): document required flags, env vars, and inherited options The docs reference lost three things --help shows: which flags are required (lk agent promote --deployment, lk agent rollback --version), the environment variable a flag reads (LIVEKIT_AGENT_DEPLOYMENT on lk agent dev), and that lk agent simulate text and audio take the options on lk agent simulate. --- cmd/lk/generate_docs.go | 26 +++++++++++++++++++++++++- cmd/lk/simulate.go | 2 ++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cmd/lk/generate_docs.go b/cmd/lk/generate_docs.go index 4a86d383..2d7fcc23 100644 --- a/cmd/lk/generate_docs.go +++ b/cmd/lk/generate_docs.go @@ -43,6 +43,11 @@ func generateDocs(ctx context.Context, cmd *cli.Command) error { return nil } +// docsInheritsOptions marks a subcommand whose behavior depends on its parent's +// persistent options, so its docs point to them; other subcommands parse them +// too but ignore them. +const docsInheritsOptions = "docsInheritsOptions" + // 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 { @@ -97,6 +102,10 @@ func commandDocs(cmd *cli.Command, path []string) string { b.WriteString(flagDocs(f)) } } + + if inheritsOptions(cmd) { + fmt.Fprintf(&b, "\nAlso takes the options for `%s`.\n", strings.Join(path[1:len(path)-1], " ")) + } return b.String() } @@ -105,7 +114,7 @@ func usageLine(cmd *cli.Command, path []string) string { if len(cmd.VisibleCommands()) > 0 { usage += " " } - if len(docFlags(cmd)) > 0 { + if len(docFlags(cmd)) > 0 || inheritsOptions(cmd) { usage += " [options]" } if cmd.ArgsUsage != "" { @@ -114,6 +123,11 @@ func usageLine(cmd *cli.Command, path []string) string { return usage } +func inheritsOptions(cmd *cli.Command) bool { + inherits, _ := cmd.Metadata[docsInheritsOptions].(bool) + return inherits +} + // docFlags is the command's own visible flags, without the --help flag urfave // copies into every command. func docFlags(cmd *cli.Command) []cli.Flag { @@ -150,9 +164,19 @@ func flagDocs(f cli.Flag) string { line += fmt.Sprintf(" (alias %s)", strings.Join(aliases, ", ")) } line += ": " + sentence(usage) + if r, ok := f.(cli.RequiredFlag); ok && r.IsRequired() && !strings.Contains(usage, "Required") { + line += " Required." + } if def := defaultText(doc); def != "" && !strings.Contains(usage, "Default") { line += fmt.Sprintf(" Default: `%s`.", def) } + if envs := doc.GetEnvVars(); len(envs) > 0 { + var quoted []string + for _, e := range envs { + quoted = append(quoted, "`"+e+"`") + } + line += fmt.Sprintf(" Also read from %s.", strings.Join(quoted, ", ")) + } return line + "\n" } diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index 5fae6409..b5e97723 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -141,6 +141,7 @@ var simulateTextCommand = &cli.Command{ Name: "text", Usage: "Run scenarios in text mode", Description: "Options on lk agent simulate apply here too, e.g. --scenarios and --agent-name.", + Metadata: map[string]any{docsInheritsOptions: true}, ArgsUsage: "[entrypoint]", HideHelpCommand: true, Before: loadSimulateProject, @@ -156,6 +157,7 @@ var simulateAudioCommand = &cli.Command{ Name: "audio", 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.", + Metadata: map[string]any{docsInheritsOptions: true}, ArgsUsage: "[entrypoint]", HideHelpCommand: true, Before: loadSimulateProject, From 7f0b6f0af0646ee9408957aa8ba7fdd89a74b3bc Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 15 Oct 2026 16:14:24 -0400 Subject: [PATCH 6/9] fix(agent): stop documenting --region on deploy and --deployment on create Neither flag is read: lk agent create always deploys to production, and lk agent deploy keeps the region the agent was created in. Both still parse, hidden, so existing scripts keep working. --- cmd/lk/agent.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index fa23a54c..79a9b5c4 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -201,6 +201,18 @@ var ( Aliases: []string{"d"}, } + // create always deploys to production and deploy keeps the region the agent + // was created in; both still accept these flags so existing scripts keep working. + ignoredDeploymentFlag = &cli.StringFlag{ + Name: "deployment", + Aliases: []string{"d"}, + Hidden: true, + } + ignoredRegionFlag = &cli.StringFlag{ + Name: "region", + Hidden: true, + } + deleteDeploymentFlag = &cli.StringFlag{ Name: "deployment", Usage: "`NAME` of the deployment to delete. If unset, deletes the entire agent, including production.", @@ -288,7 +300,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", secretsMountFlag, ignoreEmptySecretsFlag, regionFlag, - deploymentFlag, + ignoredDeploymentFlag, skipSDKCheckFlag, agentPrebuiltImageFlag, agentPrebuiltImageTarFlag, @@ -339,7 +351,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", secretsFlag, secretsFileFlag, secretsMountFlag, - regionFlag, + ignoredRegionFlag, deploymentFlag, ignoreEmptySecretsFlag, skipSDKCheckFlag, From 8bdd89fff5a9b30285b36ed21be129123d50cb01 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Thu, 15 Oct 2026 16:17:25 -0400 Subject: [PATCH 7/9] docs: fix and complete help text the docs reference renders Comparing the generated reference with the hand-written docs.livekit.io pages found help text that was wrong or dropped what the pages said: - agent rollback --version is required; it never defaulted to the previous version. - agent update-secrets --overwrite replaces every secret, not only the keys given. - agent deploy --image and --image-tar ignore --deployment. - agent versions --attribute and --attributes filter versions instead of recording attributes. - project list --json includes each project's API key and secret. - agent init: when --lang, --install, --skills, and --region prompt or fall back, that --region is used only with --deploy, and that --deploy implies --install. - --log-level accepts CRITICAL for Python and FATAL for Node.js. - Each debugger subcommand's --json says what it prints (NDJSON for events), and debugger logs has no --json. - simulate --samples accepts 1 or 3, --pass-rate needs more than one sample, and export takes any finished run. - private-link create --endpoint accepts an Azure Private Link Service alias. --- cmd/lk/agent.go | 44 ++++++++++++++++++++++++------------ cmd/lk/agent_private_link.go | 2 +- cmd/lk/agent_run.go | 4 ++-- cmd/lk/debugger.go | 20 ++++++++-------- cmd/lk/project.go | 2 +- cmd/lk/simulate.go | 6 ++--- cmd/lk/utils.go | 15 +++++++----- 7 files changed, 55 insertions(+), 38 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index 79a9b5c4..2ed67ba9 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -127,7 +127,7 @@ var ( idFlag = func(required bool) *cli.StringFlag { return &cli.StringFlag{ Name: "id", - Usage: fmt.Sprintf("Agent `ID`. If unset and %s is present, uses the ID found there.", config.LiveKitTOMLFile), + Usage: fmt.Sprintf("Agent `ID`. If unset, uses the ID in %s; without that file, the CLI prompts you to choose an agent.", config.LiveKitTOMLFile), Required: required, } } @@ -146,13 +146,23 @@ var ( attributeFlag = &cli.StringSliceFlag{ Name: "attribute", - Usage: "`KEY=VALUE` attribute pair, may be repeated. Merged with --attributes, taking precedence on conflicting keys.", + Usage: "`KEY=VALUE` attribute to record on the new version. Repeatable. Merged with --attributes, taking precedence on conflicting keys.", Required: false, } + // versions filters by attributes instead of recording them. + versionsAttributesFlag = &cli.StringFlag{ + Name: "attributes", + Usage: "Show only versions that have every attribute in this `JSON` object of string key-value pairs, given as a literal or file path. Use \"-\" to read from stdin.", + } + versionsAttributeFlag = &cli.StringSliceFlag{ + Name: "attribute", + Usage: "Show only versions that have this `KEY=VALUE` attribute. Repeatable; a version must have all of them.", + } + noDefaultAttributesFlag = &cli.BoolFlag{ Name: "no-default-attributes", - Usage: `Omit default git attributes from deployment.`, + Usage: "Don't record the default git_commit, git_branch, and git_tree attributes, which are added when the working directory is a git repository.", } secretsFileFlag = &cli.StringFlag{ @@ -190,10 +200,15 @@ var ( regionFlag = &cli.StringFlag{ Name: "region", - Usage: "`REGION` code to deploy the agent to. If unset, the CLI prompts you to select a region.", + Usage: "`REGION` code to deploy the agent to. If unset, the CLI prompts you to select a region; with --yes or without a terminal, it fails.", Required: false, } + initRegionFlag = &cli.StringFlag{ + Name: "region", + Usage: "`REGION` code to deploy the agent to. Used only with --deploy. If unset, the CLI prompts you to select a region; with --yes or without a terminal, it fails.", + } + deploymentFlag = &cli.StringFlag{ Name: "deployment", Usage: "`NAME` of the deployment. Defaults to production.", @@ -215,7 +230,7 @@ var ( deleteDeploymentFlag = &cli.StringFlag{ Name: "deployment", - Usage: "`NAME` of the deployment to delete. If unset, deletes the entire agent, including production.", + Usage: "`NAME` of the non-production deployment to delete. If unset, deletes the entire agent, including production.", Aliases: []string{"d"}, } @@ -228,12 +243,12 @@ var ( 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. lk agent deploy ignores --deployment with this flag.", } 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. lk agent deploy ignores --deployment with this flag.", } skipSDKCheckFlag = &cli.BoolFlag{ @@ -267,7 +282,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. If unset, the CLI asks, with Python preselected; with --yes or without a terminal, it uses Python. Ignored with --template or --template-url.", Action: func(ctx context.Context, cmd *cli.Command, l string) error { if _, ok := starterTemplateURLs[l]; !ok { return fmt.Errorf("unsupported language: %s", l) @@ -277,12 +292,12 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", }, &cli.BoolFlag{ Name: "deploy", - Usage: "Deploy the agent to LiveKit Cloud after initialization.", + Usage: "Deploy the agent to LiveKit Cloud after initialization. Implies --install.", Value: false, }, templateFlag, templateURLFlag, - regionFlag, + initRegionFlag, installFlag, skillsSetupFlag, }, @@ -419,8 +434,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Flags: []cli.Flag{ &cli.StringFlag{ Name: "version", - Usage: "`VERSION` to roll back to. Defaults to the most recent version prior to the current.", - Value: "latest", + Usage: "`VERSION` to roll back to, as listed by lk agent versions.", Required: true, }, idFlag(false), @@ -459,8 +473,8 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Action: listAgentVersions, Flags: []cli.Flag{ idFlag(false), - attributeFlag, - attributesFlag, + versionsAttributeFlag, + versionsAttributesFlag, jsonFlag, }, ArgsUsage: "[working-dir]", @@ -499,7 +513,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", idFlag(false), &cli.BoolFlag{ Name: "overwrite", - Usage: "Overwrite existing secrets.", + Usage: "Replace all of the agent's existing secrets with the ones given, instead of adding to and updating them. Asks for confirmation unless --yes.", Required: false, Value: false, }, diff --git a/cmd/lk/agent_private_link.go b/cmd/lk/agent_private_link.go index 0159c3bf..4fe8ace1 100644 --- a/cmd/lk/agent_private_link.go +++ b/cmd/lk/agent_private_link.go @@ -42,7 +42,7 @@ var privateLinkCommands = &cli.Command{ }, &cli.StringFlag{ Name: "endpoint", - Usage: "The `ENDPOINT` to connect to: the AWS endpoint service name or the Azure Resource ID. Required.", + Usage: "The `ENDPOINT` to connect to: the AWS endpoint service name, the Azure Private Link Service alias, or the Azure Resource ID.", Required: true, }, &cli.StringFlag{ diff --git a/cmd/lk/agent_run.go b/cmd/lk/agent_run.go index 9535eb48..8c15dbf5 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`. One of TRACE, DEBUG, INFO, WARN, ERROR.", + Usage: "Log `LEVEL`. One of TRACE, DEBUG, INFO, WARN, ERROR, plus CRITICAL for Python and FATAL for Node.js.", }, } @@ -127,7 +127,7 @@ var devCommand = &cli.Command{ &cli.StringFlag{ Name: "deployment", Sources: cli.EnvVars(agentDeploymentEnv), - 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.", + Usage: "Deployment `NAME` to register the agent under. Defaults to a random dev- ID so dev traffic is isolated from production; pass production to explicitly serve production traffic. Overrides any value in the agent's .env file.", }, ), Action: runAgentDev, diff --git a/cmd/lk/debugger.go b/cmd/lk/debugger.go index c0d1149d..2263dc31 100644 --- a/cmd/lk/debugger.go +++ b/cmd/lk/debugger.go @@ -83,7 +83,7 @@ var sessionIdleFlag = &cli.DurationFlag{ var sessionAudioFlag = &cli.BoolFlag{ Name: "audio", - Usage: "Speak each turn into the agent's microphone input instead of sending text.", + 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.", } var sessionMetricsFlag = &cli.BoolFlag{ @@ -153,7 +153,7 @@ file you name explicitly: lk agent debugger start agent.ts -- --env-file=.env # args after -- go to node/python After editing the agent's code, run "lk agent debugger restart" to relaunch it -with a fresh conversation. Add --json to any command for machine-readable +with a fresh conversation. Add --json to any command but logs for machine-readable output: each turn is a document with "text", "reply", "duration_ms", and an "events" list of message, tool_call, handoff, config, error, and log entries. Exit codes are non-zero when a turn fails or times out, or no session is @@ -179,7 +179,7 @@ The agent reads its own .env for credentials, exactly like "lk agent console". Only one session runs per port; use --port for more, or "restart" to replace the current one. The session stops itself after --idle-timeout (default 30m) without any command, so a forgotten session does not linger.`, - Flags: []cli.Flag{sessionPortFlag, sessionIdleFlag, sessionAudioFlag, jsonFlag}, + Flags: []cli.Flag{sessionPortFlag, sessionIdleFlag, sessionAudioFlag, jsonFlagWithUsage("Print the greeting events and session status as JSON.")}, Action: runSessionStart, }, { @@ -205,7 +205,7 @@ be piped on stdin: echo "Book me a table for two" | lk agent debugger say`, Flags: []cli.Flag{ sessionPortFlag, - jsonFlag, + jsonFlagWithUsage("Print the turn as one JSON document."), sessionMetricsFlag, &cli.DurationFlag{ Name: "timeout", @@ -215,7 +215,7 @@ be piped on stdin: }, &cli.BoolFlag{ Name: "logs", - Usage: "Show the agent's log lines emitted during the turn, beneath the step they belong to.", + Usage: "Show the agent's log lines emitted during the turn, beneath the step they belong to. Useful for finding a tool's traceback.", }, }, Action: runSessionSay, @@ -227,7 +227,7 @@ be piped on stdin: Description: `Fetches the agent's own chat history, so it reflects exactly what the LLM has seen: user and agent messages, tool calls with results, handoffs, and instruction/tool changes. Works while a turn is in progress.`, - Flags: []cli.Flag{sessionPortFlag, jsonFlag, sessionMetricsFlag}, + Flags: []cli.Flag{sessionPortFlag, jsonFlagWithUsage("Print the chat history as one JSON document."), sessionMetricsFlag}, Action: runSessionHistory, }, { @@ -245,7 +245,7 @@ emits one JSON object per line (NDJSON), suitable for piping into jq or another program in real time.`, Flags: []cli.Flag{ sessionPortFlag, - jsonFlag, + jsonFlagWithUsage("Print one JSON object per event, one per line (NDJSON)."), &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."}, }, @@ -259,7 +259,7 @@ agent (after handoffs) with its state, tools, and the first line of its instructions, the number of turns so far, and the path of the agent's log file. Exits 1 when no session is running. With --json the full instructions are included, and "running": false is printed instead of an error.`, - Flags: []cli.Flag{sessionPortFlag, jsonFlag}, + Flags: []cli.Flag{sessionPortFlag, jsonFlagWithUsage("Print the session status as JSON, including the active agent's full instructions. With no session running, prints \"running\": false and exits 1.")}, Action: runSessionStatus, }, { @@ -281,7 +281,7 @@ stripped). The whole log is also kept in a file; "status" shows its path. Use Description: `Stops the running session, then starts a new one with the same entrypoint and port. The agent process is relaunched, so code changes take effect and the conversation starts fresh; the new greeting (if any) is printed like "start".`, - Flags: []cli.Flag{sessionPortFlag, jsonFlag}, + Flags: []cli.Flag{sessionPortFlag, jsonFlagWithUsage("Print the greeting events and session status as JSON, like start.")}, Action: runSessionRestart, }, { @@ -293,7 +293,7 @@ was active at the end, and where the agent's log file is kept. Add entire log before the summary; --json returns everything in one document.`, Flags: []cli.Flag{ sessionPortFlag, - jsonFlag, + jsonFlagWithUsage("Print the summary, conversation, and logs in one JSON document."), &cli.BoolFlag{ Name: "metrics", Usage: "Show latency metrics with the conversation.", diff --git a/cmd/lk/project.go b/cmd/lk/project.go index 47b02566..70fd2803 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -70,7 +70,7 @@ var ( Usage: "List all configured projects", UsageText: "lk project list [options]", Action: listProjects, - Flags: []cli.Flag{jsonFlag}, + Flags: []cli.Flag{jsonFlagWithUsage("Output as JSON, including each project's API key and secret.")}, }, { Name: "get", diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index b5e97723..137e1413 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -87,12 +87,12 @@ var simulateCommand = &cli.Command{ &cli.IntFlag{ Name: "samples", Aliases: []string{"k"}, - 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.", + 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 when K is more than 1. LiveKit Cloud accepts 1 or 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 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.", + 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 of more than 1.", }, &cli.StringFlag{ Name: "scenarios", @@ -193,7 +193,7 @@ var simulateViewCommand = &cli.Command{ var simulateExportCommand = &cli.Command{ Name: "export", - Usage: "Print a finished run and its per-job chat contexts to stdout as JSON. The run must already be complete", + Usage: "Print a finished run and its per-job chat contexts to stdout as JSON. The run must have finished: completed, failed, or cancelled", ArgsUsage: "RUN_ID", HideHelpCommand: true, Before: loadSimulateProject, diff --git a/cmd/lk/utils.go b/cmd/lk/utils.go index 2dd4e0c7..80f13ee4 100644 --- a/cmd/lk/utils.go +++ b/cmd/lk/utils.go @@ -64,10 +64,13 @@ var ( Usage: "`ID` of participant (supports templates)", Required: true, } - jsonFlag = &cli.BoolFlag{ - Name: "json", - Aliases: []string{"j"}, - Usage: "Output as JSON.", + jsonFlag = jsonFlagWithUsage("Output as JSON.") + jsonFlagWithUsage = func(usage string) *cli.BoolFlag { + return &cli.BoolFlag{ + Name: "json", + Aliases: []string{"j"}, + Usage: usage, + } } // 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 @@ -103,13 +106,13 @@ var ( } installFlag = &cli.BoolFlag{ Name: "install", - Usage: "Run installation after creating the application.", + Usage: "Install the project's dependencies after creating it. If unset, the CLI asks; with --yes or without a terminal, it skips installation. Pass --install=false to skip.", } // 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 Agent Skills and the Docs MCP server for your coding agents (see lk skills). If unset, the CLI asks; with --yes or without a terminal, it installs them. Pass --skills=false to skip.", } // roleFlag selects a member/invite access level for the Public API commands. roleFlag = &cli.StringFlag{ From a2d756bceebe4a6ea8c9f628c2a9211f95c5a2d5 Mon Sep 17 00:00:00 2001 From: Kat Ahn Date: Fri, 16 Oct 2026 12:05:39 -0700 Subject: [PATCH 8/9] add test, update output, fix no command path panic - `lk generate-docs` with no command path panicked; it now errors and points to --list. --list no longer prints a blank line for the root. - Required and Default markers come only from the flag definition. The generator no longer skips them when the usage text happens to contain "Required" or "Default", private-link drops its hand-written "Required.", and --help now marks required flags with "(required)". - The generator prints duration defaults as 30m, not 30m0s, and honors HideDefault. The debugger's DefaultText derives from its constants. - `project add` shows its arguments as optional, since the CLI asks for any that are missing. - `debugger stop --metrics` says it requires --chat-history. - Help text says "non-interactive mode" instead of "with --yes or without a terminal". - notify-docs fires on `released`, so promoting a prerelease to a full release also regenerates the docs. Prereleases still don't trigger it. - Add generate_docs_test.go, including a pass over the real command tree. --- .github/workflows/notify-docs.yaml | 7 +- cmd/lk/agent.go | 6 +- cmd/lk/agent_private_link.go | 8 +- cmd/lk/debugger.go | 6 +- cmd/lk/generate_docs.go | 33 ++++++- cmd/lk/generate_docs_test.go | 147 +++++++++++++++++++++++++++++ cmd/lk/main.go | 10 ++ cmd/lk/project.go | 9 +- cmd/lk/utils.go | 4 +- 9 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 cmd/lk/generate_docs_test.go diff --git a/.github/workflows/notify-docs.yaml b/.github/workflows/notify-docs.yaml index fc574451..e2a963f4 100644 --- a/.github/workflows/notify-docs.yaml +++ b/.github/workflows/notify-docs.yaml @@ -16,17 +16,18 @@ 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. +# from `lk generate-docs` at this tag and opens a PR. `released` fires when a +# release is published as a full release or a prerelease is later promoted to +# one; publishing a prerelease doesn't fire it. on: release: - types: [published] + types: [released] # 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 diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index 2ed67ba9..5585db77 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -200,13 +200,13 @@ var ( regionFlag = &cli.StringFlag{ Name: "region", - Usage: "`REGION` code to deploy the agent to. If unset, the CLI prompts you to select a region; with --yes or without a terminal, it fails.", + Usage: "`REGION` code to deploy the agent to. If unset, the CLI prompts you to select a region. Required in non-interactive mode.", Required: false, } initRegionFlag = &cli.StringFlag{ Name: "region", - Usage: "`REGION` code to deploy the agent to. Used only with --deploy. If unset, the CLI prompts you to select a region; with --yes or without a terminal, it fails.", + Usage: "`REGION` code to deploy the agent to. Used only with --deploy. If unset, the CLI prompts you to select a region. Required in non-interactive mode.", } deploymentFlag = &cli.StringFlag{ @@ -282,7 +282,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. If unset, the CLI asks, with Python preselected; with --yes or without a terminal, it uses Python. Ignored with --template or --template-url.", + Usage: "`LANGUAGE` of the starter template. One of python, node. If unset, the CLI asks, with Python preselected. Defaults to Python in non-interactive mode. Ignored with --template or --template-url.", Action: func(ctx context.Context, cmd *cli.Command, l string) error { if _, ok := starterTemplateURLs[l]; !ok { return fmt.Errorf("unsupported language: %s", l) diff --git a/cmd/lk/agent_private_link.go b/cmd/lk/agent_private_link.go index 4fe8ace1..50c588d3 100644 --- a/cmd/lk/agent_private_link.go +++ b/cmd/lk/agent_private_link.go @@ -28,7 +28,7 @@ var privateLinkCommands = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "name", - Usage: "`NAME` for the private link. Required.", + Usage: "`NAME` for the private link.", Required: true, }, &cli.StringFlag{ @@ -37,7 +37,7 @@ var privateLinkCommands = &cli.Command{ }, &cli.UintFlag{ Name: "port", - Usage: "TCP `PORT` to reach on the target service. Required. Must match the port your endpoint service listens on.", + Usage: "TCP `PORT` to reach on the target service. Must match the port your endpoint service listens on.", Required: true, }, &cli.StringFlag{ @@ -69,7 +69,7 @@ var privateLinkCommands = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", - Usage: "Private link `ID`. Required.", + Usage: "Private link `ID`.", Required: true, }, jsonFlag, @@ -83,7 +83,7 @@ var privateLinkCommands = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", - Usage: "Private link `ID`. Required.", + Usage: "Private link `ID`.", Required: true, }, jsonFlag, diff --git a/cmd/lk/debugger.go b/cmd/lk/debugger.go index 2263dc31..f73b07ad 100644 --- a/cmd/lk/debugger.go +++ b/cmd/lk/debugger.go @@ -77,7 +77,7 @@ var sessionPortFlag = &cli.IntFlag{ var sessionIdleFlag = &cli.DurationFlag{ Name: "idle-timeout", Value: defaultIdleTimeout, - DefaultText: "30m", + DefaultText: shortDuration(defaultIdleTimeout), 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.", } @@ -210,7 +210,7 @@ be piped on stdin: &cli.DurationFlag{ Name: "timeout", Value: defaultSayTimeout, - DefaultText: "2m", + DefaultText: shortDuration(defaultSayTimeout), Usage: "Stop waiting for the agent's reply after `DURATION`. Takes a duration with a unit, such as 2m or 90s.", }, &cli.BoolFlag{ @@ -296,7 +296,7 @@ entire log before the summary; --json returns everything in one document.`, jsonFlagWithUsage("Print the summary, conversation, and logs in one JSON document."), &cli.BoolFlag{ Name: "metrics", - Usage: "Show latency metrics with the conversation.", + Usage: "Show latency metrics for each turn of the conversation. Requires --chat-history.", }, &cli.BoolFlag{ Name: "chat-history", diff --git a/cmd/lk/generate_docs.go b/cmd/lk/generate_docs.go index 2d7fcc23..9dbfaecb 100644 --- a/cmd/lk/generate_docs.go +++ b/cmd/lk/generate_docs.go @@ -16,8 +16,10 @@ package main import ( "context" + "errors" "fmt" "strings" + "time" "github.com/urfave/cli/v3" ) @@ -39,6 +41,11 @@ func generateDocs(ctx context.Context, cmd *cli.Command) error { out.Result(strings.Join(documentedPaths(target, path), "\n")) return nil } + // The root's options are the global options, which the reference covers + // on its own page. + if len(path) == 0 { + return errors.New("give a command path, such as agent deploy, or pass --list") + } out.Result(commandDocs(target, path)) return nil } @@ -50,10 +57,11 @@ const docsInheritsOptions = "docsInheritsOptions" // 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. +// The root is never listed: its options are the global ones. func documentedPaths(cmd *cli.Command, path []string) []string { subs := cmd.VisibleCommands() var paths []string - if len(subs) == 0 || len(docFlags(cmd)) > 0 { + if len(path) > 0 && (len(subs) == 0 || len(docFlags(cmd)) > 0) { paths = append(paths, strings.Join(path, " ")) } for _, sub := range subs { @@ -164,10 +172,10 @@ func flagDocs(f cli.Flag) string { line += fmt.Sprintf(" (alias %s)", strings.Join(aliases, ", ")) } line += ": " + sentence(usage) - if r, ok := f.(cli.RequiredFlag); ok && r.IsRequired() && !strings.Contains(usage, "Required") { + if r, ok := f.(cli.RequiredFlag); ok && r.IsRequired() { line += " Required." } - if def := defaultText(doc); def != "" && !strings.Contains(usage, "Default") { + if def := defaultText(doc); def != "" { line += fmt.Sprintf(" Default: `%s`.", def) } if envs := doc.GetEnvVars(); len(envs) > 0 { @@ -181,9 +189,15 @@ func flagDocs(f cli.Flag) string { } func defaultText(f cli.DocGenerationFlag) string { + if !f.IsDefaultVisible() { + return "" + } if t := f.GetDefaultText(); t != "" { return t } + if d, ok := f.(*cli.DurationFlag); ok && d.Value != 0 { + return shortDuration(d.Value) + } switch v := strings.Trim(f.GetValue(), `"`); v { case "", "false", "0", "[]", "0s": return "" @@ -192,6 +206,19 @@ func defaultText(f cli.DocGenerationFlag) string { } } +// shortDuration formats d the way the flag usage writes durations: 30m, not +// the time.Duration format, 30m0s. +func shortDuration(d time.Duration) string { + s := d.String() + if strings.HasSuffix(s, "m0s") { + s = strings.TrimSuffix(s, "0s") + } + if strings.HasSuffix(s, "h0m") { + s = strings.TrimSuffix(s, "0m") + } + return s +} + func flagPrefix(name string) string { if len(name) == 1 { return "-" diff --git a/cmd/lk/generate_docs_test.go b/cmd/lk/generate_docs_test.go new file mode 100644 index 00000000..6d7fa1df --- /dev/null +++ b/cmd/lk/generate_docs_test.go @@ -0,0 +1,147 @@ +// 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" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func testDocsTree() *cli.Command { + return &cli.Command{ + Name: "lk", + Flags: []cli.Flag{&cli.BoolFlag{Name: "quiet"}}, + Commands: []*cli.Command{ + { + Name: "generate-docs", + Action: generateDocs, + Hidden: true, + Flags: []cli.Flag{&cli.BoolFlag{Name: "list"}}, + ArgsUsage: "[command path...]", + }, + { + Name: "agent", + Commands: []*cli.Command{ + { + Name: "deploy", + Usage: "Deploy the agent", + ArgsUsage: "[working-dir]", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "deployment", Aliases: []string{"d"}, Usage: "`NAME` of the deployment", Required: true}, + &cli.DurationFlag{Name: "timeout", Value: 90 * time.Minute, Usage: "Give up after `DURATION`."}, + &cli.IntFlag{Name: "port", Value: 8080, Sources: cli.EnvVars("TEST_PORT"), Usage: "Local `PORT`"}, + &cli.BoolFlag{Name: "verbose", Usage: "Log more"}, + &cli.StringFlag{Name: "region", Hidden: true}, + }, + }, + {Name: "logs", Aliases: []string{"tail"}, Usage: "Stream logs"}, + {Name: "secret", Hidden: true}, + }, + }, + }, + } +} + +func TestCommandDocs(t *testing.T) { + root := testDocsTree() + deploy := root.Command("agent").Command("deploy") + + want := "```shell\n" + + "lk agent deploy [options] [working-dir]\n" + + "```\n" + + "\n" + + "Options for `deploy`:\n" + + "\n" + + "- `--deployment NAME` (alias `-d`): NAME of the deployment. Required.\n" + + "- `--timeout DURATION`: Give up after DURATION. Default: `1h30m`.\n" + + "- `--port PORT`: Local PORT. Default: `8080`. Also read from `TEST_PORT`.\n" + + "- `--verbose`: Log more.\n" + assert.Equal(t, want, commandDocs(deploy, []string{"agent", "deploy"})) + + logs := root.Command("agent").Command("logs") + assert.Equal(t, "```shell\nlk agent logs\n# or\nlk agent tail\n```\n", commandDocs(logs, []string{"agent", "logs"})) +} + +func TestDocumentedPaths(t *testing.T) { + root := testDocsTree() + pruneHiddenCommands(root) + // The root has (global) options but is never listed, so --list output has + // no blank line for it. + assert.Equal(t, []string{"agent deploy", "agent logs"}, documentedPaths(root, nil)) + assert.Equal(t, []string{"agent deploy"}, documentedPaths(root.Command("agent").Command("deploy"), []string{"agent", "deploy"})) +} + +func TestGenerateDocsArgs(t *testing.T) { + err := testDocsTree().Run(context.Background(), []string{"lk", "generate-docs"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "command path") + + err = testDocsTree().Run(context.Background(), []string{"lk", "generate-docs", "agent", "nope"}) + require.EqualError(t, err, "unknown command: lk agent nope") + + err = testDocsTree().Run(context.Background(), []string{"lk", "generate-docs", "agent", "secret"}) + require.EqualError(t, err, "unknown command: lk agent secret") + + require.NoError(t, testDocsTree().Run(context.Background(), []string{"lk", "generate-docs", "--list"})) + require.NoError(t, testDocsTree().Run(context.Background(), []string{"lk", "generate-docs", "agent", "deploy"})) +} + +func TestShortDuration(t *testing.T) { + for d, want := range map[time.Duration]string{ + 30 * time.Minute: "30m", + 2 * time.Minute: "2m", + 2 * time.Hour: "2h", + 90 * time.Minute: "1h30m", + 90 * time.Second: "1m30s", + 10 * time.Second: "10s", + 500 * time.Millisecond: "500ms", + time.Hour + time.Second: "1h0m1s", + } { + assert.Equal(t, want, shortDuration(d), d.String()) + } +} + +// Every command the real CLI lists must render without panicking, and with +// each marker the generator adds appearing once. +func TestCommandDocsRealTree(t *testing.T) { + root := &cli.Command{Name: "lk", Flags: globalFlags} + for _, group := range [][]*cli.Command{ + AppCommands, AgentCommands, AnalyticsCommands, CloudCommands, DocsCommands, + SkillsCommands, ProjectCommands, WorkspaceCommands, UserCommands, ThemeCommands, + RoomCommands, TokenCommands, + } { + root.Commands = append(root.Commands, group...) + } + + paths := documentedPaths(root, nil) + require.NotEmpty(t, paths) + for _, p := range paths { + require.NotEmpty(t, p) + path := strings.Fields(p) + target := root + for _, name := range path { + target = target.Command(name) + require.NotNil(t, target, p) + } + docs := commandDocs(target, path) + assert.NotContains(t, docs, "Required. Required.", p) + } +} diff --git a/cmd/lk/main.go b/cmd/lk/main.go index 3a8644b1..8e7de1ff 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -94,6 +94,16 @@ Docs: https://docs.livekit.io/intro/basics/cli/`, Before: initLogger, } + // urfave's help doesn't mark required flags; mark them here, as + // generate-docs does, rather than in each flag's usage. + flagString := cli.FlagStringer + cli.FlagStringer = func(f cli.Flag) string { + s := flagString(f) + if r, ok := f.(cli.RequiredFlag); ok && r.IsRequired() { + s += " (required)" + } + return s + } cli.HelpPrinter = func(w io.Writer, templ string, data any) { // cli/v3 renders `lk agent --help` with its stock subcommand template // regardless of CustomHelpTemplate; swap in the grouped one here. diff --git a/cmd/lk/project.go b/cmd/lk/project.go index 70fd2803..e6e2e1d1 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -43,21 +43,20 @@ var ( { Name: "add", Usage: "Add a new project (for LiveKit Cloud projects, also see `lk cloud auth`)", - UsageText: "lk project add PROJECT_NAME --url URL --api-key KEY --api-secret SECRET [--default]", - ArgsUsage: "PROJECT_NAME", + ArgsUsage: "[PROJECT_NAME]", Action: addProject, Flags: []cli.Flag{ &cli.StringFlag{ Name: "url", - Usage: "WebSocket `URL` of the LiveKit server.", + Usage: "WebSocket `URL` of the LiveKit server. If unset, the CLI asks for it. Required in non-interactive mode.", }, &cli.StringFlag{ Name: "api-key", - Usage: "Project API `KEY`.", + Usage: "Project API `KEY`. If unset, the CLI asks for it. Required in non-interactive mode.", }, &cli.StringFlag{ Name: "api-secret", - Usage: "Project API `SECRET`.", + Usage: "Project API `SECRET`. If unset, the CLI asks for it. Required in non-interactive mode.", }, &cli.BoolFlag{ Name: "default", diff --git a/cmd/lk/utils.go b/cmd/lk/utils.go index 80f13ee4..0c07a570 100644 --- a/cmd/lk/utils.go +++ b/cmd/lk/utils.go @@ -106,13 +106,13 @@ var ( } installFlag = &cli.BoolFlag{ Name: "install", - Usage: "Install the project's dependencies after creating it. If unset, the CLI asks; with --yes or without a terminal, it skips installation. Pass --install=false to skip.", + Usage: "Install the project's dependencies after creating it. If unset, the CLI asks. Skipped in non-interactive mode. Pass --install=false to skip.", } // 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 Agent Skills and the Docs MCP server for your coding agents (see lk skills). If unset, the CLI asks; with --yes or without a terminal, it installs them. Pass --skills=false to skip.", + Usage: "Install LiveKit Agent Skills and the Docs MCP server for your coding agents (see lk skills). If unset, the CLI asks. Automatically installed in non-interactive mode. Pass --skills=false to skip.", } // roleFlag selects a member/invite access level for the Public API commands. roleFlag = &cli.StringFlag{ From 0e2cf66a3cb75c15c8ebd1ecda48cd6194a4df4c Mon Sep 17 00:00:00 2001 From: Kat Ahn Date: Fri, 16 Oct 2026 12:42:16 -0700 Subject: [PATCH 9/9] Add backticks for flags in docs output --- cmd/lk/generate_docs.go | 10 +++++++++- cmd/lk/generate_docs_test.go | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd/lk/generate_docs.go b/cmd/lk/generate_docs.go index 9dbfaecb..b13351c4 100644 --- a/cmd/lk/generate_docs.go +++ b/cmd/lk/generate_docs.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" "time" @@ -241,8 +242,15 @@ func unquoteUsage(usage string) (string, string) { return name, usage[:i] + name + usage[i+2+j:] } +// flagMention matches a flag named in help text, such as --scenarios or +// --install=false, that starts a word and isn't already in backticks. It stops +// before trailing punctuation, so "Requires --scenarios." keeps its period. +var flagMention = regexp.MustCompile(`(^|[\s(])(--[a-zA-Z0-9][a-zA-Z0-9-]*(?:=[a-zA-Z0-9_./:-]*[a-zA-Z0-9_])?)`) + +// sentence formats help text as a docs sentence: flag names it mentions are +// code, and it ends with a period. func sentence(s string) string { - s = strings.TrimSpace(s) + s = flagMention.ReplaceAllString(strings.TrimSpace(s), "$1`$2`") if s == "" || strings.HasSuffix(s, ".") || strings.HasSuffix(s, ")") && strings.HasSuffix(strings.TrimSuffix(s, ")"), ".") { return s } diff --git a/cmd/lk/generate_docs_test.go b/cmd/lk/generate_docs_test.go index 6d7fa1df..c92bdd14 100644 --- a/cmd/lk/generate_docs_test.go +++ b/cmd/lk/generate_docs_test.go @@ -119,6 +119,22 @@ func TestShortDuration(t *testing.T) { } } +func TestSentence(t *testing.T) { + for in, want := range map[string]string{ + "Log more": "Log more.", + "Log more.": "Log more.", + "Requires --scenarios": "Requires `--scenarios`.", + "Requires --samples of more than 1.": "Requires `--samples` of more than 1.", + "Pass --install=false to skip.": "Pass `--install=false` to skip.", + "Merged with --attributes, taking priority": "Merged with `--attributes`, taking priority.", + "Targets a room (with --room).": "Targets a room (with `--room`).", + "Already `--quoted`.": "Already `--quoted`.", + "Skipped in non-interactive mode.": "Skipped in non-interactive mode.", + } { + assert.Equal(t, want, sentence(in), in) + } +} + // Every command the real CLI lists must render without panicking, and with // each marker the generator adds appearing once. func TestCommandDocsRealTree(t *testing.T) {