CLI: improve command descriptions in help output (#18486)

* CLI: clarify config vs configure descriptions

* CLI: improve top-level command descriptions

* CLI: make direct command help more descriptive

* CLI: add commands hint to root help

* CLI: show root help hint in implicit help output

* CLI: add help example for command-specific help

* CLI: tweak root subcommand marker spacing

* CLI: mark clawbot as subcommand root in help

* CLI: derive subcommand markers from registry metadata

* CLI: escape help regex CLI name
This commit is contained in:
Benjamin Jesuiter
2026-02-16 22:06:25 +01:00
committed by GitHub
parent 05a83b9e97
commit b25f334fa2
15 changed files with 288 additions and 69 deletions

View File

@@ -15,8 +15,14 @@ export type CommandRegistration = {
register: (params: CommandRegisterParams) => void;
};
type CoreCliCommandDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
type CoreCliEntry = {
commands: Array<{ name: string; description: string }>;
commands: CoreCliCommandDescriptor[];
register: (params: CommandRegisterParams) => Promise<void> | void;
};
@@ -27,30 +33,59 @@ const shouldRegisterCorePrimaryOnly = (argv: string[]) => {
return true;
};
// Note for humans and agents:
// If you update the list of commands, also check whether they have subcommands
// and set the flag accordingly.
const coreEntries: CoreCliEntry[] = [
{
commands: [{ name: "setup", description: "Setup helpers" }],
commands: [
{
name: "setup",
description: "Initialize local config and agent workspace",
hasSubcommands: false,
},
],
register: async ({ program }) => {
const mod = await import("./register.setup.js");
mod.registerSetupCommand(program);
},
},
{
commands: [{ name: "onboard", description: "Onboarding helpers" }],
commands: [
{
name: "onboard",
description: "Interactive onboarding wizard for gateway, workspace, and skills",
hasSubcommands: false,
},
],
register: async ({ program }) => {
const mod = await import("./register.onboard.js");
mod.registerOnboardCommand(program);
},
},
{
commands: [{ name: "configure", description: "Configure wizard" }],
commands: [
{
name: "configure",
description:
"Interactive setup wizard for credentials, channels, gateway, and agent defaults",
hasSubcommands: false,
},
],
register: async ({ program }) => {
const mod = await import("./register.configure.js");
mod.registerConfigureCommand(program);
},
},
{
commands: [{ name: "config", description: "Config helpers" }],
commands: [
{
name: "config",
description:
"Non-interactive config helpers (get/set/unset). Default: starts setup wizard.",
hasSubcommands: true,
},
],
register: async ({ program }) => {
const mod = await import("../config-cli.js");
mod.registerConfigCli(program);
@@ -58,12 +93,25 @@ const coreEntries: CoreCliEntry[] = [
},
{
commands: [
{ name: "doctor", description: "Health checks + quick fixes for the gateway and channels" },
{ name: "dashboard", description: "Open the Control UI with your current token" },
{ name: "reset", description: "Reset local config/state (keeps the CLI installed)" },
{
name: "doctor",
description: "Health checks + quick fixes for the gateway and channels",
hasSubcommands: false,
},
{
name: "dashboard",
description: "Open the Control UI with your current token",
hasSubcommands: false,
},
{
name: "reset",
description: "Reset local config/state (keeps the CLI installed)",
hasSubcommands: false,
},
{
name: "uninstall",
description: "Uninstall the gateway service + local data (CLI remains)",
hasSubcommands: false,
},
],
register: async ({ program }) => {
@@ -72,14 +120,26 @@ const coreEntries: CoreCliEntry[] = [
},
},
{
commands: [{ name: "message", description: "Send, read, and manage messages" }],
commands: [
{
name: "message",
description: "Send, read, and manage messages",
hasSubcommands: true,
},
],
register: async ({ program, ctx }) => {
const mod = await import("./register.message.js");
mod.registerMessageCommands(program, ctx);
},
},
{
commands: [{ name: "memory", description: "Memory commands" }],
commands: [
{
name: "memory",
description: "Search and reindex memory files",
hasSubcommands: true,
},
],
register: async ({ program }) => {
const mod = await import("../memory-cli.js");
mod.registerMemoryCli(program);
@@ -87,19 +147,41 @@ const coreEntries: CoreCliEntry[] = [
},
{
commands: [
{ name: "agent", description: "Agent commands" },
{ name: "agents", description: "Manage isolated agents" },
{
name: "agent",
description: "Run one agent turn via the Gateway",
hasSubcommands: false,
},
{
name: "agents",
description: "Manage isolated agents (workspaces, auth, routing)",
hasSubcommands: true,
},
],
register: async ({ program, ctx }) => {
const mod = await import("./register.agent.js");
mod.registerAgentCommands(program, { agentChannelOptions: ctx.agentChannelOptions });
mod.registerAgentCommands(program, {
agentChannelOptions: ctx.agentChannelOptions,
});
},
},
{
commands: [
{ name: "status", description: "Gateway status" },
{ name: "health", description: "Gateway health" },
{ name: "sessions", description: "Session management" },
{
name: "status",
description: "Show channel health and recent session recipients",
hasSubcommands: false,
},
{
name: "health",
description: "Fetch health from the running gateway",
hasSubcommands: false,
},
{
name: "sessions",
description: "List stored conversation sessions",
hasSubcommands: false,
},
],
register: async ({ program }) => {
const mod = await import("./register.status-health-sessions.js");
@@ -107,7 +189,13 @@ const coreEntries: CoreCliEntry[] = [
},
},
{
commands: [{ name: "browser", description: "Browser tools" }],
commands: [
{
name: "browser",
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
hasSubcommands: true,
},
],
register: async ({ program }) => {
const mod = await import("../browser-cli.js");
mod.registerBrowserCli(program);
@@ -115,21 +203,32 @@ const coreEntries: CoreCliEntry[] = [
},
];
export function getCoreCliCommandNames(): string[] {
function collectCoreCliCommandNames(predicate?: (command: CoreCliCommandDescriptor) => boolean) {
const seen = new Set<string>();
const names: string[] = [];
for (const entry of coreEntries) {
for (const cmd of entry.commands) {
if (seen.has(cmd.name)) {
for (const command of entry.commands) {
if (predicate && !predicate(command)) {
continue;
}
seen.add(cmd.name);
names.push(cmd.name);
if (seen.has(command.name)) {
continue;
}
seen.add(command.name);
names.push(command.name);
}
}
return names;
}
export function getCoreCliCommandNames(): string[] {
return collectCoreCliCommandNames();
}
export function getCoreCliCommandsWithSubcommands(): string[] {
return collectCoreCliCommandNames((command) => command.hasSubcommands);
}
function removeCommand(program: Command, command: Command) {
const commands = program.commands as Command[];
const index = commands.indexOf(command);
@@ -142,7 +241,7 @@ function registerLazyCoreCommand(
program: Command,
ctx: ProgramContext,
entry: CoreCliEntry,
command: { name: string; description: string },
command: CoreCliCommandDescriptor,
) {
const placeholder = program.command(command.name).description(command.description);
placeholder.allowUnknownOption(true);

View File

@@ -2,12 +2,23 @@ import type { Command } from "commander";
import type { ProgramContext } from "./context.js";
import { formatDocsLink } from "../../terminal/links.js";
import { isRich, theme } from "../../terminal/theme.js";
import { escapeRegExp } from "../../utils.js";
import { formatCliBannerLine, hasEmittedCliBanner } from "../banner.js";
import { replaceCliName, resolveCliName } from "../cli-name.js";
import { getCoreCliCommandsWithSubcommands } from "./command-registry.js";
import { getSubCliCommandsWithSubcommands } from "./register.subclis.js";
const CLI_NAME = resolveCliName();
const CLI_NAME_PATTERN = escapeRegExp(CLI_NAME);
const ROOT_COMMANDS_WITH_SUBCOMMANDS = new Set([
...getCoreCliCommandsWithSubcommands(),
...getSubCliCommandsWithSubcommands(),
]);
const ROOT_COMMANDS_HINT =
"Hint: commands suffixed with * have subcommands. Run <command> --help for details.";
const EXAMPLES = [
["openclaw models --help", "Show detailed help for the models command."],
[
"openclaw channels login --verbose",
"Link personal WhatsApp Web and show QR + connection logs.",
@@ -51,18 +62,36 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
sortSubcommands: true,
sortOptions: true,
optionTerm: (option) => theme.option(option.flags),
subcommandTerm: (cmd) => theme.command(cmd.name()),
subcommandTerm: (cmd) => {
const isRootCommand = cmd.parent === program;
const hasSubcommands = isRootCommand && ROOT_COMMANDS_WITH_SUBCOMMANDS.has(cmd.name());
return theme.command(hasSubcommands ? `${cmd.name()} *` : cmd.name());
},
});
const formatHelpOutput = (str: string) => {
let output = str;
const isRootHelp = new RegExp(
`^Usage:\\s+${CLI_NAME_PATTERN}\\s+\\[options\\]\\s+\\[command\\]\\s*$`,
"m",
).test(output);
if (isRootHelp && /^Commands:/m.test(output)) {
output = output.replace(/^Commands:/m, `Commands:\n ${theme.muted(ROOT_COMMANDS_HINT)}`);
}
return output
.replace(/^Usage:/gm, theme.heading("Usage:"))
.replace(/^Options:/gm, theme.heading("Options:"))
.replace(/^Commands:/gm, theme.heading("Commands:"));
};
program.configureOutput({
writeOut: (str) => {
const colored = str
.replace(/^Usage:/gm, theme.heading("Usage:"))
.replace(/^Options:/gm, theme.heading("Options:"))
.replace(/^Commands:/gm, theme.heading("Commands:"));
process.stdout.write(colored);
process.stdout.write(formatHelpOutput(str));
},
writeErr: (str) => {
process.stderr.write(formatHelpOutput(str));
},
writeErr: (str) => process.stderr.write(str),
outputError: (str, write) => write(theme.error(str)),
});

View File

@@ -11,7 +11,7 @@ import { runCommandWithRuntime } from "../cli-utils.js";
export function registerConfigureCommand(program: Command) {
program
.command("configure")
.description("Interactive prompt to set up credentials, devices, and agent defaults")
.description("Interactive setup wizard for credentials, channels, gateway, and agent defaults")
.addHelpText(
"after",
() =>

View File

@@ -24,7 +24,7 @@ import { registerMessageThreadCommands } from "./message/register.thread.js";
export function registerMessageCommands(program: Command, ctx: ProgramContext) {
const message = program
.command("message")
.description("Send messages and channel actions")
.description("Send, read, and manage messages and channel actions")
.addHelpText(
"after",
() =>

View File

@@ -9,6 +9,7 @@ type SubCliRegistrar = (program: Command) => Promise<void> | void;
type SubCliEntry = {
name: string;
description: string;
hasSubcommands: boolean;
register: SubCliRegistrar;
};
@@ -31,10 +32,14 @@ const loadConfig = async (): Promise<OpenClawConfig> => {
return mod.loadConfig();
};
// Note for humans and agents:
// If you update the list of commands, also check whether they have subcommands
// and set the flag accordingly.
const entries: SubCliEntry[] = [
{
name: "acp",
description: "Agent Control Protocol tools",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../acp-cli.js");
mod.registerAcpCli(program);
@@ -42,7 +47,8 @@ const entries: SubCliEntry[] = [
},
{
name: "gateway",
description: "Gateway control",
description: "Run, inspect, and query the WebSocket Gateway",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../gateway-cli.js");
mod.registerGatewayCli(program);
@@ -51,6 +57,7 @@ const entries: SubCliEntry[] = [
{
name: "daemon",
description: "Gateway service (legacy alias)",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../daemon-cli.js");
mod.registerDaemonCli(program);
@@ -58,7 +65,8 @@ const entries: SubCliEntry[] = [
},
{
name: "logs",
description: "Gateway logs",
description: "Tail gateway file logs via RPC",
hasSubcommands: false,
register: async (program) => {
const mod = await import("../logs-cli.js");
mod.registerLogsCli(program);
@@ -67,6 +75,7 @@ const entries: SubCliEntry[] = [
{
name: "system",
description: "System events, heartbeat, and presence",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../system-cli.js");
mod.registerSystemCli(program);
@@ -74,7 +83,8 @@ const entries: SubCliEntry[] = [
},
{
name: "models",
description: "Model configuration",
description: "Discover, scan, and configure models",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../models-cli.js");
mod.registerModelsCli(program);
@@ -82,7 +92,8 @@ const entries: SubCliEntry[] = [
},
{
name: "approvals",
description: "Exec approvals",
description: "Manage exec approvals (gateway or node host)",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../exec-approvals-cli.js");
mod.registerExecApprovalsCli(program);
@@ -90,7 +101,8 @@ const entries: SubCliEntry[] = [
},
{
name: "nodes",
description: "Node commands",
description: "Manage gateway-owned node pairing and node commands",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../nodes-cli.js");
mod.registerNodesCli(program);
@@ -99,6 +111,7 @@ const entries: SubCliEntry[] = [
{
name: "devices",
description: "Device pairing + token management",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../devices-cli.js");
mod.registerDevicesCli(program);
@@ -106,7 +119,8 @@ const entries: SubCliEntry[] = [
},
{
name: "node",
description: "Node control",
description: "Run and manage the headless node host service",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../node-cli.js");
mod.registerNodeCli(program);
@@ -114,7 +128,8 @@ const entries: SubCliEntry[] = [
},
{
name: "sandbox",
description: "Sandbox tools",
description: "Manage sandbox containers for agent isolation",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../sandbox-cli.js");
mod.registerSandboxCli(program);
@@ -122,7 +137,8 @@ const entries: SubCliEntry[] = [
},
{
name: "tui",
description: "Terminal UI",
description: "Open a terminal UI connected to the Gateway",
hasSubcommands: false,
register: async (program) => {
const mod = await import("../tui-cli.js");
mod.registerTuiCli(program);
@@ -130,7 +146,8 @@ const entries: SubCliEntry[] = [
},
{
name: "cron",
description: "Cron scheduler",
description: "Manage cron jobs via the Gateway scheduler",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../cron-cli.js");
mod.registerCronCli(program);
@@ -138,7 +155,8 @@ const entries: SubCliEntry[] = [
},
{
name: "dns",
description: "DNS helpers",
description: "DNS helpers for wide-area discovery (Tailscale + CoreDNS)",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../dns-cli.js");
mod.registerDnsCli(program);
@@ -146,7 +164,8 @@ const entries: SubCliEntry[] = [
},
{
name: "docs",
description: "Docs helpers",
description: "Search the live OpenClaw docs",
hasSubcommands: false,
register: async (program) => {
const mod = await import("../docs-cli.js");
mod.registerDocsCli(program);
@@ -154,7 +173,8 @@ const entries: SubCliEntry[] = [
},
{
name: "hooks",
description: "Hooks tooling",
description: "Manage internal agent hooks",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../hooks-cli.js");
mod.registerHooksCli(program);
@@ -162,7 +182,8 @@ const entries: SubCliEntry[] = [
},
{
name: "webhooks",
description: "Webhook helpers",
description: "Webhook helpers and integrations",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../webhooks-cli.js");
mod.registerWebhooksCli(program);
@@ -171,6 +192,7 @@ const entries: SubCliEntry[] = [
{
name: "qr",
description: "Generate iOS pairing QR/setup code",
hasSubcommands: false,
register: async (program) => {
const mod = await import("../qr-cli.js");
mod.registerQrCli(program);
@@ -179,6 +201,7 @@ const entries: SubCliEntry[] = [
{
name: "clawbot",
description: "Legacy clawbot command aliases",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../clawbot-cli.js");
mod.registerClawbotCli(program);
@@ -186,7 +209,8 @@ const entries: SubCliEntry[] = [
},
{
name: "pairing",
description: "Pairing helpers",
description: "Secure DM pairing (approve inbound requests)",
hasSubcommands: true,
register: async (program) => {
// Initialize plugins before registering pairing CLI.
// The pairing CLI calls listPairingChannels() at registration time,
@@ -199,7 +223,8 @@ const entries: SubCliEntry[] = [
},
{
name: "plugins",
description: "Plugin management",
description: "Manage OpenClaw plugins and extensions",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../plugins-cli.js");
mod.registerPluginsCli(program);
@@ -209,7 +234,8 @@ const entries: SubCliEntry[] = [
},
{
name: "channels",
description: "Channel management",
description: "Manage connected chat channels (Telegram, Discord, etc.)",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../channels-cli.js");
mod.registerChannelsCli(program);
@@ -217,7 +243,8 @@ const entries: SubCliEntry[] = [
},
{
name: "directory",
description: "Directory commands",
description: "Lookup contact and group IDs (self, peers, groups) for supported chat channels",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../directory-cli.js");
mod.registerDirectoryCli(program);
@@ -225,7 +252,8 @@ const entries: SubCliEntry[] = [
},
{
name: "security",
description: "Security helpers",
description: "Security tools and local config audits",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../security-cli.js");
mod.registerSecurityCli(program);
@@ -233,7 +261,8 @@ const entries: SubCliEntry[] = [
},
{
name: "skills",
description: "Skills management",
description: "List and inspect available skills",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../skills-cli.js");
mod.registerSkillsCli(program);
@@ -241,7 +270,8 @@ const entries: SubCliEntry[] = [
},
{
name: "update",
description: "CLI update helpers",
description: "Update OpenClaw and inspect update channel status",
hasSubcommands: true,
register: async (program) => {
const mod = await import("../update-cli.js");
mod.registerUpdateCli(program);
@@ -250,6 +280,7 @@ const entries: SubCliEntry[] = [
{
name: "completion",
description: "Generate shell completion script",
hasSubcommands: false,
register: async (program) => {
const mod = await import("../completion-cli.js");
mod.registerCompletionCli(program);
@@ -261,6 +292,10 @@ export function getSubCliEntries(): SubCliEntry[] {
return entries;
}
export function getSubCliCommandsWithSubcommands(): string[] {
return entries.filter((entry) => entry.hasSubcommands).map((entry) => entry.name);
}
function removeCommand(program: Command, command: Command) {
const commands = program.commands as Command[];
const index = commands.indexOf(command);