Skip to content

Commit eb6dd2c

Browse files
authored
Fix memory plugin CLI help dispatch (#83841)
* fix cli help for active memory plugin * docs add changelog for memory cli help * test fix root help mock type
1 parent 0b4fc26 commit eb6dd2c

14 files changed

Lines changed: 434 additions & 24 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Docs: https://docs.openclaw.ai
137137
- Mac app: render channel quick config as aligned Settings rows and hide schema-only variants that cannot be edited safely from the quick pane.
138138
- Gateway/webchat: hide internal runtime-context and other `display: false` transcript messages from Chat history and live message events. Fixes #83216. Thanks @EmpireCreator.
139139
- CLI/help: keep `gateway`, `doctor`, `status`, and `health` help registration out of action/runtime imports so subcommand `--help` stays lightweight in constrained terminals. Fixes #83228. Thanks @dfguerrerom.
140+
- CLI/help: show plugin-owned command help based on the active memory slot so LanceDB memory users see `ltm` instead of unavailable `memory` commands. Fixes #83745. (#83841) Thanks @joshavant.
140141
- Cron/Discord: keep explicit announce runs in message-tool-only source-reply mode so scheduled agent turns post once instead of also echoing through automatic visible replies. Fixes #83261. Thanks @Theralley.
141142
- Telegram: preserve forum-topic origin targets in inbound, audio-preflight, and skipped-message hook contexts so follow-up delivery stays bound to the originating topic. Fixes #83302. Thanks @M00zyx.
142143
- Telegram: retry HTTP 421 Misdirected Request send failures on a fresh fallback transport so transient edge-node routing errors no longer drop outbound replies. Fixes #48892. (#48908) Thanks @MarsDoge.

docs/cli/memory.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ title: "Memory"
1010
# `openclaw memory`
1111

1212
Manage semantic memory indexing and search.
13-
Provided by the active memory plugin (default: `memory-core`; set `plugins.slots.memory = "none"` to disable).
13+
Provided by the bundled `memory-core` plugin. The command is available when
14+
`plugins.slots.memory` selects `memory-core` (the default); other memory plugins
15+
expose their own CLI namespaces.
1416

1517
Related:
1618

docs/plugins/memory-lancedb.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,12 @@ openclaw ltm search "project preferences"
238238
openclaw ltm stats
239239
```
240240

241-
The plugin also extends `openclaw memory` with a non-vector `query` subcommand
242-
that runs against the LanceDB table directly:
241+
The `query` subcommand runs a non-vector query against the LanceDB table
242+
directly:
243243

244244
```bash
245-
openclaw memory query --cols id,text,createdAt --limit 20
246-
openclaw memory query --filter "category = 'preference'" --order-by createdAt:desc
245+
openclaw ltm query --cols id,text,createdAt --limit 20
246+
openclaw ltm query --filter "category = 'preference'" --order-by createdAt:desc
247247
```
248248

249249
- `--cols <columns>`: comma-separated column allowlist (defaults to `id`, `text`, `importance`, `category`, `createdAt`).

extensions/memory-lancedb/cli-metadata.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ export default definePluginEntry({
55
name: "Memory LanceDB",
66
description: "LanceDB-backed memory provider",
77
register(api) {
8-
api.registerCli(() => {}, { commands: ["ltm"] });
8+
api.registerCli(() => {}, {
9+
descriptors: [
10+
{
11+
name: "ltm",
12+
description: "Inspect and query LanceDB-backed memory",
13+
hasSubcommands: true,
14+
},
15+
],
16+
});
917
},
1018
});

openclaw.mjs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,72 @@ const isBrowserHelpInvocation = (argv) =>
334334
const isHelpFastPathDisabled = () =>
335335
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
336336

337+
const normalizeLauncherHomeValue = (value) => {
338+
const trimmed = value?.trim();
339+
return trimmed && trimmed !== "undefined" && trimmed !== "null" ? trimmed : undefined;
340+
};
341+
342+
const resolveLauncherOsHomeDir = () =>
343+
normalizeLauncherHomeValue(process.env.HOME) ??
344+
normalizeLauncherHomeValue(process.env.USERPROFILE) ??
345+
os.homedir();
346+
347+
const resolveLauncherHomeDir = () => {
348+
const explicit = normalizeLauncherHomeValue(process.env.OPENCLAW_HOME);
349+
const rawHome =
350+
explicit && (explicit === "~" || explicit.startsWith("~/") || explicit.startsWith("~\\"))
351+
? explicit.replace(/^~(?=$|[\\/])/, resolveLauncherOsHomeDir())
352+
: (explicit ?? resolveLauncherOsHomeDir());
353+
return path.resolve(rawHome);
354+
};
355+
356+
const resolveLauncherUserPath = (input) => {
357+
if (input === "~") {
358+
return resolveLauncherHomeDir();
359+
}
360+
if (input.startsWith("~/") || input.startsWith("~\\")) {
361+
return path.join(resolveLauncherHomeDir(), input.slice(2));
362+
}
363+
return path.resolve(input);
364+
};
365+
366+
const resolveLauncherConfigPaths = () => {
367+
const explicit = process.env.OPENCLAW_CONFIG_PATH?.trim();
368+
if (explicit) {
369+
return [resolveLauncherUserPath(explicit)];
370+
}
371+
const stateOverride = process.env.OPENCLAW_STATE_DIR?.trim();
372+
if (stateOverride) {
373+
const stateDir = resolveLauncherUserPath(stateOverride);
374+
return [path.join(stateDir, "openclaw.json"), path.join(stateDir, "clawdbot.json")];
375+
}
376+
const homeDir = resolveLauncherHomeDir();
377+
return [
378+
path.join(homeDir, ".openclaw", "openclaw.json"),
379+
path.join(homeDir, ".openclaw", "clawdbot.json"),
380+
path.join(homeDir, ".clawdbot", "openclaw.json"),
381+
path.join(homeDir, ".clawdbot", "clawdbot.json"),
382+
];
383+
};
384+
385+
const shouldDeferRootHelpToRuntimeEntry = () => {
386+
if (
387+
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim() ||
388+
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim()
389+
) {
390+
return true;
391+
}
392+
for (const configPath of resolveLauncherConfigPaths()) {
393+
try {
394+
const raw = readFileSync(configPath, "utf8");
395+
return /\bplugins\b|\$include\b/.test(raw);
396+
} catch {
397+
continue;
398+
}
399+
}
400+
return false;
401+
};
402+
337403
const loadPrecomputedHelpText = (key) => {
338404
try {
339405
const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
@@ -349,6 +415,9 @@ const tryOutputBareRootHelp = async () => {
349415
if (!isBareRootHelpInvocation(process.argv)) {
350416
return false;
351417
}
418+
if (shouldDeferRootHelpToRuntimeEntry()) {
419+
return false;
420+
}
352421
const precomputed = loadPrecomputedHelpText("rootHelpText");
353422
if (precomputed) {
354423
process.stdout.write(precomputed);
@@ -358,7 +427,7 @@ const tryOutputBareRootHelp = async () => {
358427
try {
359428
const mod = await import(specifier);
360429
if (typeof mod.outputRootHelp === "function") {
361-
mod.outputRootHelp();
430+
await mod.outputRootHelp();
362431
return true;
363432
}
364433
} catch (err) {

src/cli/command-registration-policy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("command-registration-policy", () => {
3535
primary: "voicecall",
3636
hasBuiltinPrimary: false,
3737
}),
38-
).toBe(true);
38+
).toBe(false);
3939
expect(
4040
shouldSkipPluginCommandRegistration({
4141
argv: ["node", "openclaw", "help", "--help"],

src/cli/command-registration-policy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export function shouldSkipPluginCommandRegistration(params: {
2222
return invocation.hasHelpOrVersion && invocation.commandPath.length <= 1;
2323
}
2424
if (invocation.hasHelpOrVersion) {
25-
return true;
25+
return (
26+
!params.primary || params.hasBuiltinPrimary || isReservedNonPluginCommandRoot(params.primary)
27+
);
2628
}
2729
if (params.hasBuiltinPrimary) {
2830
return true;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { loadRootHelpRenderOptionsForConfigSensitivePlugins } from "./root-help-live-config.js";
3+
4+
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
5+
6+
vi.mock("../config/config.js", () => ({
7+
readConfigFileSnapshot: readConfigFileSnapshotMock,
8+
}));
9+
10+
describe("root help live config", () => {
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
});
14+
15+
it("uses precomputed help when plugin-sensitive config is invalid", async () => {
16+
readConfigFileSnapshotMock.mockResolvedValueOnce({
17+
valid: false,
18+
sourceConfig: {
19+
plugins: {
20+
slots: {
21+
memory: "memory-lancedb",
22+
},
23+
},
24+
},
25+
runtimeConfig: {},
26+
});
27+
28+
await expect(loadRootHelpRenderOptionsForConfigSensitivePlugins({})).resolves.toBeNull();
29+
});
30+
31+
it("uses snapshot runtime config when plugin config affects help", async () => {
32+
const runtimeConfig = {
33+
plugins: {
34+
slots: {
35+
memory: "memory-lancedb",
36+
},
37+
},
38+
};
39+
const env = {};
40+
readConfigFileSnapshotMock.mockResolvedValueOnce({
41+
valid: true,
42+
sourceConfig: runtimeConfig,
43+
runtimeConfig,
44+
});
45+
46+
await expect(loadRootHelpRenderOptionsForConfigSensitivePlugins(env)).resolves.toEqual({
47+
config: runtimeConfig,
48+
env,
49+
});
50+
});
51+
});

src/cli/root-help-live-config.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import type { RootHelpRenderOptions } from "./program/root-help.js";
3+
4+
function hasEntries(value: object | undefined): boolean {
5+
return !!value && Object.keys(value).length > 0;
6+
}
7+
8+
function hasListEntries(value: string[] | undefined): boolean {
9+
return Array.isArray(value) && value.length > 0;
10+
}
11+
12+
export function hasPluginHelpAffectingConfig(config: OpenClawConfig | null | undefined): boolean {
13+
const plugins = config?.plugins;
14+
if (!plugins) {
15+
return false;
16+
}
17+
return (
18+
plugins.enabled === false ||
19+
hasListEntries(plugins.allow) ||
20+
hasListEntries(plugins.deny) ||
21+
plugins.bundledDiscovery !== undefined ||
22+
hasListEntries(plugins.load?.paths) ||
23+
hasEntries(plugins.slots) ||
24+
hasEntries(plugins.entries) ||
25+
hasEntries(plugins.installs)
26+
);
27+
}
28+
29+
export function hasPluginHelpAffectingEnv(env: NodeJS.ProcessEnv): boolean {
30+
return Boolean(
31+
env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim() || env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim(),
32+
);
33+
}
34+
35+
export async function loadRootHelpRenderOptionsForConfigSensitivePlugins(
36+
env: NodeJS.ProcessEnv = process.env,
37+
): Promise<RootHelpRenderOptions | null> {
38+
const configModule = await import("../config/config.js");
39+
const snapshot = await configModule.readConfigFileSnapshot({
40+
observe: false,
41+
skipPluginValidation: true,
42+
});
43+
if (!snapshot.valid) {
44+
return null;
45+
}
46+
if (!hasPluginHelpAffectingEnv(env) && !hasPluginHelpAffectingConfig(snapshot.sourceConfig)) {
47+
return null;
48+
}
49+
return {
50+
config: snapshot.runtimeConfig,
51+
env,
52+
};
53+
}

src/cli/run-main.exit.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import process from "node:process";
22
import { CommanderError } from "commander";
33
import { beforeEach, describe, expect, it, vi } from "vitest";
44
import { loggingState } from "../logging/state.js";
5+
import type { RootHelpRenderOptions } from "./program/root-help.js";
56
import { runCli, shouldStartProxyForCli } from "./run-main.js";
67

78
const tryRouteCliMock = vi.hoisted(() => vi.fn());
@@ -18,6 +19,9 @@ const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
1819
const outputRootHelpMock = vi.hoisted(() => vi.fn());
1920
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
2021
const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false));
22+
const loadRootHelpRenderOptionsForConfigSensitivePluginsMock = vi.hoisted(() =>
23+
vi.fn<() => Promise<RootHelpRenderOptions | null>>(async () => null),
24+
);
2125
const buildProgramMock = vi.hoisted(() => vi.fn());
2226
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
2327
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
@@ -168,6 +172,11 @@ vi.mock("./root-help-metadata.js", () => ({
168172
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
169173
}));
170174

175+
vi.mock("./root-help-live-config.js", () => ({
176+
loadRootHelpRenderOptionsForConfigSensitivePlugins:
177+
loadRootHelpRenderOptionsForConfigSensitivePluginsMock,
178+
}));
179+
171180
vi.mock("./program.js", () => ({
172181
buildProgram: buildProgramMock,
173182
}));
@@ -242,6 +251,7 @@ describe("runCli exit behavior", () => {
242251
listAgentHarnessIdsMock.mockReturnValue([]);
243252
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
244253
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
254+
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValue(null);
245255
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
246256
loadConfigMock.mockReturnValue({});
247257
startProxyMock.mockResolvedValue(null);
@@ -401,6 +411,7 @@ describe("runCli exit behavior", () => {
401411

402412
await runCli(["node", "openclaw", "--help"]);
403413

414+
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
404415
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
405416
expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled();
406417
expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled();
@@ -416,6 +427,7 @@ describe("runCli exit behavior", () => {
416427

417428
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "--help"]);
418429
expect(tryRouteCliMock).not.toHaveBeenCalled();
430+
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
419431
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
420432
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
421433
expect(buildProgramMock).not.toHaveBeenCalled();
@@ -424,6 +436,28 @@ describe("runCli exit behavior", () => {
424436
exitSpy.mockRestore();
425437
});
426438

439+
it("renders config-sensitive root help live instead of precomputed metadata", async () => {
440+
const liveOptions: RootHelpRenderOptions = {
441+
config: {
442+
plugins: {
443+
slots: {
444+
memory: "memory-lancedb",
445+
},
446+
},
447+
},
448+
env: process.env,
449+
};
450+
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValueOnce(liveOptions);
451+
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
452+
453+
await runCli(["node", "openclaw", "--help"]);
454+
455+
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
456+
expect(outputPrecomputedRootHelpTextMock).not.toHaveBeenCalled();
457+
expect(outputRootHelpMock).toHaveBeenCalledWith(liveOptions);
458+
expect(buildProgramMock).not.toHaveBeenCalled();
459+
});
460+
427461
it("does not start the managed proxy for local gateway client commands", async () => {
428462
tryRouteCliMock.mockResolvedValueOnce(true);
429463

0 commit comments

Comments
 (0)