mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 13:11:40 +00:00
feat(memory-wiki): add wiki apply cli commands
This commit is contained in:
126
extensions/memory-wiki/src/cli.test.ts
Normal file
126
extensions/memory-wiki/src/cli.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { registerWikiCli } from "./cli.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
import { parseWikiMarkdown, renderWikiMarkdown } from "./markdown.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("memory-wiki cli", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(process.stdout, "write").mockImplementation(
|
||||
(() => true) as typeof process.stdout.write,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("registers apply synthesis and writes a synthesis page", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-cli-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: rootDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
const program = new Command();
|
||||
program.name("test");
|
||||
registerWikiCli(program, config);
|
||||
|
||||
await program.parseAsync(
|
||||
[
|
||||
"wiki",
|
||||
"apply",
|
||||
"synthesis",
|
||||
"CLI Alpha",
|
||||
"--body",
|
||||
"Alpha from CLI.",
|
||||
"--source-id",
|
||||
"source.alpha",
|
||||
"--source-id",
|
||||
"source.beta",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const page = await fs.readFile(path.join(rootDir, "syntheses", "cli-alpha.md"), "utf8");
|
||||
expect(page).toContain("Alpha from CLI.");
|
||||
expect(page).toContain("source.alpha");
|
||||
await expect(fs.readFile(path.join(rootDir, "index.md"), "utf8")).resolves.toContain(
|
||||
"[CLI Alpha](syntheses/cli-alpha.md)",
|
||||
);
|
||||
});
|
||||
|
||||
it("registers apply metadata and preserves the page body", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-cli-"));
|
||||
tempDirs.push(rootDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: rootDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "entities", "alpha.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "entity",
|
||||
id: "entity.alpha",
|
||||
title: "Alpha",
|
||||
sourceIds: ["source.old"],
|
||||
confidence: 0.2,
|
||||
},
|
||||
body: `# Alpha
|
||||
|
||||
## Notes
|
||||
<!-- openclaw:human:start -->
|
||||
cli note
|
||||
<!-- openclaw:human:end -->
|
||||
`,
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const program = new Command();
|
||||
program.name("test");
|
||||
registerWikiCli(program, config);
|
||||
|
||||
await program.parseAsync(
|
||||
[
|
||||
"wiki",
|
||||
"apply",
|
||||
"metadata",
|
||||
"entity.alpha",
|
||||
"--source-id",
|
||||
"source.new",
|
||||
"--contradiction",
|
||||
"Conflicts with source.beta",
|
||||
"--question",
|
||||
"Still active?",
|
||||
"--status",
|
||||
"review",
|
||||
"--clear-confidence",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const page = await fs.readFile(path.join(rootDir, "entities", "alpha.md"), "utf8");
|
||||
const parsed = parseWikiMarkdown(page);
|
||||
expect(parsed.frontmatter).toMatchObject({
|
||||
sourceIds: ["source.new"],
|
||||
contradictions: ["Conflicts with source.beta"],
|
||||
questions: ["Still active?"],
|
||||
status: "review",
|
||||
});
|
||||
expect(parsed.frontmatter).not.toHaveProperty("confidence");
|
||||
expect(parsed.body).toContain("cli note");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import type { Command } from "commander";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import { applyMemoryWikiMutation } from "./apply.js";
|
||||
import { compileMemoryWikiVault } from "./compile.js";
|
||||
import type { MemoryWikiPluginConfig, ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
@@ -49,6 +51,27 @@ type WikiGetCommandOptions = {
|
||||
lines?: number;
|
||||
};
|
||||
|
||||
type WikiApplySynthesisCommandOptions = {
|
||||
json?: boolean;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
sourceId?: string[];
|
||||
contradiction?: string[];
|
||||
question?: string[];
|
||||
confidence?: number;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type WikiApplyMetadataCommandOptions = {
|
||||
json?: boolean;
|
||||
sourceId?: string[];
|
||||
contradiction?: string[];
|
||||
question?: string[];
|
||||
confidence?: number;
|
||||
clearConfidence?: boolean;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type WikiBridgeImportCommandOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
@@ -90,6 +113,27 @@ function writeOutput(output: string, writer: Pick<NodeJS.WriteStream, "write"> =
|
||||
writer.write(output.endsWith("\n") ? output : `${output}\n`);
|
||||
}
|
||||
|
||||
function normalizeCliStringList(values?: string[]): string[] | undefined {
|
||||
if (!values) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = values
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.filter((value, index, all) => all.indexOf(value) === index);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
async function resolveWikiApplyBody(params: { body?: string; bodyFile?: string }): Promise<string> {
|
||||
if (params.body?.trim()) {
|
||||
return params.body;
|
||||
}
|
||||
if (params.bodyFile?.trim()) {
|
||||
return await fs.readFile(params.bodyFile, "utf8");
|
||||
}
|
||||
throw new Error("wiki apply synthesis requires --body or --body-file.");
|
||||
}
|
||||
|
||||
export async function runWikiStatus(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
@@ -218,6 +262,93 @@ export async function runWikiGet(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiApplySynthesis(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
title: string;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
sourceIds?: string[];
|
||||
contradictions?: string[];
|
||||
questions?: string[];
|
||||
confidence?: number;
|
||||
status?: string;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const sourceIds = normalizeCliStringList(params.sourceIds);
|
||||
if (!sourceIds) {
|
||||
throw new Error("wiki apply synthesis requires at least one --source-id.");
|
||||
}
|
||||
const body = await resolveWikiApplyBody({ body: params.body, bodyFile: params.bodyFile });
|
||||
await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
|
||||
const result = await applyMemoryWikiMutation({
|
||||
config: params.config,
|
||||
mutation: {
|
||||
op: "create_synthesis",
|
||||
title: params.title,
|
||||
body,
|
||||
sourceIds,
|
||||
...(normalizeCliStringList(params.contradictions)
|
||||
? { contradictions: normalizeCliStringList(params.contradictions) }
|
||||
: {}),
|
||||
...(normalizeCliStringList(params.questions)
|
||||
? { questions: normalizeCliStringList(params.questions) }
|
||||
: {}),
|
||||
...(typeof params.confidence === "number" ? { confidence: params.confidence } : {}),
|
||||
...(params.status?.trim() ? { status: params.status.trim() } : {}),
|
||||
},
|
||||
});
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: `${result.changed ? "Updated" : "No changes for"} ${result.pagePath} via ${result.operation}. ${result.compile.updatedFiles.length > 0 ? `Refreshed ${result.compile.updatedFiles.length} index file${result.compile.updatedFiles.length === 1 ? "" : "s"}.` : "Indexes unchanged."}`;
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiApplyMetadata(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
lookup: string;
|
||||
sourceIds?: string[];
|
||||
contradictions?: string[];
|
||||
questions?: string[];
|
||||
confidence?: number;
|
||||
clearConfidence?: boolean;
|
||||
status?: string;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
|
||||
const result = await applyMemoryWikiMutation({
|
||||
config: params.config,
|
||||
mutation: {
|
||||
op: "update_metadata",
|
||||
lookup: params.lookup,
|
||||
...(normalizeCliStringList(params.sourceIds)
|
||||
? { sourceIds: normalizeCliStringList(params.sourceIds) }
|
||||
: {}),
|
||||
...(normalizeCliStringList(params.contradictions)
|
||||
? { contradictions: normalizeCliStringList(params.contradictions) }
|
||||
: {}),
|
||||
...(normalizeCliStringList(params.questions)
|
||||
? { questions: normalizeCliStringList(params.questions) }
|
||||
: {}),
|
||||
...(params.clearConfidence
|
||||
? { confidence: null }
|
||||
: typeof params.confidence === "number"
|
||||
? { confidence: params.confidence }
|
||||
: {}),
|
||||
...(params.status?.trim() ? { status: params.status.trim() } : {}),
|
||||
},
|
||||
});
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: `${result.changed ? "Updated" : "No changes for"} ${result.pagePath} via ${result.operation}. ${result.compile.updatedFiles.length > 0 ? `Refreshed ${result.compile.updatedFiles.length} index file${result.compile.updatedFiles.length === 1 ? "" : "s"}.` : "Indexes unchanged."}`;
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiBridgeImport(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
@@ -406,6 +537,82 @@ export function registerWikiCli(
|
||||
});
|
||||
});
|
||||
|
||||
const apply = wiki.command("apply").description("Apply narrow wiki mutations");
|
||||
apply
|
||||
.command("synthesis")
|
||||
.description("Create or refresh a synthesis page with managed summary content")
|
||||
.argument("<title>", "Synthesis title")
|
||||
.option("--body <text>", "Summary body text")
|
||||
.option("--body-file <path>", "Read summary body text from a file")
|
||||
.option("--source-id <id>", "Source id", (value: string, acc: string[] = []) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
})
|
||||
.option("--contradiction <text>", "Contradiction note", (value: string, acc: string[] = []) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
})
|
||||
.option("--question <text>", "Open question", (value: string, acc: string[] = []) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
})
|
||||
.option("--confidence <n>", "Confidence score between 0 and 1", (value: string) =>
|
||||
Number(value),
|
||||
)
|
||||
.option("--status <status>", "Page status")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (title: string, opts: WikiApplySynthesisCommandOptions) => {
|
||||
await runWikiApplySynthesis({
|
||||
config,
|
||||
appConfig,
|
||||
title,
|
||||
body: opts.body,
|
||||
bodyFile: opts.bodyFile,
|
||||
sourceIds: opts.sourceId,
|
||||
contradictions: opts.contradiction,
|
||||
questions: opts.question,
|
||||
confidence: opts.confidence,
|
||||
status: opts.status,
|
||||
json: opts.json,
|
||||
});
|
||||
});
|
||||
apply
|
||||
.command("metadata")
|
||||
.description("Update metadata on an existing page")
|
||||
.argument("<lookup>", "Relative path or page id")
|
||||
.option("--source-id <id>", "Source id", (value: string, acc: string[] = []) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
})
|
||||
.option("--contradiction <text>", "Contradiction note", (value: string, acc: string[] = []) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
})
|
||||
.option("--question <text>", "Open question", (value: string, acc: string[] = []) => {
|
||||
acc.push(value);
|
||||
return acc;
|
||||
})
|
||||
.option("--confidence <n>", "Confidence score between 0 and 1", (value: string) =>
|
||||
Number(value),
|
||||
)
|
||||
.option("--clear-confidence", "Remove any stored confidence value")
|
||||
.option("--status <status>", "Page status")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (lookup: string, opts: WikiApplyMetadataCommandOptions) => {
|
||||
await runWikiApplyMetadata({
|
||||
config,
|
||||
appConfig,
|
||||
lookup,
|
||||
sourceIds: opts.sourceId,
|
||||
contradictions: opts.contradiction,
|
||||
questions: opts.question,
|
||||
confidence: opts.confidence,
|
||||
clearConfidence: opts.clearConfidence,
|
||||
status: opts.status,
|
||||
json: opts.json,
|
||||
});
|
||||
});
|
||||
|
||||
const bridge = wiki
|
||||
.command("bridge")
|
||||
.description("Import public memory-core artifacts into the wiki vault");
|
||||
|
||||
Reference in New Issue
Block a user