diff --git a/index.ts b/index.ts index df113bc..8397aa5 100755 --- a/index.ts +++ b/index.ts @@ -28,6 +28,15 @@ import { DEFAULT_CONFIG, type GmConfig, type RecallResult, type EdgeType } from import { registerCrudRoutes } from "./src/routes/crud.ts"; import { createGraphMemoryCli } from "./src/cli.ts"; +/** + * OpenClaw < 2026.7 does not expose registrationMode and loads the full plugin + * while discovering plugin CLI commands. Avoid starting the Neo4j runtime for + * an invocation that only needs the graph-memory CLI registrar. + */ +export function isGraphMemoryCliInvocation(argv: readonly string[] = process.argv): boolean { + return argv.slice(2).includes("graph-memory"); +} + // ─── 从 OpenClaw config 读默认 model 名 ────────────────────── /** @@ -239,6 +248,27 @@ const graphMemoryProPlugin = { api.pluginConfig && typeof api.pluginConfig === "object" ? (api.pluginConfig as any) : {}; + + // Register CLI metadata before any database, schema, or embedding work. + // New OpenClaw versions load plugins in cli-metadata mode; older supported + // versions are covered by the argv fallback above. + if (typeof api.registerCli === "function") { + api.registerCli( + createGraphMemoryCli({ + pluginId: "graph-memory-pro", + pluginConfig: raw as Record | undefined, + resolveConfigPath: (p: string) => api.resolvePath?.(p) ?? p, + }), + { commands: ["graph-memory"] }, + ); + } + if ( + api.registrationMode === "cli-metadata" || + isGraphMemoryCliInvocation() + ) { + return; + } + const cfg: GmConfig = { ...DEFAULT_CONFIG, ...raw }; if (raw.neo4j) cfg.neo4j = { ...DEFAULT_CONFIG.neo4j, ...raw.neo4j }; @@ -1115,18 +1145,6 @@ const graphMemoryProPlugin = { // ── CRUD REST 路由(给 ClawX 前端用) ───────────────── registerCrudRoutes(api, driver, recaller); - // ── CLI:`openclaw graph-memory auth login`(OAuth 触发入口) ── - if (typeof api.registerCli === "function") { - api.registerCli( - createGraphMemoryCli({ - pluginId: "graph-memory-pro", - pluginConfig: raw as Record | undefined, - resolveConfigPath: (p: string) => api.resolvePath?.(p) ?? p, - }), - { commands: ["graph-memory"] }, - ); - } - // ── Neovis 配置接口(给 ClawX 前端用) ────────────────── api.registerHttpRoute({ diff --git a/src/cli.ts b/src/cli.ts index b5ce49c..f4c5a25 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -34,6 +34,7 @@ export interface CliCommand { description(text: string): CliCommand; option(flags: string, description?: string, defaultValue?: unknown): CliCommand; action(handler: (options: Record) => void | Promise): CliCommand; + outputHelp(): void; } export interface GraphMemoryCliRegistrarContext { @@ -229,11 +230,17 @@ export function createGraphMemoryCli(deps: GraphMemoryCliDeps) { const root = program .command("graph-memory") - .description("graph-memory-pro: Neo4j 知识图谱记忆引擎管理命令"); + .description("graph-memory-pro: Neo4j 知识图谱记忆引擎管理命令") + .action(() => { + root.outputHelp(); + }); const auth = root .command("auth") - .description("管理用于 LLM 智能抽取的 OAuth 认证"); + .description("管理用于 LLM 智能抽取的 OAuth 认证") + .action(() => { + auth.outputHelp(); + }); auth .command("login") diff --git a/test/plugin-cli-lifecycle.test.ts b/test/plugin-cli-lifecycle.test.ts new file mode 100644 index 0000000..22de604 --- /dev/null +++ b/test/plugin-cli-lifecycle.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it, vi } from "vitest"; +import graphMemoryProPlugin, { isGraphMemoryCliInvocation } from "../index.ts"; +import { createGraphMemoryCli } from "../src/cli.ts"; + +class FakeCommand { + readonly children = new Map(); + actionHandler?: (options: Record) => void | Promise; + helpCount = 0; + + command(name: string): FakeCommand { + const command = new FakeCommand(); + this.children.set(name, command); + return command; + } + + description(): FakeCommand { + return this; + } + + option(): FakeCommand { + return this; + } + + action(handler: (options: Record) => void | Promise): FakeCommand { + this.actionHandler = handler; + return this; + } + + outputHelp(): void { + this.helpCount += 1; + } +} + +function metadataApi(registrationMode?: string) { + return { + registrationMode, + pluginConfig: {}, + config: {}, + resolvePath: (value: string) => value, + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, + registerCli: vi.fn(), + registerTool: vi.fn(() => { + throw new Error("runtime tools must not load during CLI discovery"); + }), + registerContextEngine: vi.fn(() => { + throw new Error("context engine must not load during CLI discovery"); + }), + registerHttpRoute: vi.fn(() => { + throw new Error("HTTP routes must not load during CLI discovery"); + }), + on: vi.fn(() => { + throw new Error("runtime hooks must not load during CLI discovery"); + }), + }; +} + +describe("plugin CLI lifecycle", () => { + it("prints help for the graph-memory and auth parent commands", async () => { + const program = new FakeCommand(); + createGraphMemoryCli({})({ program }); + + const root = program.children.get("graph-memory"); + const auth = root?.children.get("auth"); + expect(root?.actionHandler).toBeTypeOf("function"); + expect(auth?.actionHandler).toBeTypeOf("function"); + + await root?.actionHandler?.({}); + await auth?.actionHandler?.({}); + expect(root?.helpCount).toBe(1); + expect(auth?.helpCount).toBe(1); + }); + + it("registers only CLI metadata in modern OpenClaw metadata mode", () => { + const api = metadataApi("cli-metadata"); + graphMemoryProPlugin.register(api as any); + expect(api.registerCli).toHaveBeenCalledOnce(); + expect(api.registerTool).not.toHaveBeenCalled(); + expect(api.registerContextEngine).not.toHaveBeenCalled(); + expect(api.registerHttpRoute).not.toHaveBeenCalled(); + }); + + it("recognizes the plugin command for legacy OpenClaw discovery", () => { + expect(isGraphMemoryCliInvocation(["node", "openclaw", "graph-memory", "auth", "login"])).toBe(true); + expect(isGraphMemoryCliInvocation(["node", "openclaw", "gateway"])).toBe(false); + }); + + it("skips runtime initialization when OpenClaw executes the CLI registrar", () => { + const originalArgv = process.argv; + process.argv = ["node", "openclaw", "graph-memory"]; + try { + const api = metadataApi("full"); + graphMemoryProPlugin.register(api as any); + expect(api.registerCli).toHaveBeenCalledOnce(); + expect(api.registerTool).not.toHaveBeenCalled(); + } finally { + process.argv = originalArgv; + } + }); +}); diff --git a/types/openclaw-plugin-sdk.d.ts b/types/openclaw-plugin-sdk.d.ts index b7c4ca6..f70bcb2 100644 --- a/types/openclaw-plugin-sdk.d.ts +++ b/types/openclaw-plugin-sdk.d.ts @@ -28,6 +28,7 @@ declare module "openclaw/plugin-sdk" { description(text: string): OpenClawPluginCliCommand; option(flags: string, description?: string, defaultValue?: unknown): OpenClawPluginCliCommand; action(handler: (options: Record) => void | Promise): OpenClawPluginCliCommand; + outputHelp(): void; } export interface OpenClawPluginCliContext { @@ -56,6 +57,7 @@ declare module "openclaw/plugin-sdk" { logger: OpenClawPluginLogger; config: any; pluginConfig: unknown; + registrationMode?: "full" | "discovery" | "tool-discovery" | "setup-only" | "setup-runtime" | "cli-metadata"; resolvePath(path: string): string; on(event: string, handler: (...args: any[]) => any): void; registerContextEngine(id: string, factory: (...args: any[]) => any): void;