Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
// Memory Wiki plugin module implements status behavior.
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { listActiveMemoryPublicArtifacts } from "openclaw/plugin-sdk/memory-host-core";
|
|
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
|
|
import type { OpenClawConfig } from "../api.js";
|
|
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
|
import { toWikiPageSummary, type WikiPageKind } from "./markdown.js";
|
|
import { probeObsidianCli } from "./obsidian.js";
|
|
|
|
type MemoryWikiStatusWarning = {
|
|
code:
|
|
| "vault-missing"
|
|
| "obsidian-cli-missing"
|
|
| "bridge-disabled"
|
|
| "bridge-artifacts-missing"
|
|
| "unsafe-local-disabled"
|
|
| "unsafe-local-paths-missing"
|
|
| "unsafe-local-without-mode";
|
|
message: string;
|
|
};
|
|
|
|
export type MemoryWikiStatus = {
|
|
vaultMode: ResolvedMemoryWikiConfig["vaultMode"];
|
|
renderMode: ResolvedMemoryWikiConfig["vault"]["renderMode"];
|
|
vaultPath: string;
|
|
vaultExists: boolean;
|
|
bridge: ResolvedMemoryWikiConfig["bridge"];
|
|
bridgePublicArtifactCount: number | null;
|
|
obsidianCli: {
|
|
enabled: boolean;
|
|
requested: boolean;
|
|
available: boolean;
|
|
command: string | null;
|
|
};
|
|
unsafeLocal: {
|
|
allowPrivateMemoryCoreAccess: boolean;
|
|
pathCount: number;
|
|
};
|
|
pageCounts: Record<WikiPageKind, number>;
|
|
sourceCounts: {
|
|
native: number;
|
|
bridge: number;
|
|
bridgeEvents: number;
|
|
unsafeLocal: number;
|
|
other: number;
|
|
};
|
|
warnings: MemoryWikiStatusWarning[];
|
|
};
|
|
|
|
type MemoryWikiDoctorFix = {
|
|
code: MemoryWikiStatusWarning["code"];
|
|
message: string;
|
|
};
|
|
|
|
export type MemoryWikiDoctorReport = {
|
|
healthy: boolean;
|
|
warningCount: number;
|
|
status: MemoryWikiStatus;
|
|
fixes: MemoryWikiDoctorFix[];
|
|
};
|
|
|
|
type ResolveMemoryWikiStatusDeps = {
|
|
appConfig?: OpenClawConfig;
|
|
pathExists?: (inputPath: string) => Promise<boolean>;
|
|
listPublicArtifacts?: typeof listActiveMemoryPublicArtifacts;
|
|
resolveCommand?: (command: string) => Promise<string | null>;
|
|
};
|
|
|
|
async function collectVaultCounts(vaultPath: string): Promise<{
|
|
pageCounts: Record<WikiPageKind, number>;
|
|
sourceCounts: MemoryWikiStatus["sourceCounts"];
|
|
}> {
|
|
const pageCounts: Record<WikiPageKind, number> = {
|
|
entity: 0,
|
|
concept: 0,
|
|
source: 0,
|
|
synthesis: 0,
|
|
report: 0,
|
|
};
|
|
const sourceCounts: MemoryWikiStatus["sourceCounts"] = {
|
|
native: 0,
|
|
bridge: 0,
|
|
bridgeEvents: 0,
|
|
unsafeLocal: 0,
|
|
other: 0,
|
|
};
|
|
const dirs = ["entities", "concepts", "sources", "syntheses", "reports"] as const;
|
|
for (const dir of dirs) {
|
|
const dirPath = path.join(vaultPath, dir);
|
|
const entries = await fs
|
|
.readdir(dirPath, { withFileTypes: true, recursive: true })
|
|
.catch(() => []);
|
|
for (const entry of entries) {
|
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "index.md") {
|
|
continue;
|
|
}
|
|
const absolutePath = path.join(entry.parentPath ?? dirPath, entry.name);
|
|
const relativeToVault = path.relative(vaultPath, absolutePath).split(path.sep).join("/");
|
|
const raw = await fs.readFile(absolutePath, "utf8").catch(() => null);
|
|
if (raw === null) {
|
|
continue;
|
|
}
|
|
const page = toWikiPageSummary({
|
|
absolutePath,
|
|
relativePath: relativeToVault,
|
|
raw,
|
|
});
|
|
if (!page) {
|
|
continue;
|
|
}
|
|
pageCounts[page.kind] += 1;
|
|
if (page.kind !== "source") {
|
|
continue;
|
|
}
|
|
if (page.sourceType === "memory-bridge-events") {
|
|
sourceCounts.bridgeEvents += 1;
|
|
} else if (page.sourceType === "memory-bridge") {
|
|
sourceCounts.bridge += 1;
|
|
} else if (
|
|
page.provenanceMode === "unsafe-local" ||
|
|
page.sourceType === "memory-unsafe-local"
|
|
) {
|
|
sourceCounts.unsafeLocal += 1;
|
|
} else if (!page.sourceType) {
|
|
sourceCounts.native += 1;
|
|
} else {
|
|
sourceCounts.other += 1;
|
|
}
|
|
}
|
|
}
|
|
return { pageCounts, sourceCounts };
|
|
}
|
|
|
|
function buildWarnings(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
bridgePublicArtifactCount: number | null;
|
|
vaultExists: boolean;
|
|
obsidianCommand: string | null;
|
|
}): MemoryWikiStatusWarning[] {
|
|
const warnings: MemoryWikiStatusWarning[] = [];
|
|
if (!params.vaultExists) {
|
|
warnings.push({
|
|
code: "vault-missing",
|
|
message: "Wiki vault has not been initialized yet.",
|
|
});
|
|
}
|
|
if (
|
|
params.config.obsidian.enabled &&
|
|
params.config.obsidian.useOfficialCli &&
|
|
!params.obsidianCommand
|
|
) {
|
|
warnings.push({
|
|
code: "obsidian-cli-missing",
|
|
message: "Obsidian CLI is enabled in config but `obsidian` is not available on PATH.",
|
|
});
|
|
}
|
|
if (params.config.vaultMode === "bridge" && !params.config.bridge.enabled) {
|
|
warnings.push({
|
|
code: "bridge-disabled",
|
|
message: "vaultMode is `bridge` but bridge.enabled is false.",
|
|
});
|
|
}
|
|
if (
|
|
params.config.vaultMode === "bridge" &&
|
|
params.config.bridge.enabled &&
|
|
params.config.bridge.readMemoryArtifacts &&
|
|
params.bridgePublicArtifactCount === 0
|
|
) {
|
|
warnings.push({
|
|
code: "bridge-artifacts-missing",
|
|
message:
|
|
"Bridge mode is enabled but the active memory plugin is not exporting any public memory artifacts yet.",
|
|
});
|
|
}
|
|
if (
|
|
params.config.vaultMode === "unsafe-local" &&
|
|
!params.config.unsafeLocal.allowPrivateMemoryCoreAccess
|
|
) {
|
|
warnings.push({
|
|
code: "unsafe-local-disabled",
|
|
message: "vaultMode is `unsafe-local` but private memory-core access is disabled.",
|
|
});
|
|
}
|
|
if (
|
|
params.config.vaultMode === "unsafe-local" &&
|
|
params.config.unsafeLocal.allowPrivateMemoryCoreAccess &&
|
|
params.config.unsafeLocal.paths.length === 0
|
|
) {
|
|
warnings.push({
|
|
code: "unsafe-local-paths-missing",
|
|
message: "unsafe-local access is enabled but no private paths are configured.",
|
|
});
|
|
}
|
|
if (
|
|
params.config.vaultMode !== "unsafe-local" &&
|
|
params.config.unsafeLocal.allowPrivateMemoryCoreAccess
|
|
) {
|
|
warnings.push({
|
|
code: "unsafe-local-without-mode",
|
|
message: "Private memory-core access is enabled outside unsafe-local mode.",
|
|
});
|
|
}
|
|
return warnings;
|
|
}
|
|
|
|
export async function resolveMemoryWikiStatus(
|
|
config: ResolvedMemoryWikiConfig,
|
|
deps?: ResolveMemoryWikiStatusDeps,
|
|
): Promise<MemoryWikiStatus> {
|
|
const exists = deps?.pathExists ?? pathExists;
|
|
const vaultExists = await exists(config.vault.path);
|
|
const bridgePublicArtifactCount =
|
|
deps?.appConfig && config.vaultMode === "bridge" && config.bridge.enabled
|
|
? (
|
|
await (deps.listPublicArtifacts ?? listActiveMemoryPublicArtifacts)({
|
|
cfg: deps.appConfig,
|
|
})
|
|
).length
|
|
: null;
|
|
const obsidianProbe = await probeObsidianCli({ resolveCommand: deps?.resolveCommand });
|
|
const counts = vaultExists
|
|
? await collectVaultCounts(config.vault.path)
|
|
: {
|
|
pageCounts: {
|
|
entity: 0,
|
|
concept: 0,
|
|
source: 0,
|
|
synthesis: 0,
|
|
report: 0,
|
|
},
|
|
sourceCounts: {
|
|
native: 0,
|
|
bridge: 0,
|
|
bridgeEvents: 0,
|
|
unsafeLocal: 0,
|
|
other: 0,
|
|
},
|
|
};
|
|
|
|
return {
|
|
vaultMode: config.vaultMode,
|
|
renderMode: config.vault.renderMode,
|
|
vaultPath: config.vault.path,
|
|
vaultExists,
|
|
bridge: config.bridge,
|
|
bridgePublicArtifactCount,
|
|
obsidianCli: {
|
|
enabled: config.obsidian.enabled,
|
|
requested: config.obsidian.enabled && config.obsidian.useOfficialCli,
|
|
available: obsidianProbe.available,
|
|
command: obsidianProbe.command,
|
|
},
|
|
unsafeLocal: {
|
|
allowPrivateMemoryCoreAccess: config.unsafeLocal.allowPrivateMemoryCoreAccess,
|
|
pathCount: config.unsafeLocal.paths.length,
|
|
},
|
|
pageCounts: counts.pageCounts,
|
|
sourceCounts: counts.sourceCounts,
|
|
warnings: buildWarnings({
|
|
config,
|
|
bridgePublicArtifactCount,
|
|
vaultExists,
|
|
obsidianCommand: obsidianProbe.command,
|
|
}),
|
|
};
|
|
}
|
|
|
|
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 === "bridge-artifacts-missing"
|
|
? "Use a memory plugin that exports public artifacts, create/import memory artifacts first, or switch the wiki back to isolated mode."
|
|
: 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}`,
|
|
`Vault: ${status.vaultExists ? "ready" : "missing"} (${status.vaultPath})`,
|
|
`Render mode: ${status.renderMode}`,
|
|
`Obsidian CLI: ${status.obsidianCli.available ? "available" : "missing"}${status.obsidianCli.requested ? " (requested)" : ""}`,
|
|
`Bridge: ${status.bridge.enabled ? "enabled" : "disabled"}${typeof status.bridgePublicArtifactCount === "number" ? ` (${status.bridgePublicArtifactCount} exported artifact${status.bridgePublicArtifactCount === 1 ? "" : "s"})` : ""}`,
|
|
`Unsafe local: ${status.unsafeLocal.allowPrivateMemoryCoreAccess ? `enabled (${status.unsafeLocal.pathCount} paths)` : "disabled"}`,
|
|
`Pages: ${status.pageCounts.source} sources, ${status.pageCounts.entity} entities, ${status.pageCounts.concept} concepts, ${status.pageCounts.synthesis} syntheses, ${status.pageCounts.report} reports`,
|
|
`Source provenance: ${status.sourceCounts.native} native, ${status.sourceCounts.bridge} bridge, ${status.sourceCounts.bridgeEvents} bridge-events, ${status.sourceCounts.unsafeLocal} unsafe-local, ${status.sourceCounts.other} other`,
|
|
];
|
|
|
|
if (status.warnings.length > 0) {
|
|
lines.push("", "Warnings:");
|
|
for (const warning of status.warnings) {
|
|
lines.push(`- ${warning.message}`);
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|