feat(memory-wiki): add wiki doctor diagnostics

This commit is contained in:
Vincent Koc
2026-04-05 21:19:02 +01:00
parent afb89b439a
commit 5a6d80da7f
4 changed files with 142 additions and 2 deletions

View File

@@ -19,10 +19,12 @@ describe("memory-wiki cli", () => {
vi.spyOn(process.stdout, "write").mockImplementation(
(() => true) as typeof process.stdout.write,
);
process.exitCode = undefined;
});
afterEach(() => {
vi.restoreAllMocks();
process.exitCode = undefined;
});
it("registers apply synthesis and writes a synthesis page", async () => {
@@ -123,4 +125,24 @@ cli note
expect(parsed.frontmatter).not.toHaveProperty("confidence");
expect(parsed.body).toContain("cli note");
});
it("runs wiki doctor and sets a non-zero exit code when warnings exist", async () => {
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-cli-"));
tempDirs.push(rootDir);
const config = resolveMemoryWikiConfig(
{
vault: { path: rootDir },
obsidian: { enabled: true, useOfficialCli: true },
},
{ homedir: "/Users/tester" },
);
const program = new Command();
program.name("test");
registerWikiCli(program, config);
await fs.rm(rootDir, { recursive: true, force: true });
await program.parseAsync(["wiki", "doctor", "--json"], { from: "user" });
expect(process.exitCode).toBe(1);
});
});

View File

@@ -16,13 +16,22 @@ import {
} from "./obsidian.js";
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
import { syncMemoryWikiImportedSources } from "./source-sync.js";
import { renderMemoryWikiStatus, resolveMemoryWikiStatus } from "./status.js";
import {
buildMemoryWikiDoctorReport,
renderMemoryWikiDoctor,
renderMemoryWikiStatus,
resolveMemoryWikiStatus,
} from "./status.js";
import { initializeMemoryWikiVault } from "./vault.js";
type WikiStatusCommandOptions = {
json?: boolean;
};
type WikiDoctorCommandOptions = {
json?: boolean;
};
type WikiInitCommandOptions = {
json?: boolean;
};
@@ -149,6 +158,24 @@ export async function runWikiStatus(params: {
return status;
}
export async function runWikiDoctor(params: {
config: ResolvedMemoryWikiConfig;
appConfig?: OpenClawConfig;
json?: boolean;
stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
const report = buildMemoryWikiDoctorReport(await resolveMemoryWikiStatus(params.config));
if (!report.healthy) {
process.exitCode = 1;
}
writeOutput(
params.json ? JSON.stringify(report, null, 2) : renderMemoryWikiDoctor(report),
params.stdout,
);
return report;
}
export async function runWikiInit(params: {
config: ResolvedMemoryWikiConfig;
json?: boolean;
@@ -469,6 +496,14 @@ export function registerWikiCli(
await runWikiStatus({ config, appConfig, json: opts.json });
});
wiki
.command("doctor")
.description("Audit wiki vault setup and report actionable fixes")
.option("--json", "Print JSON")
.action(async (opts: WikiDoctorCommandOptions) => {
await runWikiDoctor({ config, appConfig, json: opts.json });
});
wiki
.command("init")
.description("Initialize the wiki vault layout")

View File

@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
import { resolveMemoryWikiConfig } from "./config.js";
import { renderMemoryWikiStatus, resolveMemoryWikiStatus } from "./status.js";
import {
buildMemoryWikiDoctorReport,
renderMemoryWikiDoctor,
renderMemoryWikiStatus,
resolveMemoryWikiStatus,
} from "./status.js";
describe("resolveMemoryWikiStatus", () => {
it("reports missing vault and missing requested obsidian cli", async () => {
@@ -83,3 +88,28 @@ describe("renderMemoryWikiStatus", () => {
expect(rendered).toContain("Wiki vault has not been initialized yet.");
});
});
describe("memory wiki doctor", () => {
it("builds actionable fixes from status warnings", async () => {
const config = resolveMemoryWikiConfig(
{
vault: { path: "/tmp/wiki" },
obsidian: { enabled: true, useOfficialCli: true },
},
{ homedir: "/Users/tester" },
);
const status = await resolveMemoryWikiStatus(config, {
pathExists: async () => false,
resolveCommand: async () => null,
});
const report = buildMemoryWikiDoctorReport(status);
const rendered = renderMemoryWikiDoctor(report);
expect(report.healthy).toBe(false);
expect(report.warningCount).toBe(2);
expect(report.fixes.map((fix) => fix.code)).toEqual(["vault-missing", "obsidian-cli-missing"]);
expect(rendered).toContain("Suggested fixes:");
expect(rendered).toContain("openclaw wiki init");
});
});

View File

@@ -35,6 +35,18 @@ export type MemoryWikiStatus = {
warnings: MemoryWikiStatusWarning[];
};
export type MemoryWikiDoctorFix = {
code: MemoryWikiStatusWarning["code"];
message: string;
};
export type MemoryWikiDoctorReport = {
healthy: boolean;
warningCount: number;
status: MemoryWikiStatus;
fixes: MemoryWikiDoctorFix[];
};
type ResolveMemoryWikiStatusDeps = {
pathExists?: (inputPath: string) => Promise<boolean>;
resolveCommand?: (command: string) => Promise<string | null>;
@@ -172,6 +184,30 @@ export async function resolveMemoryWikiStatus(
};
}
export function buildMemoryWikiDoctorReport(status: MemoryWikiStatus): MemoryWikiDoctorReport {
const fixes = status.warnings.map((warning) => ({
code: warning.code,
message:
warning.code === "vault-missing"
? "Run `openclaw wiki init` to create the vault layout."
: warning.code === "obsidian-cli-missing"
? "Install the official Obsidian CLI or disable `obsidian.useOfficialCli`."
: warning.code === "bridge-disabled"
? "Enable `plugins.entries.memory-wiki.config.bridge.enabled` or switch vaultMode away from `bridge`."
: warning.code === "unsafe-local-disabled"
? "Enable `unsafeLocal.allowPrivateMemoryCoreAccess` or switch vaultMode away from `unsafe-local`."
: warning.code === "unsafe-local-paths-missing"
? "Add explicit `unsafeLocal.paths` entries before running unsafe-local imports."
: "Disable private memory-core access unless you explicitly want unsafe-local mode.",
}));
return {
healthy: status.warnings.length === 0,
warningCount: status.warnings.length,
status,
fixes,
};
}
export function renderMemoryWikiStatus(status: MemoryWikiStatus): string {
const lines = [
`Wiki vault mode: ${status.vaultMode}`,
@@ -192,3 +228,20 @@ export function renderMemoryWikiStatus(status: MemoryWikiStatus): string {
return lines.join("\n");
}
export function renderMemoryWikiDoctor(report: MemoryWikiDoctorReport): string {
const lines = [
report.healthy ? "Wiki doctor: healthy" : `Wiki doctor: ${report.warningCount} issue(s) found`,
"",
renderMemoryWikiStatus(report.status),
];
if (report.fixes.length > 0) {
lines.push("", "Suggested fixes:");
for (const fix of report.fixes) {
lines.push(`- ${fix.message}`);
}
}
return lines.join("\n");
}