diff --git a/.github/workflows/notify-docs.yaml b/.github/workflows/notify-docs.yaml new file mode 100644 index 00000000..e2a963f4 --- /dev/null +++ b/.github/workflows/notify-docs.yaml @@ -0,0 +1,41 @@ +# 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. `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: [released] + +# The dispatch authenticates with SYNC_DISPATCH_TOKEN; GITHUB_TOKEN isn't used. +permissions: {} + +jobs: + dispatch: + 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" diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index 923170dd..e7011eb2 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, uses the ID in %s; without that file, the CLI prompts you to choose an agent.", 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, } @@ -147,31 +147,41 @@ 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{ 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 +194,62 @@ 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 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. Required in non-interactive mode.", + } + deploymentFlag = &cli.StringFlag{ Name: "deployment", - Usage: "Agent deployment", + Usage: "`NAME` of the deployment. Defaults to production.", Required: false, 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 non-production 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.", + 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{ @@ -244,7 +284,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. 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) @@ -254,7 +294,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. Implies --install.", Value: false, }, templateFlag, @@ -270,7 +310,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", }}, }}, Flags: []cli.Flag{ - regionFlag, + initRegionFlag, installFlag, skillsSetupFlag, }, @@ -288,7 +328,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", secretsMountFlag, ignoreEmptySecretsFlag, regionFlag, - deploymentFlag, + ignoredDeploymentFlag, skipSDKCheckFlag, agentPrebuiltImageFlag, agentPrebuiltImageTarFlag, @@ -309,7 +349,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, }, @@ -339,7 +379,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", secretsFlag, secretsFileFlag, secretsMountFlag, - regionFlag, + ignoredRegionFlag, deploymentFlag, ignoreEmptySecretsFlag, skipSDKCheckFlag, @@ -358,7 +398,7 @@ On LiveKit Cloud: "create" and "deploy" ship it, then "status", "logs", Action: promoteAgent, Flags: []cli.Flag{ idFlag(false), - deploymentFlag, + promoteDeploymentFlag, }, ArgsUsage: "[working-dir]", }, @@ -407,8 +447,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.", - Value: "latest", + Usage: "`VERSION` to roll back to, as listed by lk agent versions.", Required: true, }, idFlag(false), @@ -436,7 +475,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]", }, @@ -447,8 +486,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]", @@ -487,7 +526,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: "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 6e74e45b..50c588d3 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: 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. 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, the Azure Private Link Service alias, or the Azure Resource ID.", 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: 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: true, }, jsonFlag, diff --git a/cmd/lk/agent_run.go b/cmd/lk/agent_run.go index 16376c3d..2c743cc9 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, plus CRITICAL for Python and FATAL for Node.js.", }, } @@ -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 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/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..f73b07ad 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: 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.", } 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, running its full audio pipeline. Uses LiveKit Inference TTS on your connected LiveKit Cloud project.", } 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. @@ -152,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 @@ -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. @@ -178,13 +179,13 @@ 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, }, { 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 @@ -204,16 +205,17 @@ 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", - Value: defaultSayTimeout, - Usage: "Give up waiting for the agent's reply after `DURATION`, such as 2m or 90s", + Name: "timeout", + Value: defaultSayTimeout, + 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{ 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. Useful for finding a tool's traceback.", }, }, Action: runSessionSay, @@ -225,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, }, { @@ -243,9 +245,9 @@ emits one JSON object per line (NDJSON), suitable for piping into jq or 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"}, + 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."}, }, Action: runSessionEvents, }, @@ -257,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, }, { @@ -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, }, @@ -279,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, }, { @@ -291,15 +293,18 @@ 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, - sessionMetricsFlag, + jsonFlagWithUsage("Print the summary, conversation, and logs in one JSON document."), + &cli.BoolFlag{ + Name: "metrics", + Usage: "Show latency metrics for each turn of the conversation. Requires --chat-history.", + }, &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/generate_docs.go b/cmd/lk/generate_docs.go new file mode 100644 index 00000000..b13351c4 --- /dev/null +++ b/cmd/lk/generate_docs.go @@ -0,0 +1,258 @@ +// 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" + "errors" + "fmt" + "regexp" + "strings" + "time" + + "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, " ")) + } + } + if cmd.Bool("list") { + 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 +} + +// 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. +// 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(path) > 0 && (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 == "" { + 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)) + + 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`". + // Aliases are joined into one label: "Options for `logs`/`tail`". + parent := strings.Join(path[:len(path)-1], " ") + if len(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", strings.Join(labels, "/")) + for _, f := range flags { + 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() +} + +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 || inheritsOptions(cmd) { + usage += " [options]" + } + if cmd.ArgsUsage != "" { + usage += " " + cmd.ArgsUsage + } + 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 { + 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 r, ok := f.(cli.RequiredFlag); ok && r.IsRequired() { + line += " Required." + } + if def := defaultText(doc); def != "" { + 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" +} + +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 "" + default: + return v + } +} + +// 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 "-" + } + 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:] +} + +// 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 = flagMention.ReplaceAllString(strings.TrimSpace(s), "$1`$2`") + if s == "" || strings.HasSuffix(s, ".") || strings.HasSuffix(s, ")") && strings.HasSuffix(strings.TrimSuffix(s, ")"), ".") { + return s + } + return s + "." +} diff --git a/cmd/lk/generate_docs_test.go b/cmd/lk/generate_docs_test.go new file mode 100644 index 00000000..c92bdd14 --- /dev/null +++ b/cmd/lk/generate_docs_test.go @@ -0,0 +1,163 @@ +// 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()) + } +} + +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) { + 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 aed56094..e176fa0b 100644 --- a/cmd/lk/main.go +++ b/cmd/lk/main.go @@ -73,6 +73,19 @@ 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, + 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, After: func(context.Context, *cli.Command) error { @@ -82,6 +95,16 @@ Docs: https://docs.livekit.io/intro/basics/cli/`, } categorizeAgentCommands() + // 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 fbad331a..681aa62f 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -43,34 +43,33 @@ var ( { Name: "add", Usage: "Add a new project (for LiveKit Cloud projects, also see `lk cloud auth`)", - UsageText: "lk project add PROJECT_NAME", - ArgsUsage: "PROJECT_NAME", + 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. If unset, the CLI asks for it. Required in non-interactive mode.", }, &cli.StringFlag{ Name: "api-key", - Usage: "Project `KEY`", + Usage: "Project API `KEY`. If unset, the CLI asks for it. Required in non-interactive mode.", }, &cli.StringFlag{ Name: "api-secret", - Usage: "Project `SECRET`", + Usage: "Project API `SECRET`. If unset, the CLI asks for it. Required in non-interactive mode.", }, &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}, + Flags: []cli.Flag{jsonFlagWithUsage("Output as JSON, including each project's API key and secret.")}, }, { Name: "get", @@ -100,7 +99,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..e8d512cb 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 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 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 of more than 1.", }, &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,8 +139,9 @@ 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.", + Metadata: map[string]any{docsInheritsOptions: true}, ArgsUsage: "[entrypoint]", HideHelpCommand: true, Before: loadSimulateProject, @@ -154,8 +155,9 @@ 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.", + Metadata: map[string]any{docsInheritsOptions: true}, ArgsUsage: "[entrypoint]", HideHelpCommand: true, Before: loadSimulateProject, @@ -182,8 +184,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 +193,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 have finished: completed, failed, or cancelled", + ArgsUsage: "RUN_ID", HideHelpCommand: true, Before: loadSimulateProject, Action: func(ctx context.Context, cmd *cli.Command) error { @@ -220,8 +222,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..0db8906c 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 @@ -92,12 +95,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 +111,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. 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 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. 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{