feat(memory-wiki): extend gateway wiki controls

This commit is contained in:
Vincent Koc
2026-04-05 21:23:50 +01:00
parent c2a8aac282
commit d66960206b
5 changed files with 167 additions and 50 deletions

View File

@@ -32,8 +32,13 @@ describe("memory-wiki plugin", () => {
"wiki.compile",
"wiki.lint",
"wiki.search",
"wiki.apply",
"wiki.get",
"wiki.obsidian.status",
"wiki.obsidian.search",
"wiki.obsidian.open",
"wiki.obsidian.command",
"wiki.obsidian.daily",
]);
expect(registerTool).toHaveBeenCalledTimes(5);
expect(registerTool.mock.calls.map((call) => call[1]?.name)).toEqual([

View File

@@ -57,6 +57,53 @@ export type ApplyMemoryWikiMutationResult = {
compile: CompileMemoryWikiResult;
};
export function normalizeMemoryWikiMutationInput(rawParams: unknown): ApplyMemoryWikiMutation {
const params = rawParams as {
op: ApplyMemoryWikiMutation["op"];
title?: string;
body?: string;
lookup?: string;
sourceIds?: string[];
contradictions?: string[];
questions?: string[];
confidence?: number | null;
status?: string;
};
if (params.op === "create_synthesis") {
if (!params.title?.trim()) {
throw new Error("wiki mutation requires title for create_synthesis.");
}
if (!params.body?.trim()) {
throw new Error("wiki mutation requires body for create_synthesis.");
}
if (!params.sourceIds || params.sourceIds.length === 0) {
throw new Error("wiki mutation requires at least one sourceId for create_synthesis.");
}
return {
op: "create_synthesis",
title: params.title,
body: params.body,
sourceIds: params.sourceIds,
...(params.contradictions ? { contradictions: params.contradictions } : {}),
...(params.questions ? { questions: params.questions } : {}),
...(typeof params.confidence === "number" ? { confidence: params.confidence } : {}),
...(params.status ? { status: params.status } : {}),
};
}
if (!params.lookup?.trim()) {
throw new Error("wiki mutation requires lookup for update_metadata.");
}
return {
op: "update_metadata",
lookup: params.lookup,
...(params.sourceIds ? { sourceIds: params.sourceIds } : {}),
...(params.contradictions ? { contradictions: params.contradictions } : {}),
...(params.questions ? { questions: params.questions } : {}),
...(params.confidence !== undefined ? { confidence: params.confidence } : {}),
...(params.status ? { status: params.status } : {}),
};
}
function normalizeUniqueStrings(values: string[] | undefined): string[] | undefined {
if (!values) {
return undefined;

View File

@@ -108,4 +108,39 @@ describe("memory-wiki gateway methods", () => {
expect.objectContaining({ message: "query is required." }),
);
});
it("applies wiki mutations over the gateway", async () => {
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-gateway-"));
tempDirs.push(rootDir);
const { api, registerGatewayMethod } = createGatewayApi();
const config = resolveMemoryWikiConfig(
{ vault: { path: rootDir } },
{ homedir: "/Users/tester" },
);
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.apply");
if (!handler) {
throw new Error("wiki.apply handler missing");
}
const respond = vi.fn();
await handler({
params: {
op: "create_synthesis",
title: "Gateway Alpha",
body: "Gateway summary.",
sourceIds: ["source.alpha"],
},
respond,
});
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
operation: "create_synthesis",
pagePath: "syntheses/gateway-alpha.md",
}),
);
});
});

View File

@@ -1,8 +1,15 @@
import type { OpenClawConfig, OpenClawPluginApi } from "../api.js";
import { applyMemoryWikiMutation, normalizeMemoryWikiMutationInput } from "./apply.js";
import { compileMemoryWikiVault } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { lintMemoryWikiVault } from "./lint.js";
import { probeObsidianCli } from "./obsidian.js";
import {
probeObsidianCli,
runObsidianCommand,
runObsidianDaily,
runObsidianOpen,
runObsidianSearch,
} from "./obsidian.js";
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
import { syncMemoryWikiImportedSources } from "./source-sync.js";
import { buildMemoryWikiDoctorReport, resolveMemoryWikiStatus } from "./status.js";
@@ -140,6 +147,25 @@ export function registerMemoryWikiGatewayMethods(params: {
{ scope: READ_SCOPE },
);
api.registerGatewayMethod(
"wiki.apply",
async ({ params: requestParams, respond }) => {
try {
await syncImportedSourcesIfNeeded(config, appConfig);
respond(
true,
await applyMemoryWikiMutation({
config,
mutation: normalizeMemoryWikiMutationInput(requestParams),
}),
);
} catch (error) {
respondError(respond, error);
}
},
{ scope: WRITE_SCOPE },
);
api.registerGatewayMethod(
"wiki.get",
async ({ params: requestParams, respond }) => {
@@ -175,4 +201,55 @@ export function registerMemoryWikiGatewayMethods(params: {
},
{ scope: READ_SCOPE },
);
api.registerGatewayMethod(
"wiki.obsidian.search",
async ({ params: requestParams, respond }) => {
try {
const query = readStringParam(requestParams, "query", { required: true });
respond(true, await runObsidianSearch({ config, query }));
} catch (error) {
respondError(respond, error);
}
},
{ scope: READ_SCOPE },
);
api.registerGatewayMethod(
"wiki.obsidian.open",
async ({ params: requestParams, respond }) => {
try {
const vaultPath = readStringParam(requestParams, "path", { required: true });
respond(true, await runObsidianOpen({ config, vaultPath }));
} catch (error) {
respondError(respond, error);
}
},
{ scope: WRITE_SCOPE },
);
api.registerGatewayMethod(
"wiki.obsidian.command",
async ({ params: requestParams, respond }) => {
try {
const id = readStringParam(requestParams, "id", { required: true });
respond(true, await runObsidianCommand({ config, id }));
} catch (error) {
respondError(respond, error);
}
},
{ scope: WRITE_SCOPE },
);
api.registerGatewayMethod(
"wiki.obsidian.daily",
async ({ respond }) => {
try {
respond(true, await runObsidianDaily({ config }));
} catch (error) {
respondError(respond, error);
}
},
{ scope: WRITE_SCOPE },
);
}

View File

@@ -1,6 +1,6 @@
import { Type } from "@sinclair/typebox";
import type { AnyAgentTool, OpenClawConfig } from "../api.js";
import { applyMemoryWikiMutation, type ApplyMemoryWikiMutation } from "./apply.js";
import { applyMemoryWikiMutation, normalizeMemoryWikiMutationInput } from "./apply.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { lintMemoryWikiVault } from "./lint.js";
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
@@ -46,53 +46,6 @@ async function syncImportedSourcesIfNeeded(
await syncMemoryWikiImportedSources({ config, appConfig });
}
function normalizeWikiApplyMutation(rawParams: unknown): ApplyMemoryWikiMutation {
const params = rawParams as {
op: ApplyMemoryWikiMutation["op"];
title?: string;
body?: string;
lookup?: string;
sourceIds?: string[];
contradictions?: string[];
questions?: string[];
confidence?: number | null;
status?: string;
};
if (params.op === "create_synthesis") {
if (!params.title?.trim()) {
throw new Error("wiki_apply requires title for create_synthesis.");
}
if (!params.body?.trim()) {
throw new Error("wiki_apply requires body for create_synthesis.");
}
if (!params.sourceIds || params.sourceIds.length === 0) {
throw new Error("wiki_apply requires at least one sourceId for create_synthesis.");
}
return {
op: "create_synthesis",
title: params.title,
body: params.body,
sourceIds: params.sourceIds,
...(params.contradictions ? { contradictions: params.contradictions } : {}),
...(params.questions ? { questions: params.questions } : {}),
...(typeof params.confidence === "number" ? { confidence: params.confidence } : {}),
...(params.status ? { status: params.status } : {}),
};
}
if (!params.lookup?.trim()) {
throw new Error("wiki_apply requires lookup for update_metadata.");
}
return {
op: "update_metadata",
lookup: params.lookup,
...(params.sourceIds ? { sourceIds: params.sourceIds } : {}),
...(params.contradictions ? { contradictions: params.contradictions } : {}),
...(params.questions ? { questions: params.questions } : {}),
...(params.confidence !== undefined ? { confidence: params.confidence } : {}),
...(params.status ? { status: params.status } : {}),
};
}
export function createWikiStatusTool(
config: ResolvedMemoryWikiConfig,
appConfig?: OpenClawConfig,
@@ -195,7 +148,7 @@ export function createWikiApplyTool(
"Apply narrow wiki mutations for syntheses and page metadata without freeform markdown surgery.",
parameters: WikiApplySchema,
execute: async (_toolCallId, rawParams) => {
const mutation = normalizeWikiApplyMutation(rawParams);
const mutation = normalizeMemoryWikiMutationInput(rawParams);
await syncImportedSourcesIfNeeded(config, appConfig);
const result = await applyMemoryWikiMutation({ config, mutation });
const action = result.changed ? "Updated" : "No changes for";