diff --git a/src/cli/channel-auth.test.ts b/src/cli/channel-auth.test.ts index 59fdc05bd60..1c7d0c30242 100644 --- a/src/cli/channel-auth.test.ts +++ b/src/cli/channel-auth.test.ts @@ -353,7 +353,7 @@ describe("channel-auth", () => { }); await expect(runChannelLogin({ channel: "whatsapp" }, runtime)).rejects.toThrow( - 'Channel "whatsapp" does not support login.', + 'Channel "whatsapp" does not support login. Run `openclaw channels status --channel whatsapp` to inspect supported actions.', ); }); @@ -564,7 +564,7 @@ describe("channel-auth", () => { }); await expect(runChannelLogout({ channel: "whatsapp" }, runtime)).rejects.toThrow( - 'Channel "whatsapp" does not support logout.', + 'Channel "whatsapp" does not support logout. Run `openclaw channels status --channel whatsapp` to inspect supported actions.', ); }); }); diff --git a/src/cli/channel-auth.ts b/src/cli/channel-auth.ts index 2ba2d92f41c..39403bac70f 100644 --- a/src/cli/channel-auth.ts +++ b/src/cli/channel-auth.ts @@ -16,6 +16,7 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; import { sanitizeForLog } from "../terminal/ansi.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { formatCliCommand } from "./command-format.js"; +import { formatUnsupportedChannelActionMessage } from "./error-format.js"; import { commitConfigWithPendingPluginInstalls } from "./plugins-install-record-commit.js"; type ChannelAuthOptions = { @@ -118,7 +119,11 @@ async function resolveChannelPluginForMode( const plugin = resolved.plugin; if (!plugin || !supportsChannelAuthMode(plugin, mode)) { throw new Error( - `Channel "${channelId}" does not support ${mode}. Run ${formatCliCommand("openclaw channels status --channel " + channelId)} for its supported actions.`, + formatUnsupportedChannelActionMessage({ + channel: channelId, + action: mode, + inspectCommand: "openclaw channels status --channel " + channelId, + }), ); } return { @@ -227,7 +232,13 @@ export async function runChannelLogin( } const login = plugin.auth?.login; if (!login) { - throw new Error(`Channel "${channelInput}" does not support login.`); + throw new Error( + formatUnsupportedChannelActionMessage({ + channel: channelInput, + action: "login", + inspectCommand: "openclaw channels status --channel " + channelInput, + }), + ); } // Auth-only flow: do not mutate channel config here. setVerbose(Boolean(opts.verbose)); @@ -270,7 +281,13 @@ export async function runChannelLogout( } const logoutAccount = plugin.gateway?.logoutAccount; if (!logoutAccount) { - throw new Error(`Channel "${channelInput}" does not support logout.`); + throw new Error( + formatUnsupportedChannelActionMessage({ + channel: channelInput, + action: "logout", + inspectCommand: "openclaw channels status --channel " + channelInput, + }), + ); } // Prefer the live gateway so logout also stops any active channel runtime. const { accountId } = resolveAccountContext(plugin, opts, cfg); diff --git a/src/cli/daemon-cli/install.ts b/src/cli/daemon-cli/install.ts index 64cb8d52de3..8fb424b322c 100644 --- a/src/cli/daemon-cli/install.ts +++ b/src/cli/daemon-cli/install.ts @@ -23,6 +23,7 @@ import { import { defaultRuntime } from "../../runtime.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { formatCliCommand } from "../command-format.js"; +import { formatInvalidConfigPort, formatInvalidPortOption } from "../error-format.js"; import { buildDaemonServiceSnapshot, installDaemonServiceAndEmit } from "./response.js"; import { createDaemonInstallActionContext, @@ -94,12 +95,12 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { const cfg = configSnapshot.valid ? configSnapshot.sourceConfig : configSnapshot.config; const portOverride = parsePort(opts.port); if (opts.port !== undefined && portOverride === null) { - fail("Invalid --port. Use a port number from 1 to 65535, for example 18789."); + fail(formatInvalidPortOption("--port")); return; } const port = portOverride ?? resolveGatewayPort(cfg); if (!Number.isFinite(port) || port <= 0 || port > 65_535) { - fail("Invalid Gateway port in config. Set gateway.port to a number from 1 to 65535."); + fail(formatInvalidConfigPort("gateway.port")); return; } const runtimeRaw = opts.runtime ? opts.runtime : DEFAULT_GATEWAY_DAEMON_RUNTIME; diff --git a/src/cli/error-format.ts b/src/cli/error-format.ts new file mode 100644 index 00000000000..2b784dc46dc --- /dev/null +++ b/src/cli/error-format.ts @@ -0,0 +1,71 @@ +import { formatCliCommand } from "./command-format.js"; + +const DEFAULT_GATEWAY_PORT_EXAMPLE = 18789; + +export function formatPortRangeHint(example = DEFAULT_GATEWAY_PORT_EXAMPLE): string { + return `Use a port number from 1 to 65535, for example ${example}.`; +} + +export function formatInvalidPortOption( + option: string, + example = DEFAULT_GATEWAY_PORT_EXAMPLE, +): string { + return `Invalid ${option}. ${formatPortRangeHint(example)}`; +} + +export function formatInvalidConfigPort( + path: string, + example = DEFAULT_GATEWAY_PORT_EXAMPLE, +): string { + return `Invalid ${path} in config. Set ${path} to a number from 1 to 65535, or pass --port ${example}.`; +} + +export function formatUnknownChannelMessage(params: { + channel: string; + listCommand?: string; + purpose?: string; +}): string { + const purpose = params.purpose ? ` for ${params.purpose}` : ""; + const listCommand = params.listCommand ?? "openclaw channels list --all"; + return `Unknown channel "${params.channel}"${purpose}. Run ${formatCliCommand( + listCommand, + )} to see configured and installable channels.`; +} + +export function formatUnsupportedChannelActionMessage(params: { + channel: string; + action: string; + inspectCommand?: string; +}): string { + const inspectCommand = + params.inspectCommand ?? `openclaw channels capabilities --channel ${params.channel}`; + return `Channel "${params.channel}" does not support ${params.action}. Run ${formatCliCommand( + inspectCommand, + )} to inspect supported actions.`; +} + +export function formatLookupMiss(params: { + noun: string; + value: string; + listCommand: string; + valueLabel?: string; +}): string { + const valueLabel = params.valueLabel ?? params.noun.toLowerCase(); + return `${params.noun} not found: ${params.value}. Run ${formatCliCommand( + params.listCommand, + )} to see recent ${valueLabel}s.`; +} + +export function formatMissingPluginMessage(params: { + id: string; + listCommand?: string; + includeSearch?: boolean; +}): string { + const listCommand = params.listCommand ?? "openclaw plugins list"; + const searchHint = params.includeSearch + ? `, or ${formatCliCommand("openclaw plugins search " + params.id)} to look for installable plugins` + : ""; + return `Plugin not found: ${params.id}. Run ${formatCliCommand( + listCommand, + )} to see installed plugins${searchHint}.`; +} diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index 6690eb51943..c7e623e09dd 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -34,6 +34,7 @@ import { } from "../../shared/string-coerce.js"; import { formatCliCommand } from "../command-format.js"; import { inheritOptionFromParent } from "../command-options.js"; +import { formatInvalidConfigPort, formatInvalidPortOption } from "../error-format.js"; import { withProgress } from "../progress.js"; import { parsePort } from "../shared/parse-port.js"; import { installQaParentWatchdog } from "./qa-parent-watchdog.js"; @@ -520,15 +521,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) { }); const portOverride = parsePort(opts.port); if (opts.port !== undefined && portOverride === null) { - defaultRuntime.error("Invalid --port. Use a positive port number, for example 3000."); + defaultRuntime.error(formatInvalidPortOption("--port")); defaultRuntime.exit(1); + return; } const port = portOverride ?? resolveGatewayPort(cfg); - if (!Number.isFinite(port) || port <= 0) { - defaultRuntime.error( - `Gateway port is invalid in config. Set it with ${formatCliCommand("openclaw config set gateway.port 3000")} or pass --port 3000.`, - ); + if (!Number.isFinite(port) || port <= 0 || port > 65_535) { + defaultRuntime.error(formatInvalidConfigPort("gateway.port")); defaultRuntime.exit(1); + return; } const { formatFutureConfigActionBlock, resolveFutureConfigActionBlock } = await import("../../config/future-version-guard.js"); diff --git a/src/cli/node-cli/daemon.ts b/src/cli/node-cli/daemon.ts index a964347de8b..493816d2cab 100644 --- a/src/cli/node-cli/daemon.ts +++ b/src/cli/node-cli/daemon.ts @@ -34,6 +34,7 @@ import { parsePort, resolveRuntimeStatusColor, } from "../daemon-cli/shared.js"; +import { formatInvalidConfigPort, formatInvalidPortOption } from "../error-format.js"; type NodeDaemonInstallOptions = { host?: string; @@ -95,7 +96,11 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) { const config = await loadNodeHostConfig(); const { host, port } = resolveNodeDefaults(opts, config); if (!Number.isFinite(port ?? Number.NaN) || (port ?? 0) <= 0 || (port ?? 0) > 65_535) { - fail("Invalid node host port. Use a port number from 1 to 65535."); + fail( + opts.port !== undefined + ? formatInvalidPortOption("--port") + : formatInvalidConfigPort("node.gateway.port"), + ); return; } diff --git a/src/cli/pairing-cli.ts b/src/cli/pairing-cli.ts index 20119eaa9ca..ca49fc926a9 100644 --- a/src/cli/pairing-cli.ts +++ b/src/cli/pairing-cli.ts @@ -46,7 +46,9 @@ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel if (/^[a-z][a-z0-9_-]{0,63}$/.test(value)) { return value as PairingChannel; } - throw new Error(`Invalid channel "${value}". Use lowercase letters, numbers, "_" or "-".`); + throw new Error( + `Invalid channel "${value}". Use lowercase letters, numbers, "_" or "-", for example "telegram".`, + ); } async function notifyApproved(channel: PairingChannel, id: string) { diff --git a/src/cli/plugins-cli.policy.test.ts b/src/cli/plugins-cli.policy.test.ts index 5701656dcca..d884a128953 100644 --- a/src/cli/plugins-cli.policy.test.ts +++ b/src/cli/plugins-cli.policy.test.ts @@ -189,7 +189,7 @@ describe("plugins cli policy mutations", () => { ); expect(runtimeErrors).toContain( - "Plugin not found: missing-plugin. Run `openclaw plugins list` to see installed plugins.", + "Plugin not found: missing-plugin. Run `openclaw plugins list` to see installed plugins, or `openclaw plugins search missing-plugin` to look for installable plugins.", ); expect(enablePluginInConfig).not.toHaveBeenCalled(); expect(writeConfigFile).not.toHaveBeenCalled(); diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 6918faafb96..d635e856caf 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -11,6 +11,7 @@ import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { shortenHomeInString } from "../utils.js"; +import { formatMissingPluginMessage } from "./error-format.js"; import type { PluginInspectOptions } from "./plugins-inspect-command.js"; import type { PluginsListOptions } from "./plugins-list-command.js"; import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; @@ -58,9 +59,7 @@ function formatRegistryState(state: "missing" | "fresh" | "stale"): string { } function reportMissingPlugin(id: string) { - defaultRuntime.error( - `Plugin not found: ${id}. Run \`openclaw plugins list\` to see installed plugins.`, - ); + defaultRuntime.error(formatMissingPluginMessage({ id, includeSearch: true })); return defaultRuntime.exit(1); } diff --git a/src/cli/plugins-inspect-command.ts b/src/cli/plugins-inspect-command.ts index e6233c2f76e..fffd0f3f75b 100644 --- a/src/cli/plugins-inspect-command.ts +++ b/src/cli/plugins-inspect-command.ts @@ -9,6 +9,7 @@ import { getTerminalTableWidth, renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; import { shortenHomeInString, shortenHomePath } from "../utils.js"; import { formatCliCommand } from "./command-format.js"; +import { formatMissingPluginMessage } from "./error-format.js"; import { quietPluginJsonLogger } from "./plugins-command-helpers.js"; export type PluginInspectOptions = { @@ -233,9 +234,7 @@ export async function runPluginsInspectCommand( ); const targetPlugin = snapshotReport.plugins.find((entry) => entry.id === id || entry.name === id); if (!targetPlugin) { - defaultRuntime.error( - `Plugin not found: ${id}. Run ${formatCliCommand("openclaw plugins list")} to see installed plugins.`, - ); + defaultRuntime.error(formatMissingPluginMessage({ id, includeSearch: true })); return defaultRuntime.exit(1); } const report = runtimeInspect @@ -258,7 +257,7 @@ export async function runPluginsInspectCommand( }); if (!inspect) { defaultRuntime.error( - `Plugin not found: ${id}. Run ${formatCliCommand("openclaw plugins list --json")} to inspect raw discovery state.`, + formatMissingPluginMessage({ id, listCommand: "openclaw plugins list --json" }), ); return defaultRuntime.exit(1); } diff --git a/src/commands/agents.bindings.ts b/src/commands/agents.bindings.ts index 283670c521d..af866f33716 100644 --- a/src/commands/agents.bindings.ts +++ b/src/commands/agents.bindings.ts @@ -3,6 +3,7 @@ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; import { getLoadedChannelPlugin } from "../channels/plugins/index.js"; import type { ChannelId } from "../channels/plugins/types.public.js"; import { normalizeChannelId as normalizeBundledChannelId } from "../channels/registry.js"; +import { formatUnknownChannelMessage } from "../cli/error-format.js"; import { isRouteBinding, listRouteBindings } from "../config/bindings.js"; import type { AgentRouteBinding } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -313,12 +314,14 @@ export function parseBindingSpecs(params: { const [channelRaw, accountRaw] = trimmed.split(":", 2); const channel = normalizeBindingChannelId(channelRaw, params.config); if (!channel) { - errors.push(`Unknown channel "${channelRaw}".`); + errors.push(formatUnknownChannelMessage({ channel: channelRaw })); continue; } let accountId: string | undefined = accountRaw?.trim(); if (accountRaw !== undefined && !accountId) { - errors.push(`Invalid binding "${trimmed}" (empty account id).`); + errors.push( + `Invalid binding "${trimmed}". Account id is empty. Use :, for example telegram:default.`, + ); continue; } accountId = resolveBindingAccountId({ diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 37565e31d60..9e87c280f77 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -6,6 +6,11 @@ import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/ import type { ChannelSetupPlugin } from "../../channels/plugins/setup-wizard-types.js"; import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.public.js"; +import { formatCliCommand } from "../../cli/command-format.js"; +import { + formatUnknownChannelMessage, + formatUnsupportedChannelActionMessage, +} from "../../cli/error-format.js"; import { commitConfigWithPendingPluginInstalls } from "../../cli/plugins-install-record-commit.js"; import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js"; import type { OpenClawConfig } from "../../config/config.js"; @@ -345,7 +350,7 @@ export async function channelsAddCommand( if (!channel) { const hint = catalogEntry ? `Plugin ${catalogEntry.meta.label} could not be loaded after install. Run openclaw doctor --fix, then retry openclaw channels add.` - : `Unknown channel: ${rawChannel}. Run openclaw channels list --all to see configured and installable channels.`; + : formatUnknownChannelMessage({ channel: rawChannel }); runtime.error(hint); runtime.exit(1); return; @@ -354,7 +359,10 @@ export async function channelsAddCommand( const plugin = await loadScopedPlugin(channel, catalogEntry?.pluginId); if (!plugin?.setup?.applyAccountConfig) { runtime.error( - `Channel ${channel} does not support non-interactive add. Run openclaw channels add with no flags for guided setup, or openclaw channels list --all to inspect available channels.`, + `${formatUnsupportedChannelActionMessage({ + channel, + action: "non-interactive add", + })} Run ${formatCliCommand("openclaw channels add")} with no flags for guided setup.`, ); runtime.exit(1); return; diff --git a/src/commands/channels/capabilities.ts b/src/commands/channels/capabilities.ts index 6ac3079664c..2249a3494d5 100644 --- a/src/commands/channels/capabilities.ts +++ b/src/commands/channels/capabilities.ts @@ -11,6 +11,7 @@ import type { ChannelPlugin, } from "../../channels/plugins/types.public.js"; import { formatCliCommand } from "../../cli/command-format.js"; +import { formatUnknownChannelMessage } from "../../cli/error-format.js"; import { commitConfigWithPendingPluginInstalls } from "../../cli/plugins-install-record-commit.js"; import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js"; import { @@ -296,11 +297,7 @@ export async function channelsCapabilitiesCommand( })(); if (!selected || selected.length === 0) { - runtime.error( - danger( - `Unknown channel "${rawChannel}". Run ${formatCliCommand("openclaw channels list")} to see installed channels.`, - ), - ); + runtime.error(danger(formatUnknownChannelMessage({ channel: rawChannel }))); runtime.exit(1); return; } diff --git a/src/commands/channels/remove.ts b/src/commands/channels/remove.ts index 2803ab9fa17..bd43ad25d40 100644 --- a/src/commands/channels/remove.ts +++ b/src/commands/channels/remove.ts @@ -3,6 +3,10 @@ import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/ind import { listReadOnlyChannelPluginsForConfig } from "../../channels/plugins/read-only.js"; import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; import { formatCliCommand } from "../../cli/command-format.js"; +import { + formatUnknownChannelMessage, + formatUnsupportedChannelActionMessage, +} from "../../cli/error-format.js"; import { commitConfigWithPendingPluginInstalls } from "../../cli/plugins-install-record-commit.js"; import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js"; import { replaceConfigFile, type OpenClawConfig } from "../../config/config.js"; @@ -159,9 +163,7 @@ export async function channelsRemoveCommand( } const resolvedChannel = resolvedPluginState?.channelId ?? channel; if (!resolvedChannel) { - runtime.error( - `Unknown channel "${rawChannel}". Run ${formatCliCommand("openclaw channels list")} to see supported channels.`, - ); + runtime.error(formatUnknownChannelMessage({ channel: rawChannel })); runtime.exit(1); return; } @@ -175,7 +177,7 @@ export async function channelsRemoveCommand( runtime.exit(1); return; } - runtime.error(`Unknown channel "${resolvedChannel}".`); + runtime.error(formatUnknownChannelMessage({ channel: resolvedChannel })); runtime.exit(1); return; } @@ -197,7 +199,7 @@ export async function channelsRemoveCommand( if (deleteConfig) { if (!plugin.config.deleteAccount) { runtime.error( - `Channel "${channel}" does not support delete. Use disable/remove without --delete.`, + `${formatUnsupportedChannelActionMessage({ channel, action: "delete" })} Use ${formatCliCommand("openclaw channels remove --channel " + channel)} to disable it without deleting config.`, ); runtime.exit(1); return; @@ -214,7 +216,7 @@ export async function channelsRemoveCommand( } else { if (!plugin.config.setAccountEnabled) { runtime.error( - `Channel "${channel}" does not support disable. Use --delete only if you want to remove config.`, + `${formatUnsupportedChannelActionMessage({ channel, action: "disable" })} Use ${formatCliCommand("openclaw channels remove --channel " + channel + " --delete")} only if you want to remove config.`, ); runtime.exit(1); return; diff --git a/src/commands/channels/resolve.ts b/src/commands/channels/resolve.ts index f3adc357d87..69cf1fe5b3b 100644 --- a/src/commands/channels/resolve.ts +++ b/src/commands/channels/resolve.ts @@ -6,6 +6,7 @@ import type { import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js"; import { formatCliCommand } from "../../cli/command-format.js"; import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js"; +import { formatUnsupportedChannelActionMessage } from "../../cli/error-format.js"; import { commitConfigWithPendingPluginInstalls } from "../../cli/plugins-install-record-commit.js"; import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js"; import { @@ -197,7 +198,10 @@ export async function channelsResolveCommand(opts: ChannelsResolveOptions, runti if (!plugin?.resolver?.resolveTargets) { const channelText = selection.channel ?? explicitChannel ?? ""; throw new Error( - `Channel ${channelText} does not support resolve. Run ${formatCliCommand("openclaw channels capabilities --channel " + channelText)} to inspect supported actions.`, + formatUnsupportedChannelActionMessage({ + channel: channelText, + action: "resolve", + }), ); } const preferredKind = resolvePreferredKind(opts.kind); diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts index 3c5c78780a0..6c14cf6fce6 100644 --- a/src/commands/configure.gateway.ts +++ b/src/commands/configure.gateway.ts @@ -1,3 +1,4 @@ +import { formatPortRangeHint } from "../cli/error-format.js"; import { resolveGatewayPort } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isValidEnvSecretRefId, type SecretInput } from "../config/types.secrets.js"; @@ -29,7 +30,7 @@ type GatewayTokenInputMode = "plaintext" | "ref"; function validateGatewayPortInput(value: unknown): string | undefined { const port = Number(typeof value === "string" ? value.trim() : value); if (!Number.isInteger(port) || port < 1 || port > 65_535) { - return "Use a port number from 1 to 65535, for example 18789."; + return formatPortRangeHint(); } return undefined; } diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 857263b3473..b5ac3bca261 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -3,6 +3,7 @@ import nodePath from "node:path"; import { isDeepStrictEqual } from "node:util"; import { describeCodexNativeWebSearch } from "../agents/codex-native-web-search.shared.js"; import { formatCliCommand } from "../cli/command-format.js"; +import { formatPortRangeHint } from "../cli/error-format.js"; import { commitConfigWithPendingPluginInstalls } from "../cli/plugins-install-record-commit.js"; import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; @@ -64,7 +65,7 @@ const setupPluginConfigModuleLoader = createLazyImportLoader 65_535) { - return "Use a port number from 1 to 65535, for example 18789."; + return formatPortRangeHint(); } return undefined; } diff --git a/src/commands/message.ts b/src/commands/message.ts index b63e4a8571a..0d6b98f3941 100644 --- a/src/commands/message.ts +++ b/src/commands/message.ts @@ -2,6 +2,7 @@ import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { CHANNEL_MESSAGE_ACTION_NAMES } from "../channels/plugins/message-action-names.js"; import type { ChannelMessageActionName } from "../channels/plugins/types.public.js"; import { resolveCommandConfigWithSecrets } from "../cli/command-config-resolution.js"; +import { formatCliCommand } from "../cli/command-format.js"; import { getScopedChannelsCommandSecretTargets } from "../cli/command-secret-targets.js"; import { resolveMessageSecretScope } from "../cli/message-secret-scope.js"; import { createOutboundSendDeps, type CliDeps } from "../cli/outbound-send-deps.js"; @@ -58,7 +59,11 @@ export async function messageCommand( (name) => normalizeLowercaseStringOrEmpty(name) === normalizedActionInput, ); if (!actionMatch) { - throw new Error(`Unknown message action: ${actionInput}`); + throw new Error( + `Unknown message action "${actionInput}". Use one of ${CHANNEL_MESSAGE_ACTION_NAMES.join( + ", ", + )}. Example: ${formatCliCommand("openclaw message send --channel --target --text ")}.`, + ); } const action = actionMatch as ChannelMessageActionName; diff --git a/src/commands/onboard-non-interactive/local/gateway-config.ts b/src/commands/onboard-non-interactive/local/gateway-config.ts index 4d7e79778cc..4b3e15e03df 100644 --- a/src/commands/onboard-non-interactive/local/gateway-config.ts +++ b/src/commands/onboard-non-interactive/local/gateway-config.ts @@ -1,4 +1,5 @@ import { formatCliCommand } from "../../../cli/command-format.js"; +import { formatInvalidPortOption } from "../../../cli/error-format.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { isValidEnvSecretRefId, resolveSecretInputRef } from "../../../config/types.secrets.js"; import type { RuntimeEnv } from "../../../runtime.js"; @@ -23,8 +24,13 @@ export function applyNonInteractiveGatewayConfig(params: { const { opts, runtime } = params; const hasGatewayPort = opts.gatewayPort !== undefined; - if (hasGatewayPort && (!Number.isFinite(opts.gatewayPort) || (opts.gatewayPort ?? 0) <= 0)) { - runtime.error("Invalid --gateway-port. Use a positive port number, for example 3000."); + if ( + hasGatewayPort && + (!Number.isFinite(opts.gatewayPort) || + (opts.gatewayPort ?? 0) <= 0 || + opts.gatewayPort > 65_535) + ) { + runtime.error(formatInvalidPortOption("--gateway-port")); runtime.exit(1); return null; } diff --git a/src/commands/sessions.test.ts b/src/commands/sessions.test.ts index 11c68d625cb..52987fd085a 100644 --- a/src/commands/sessions.test.ts +++ b/src/commands/sessions.test.ts @@ -316,7 +316,7 @@ describe("sessionsCommand", () => { const { runtime, errors } = makeRuntime(); await expect(sessionsCommand({ store, active: "0" }, runtime)).rejects.toThrow("exit 1"); - expect(errors[0]).toContain("--active must be a positive integer"); + expect(errors[0]).toContain("--active must be a positive number of minutes"); fs.rmSync(store); }); @@ -334,7 +334,7 @@ describe("sessionsCommand", () => { const { runtime, errors } = makeRuntime(); await expect(sessionsCommand({ store, limit: "0" }, runtime)).rejects.toThrow("exit 1"); - expect(errors[0]).toContain('--limit must be a positive integer or "all"'); + expect(errors[0]).toContain('--limit must be a positive integer or "all", for example'); fs.rmSync(store); }); diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index 189f6b9b0b0..0549d2c88ac 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -234,7 +234,7 @@ export async function sessionsCommand( if (opts.active !== undefined) { const parsed = Number.parseInt(opts.active, 10); if (Number.isNaN(parsed) || parsed <= 0) { - runtime.error("--active must be a positive integer (minutes)"); + runtime.error("--active must be a positive number of minutes, for example --active 30."); runtime.exit(1); return; } @@ -243,7 +243,7 @@ export async function sessionsCommand( const limit = parseSessionsLimit(opts.limit); if (limit === null) { - runtime.error('--limit must be a positive integer or "all"'); + runtime.error('--limit must be a positive integer or "all", for example --limit 25.'); runtime.exit(1); return; } diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 724becce53b..12ce852bbfc 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import { formatCliCommand } from "../cli/command-format.js"; +import { formatLookupMiss } from "../cli/error-format.js"; import { getRuntimeConfig } from "../config/config.js"; import { loadSessionStore, @@ -58,7 +58,12 @@ const SESSION_REGISTRY_RETENTION_MS = 7 * 24 * 60 * 60_000; const info = theme.info; function formatTaskLookupMiss(lookup: string): string { - return `Task not found: ${lookup}. Run ${formatCliCommand("openclaw tasks list")} to see recent task ids.`; + return formatLookupMiss({ + noun: "Task", + value: lookup, + listCommand: "openclaw tasks list", + valueLabel: "task id", + }); } async function loadTaskCancelConfig() { diff --git a/src/wizard/setup.gateway-config.ts b/src/wizard/setup.gateway-config.ts index 0e1d7387a0d..8b599d631c8 100644 --- a/src/wizard/setup.gateway-config.ts +++ b/src/wizard/setup.gateway-config.ts @@ -1,3 +1,4 @@ +import { formatPortRangeHint } from "../cli/error-format.js"; import { normalizeGatewayTokenInput, randomToken, @@ -55,7 +56,7 @@ function normalizeWizardTextInput(value: unknown): string { function validateGatewayPortInput(value: unknown): string | undefined { const port = Number(normalizeWizardTextInput(value)); if (!Number.isInteger(port) || port < 1 || port > 65_535) { - return "Use a port number from 1 to 65535, for example 18789."; + return formatPortRangeHint(); } return undefined; }