Skip to content

Commit 8cb45c0

Browse files
feiskyersteipete
authored andcommitted
fix(config): give actionable guidance when command names are used in plugins.allow (#64191)
When users put a runtime command name like "dreaming" into `plugins.allow`, validation now explains that it is a command provided by a specific plugin (e.g. "memory-core") and suggests using the plugin id instead, rather than the generic "plugin not found" warning that previously created a circular trap with the CLI error message. Similarly, running `openclaw dreaming` from the CLI now explains that `/dreaming` is a runtime slash command (not a CLI command) and points users to `openclaw memory` for CLI operations or `/dreaming` in a chat session. Fixes two related UX problems: 1. `plugins.allow: ["dreaming"]` → validation warned "plugin not found" 2. `openclaw dreaming status` → CLI said "add dreaming to plugins.allow" (which then triggered problem 1) Root cause: "dreaming" is a slash command registered by the memory-core plugin via `api.registerCommand()`, not a standalone plugin or CLI command.
1 parent 65ef70b commit 8cb45c0

4 files changed

Lines changed: 91 additions & 1 deletion

File tree

src/cli/run-main.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,22 @@ describe("resolveMissingPluginCommandMessage", () => {
105105
}),
106106
).toBeNull();
107107
});
108+
109+
it("explains that dreaming is a runtime slash command, not a CLI command", () => {
110+
const message = resolveMissingPluginCommandMessage("dreaming", {});
111+
expect(message).toContain("runtime slash command");
112+
expect(message).toContain("/dreaming");
113+
expect(message).toContain("memory-core");
114+
expect(message).toContain("openclaw memory");
115+
});
116+
117+
it("returns the runtime command message even when plugins.allow is set", () => {
118+
const message = resolveMissingPluginCommandMessage("dreaming", {
119+
plugins: {
120+
allow: ["memory-core"],
121+
},
122+
});
123+
expect(message).toContain("runtime slash command");
124+
expect(message).not.toContain("plugins.allow");
125+
});
108126
});

src/cli/run-main.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ export function shouldUseRootHelpFastPath(argv: string[]): boolean {
6363
return resolveCliArgvInvocation(argv).isRootHelpInvocation;
6464
}
6565

66+
/**
67+
* Maps well-known runtime command names to the plugin that provides them.
68+
* Used to give actionable guidance when users try to run a runtime slash
69+
* command (e.g. `/dreaming`) as a CLI command (`openclaw dreaming`).
70+
*/
71+
const RUNTIME_COMMAND_TO_PLUGIN_ID: Record<string, string> = {
72+
dreaming: "memory-core",
73+
};
74+
6675
export function resolveMissingPluginCommandMessage(
6776
pluginId: string,
6877
config?: OpenClawConfig,
@@ -71,6 +80,17 @@ export function resolveMissingPluginCommandMessage(
7180
if (!normalizedPluginId) {
7281
return null;
7382
}
83+
84+
// Check if this is a runtime slash command rather than a CLI command.
85+
const parentPluginId = RUNTIME_COMMAND_TO_PLUGIN_ID[normalizedPluginId];
86+
if (parentPluginId) {
87+
return (
88+
`"${normalizedPluginId}" is a runtime slash command (/${normalizedPluginId}), not a CLI command. ` +
89+
`It is provided by the "${parentPluginId}" plugin. ` +
90+
`Use \`openclaw memory\` for CLI memory operations, or \`/${normalizedPluginId}\` in a chat session.`
91+
);
92+
}
93+
7494
const allow =
7595
Array.isArray(config?.plugins?.allow) && config.plugins.allow.length > 0
7696
? config.plugins.allow

src/config/config.plugin-validation.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,35 @@ describe("config plugin validation", () => {
278278
}
279279
});
280280

281+
it("warns with actionable guidance when a runtime command name is used in plugins.allow", async () => {
282+
const res = validateInSuite({
283+
agents: { list: [{ id: "pi" }] },
284+
plugins: {
285+
allow: ["dreaming"],
286+
entries: {
287+
"memory-core": {
288+
config: { dreaming: { enabled: true } },
289+
},
290+
},
291+
},
292+
});
293+
// Should not produce the generic "plugin not found" warning.
294+
expect(
295+
res.warnings?.some(
296+
(w) => w.path === "plugins.allow" && w.message.includes("plugin not found: dreaming"),
297+
),
298+
).toBe(false);
299+
// Should produce a helpful redirect to the parent plugin.
300+
expect(
301+
res.warnings?.some(
302+
(w) =>
303+
w.path === "plugins.allow" &&
304+
w.message.includes('"dreaming" is not a plugin') &&
305+
w.message.includes("memory-core"),
306+
),
307+
).toBe(true);
308+
});
309+
281310
it("does not fail validation for the implicit default memory slot when plugins config is explicit", async () => {
282311
const res = validateConfigObjectWithPlugins(
283312
{

src/config/validation.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ import { OpenClawSchema } from "./zod-schema.js";
4040

4141
const LEGACY_REMOVED_PLUGIN_IDS = new Set(["google-antigravity-auth", "google-gemini-cli-auth"]);
4242

43+
/**
44+
* Maps well-known runtime command names to the plugin that provides them.
45+
* Used to give actionable guidance when users accidentally put a command name
46+
* (e.g. "dreaming") into `plugins.allow` instead of the parent plugin id.
47+
*/
48+
const COMMAND_NAME_TO_PLUGIN_ID: Record<string, string> = {
49+
dreaming: "memory-core",
50+
// "active-memory" omitted: command name equals plugin id, no redirect needed.
51+
voice: "talk-voice",
52+
phone: "phone-control",
53+
pair: "device-pair",
54+
};
55+
4356
type UnknownIssueRecord = Record<string, unknown>;
4457
type ConfigPathSegment = string | number;
4558
type AllowedValuesCollection = {
@@ -1040,7 +1053,17 @@ function validateConfigObjectWithPluginsBase(
10401053
continue;
10411054
}
10421055
if (!knownIds.has(pluginId)) {
1043-
pushMissingPluginIssue("plugins.allow", pluginId, { warnOnly: true });
1056+
const parentPluginId = COMMAND_NAME_TO_PLUGIN_ID[pluginId];
1057+
if (parentPluginId && parentPluginId !== pluginId && knownIds.has(parentPluginId)) {
1058+
warnings.push({
1059+
path: "plugins.allow",
1060+
message:
1061+
`"${pluginId}" is not a plugin — it is a command provided by the "${parentPluginId}" plugin. ` +
1062+
`Use "${parentPluginId}" in plugins.allow instead.`,
1063+
});
1064+
} else {
1065+
pushMissingPluginIssue("plugins.allow", pluginId, { warnOnly: true });
1066+
}
10441067
}
10451068
}
10461069

0 commit comments

Comments
 (0)