diff --git a/CHANGELOG.md b/CHANGELOG.md index 19eab32f9ec..09ec2d9ff4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Agents/tools: stop treating `tools.deny: ["write"]` as an implicit `apply_patch` deny; operators who want to block patch writes should deny `apply_patch` or `group:fs` explicitly. Fixes #76749. (#76795) Thanks @Nek-12 and @hclsys. - Gateway/update: recover an installed-but-unloaded macOS LaunchAgent after package updates, rerun Gateway health/version/channel readiness checks, and print restart, reinstall, and rollback guidance before reporting update failure. (#76790) Thanks @jonathanlindsay. +- CLI/plugins: explain when a missing plugin command alias belongs to a bundled plugin that is disabled by default, including the `openclaw plugins enable ` repair command. (#76835) - Google Meet: route stateful CLI session commands through the gateway-owned runtime so joined realtime sessions survive after the starting CLI process exits. Fixes #76344. Thanks @coltonharris-wq. - Memory/status: split builtin sqlite-vec store readiness from embedding-provider readiness in `memory status --deep` and `openclaw status`, so local vector-store failures no longer look like provider failures and provider failures no longer hide a healthy local vector store. - CLI/doctor: trust a ready gateway memory probe when CLI-side active memory backend resolution is unavailable, preventing false "No active memory plugin is registered" warnings for healthy runtime setups. Fixes #76792. Thanks @som-686. diff --git a/scripts/deadcode-unused-files.allowlist.mjs b/scripts/deadcode-unused-files.allowlist.mjs index e5d4670b2f8..4e67f86c66b 100644 --- a/scripts/deadcode-unused-files.allowlist.mjs +++ b/scripts/deadcode-unused-files.allowlist.mjs @@ -16,6 +16,7 @@ export const KNIP_UNUSED_FILE_ALLOWLIST = [ "src/gateway/gateway-cli-backend.live-probe-helpers.ts", "src/gateway/gateway-codex-harness.live-helpers.ts", "src/infra/changelog-unreleased.ts", + "src/infra/command-analysis/index.ts", "src/mcp/openclaw-tools-serve.ts", "src/mcp/plugin-tools-handlers.ts", "src/mcp/plugin-tools-serve.ts", diff --git a/src/cli/run-main-policy.ts b/src/cli/run-main-policy.ts index fb7eb34f2c2..5782648aefb 100644 --- a/src/cli/run-main-policy.ts +++ b/src/cli/run-main-policy.ts @@ -141,6 +141,17 @@ export function resolveMissingPluginCommandMessage( "the bundled plugin command surface." ); } + if ( + commandAlias.kind !== "runtime-slash" && + commandAlias.enabledByDefault !== true && + config?.plugins?.entries?.[parentPluginId]?.enabled !== true + ) { + return ( + `The \`openclaw ${normalizedPluginId}\` command is provided by the ` + + `"${parentPluginId}" plugin, but that bundled plugin is disabled by default. Run ` + + `\`openclaw plugins enable ${parentPluginId}\` to enable that CLI surface.` + ); + } if (commandAlias.kind === "runtime-slash") { const cliHint = commandAlias.cliCommand ? `Use \`openclaw ${commandAlias.cliCommand}\` for related CLI operations, or ` diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index 92592bc8097..c1e32ecb360 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -16,6 +16,7 @@ const memoryWikiCommandAliasRegistry: PluginManifestCommandAliasRegistry = { plugins: [ { id: "memory-wiki", + enabledByDefault: true, commandAliases: [{ name: "wiki" }], }, ], @@ -271,6 +272,54 @@ describe("resolveMissingPluginCommandMessage", () => { expect(message).toContain("plugins.allow"); }); + it("explains disabled-by-default parent plugins for CLI command aliases", () => { + const message = resolveMissingPluginCommandMessage( + "voicecall", + {}, + { + registry: { + plugins: [ + { + id: "voice-call", + commandAliases: [{ name: "voicecall" }], + }, + ], + }, + }, + ); + + expect(message).toContain('"voice-call" plugin'); + expect(message).toContain("disabled by default"); + expect(message).toContain("openclaw plugins enable voice-call"); + }); + + it("returns null for CLI command aliases when disabled-by-default parent plugins are enabled", () => { + const message = resolveMissingPluginCommandMessage( + "voicecall", + { + plugins: { + entries: { + "voice-call": { + enabled: true, + }, + }, + }, + }, + { + registry: { + plugins: [ + { + id: "voice-call", + commandAliases: [{ name: "voicecall" }], + }, + ], + }, + }, + ); + + expect(message).toBeNull(); + }); + it("explains parent plugin disablement for runtime command aliases", () => { const message = resolveMissingPluginCommandMessage( "dreaming", diff --git a/src/plugins/manifest-command-aliases.test.ts b/src/plugins/manifest-command-aliases.test.ts index abca34f1ddb..4c4ea3c23d3 100644 --- a/src/plugins/manifest-command-aliases.test.ts +++ b/src/plugins/manifest-command-aliases.test.ts @@ -29,6 +29,7 @@ describe("manifest command aliases", () => { }, { id: "memory", + enabledByDefault: true, commandAliases: [{ name: "legacy-memory" }], }, ], @@ -41,6 +42,7 @@ describe("manifest command aliases", () => { resolveManifestCommandAliasOwnerInRegistry({ command: "legacy-memory", registry }), ).toMatchObject({ pluginId: "memory", + enabledByDefault: true, name: "legacy-memory", }); }); diff --git a/src/plugins/manifest-command-aliases.ts b/src/plugins/manifest-command-aliases.ts index d0de1b716d7..8ba3c51b0f8 100644 --- a/src/plugins/manifest-command-aliases.ts +++ b/src/plugins/manifest-command-aliases.ts @@ -17,11 +17,13 @@ export type PluginManifestCommandAlias = { export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & { pluginId: string; + enabledByDefault?: boolean; }; export type PluginManifestCommandAliasRegistry = { plugins: readonly { id: string; + enabledByDefault?: boolean; commandAliases?: readonly PluginManifestCommandAlias[]; }[]; }; @@ -81,7 +83,11 @@ export function resolveManifestCommandAliasOwnerInRegistry(params: { (entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand, ); if (alias) { - return { ...alias, pluginId: plugin.id }; + return { + ...alias, + pluginId: plugin.id, + ...(plugin.enabledByDefault === true ? { enabledByDefault: true } : {}), + }; } } return undefined;