diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c29ab5f643..7442db5d707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai - Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts. - Agents/models: classify empty, reasoning-only, and planning-only terminal agent runs before accepting a model fallback candidate, so invalid or incompatible models can advance to the next configured fallback instead of returning a 30-second terminal failure. Fixes #73115. Thanks @vdruts. - Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin `embedding.apiKey`, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai. +- CLI/parents: invoking `openclaw ` (channels, plugins, approvals, devices, cron, mcp) without a subcommand now prints the parent's help and exits `0`, matching ` --help` and the existing `agents` / `sessions` defaults so shell `&&` chains and pnpm wrappers no longer surface a misleading `ELIFECYCLE Command failed with exit code 1.` line. A small shared helper attaches the default action only when the parent has no action of its own. Fixes #73077. Thanks @hclsys. - Plugins/hooks: time out never-settling `agent_end` observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099. - Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every `config.get`/`config.schema`, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb. - Memory/LanceDB: call OpenAI-compatible embedding endpoints through the raw SDK transport without sending `encoding_format`, then normalize float-array or base64 responses so providers such as ZhiPu and DashScope no longer fail recall with wrong vector dimensions or rejected parameters. Fixes #63655. Thanks @kinthaiofficial. diff --git a/extensions/memory-core/src/cli.ts b/extensions/memory-core/src/cli.ts index 38bc691f2a5..91269552134 100644 --- a/extensions/memory-core/src/cli.ts +++ b/extensions/memory-core/src/cli.ts @@ -219,4 +219,9 @@ export function registerMemoryCli(program: Command) { .action(async (opts: MemoryRemBackfillOptions) => { await runMemoryRemBackfill(opts); }); + + memory.action(() => { + memory.outputHelp(); + process.exitCode = 0; + }); } diff --git a/src/cli/channels-cli.ts b/src/cli/channels-cli.ts index 9d876374344..b3001574b98 100644 --- a/src/cli/channels-cli.ts +++ b/src/cli/channels-cli.ts @@ -9,6 +9,7 @@ import { formatCliChannelOptions } from "./channel-options.js"; import { runCommandWithRuntime } from "./cli-utils.js"; import { hasExplicitOptions } from "./command-options.js"; import { formatHelpExamples } from "./help-format.js"; +import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; type ChannelsCommandsModule = typeof import("../commands/channels.js"); @@ -237,4 +238,6 @@ export function registerChannelsCli(program: Command) { ); }, "Channel logout failed"); }); + + applyParentDefaultHelpAction(channels); } diff --git a/src/cli/cron-cli/register.ts b/src/cli/cron-cli/register.ts index 35f80dbda06..8660175a9e1 100644 --- a/src/cli/cron-cli/register.ts +++ b/src/cli/cron-cli/register.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; +import { applyParentDefaultHelpAction } from "../program/parent-default-help.js"; import { registerCronAddCommand, registerCronListCommand, @@ -24,4 +25,6 @@ export function registerCronCli(program: Command) { registerCronAddCommand(cron); registerCronSimpleCommands(cron); registerCronEditCommand(cron); + + applyParentDefaultHelpAction(cron); } diff --git a/src/cli/devices-cli.ts b/src/cli/devices-cli.ts index 71c1a95f475..d94dd0532d8 100644 --- a/src/cli/devices-cli.ts +++ b/src/cli/devices-cli.ts @@ -25,6 +25,7 @@ import { import { sanitizeForLog } from "../terminal/ansi.js"; import { getTerminalTableWidth, renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; +import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; import { withProgress } from "./progress.js"; type DevicesRpcOpts = { @@ -662,4 +663,6 @@ export function registerDevicesCli(program: Command) { defaultRuntime.writeJson(result); }), ); + + applyParentDefaultHelpAction(devices); } diff --git a/src/cli/exec-approvals-cli.ts b/src/cli/exec-approvals-cli.ts index f5e113d22fb..4df0a64666e 100644 --- a/src/cli/exec-approvals-cli.ts +++ b/src/cli/exec-approvals-cli.ts @@ -23,6 +23,7 @@ import { isRich, theme } from "../terminal/theme.js"; import { callGatewayFromCli } from "./gateway-rpc.js"; import { nodesCallOpts, resolveNodeId } from "./nodes-cli/rpc.js"; import type { NodesRpcOpts } from "./nodes-cli/types.js"; +import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; type ExecApprovalsSnapshot = { path: string; @@ -616,4 +617,6 @@ export function registerExecApprovalsCli(program: Command) { return true; }, }); + + applyParentDefaultHelpAction(approvals); } diff --git a/src/cli/mcp-cli.ts b/src/cli/mcp-cli.ts index baf02c6e1a0..2b44835b3ed 100644 --- a/src/cli/mcp-cli.ts +++ b/src/cli/mcp-cli.ts @@ -12,6 +12,7 @@ import { normalizeStringifiedOptionalString, } from "../shared/string-coerce.js"; import { resolveGatewayAuthOptions } from "./gateway-secret-options.js"; +import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; function fail(message: string): never { defaultRuntime.error(message); @@ -147,4 +148,6 @@ export function registerMcpCli(program: Command) { } defaultRuntime.log(`Removed MCP server "${name}" from ${result.path}.`); }); + + applyParentDefaultHelpAction(mcp); } diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 76321d30604..414466afdab 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -13,6 +13,7 @@ import { getTerminalTableWidth, renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; import { shortenHomeInString, shortenHomePath } from "../utils.js"; import { formatPluginLine } from "./plugins-list-format.js"; +import { applyParentDefaultHelpAction } from "./program/parent-default-help.js"; export type PluginsListOptions = { json?: boolean; @@ -925,4 +926,6 @@ export function registerPluginsCli(program: Command) { defaultRuntime.log(`${theme.command(plugin.name)}${suffix}${desc}`); } }); + + applyParentDefaultHelpAction(plugins); } diff --git a/src/cli/program/parent-default-help.test.ts b/src/cli/program/parent-default-help.test.ts new file mode 100644 index 00000000000..707856fb2da --- /dev/null +++ b/src/cli/program/parent-default-help.test.ts @@ -0,0 +1,44 @@ +import { Command } from "commander"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { applyParentDefaultHelpAction } from "./parent-default-help.js"; + +describe("applyParentDefaultHelpAction (#73077)", () => { + let originalExitCode: NodeJS.Process["exitCode"]; + beforeEach(() => { + originalExitCode = process.exitCode; + process.exitCode = undefined; + }); + afterEach(() => { + process.exitCode = originalExitCode; + }); + + function buildParent(): Command { + const program = new Command(); + program.exitOverride(); + const parent = program.command("parent").description("test parent"); + parent.exitOverride(); + parent.command("list").action(() => {}); + parent.command("status").action(() => {}); + return parent; + } + + it("invokes parent help and exits 0 when invoked without subcommand", async () => { + const parent = buildParent(); + const helpSpy = vi.spyOn(parent, "outputHelp").mockImplementation(() => {}); + applyParentDefaultHelpAction(parent); + await parent.parent!.parseAsync(["node", "test", "parent"]); + expect(helpSpy).toHaveBeenCalledTimes(1); + expect(process.exitCode).toBe(0); + }); + + it("still routes through subcommand actions when one is invoked", async () => { + const parent = buildParent(); + const listAction = vi.fn(); + parent.commands.find((c) => c.name() === "list")!.action(listAction); + const helpSpy = vi.spyOn(parent, "outputHelp").mockImplementation(() => {}); + applyParentDefaultHelpAction(parent); + await parent.parent!.parseAsync(["node", "test", "parent", "list"]); + expect(listAction).toHaveBeenCalledTimes(1); + expect(helpSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/program/parent-default-help.ts b/src/cli/program/parent-default-help.ts new file mode 100644 index 00000000000..ba3e3b9885d --- /dev/null +++ b/src/cli/program/parent-default-help.ts @@ -0,0 +1,22 @@ +import type { Command } from "commander"; + +/** + * Wire a parent command so that invoking it without a subcommand prints the + * parent's own help and exits with status `0`. + * + * Commander's default behavior for a parent with subcommands is to print help + * and set `process.exitCode = 1`, which differs from ` --help` (which + * exits 0). That asymmetry breaks shell `&&` chains and surfaces a misleading + * `ELIFECYCLE Command failed with exit code 1.` line for users running through + * pnpm. See #73077. + * + * Apply this helper only to parent commands that do not have their own default + * action. Commander does not expose a public "has action handler" API, so + * callers keep that ownership explicit instead of probing private internals. + */ +export function applyParentDefaultHelpAction(parent: Command): void { + parent.action(() => { + parent.outputHelp(); + process.exitCode = 0; + }); +}