Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 名 ──────────────────────

/**
Expand Down Expand Up @@ -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<string, unknown> | 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 };

Expand Down Expand Up @@ -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<string, unknown> | undefined,
resolveConfigPath: (p: string) => api.resolvePath?.(p) ?? p,
}),
{ commands: ["graph-memory"] },
);
}

// ── Neovis 配置接口(给 ClawX 前端用) ──────────────────

api.registerHttpRoute({
Expand Down
11 changes: 9 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface CliCommand {
description(text: string): CliCommand;
option(flags: string, description?: string, defaultValue?: unknown): CliCommand;
action(handler: (options: Record<string, unknown>) => void | Promise<void>): CliCommand;
outputHelp(): void;
}

export interface GraphMemoryCliRegistrarContext {
Expand Down Expand Up @@ -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")
Expand Down
104 changes: 104 additions & 0 deletions test/plugin-cli-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, FakeCommand>();
actionHandler?: (options: Record<string, unknown>) => void | Promise<void>;
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<string, unknown>) => void | Promise<void>): 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;
}
});
});
2 changes: 2 additions & 0 deletions types/openclaw-plugin-sdk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => void | Promise<void>): OpenClawPluginCliCommand;
outputHelp(): void;
}

export interface OpenClawPluginCliContext {
Expand Down Expand Up @@ -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;
Expand Down