openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,043 lines (1,042 loc) • 43.9 kB
JavaScript
import { i as formatErrorMessage } from "../../errors-BXgSefBE.js";
import { _ as uniqueStrings } from "../../string-normalization-WNUDCpXX.js";
import { c as resolveDefaultAgentId } from "../../agent-scope-config-CgCYpZfK.js";
import { g as readPositiveIntegerParam } from "../../common-C4yy9V-D.js";
import { r as optionalFiniteNumberSchema } from "../../typebox-DCO1JbMn.js";
import "../../error-runtime-C8vbtAJt.js";
import "../../string-coerce-runtime-CEGJWkQ_.js";
import { t as definePluginEntry } from "../../plugin-entry-C7DUzV0e.js";
import "../../channel-actions-BOfvC-Gl.js";
import "../../memory-host-core-CImhJ6yP.js";
import "../../param-readers-HkqGooLN.js";
import "../../api-DLTB1023.js";
import { A as lintMemoryWikiVault, B as parseWikiMarkdown, C as resolveMemoryWikiStatus, D as runObsidianDaily, E as runObsidianCommand, F as getMemoryWikiPage, I as readQueryableWikiPages, L as searchMemoryWiki, M as applyMemoryWikiMutation, N as normalizeMemoryWikiMutationInput, O as runObsidianOpen, P as WIKI_SEARCH_MODES, R as compileMemoryWikiVault, S as renderMemoryWikiStatus, T as probeObsidianCli, j as ingestMemoryWikiSource, k as runObsidianSearch, t as registerWikiCli, w as syncMemoryWikiImportedSources, x as buildMemoryWikiDoctorReport, z as initializeMemoryWikiVault } from "../../cli-BgTPc7PL.js";
import { _ as createMemoryWikiImportRunStateStore, a as createMemoryWikiSourceSyncStateStore, h as configureMemoryWikiImportRunStateStore, i as configureMemoryWikiSourceSyncStateStore, v as listMemoryWikiImportRunRecords } from "../../source-sync-state-NJXr_mn7.js";
import { d as resolveMemoryWikiConfig, l as memoryWikiConfigSchema, o as WIKI_SEARCH_BACKENDS, s as WIKI_SEARCH_CORPORA } from "../../config-esaIRIHL.js";
import fs from "node:fs";
import path from "node:path";
import { Type } from "typebox";
//#region extensions/memory-wiki/src/corpus-supplement.ts
function createWikiCorpusSupplement(params) {
return {
search: async (input) => await searchMemoryWiki({
config: params.config,
appConfig: params.appConfig,
agentSessionKey: input.agentSessionKey,
query: input.query,
maxResults: input.maxResults,
searchBackend: "local",
searchCorpus: "wiki"
}),
get: async (input) => await getMemoryWikiPage({
config: params.config,
appConfig: params.appConfig,
agentSessionKey: input.agentSessionKey,
lookup: input.lookup,
fromLine: input.fromLine,
lineCount: input.lineCount,
searchBackend: "local",
searchCorpus: "wiki"
})
};
}
//#endregion
//#region extensions/memory-wiki/src/import-insights.ts
function normalizeStringArray(value) {
if (!Array.isArray(value)) return [];
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
}
function normalizeFiniteInt(value) {
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
return Math.max(0, Math.floor(value));
}
function normalizeTimestamp$1(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function humanizeLabelSuffix(label) {
return (label.includes("/") ? label.split("/").slice(1).join("/") : label).split(/[/-]/g).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
}
function resolveTopic(labels) {
const preferred = labels.find((label) => label.startsWith("topic/")) ?? labels.find((label) => label.startsWith("area/")) ?? labels.find((label) => label.startsWith("domain/")) ?? "topic/other";
return {
key: preferred,
label: humanizeLabelSuffix(preferred)
};
}
function extractHeadingSection(body, heading) {
const lines = body.split(/\r?\n/);
const headingLine = `## ${heading}`;
const startIndex = lines.findIndex((line) => line.trim() === headingLine);
if (startIndex < 0) return [];
const section = [];
for (const line of lines.slice(startIndex + 1)) {
if (line.startsWith("## ")) break;
if (line.trim().length > 0) section.push(line.trimEnd());
}
return section;
}
function extractDigestField(lines, prefix) {
const needle = `- ${prefix}:`;
const line = lines.find((entry) => entry.startsWith(needle));
if (!line) return;
const value = line.slice(needle.length).trim();
return value.length > 0 ? value : void 0;
}
function extractIntegerField(lines, prefix) {
const raw = extractDigestField(lines, prefix);
if (!raw) return 0;
const match = raw.match(/\d+/);
return match ? normalizeFiniteInt(Number(match[0])) : 0;
}
function extractPreferenceSignals(lines) {
const startIndex = lines.findIndex((line) => line.startsWith("- Preference signals:"));
if (startIndex < 0) return [];
if (lines[startIndex]?.includes("none detected")) return [];
const signals = [];
for (const line of lines.slice(startIndex + 1)) {
const trimmed = line.trim();
if (!trimmed.startsWith("- ")) break;
const signal = trimmed.slice(2).trim();
if (signal.length > 0) signals.push(signal);
}
return signals;
}
function parseTranscriptTurns(body) {
const transcriptLines = extractHeadingSection(body, "Active Branch Transcript");
if (transcriptLines.length === 0) return [];
const turns = [];
let currentRole = null;
let currentLines = [];
const flush = () => {
if (!currentRole) {
currentLines = [];
return;
}
const text = currentLines.join("\n").trim();
if (text) turns.push({
role: currentRole,
text
});
currentLines = [];
};
for (const rawLine of transcriptLines) {
const line = rawLine.trimEnd();
if (line.trim() === "### User") {
flush();
currentRole = "user";
continue;
}
if (line.trim() === "### Assistant") {
flush();
currentRole = "assistant";
continue;
}
if (currentRole) currentLines.push(line);
}
flush();
return turns;
}
function firstParagraph(text) {
return text.split(/\n\s*\n/).map((entry) => entry.trim()).find((entry) => entry.length > 0);
}
function shortenSentence(value, maxLength = 180) {
const compact = value.replace(/\s+/g, " ").trim();
if (compact.length <= maxLength) return compact;
return `${compact.slice(0, maxLength - 1).trimEnd()}…`;
}
function extractCorrectionSignals(turns) {
const correctionPatterns = [
"you're right",
"you’re right",
"bad assumption",
"let's reset",
"let’s reset",
"does not exist anymore",
"that was a bad assumption",
"what actually works today"
];
return turns.filter((turn) => turn.role === "assistant").flatMap((turn) => {
const first = firstParagraph(turn.text);
if (!first) return [];
const normalized = first.toLowerCase();
return correctionPatterns.some((pattern) => normalized.includes(pattern)) ? [shortenSentence(first, 160)] : [];
}).slice(0, 2);
}
function deriveCandidateSignals(params) {
const output = [];
for (const signal of params.preferenceSignals) if (!output.includes(signal)) output.push(signal);
for (const correction of params.correctionSignals) {
const summary = `Correction detected: ${correction}`;
if (!output.includes(summary)) output.push(summary);
}
return output.slice(0, 4);
}
function deriveSummary(params) {
if (params.digestStatus === "withheld") {
if (params.riskReasons.length > 0) return `Sensitive ${params.topicLabel.toLowerCase()} chat withheld from durable-memory extraction because it touches ${params.riskReasons.join(", ")}.`;
return `Sensitive ${params.topicLabel.toLowerCase()} chat withheld from durable-memory extraction pending review.`;
}
if (params.assistantOpener) return shortenSentence(params.assistantOpener, 180);
if (params.firstUserLine) return shortenSentence(params.firstUserLine, 180);
return params.title;
}
function shouldExposeImportContent(digestStatus) {
return digestStatus === "available";
}
function normalizeRiskLevel(value) {
if (value === "low" || value === "medium" || value === "high") return value;
return "unknown";
}
function compareItemsByUpdated(left, right) {
const leftKey = left.updatedAt ?? left.createdAt ?? "";
const rightKey = right.updatedAt ?? right.createdAt ?? "";
if (rightKey !== leftKey) return rightKey.localeCompare(leftKey);
return left.title.localeCompare(right.title);
}
async function listMemoryWikiImportInsights(config) {
const items = (await readQueryableWikiPages(config.vault.path)).flatMap((page) => {
if (page.pageType !== "source") return [];
const parsed = parseWikiMarkdown(page.raw);
if (parsed.frontmatter.sourceType !== "chatgpt-export") return [];
const labels = normalizeStringArray(parsed.frontmatter.labels);
const topic = resolveTopic(labels);
const triageLines = extractHeadingSection(parsed.body, "Auto Triage");
const digestLines = extractHeadingSection(parsed.body, "Auto Digest");
const transcriptTurns = parseTranscriptTurns(parsed.body);
const digestStatus = digestLines.some((line) => line.toLowerCase().includes("withheld from durable-candidate generation")) ? "withheld" : "available";
const exposeImportContent = shouldExposeImportContent(digestStatus);
const userTurns = transcriptTurns.filter((turn) => turn.role === "user");
const assistantTurns = transcriptTurns.filter((turn) => turn.role === "assistant");
const assistantOpener = exposeImportContent ? firstParagraph(assistantTurns[0]?.text ?? "") : void 0;
const correctionSignals = exposeImportContent ? extractCorrectionSignals(transcriptTurns) : [];
const preferenceSignals = exposeImportContent ? extractPreferenceSignals(digestLines) : [];
const candidateSignals = exposeImportContent ? deriveCandidateSignals({
preferenceSignals,
correctionSignals
}) : [];
const firstUserLine = exposeImportContent ? extractDigestField(digestLines, "First user line") : void 0;
const lastUserLine = exposeImportContent ? extractDigestField(digestLines, "Last user line") : void 0;
return [{
pagePath: page.relativePath,
title: page.title.replace(/^ChatGPT Export:\s*/i, ""),
riskLevel: normalizeRiskLevel(parsed.frontmatter.riskLevel),
riskReasons: normalizeStringArray(parsed.frontmatter.riskReasons),
labels,
topicKey: topic.key,
topicLabel: topic.label,
digestStatus,
activeBranchMessages: extractIntegerField(triageLines, "Active-branch messages"),
userMessageCount: Math.max(extractIntegerField(digestLines, "User messages"), userTurns.length),
assistantMessageCount: Math.max(extractIntegerField(digestLines, "Assistant messages"), assistantTurns.length),
...firstUserLine ? { firstUserLine } : {},
...lastUserLine ? { lastUserLine } : {},
...assistantOpener ? { assistantOpener } : {},
summary: deriveSummary({
title: page.title.replace(/^ChatGPT Export:\s*/i, ""),
digestStatus,
...assistantOpener ? { assistantOpener } : {},
...firstUserLine ? { firstUserLine } : {},
riskReasons: normalizeStringArray(parsed.frontmatter.riskReasons),
topicLabel: topic.label
}),
candidateSignals,
correctionSignals,
preferenceSignals,
...normalizeTimestamp$1(parsed.frontmatter.createdAt) ? { createdAt: normalizeTimestamp$1(parsed.frontmatter.createdAt) } : {},
...normalizeTimestamp$1(parsed.frontmatter.updatedAt) ? { updatedAt: normalizeTimestamp$1(parsed.frontmatter.updatedAt) } : {}
}];
}).toSorted(compareItemsByUpdated);
const clustersByKey = /* @__PURE__ */ new Map();
for (const item of items) {
const list = clustersByKey.get(item.topicKey) ?? [];
list.push(item);
clustersByKey.set(item.topicKey, list);
}
const clusters = [...clustersByKey.entries()].map(([key, clusterItems]) => {
const sortedItems = [...clusterItems].toSorted(compareItemsByUpdated);
const updatedAt = sortedItems.map((item) => item.updatedAt ?? item.createdAt).find((value) => typeof value === "string" && value.length > 0);
return Object.assign({
key,
label: sortedItems[0]?.topicLabel ?? humanizeLabelSuffix(key),
itemCount: sortedItems.length,
highRiskCount: sortedItems.filter((item) => item.riskLevel === `high`).length,
withheldCount: sortedItems.filter((item) => item.digestStatus === `withheld`).length,
preferenceSignalCount: sortedItems.reduce((sum, item) => sum + item.preferenceSignals.length, 0)
}, updatedAt ? { updatedAt } : {}, { items: sortedItems });
}).toSorted((left, right) => {
const leftKey = left.updatedAt ?? "";
const rightKey = right.updatedAt ?? "";
if (rightKey !== leftKey) return rightKey.localeCompare(leftKey);
if (right.itemCount !== left.itemCount) return right.itemCount - left.itemCount;
return left.label.localeCompare(right.label);
});
return {
sourceType: "chatgpt",
totalItems: items.length,
totalClusters: clusters.length,
clusters
};
}
//#endregion
//#region extensions/memory-wiki/src/import-runs.ts
function asRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value;
}
function asStringArray(value) {
if (!Array.isArray(value)) return [];
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
}
function normalizeImportRunSummary(raw) {
const record = asRecord(raw);
if (!record) return null;
const runId = typeof record?.runId === "string" ? record.runId.trim() : "";
const importType = typeof record?.importType === "string" ? record.importType.trim() : "";
const appliedAt = typeof record?.appliedAt === "string" ? record.appliedAt.trim() : "";
const exportPath = typeof record?.exportPath === "string" ? record.exportPath.trim() : "";
const sourcePath = typeof record?.sourcePath === "string" ? record.sourcePath.trim() : "";
if (!runId || !importType || !appliedAt || !exportPath || !sourcePath) return null;
const createdPaths = asStringArray(record.createdPaths);
const updatedPaths = Array.isArray(record.updatedPaths) ? record.updatedPaths.map((entry) => asRecord(entry)).map((entry) => typeof entry?.path === "string" ? entry.path.trim() : "").filter((entry) => entry.length > 0) : [];
const pagePaths = uniqueStrings([...createdPaths, ...updatedPaths]);
const conversationCount = typeof record.conversationCount === "number" && Number.isFinite(record.conversationCount) ? Math.max(0, Math.floor(record.conversationCount)) : createdPaths.length + updatedPaths.length;
const createdCount = typeof record.createdCount === "number" && Number.isFinite(record.createdCount) ? Math.max(0, Math.floor(record.createdCount)) : createdPaths.length;
const updatedCount = typeof record.updatedCount === "number" && Number.isFinite(record.updatedCount) ? Math.max(0, Math.floor(record.updatedCount)) : updatedPaths.length;
const skippedCount = typeof record.skippedCount === "number" && Number.isFinite(record.skippedCount) ? Math.max(0, Math.floor(record.skippedCount)) : Math.max(0, conversationCount - createdCount - updatedCount);
const rolledBackAt = typeof record.rolledBackAt === "string" && record.rolledBackAt.trim().length > 0 ? record.rolledBackAt.trim() : void 0;
return {
runId,
importType,
appliedAt,
exportPath,
sourcePath,
conversationCount,
createdCount,
updatedCount,
skippedCount,
status: rolledBackAt ? "rolled_back" : "applied",
...rolledBackAt ? { rolledBackAt } : {},
pagePaths,
samplePaths: pagePaths.slice(0, 5)
};
}
async function listMemoryWikiImportRuns(config, options) {
const limit = Math.max(1, Math.floor(options?.limit ?? 10));
const runs = (await listMemoryWikiImportRunRecords(config.vault.path)).map((record) => normalizeImportRunSummary(record)).filter((entry) => entry !== null).toSorted((left, right) => right.appliedAt.localeCompare(left.appliedAt));
return {
runs: runs.slice(0, limit),
totalRuns: runs.length,
activeRuns: runs.filter((entry) => entry.status === "applied").length,
rolledBackRuns: runs.filter((entry) => entry.status === "rolled_back").length
};
}
//#endregion
//#region extensions/memory-wiki/src/memory-palace.ts
const PALACE_KIND_ORDER = [
"synthesis",
"entity",
"concept",
"source",
"report"
];
const PRIMARY_PALACE_KINDS = new Set([
"synthesis",
"entity",
"concept"
]);
const PALACE_KIND_LABELS = {
synthesis: "Syntheses",
entity: "Entities",
concept: "Concepts",
source: "Sources",
report: "Reports"
};
function createEmptyPalacePageCounts() {
return {
synthesis: 0,
entity: 0,
concept: 0,
source: 0,
report: 0
};
}
function normalizeTimestamp(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function extractSnippet(body) {
for (const rawLine of body.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#") || line.startsWith("```") || line.startsWith("<!--") || line.startsWith("- ") || line.startsWith("* ")) continue;
return line;
}
}
function comparePalaceItems(left, right) {
const leftKey = left.updatedAt ?? "";
const rightKey = right.updatedAt ?? "";
if (rightKey !== leftKey) return rightKey.localeCompare(leftKey);
if (right.claimCount !== left.claimCount) return right.claimCount - left.claimCount;
return left.title.localeCompare(right.title);
}
async function listMemoryWikiPalace(config) {
const pages = await readQueryableWikiPages(config.vault.path);
const pageCounts = pages.reduce((counts, page) => {
counts[page.kind] += 1;
return counts;
}, createEmptyPalacePageCounts());
const totalClaims = pages.reduce((sum, page) => sum + page.claims.length, 0);
const totalQuestions = pages.reduce((sum, page) => sum + page.questions.length, 0);
const totalContradictions = pages.reduce((sum, page) => sum + page.contradictions.length, 0);
const items = pages.map((page) => {
const parsed = parseWikiMarkdown(page.raw);
return Object.assign({
pagePath: page.relativePath,
title: page.title,
kind: page.kind
}, page.id ? { id: page.id } : {}, normalizeTimestamp(page.updatedAt) ? { updatedAt: normalizeTimestamp(page.updatedAt) } : {}, typeof page.sourceType === `string` && page.sourceType.trim().length > 0 ? { sourceType: page.sourceType.trim() } : {}, {
claimCount: page.claims.length,
questionCount: page.questions.length,
contradictionCount: page.contradictions.length,
claims: page.claims.map((claim) => claim.text).slice(0, 3),
questions: page.questions.slice(0, 3),
contradictions: page.contradictions.slice(0, 3)
}, extractSnippet(parsed.body) ? { snippet: extractSnippet(parsed.body) } : {});
}).filter((item) => PRIMARY_PALACE_KINDS.has(item.kind) || item.claimCount > 0 || item.questionCount > 0 || item.contradictionCount > 0).toSorted(comparePalaceItems);
const clusters = PALACE_KIND_ORDER.map((kind) => {
const clusterItems = items.filter((item) => item.kind === kind);
if (clusterItems.length === 0) return null;
return Object.assign({
key: kind,
label: PALACE_KIND_LABELS[kind],
itemCount: clusterItems.length,
claimCount: clusterItems.reduce((sum, item) => sum + item.claimCount, 0),
questionCount: clusterItems.reduce((sum, item) => sum + item.questionCount, 0),
contradictionCount: clusterItems.reduce((sum, item) => sum + item.contradictionCount, 0)
}, clusterItems[0]?.updatedAt ? { updatedAt: clusterItems[0].updatedAt } : {}, { items: clusterItems });
}).filter((entry) => entry !== null);
return {
totalItems: items.length,
totalPages: pages.length,
pageCounts,
totalClaims,
totalQuestions,
totalContradictions,
clusters
};
}
//#endregion
//#region extensions/memory-wiki/src/gateway.ts
const READ_SCOPE = "operator.read";
const WRITE_SCOPE = "operator.write";
const LOCAL_FILE_INGEST_SCOPE = "operator.admin";
function readStringParam(params, key, options) {
const value = params[key];
if (typeof value === "string" && value.trim()) return value.trim();
if (options?.required) throw new Error(`${key} is required.`);
}
function readEnumParam(params, key, allowed) {
const value = readStringParam(params, key);
if (!value) return;
if (allowed.includes(value)) return value;
throw new Error(`${key} must be one of: ${allowed.join(", ")}.`);
}
function respondError(respond, error) {
respond(false, void 0, {
code: "internal_error",
message: formatErrorMessage(error)
});
}
function resolveGatewayAgentId(requestParams, appConfig) {
return readStringParam(requestParams, "agentId") ?? (appConfig ? resolveDefaultAgentId(appConfig) : void 0);
}
async function syncImportedSourcesIfNeeded$1(config, appConfig) {
await syncMemoryWikiImportedSources({
config,
appConfig
});
}
function registerMemoryWikiGatewayMethods(params) {
const { api, config, appConfig } = params;
api.registerGatewayMethod("wiki.status", async ({ respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
respond(true, await resolveMemoryWikiStatus(config, { appConfig }));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.importRuns", async ({ params: requestParams, respond }) => {
try {
const limit = readPositiveIntegerParam(requestParams, "limit");
respond(true, await listMemoryWikiImportRuns(config, limit !== void 0 ? { limit } : {}));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.importInsights", async ({ respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
respond(true, await listMemoryWikiImportInsights(config));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.palace", async ({ respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
respond(true, await listMemoryWikiPalace(config));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.init", async ({ respond }) => {
try {
respond(true, await initializeMemoryWikiVault(config));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.doctor", async ({ respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
respond(true, buildMemoryWikiDoctorReport(await resolveMemoryWikiStatus(config, { appConfig })));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.compile", async ({ respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
respond(true, await compileMemoryWikiVault(config));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.ingest", async ({ params: requestParams, respond }) => {
try {
const inputPath = readStringParam(requestParams, "inputPath", { required: true });
const title = readStringParam(requestParams, "title");
respond(true, await ingestMemoryWikiSource({
config,
inputPath,
...title ? { title } : {}
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: LOCAL_FILE_INGEST_SCOPE });
api.registerGatewayMethod("wiki.lint", async ({ respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
respond(true, await lintMemoryWikiVault(config));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.bridge.import", async ({ respond }) => {
try {
respond(true, await syncMemoryWikiImportedSources({
config: {
...config,
vaultMode: "bridge"
},
appConfig
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.unsafeLocal.import", async ({ respond }) => {
try {
respond(true, await syncMemoryWikiImportedSources({
config: {
...config,
vaultMode: "unsafe-local"
},
appConfig
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.search", async ({ params: requestParams, respond }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
const query = readStringParam(requestParams, "query", { required: true });
const maxResults = readPositiveIntegerParam(requestParams, "maxResults");
const searchBackend = readEnumParam(requestParams, "backend", WIKI_SEARCH_BACKENDS);
const searchCorpus = readEnumParam(requestParams, "corpus", WIKI_SEARCH_CORPORA);
const mode = readEnumParam(requestParams, "mode", WIKI_SEARCH_MODES);
const agentId = resolveGatewayAgentId(requestParams, appConfig);
respond(true, await searchMemoryWiki({
config,
appConfig,
...agentId ? { agentId } : {},
query,
maxResults,
searchBackend,
searchCorpus,
mode
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.apply", async ({ params: requestParams, respond }) => {
try {
await syncImportedSourcesIfNeeded$1(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 }) => {
try {
await syncImportedSourcesIfNeeded$1(config, appConfig);
const lookup = readStringParam(requestParams, "lookup", { required: true });
const fromLine = readPositiveIntegerParam(requestParams, "fromLine");
const lineCount = readPositiveIntegerParam(requestParams, "lineCount");
const searchBackend = readEnumParam(requestParams, "backend", WIKI_SEARCH_BACKENDS);
const searchCorpus = readEnumParam(requestParams, "corpus", WIKI_SEARCH_CORPORA);
const agentId = resolveGatewayAgentId(requestParams, appConfig);
respond(true, await getMemoryWikiPage({
config,
appConfig,
...agentId ? { agentId } : {},
lookup,
fromLine,
lineCount,
searchBackend,
searchCorpus
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.obsidian.status", async ({ respond }) => {
try {
respond(true, await probeObsidianCli());
} catch (error) {
respondError(respond, error);
}
}, { scope: READ_SCOPE });
api.registerGatewayMethod("wiki.obsidian.search", async ({ params: requestParams, respond }) => {
try {
respond(true, await runObsidianSearch({
config,
query: readStringParam(requestParams, "query", { required: true })
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.obsidian.open", async ({ params: requestParams, respond }) => {
try {
respond(true, await runObsidianOpen({
config,
vaultPath: readStringParam(requestParams, "path", { required: true })
}));
} catch (error) {
respondError(respond, error);
}
}, { scope: WRITE_SCOPE });
api.registerGatewayMethod("wiki.obsidian.command", async ({ params: requestParams, respond }) => {
try {
respond(true, await runObsidianCommand({
config,
id: readStringParam(requestParams, "id", { required: true })
}));
} 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 });
}
//#endregion
//#region extensions/memory-wiki/src/prompt-section.ts
const AGENT_DIGEST_PATH = ".openclaw-wiki/cache/agent-digest.json";
const DIGEST_MAX_PAGES = 4;
const DIGEST_MAX_CLAIMS_PER_PAGE = 2;
function tryReadPromptDigest(config) {
const digestPath = path.join(config.vault.path, AGENT_DIGEST_PATH);
try {
const raw = fs.readFileSync(digestPath, "utf8");
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return null;
return parsed;
} catch {
return null;
}
}
function rankPromptDigestPage(page) {
return (page.contradictions?.length ?? 0) * 6 + (page.questions?.length ?? 0) * 4 + Math.min(page.claimCount ?? 0, 6) * 2 + Math.min(page.topClaims?.length ?? 0, 3);
}
function rankPromptClaimFreshness(level) {
switch (level) {
case "fresh": return 3;
case "aging": return 2;
case "stale": return 1;
default: return 0;
}
}
function sortPromptClaims(claims) {
return [...claims].toSorted((left, right) => {
const leftConfidence = typeof left.confidence === "number" ? left.confidence : -1;
const rightConfidence = typeof right.confidence === "number" ? right.confidence : -1;
if (leftConfidence !== rightConfidence) return rightConfidence - leftConfidence;
const leftFreshness = rankPromptClaimFreshness(left.freshnessLevel);
const rightFreshness = rankPromptClaimFreshness(right.freshnessLevel);
if (leftFreshness !== rightFreshness) return rightFreshness - leftFreshness;
return left.text.localeCompare(right.text);
});
}
function formatPromptClaim(claim) {
const qualifiers = [
claim.status?.trim() ? `status ${claim.status.trim()}` : null,
typeof claim.confidence === "number" ? `confidence ${claim.confidence.toFixed(2)}` : null,
claim.freshnessLevel?.trim() ? `freshness ${claim.freshnessLevel.trim()}` : null
].filter(Boolean);
if (qualifiers.length === 0) return claim.text;
return `${claim.text} (${qualifiers.join(", ")})`;
}
function buildDigestPromptSection(config) {
if (!config.context.includeCompiledDigestPrompt) return [];
const digest = tryReadPromptDigest(config);
if (!digest?.pages?.length) return [];
const selectedPages = [...digest.pages].filter((page) => (page.claimCount ?? 0) > 0 || (page.questions?.length ?? 0) > 0 || (page.contradictions?.length ?? 0) > 0).toSorted((left, right) => {
const leftScore = rankPromptDigestPage(left);
const rightScore = rankPromptDigestPage(right);
if (leftScore !== rightScore) return rightScore - leftScore;
return left.title.localeCompare(right.title);
}).slice(0, DIGEST_MAX_PAGES);
if (selectedPages.length === 0) return [];
const lines = ["## Compiled Wiki Snapshot", `Compiled wiki currently tracks ${digest.claimCount ?? 0} claims across ${selectedPages.length} high-signal pages.`];
if (Array.isArray(digest.contradictionClusters)) lines.push(`Contradiction clusters: ${digest.contradictionClusters.length}.`);
for (const page of selectedPages) {
const details = [
page.kind,
`${page.claimCount} claims`,
(page.questions?.length ?? 0) > 0 ? `${page.questions?.length} open questions` : null,
(page.contradictions?.length ?? 0) > 0 ? `${page.contradictions?.length} contradiction notes` : null
].filter(Boolean);
lines.push(`- ${page.title}: ${details.join(", ")}`);
for (const claim of sortPromptClaims(page.topClaims ?? []).slice(0, DIGEST_MAX_CLAIMS_PER_PAGE)) lines.push(` - ${formatPromptClaim(claim)}`);
}
lines.push("");
return lines;
}
function buildWikiToolGuidance(availableTools) {
const hasMemorySearch = availableTools.has("memory_search");
const hasMemoryGet = availableTools.has("memory_get");
const hasWikiSearch = availableTools.has("wiki_search");
const hasWikiGet = availableTools.has("wiki_get");
const hasWikiApply = availableTools.has("wiki_apply");
const hasWikiLint = availableTools.has("wiki_lint");
if (!hasMemorySearch && !hasMemoryGet && !hasWikiSearch && !hasWikiGet && !hasWikiApply && !hasWikiLint) return [];
const lines = ["## Compiled Wiki", "Use the wiki when the answer depends on accumulated project knowledge, prior syntheses, entity pages, or source-backed notes that should survive beyond one conversation."];
if (hasMemorySearch) lines.push("Prefer `memory_search` with `corpus=all` for one recall pass across durable memory and the compiled wiki when both are relevant.");
if (hasMemoryGet) lines.push("Use `memory_get` with `corpus=wiki` or `corpus=all` when you already know the page path and want a small excerpt without leaving the shared memory tool flow.");
if (hasWikiSearch && hasWikiGet) lines.push("Workflow: `wiki_search` first, then `wiki_get` for the exact page or imported memory file you need. Use this when you want wiki-specific ranking or provenance details instead of the broader shared memory flow.");
else if (hasWikiSearch) lines.push("Use `wiki_search` before answering from stored knowledge when you want wiki-specific ranking or provenance details.");
else if (hasWikiGet) lines.push("Use `wiki_get` to inspect specific wiki pages or imported memory files by path/id.");
if (hasWikiApply) lines.push("Use `wiki_apply` for narrow synthesis filing and metadata repair instead of rewriting managed markdown blocks by hand.");
if (hasWikiLint) lines.push("After meaningful wiki updates, run `wiki_lint` before trusting the vault.");
lines.push("");
return lines;
}
function createWikiPromptSectionBuilder(config) {
return ({ availableTools }) => {
const digestLines = buildDigestPromptSection(config);
const toolGuidance = buildWikiToolGuidance(availableTools);
if (digestLines.length === 0 && toolGuidance.length === 0) return [];
return [...toolGuidance, ...digestLines];
};
}
//#endregion
//#region extensions/memory-wiki/src/tool.ts
function formatWikiToolReportPath(config, reportPath) {
const vaultRoot = path.resolve(config.vault.path);
const resolvedReportPath = path.resolve(reportPath);
const relativeReportPath = path.relative(vaultRoot, resolvedReportPath);
if (!relativeReportPath || relativeReportPath.startsWith("..") || path.isAbsolute(relativeReportPath)) return reportPath;
return relativeReportPath.replace(/\\/g, "/");
}
const WikiStatusSchema = Type.Object({}, { additionalProperties: false });
const WikiLintSchema = Type.Object({}, { additionalProperties: false });
const WikiSearchBackendSchema = Type.Union(WIKI_SEARCH_BACKENDS.map((value) => Type.Literal(value)));
const WikiSearchCorpusSchema = Type.Union(WIKI_SEARCH_CORPORA.map((value) => Type.Literal(value)));
const WikiSearchModeSchema = Type.Union(WIKI_SEARCH_MODES.map((value) => Type.Literal(value)));
const WikiSearchSchema = Type.Object({
query: Type.String({ minLength: 1 }),
maxResults: Type.Optional(Type.Integer({ minimum: 1 })),
backend: Type.Optional(WikiSearchBackendSchema),
corpus: Type.Optional(WikiSearchCorpusSchema),
mode: Type.Optional(WikiSearchModeSchema)
}, { additionalProperties: false });
const WikiGetSchema = Type.Object({
lookup: Type.String({ minLength: 1 }),
fromLine: Type.Optional(Type.Integer({ minimum: 1 })),
lineCount: Type.Optional(Type.Integer({ minimum: 1 })),
backend: Type.Optional(WikiSearchBackendSchema),
corpus: Type.Optional(WikiSearchCorpusSchema)
}, { additionalProperties: false });
const WikiClaimEvidenceSchema = Type.Object({
kind: Type.Optional(Type.String({ minLength: 1 })),
sourceId: Type.Optional(Type.String({ minLength: 1 })),
path: Type.Optional(Type.String({ minLength: 1 })),
lines: Type.Optional(Type.String({ minLength: 1 })),
weight: optionalFiniteNumberSchema({ minimum: 0 }),
note: Type.Optional(Type.String({ minLength: 1 })),
confidence: optionalFiniteNumberSchema({
minimum: 0,
maximum: 1
}),
privacyTier: Type.Optional(Type.String({ minLength: 1 })),
updatedAt: Type.Optional(Type.String({ minLength: 1 }))
}, { additionalProperties: false });
const WikiClaimSchema = Type.Object({
id: Type.Optional(Type.String({ minLength: 1 })),
text: Type.String({ minLength: 1 }),
status: Type.Optional(Type.String({ minLength: 1 })),
confidence: optionalFiniteNumberSchema({
minimum: 0,
maximum: 1
}),
evidence: Type.Optional(Type.Array(WikiClaimEvidenceSchema)),
updatedAt: Type.Optional(Type.String({ minLength: 1 }))
}, { additionalProperties: false });
const WikiApplySchema = Type.Object({
op: Type.Union([
Type.Literal("create_synthesis"),
Type.Literal("update_metadata"),
Type.Literal("synthesis"),
Type.Literal("metadata")
]),
title: Type.Optional(Type.String({ minLength: 1 })),
body: Type.Optional(Type.String({ minLength: 1 })),
lookup: Type.Optional(Type.String({ minLength: 1 })),
sourceIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
claims: Type.Optional(Type.Array(WikiClaimSchema)),
contradictions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
questions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
confidence: Type.Optional(Type.Union([Type.Number({
minimum: 0,
maximum: 1
}), Type.Null()])),
status: Type.Optional(Type.String({ minLength: 1 }))
}, { additionalProperties: false });
async function syncImportedSourcesIfNeeded(config, appConfig) {
await syncMemoryWikiImportedSources({
config,
appConfig
});
}
function createWikiStatusTool(config, appConfig) {
return {
name: "wiki_status",
label: "Wiki Status",
description: "Inspect the current memory wiki vault mode, health, and Obsidian CLI availability.",
parameters: WikiStatusSchema,
execute: async () => {
await syncImportedSourcesIfNeeded(config, appConfig);
const status = await resolveMemoryWikiStatus(config, { appConfig });
return {
content: [{
type: "text",
text: renderMemoryWikiStatus(status)
}],
details: status
};
}
};
}
function createWikiSearchTool(config, appConfig, memoryContext = {}) {
return {
name: "wiki_search",
label: "Wiki Search",
description: "Search wiki pages and, when shared search is enabled, the active memory corpus by title, path, id, or body text.",
parameters: WikiSearchSchema,
execute: async (_toolCallId, rawParams) => {
const params = rawParams;
await syncImportedSourcesIfNeeded(config, appConfig);
const results = await searchMemoryWiki({
config,
appConfig,
agentId: memoryContext.agentId,
agentSessionKey: memoryContext.agentSessionKey,
sandboxed: memoryContext.sandboxed,
query: params.query,
maxResults: params.maxResults,
...params.backend ? { searchBackend: params.backend } : {},
...params.corpus ? { searchCorpus: params.corpus } : {},
...params.mode ? { mode: params.mode } : {}
});
return {
content: [{
type: "text",
text: results.length === 0 ? "No wiki or memory results." : results.map((result, index) => `${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}${result.provenanceLabel ? `\nProvenance: ${result.provenanceLabel}` : ""}${result.matchedClaimId ? `\nClaim: ${result.matchedClaimId}` : ""}${result.evidenceKinds && result.evidenceKinds.length > 0 ? `\nEvidence: ${result.evidenceKinds.join(", ")}` : ""}\nSnippet: ${result.snippet}`).join("\n\n")
}],
details: { results }
};
}
};
}
function createWikiLintTool(config, appConfig) {
return {
name: "wiki_lint",
label: "Wiki Lint",
description: "Lint the wiki vault and surface structural issues, provenance gaps, contradictions, and open questions.",
parameters: WikiLintSchema,
execute: async () => {
await syncImportedSourcesIfNeeded(config, appConfig);
const result = await lintMemoryWikiVault(config);
const contradictions = result.issuesByCategory.contradictions.length;
const openQuestions = result.issuesByCategory["open-questions"].length;
const provenance = result.issuesByCategory.provenance.length;
const errors = result.issues.filter((issue) => issue.severity === "error").length;
const warnings = result.issues.filter((issue) => issue.severity === "warning").length;
const reportPath = formatWikiToolReportPath(config, result.reportPath);
return {
content: [{
type: "text",
text: result.issueCount === 0 ? "No wiki lint issues." : [
`Issues: ${result.issueCount} total (${errors} errors, ${warnings} warnings)`,
`Contradictions: ${contradictions}`,
`Open questions: ${openQuestions}`,
`Provenance gaps: ${provenance}`,
`Report: ${reportPath}`
].join("\n")
}],
details: {
issueCount: result.issueCount,
issues: result.issues,
issuesByCategory: result.issuesByCategory,
reportPath
}
};
}
};
}
function createWikiApplyTool(config, appConfig) {
return {
name: "wiki_apply",
label: "Wiki Apply",
description: "Apply narrow wiki mutations for syntheses and page metadata without freeform markdown surgery.",
parameters: WikiApplySchema,
execute: async (_toolCallId, rawParams) => {
const mutation = normalizeMemoryWikiMutationInput(rawParams);
await syncImportedSourcesIfNeeded(config, appConfig);
const result = await applyMemoryWikiMutation({
config,
mutation
});
const action = result.changed ? "Updated" : "No changes for";
const compileSummary = result.compile.updatedFiles.length > 0 ? `Refreshed ${result.compile.updatedFiles.length} index file${result.compile.updatedFiles.length === 1 ? "" : "s"}.` : "Indexes unchanged.";
return {
content: [{
type: "text",
text: `${action} ${result.pagePath} via ${result.operation}. ${compileSummary}`
}],
details: result
};
}
};
}
function createWikiGetTool(config, appConfig, memoryContext = {}) {
return {
name: "wiki_get",
label: "Wiki Get",
description: "Read a wiki page by id or relative path, or fall back to the active memory corpus when shared search is enabled.",
parameters: WikiGetSchema,
execute: async (_toolCallId, rawParams) => {
const params = rawParams;
await syncImportedSourcesIfNeeded(config, appConfig);
const result = await getMemoryWikiPage({
config,
appConfig,
agentId: memoryContext.agentId,
agentSessionKey: memoryContext.agentSessionKey,
sandboxed: memoryContext.sandboxed,
lookup: params.lookup,
fromLine: params.fromLine,
lineCount: params.lineCount,
...params.backend ? { searchBackend: params.backend } : {},
...params.corpus ? { searchCorpus: params.corpus } : {}
});
if (!result) return {
content: [{
type: "text",
text: `Wiki page not found: ${params.lookup}`
}],
details: { found: false }
};
return {
content: [{
type: "text",
text: result.content
}],
details: {
found: true,
...result
}
};
}
};
}
//#endregion
//#region extensions/memory-wiki/index.ts
var memory_wiki_default = definePluginEntry({
id: "memory-wiki",
name: "Memory Wiki",
description: "Persistent wiki compiler and Obsidian-friendly knowledge vault for OpenClaw.",
configSchema: memoryWikiConfigSchema,
register(api) {
const config = resolveMemoryWikiConfig(api.pluginConfig);
configureMemoryWikiSourceSyncStateStore(createMemoryWikiSourceSyncStateStore(api.runtime.state.openKeyedStore));
configureMemoryWikiImportRunStateStore(createMemoryWikiImportRunStateStore(api.runtime.state.openKeyedStore));
api.registerMemoryPromptSupplement(createWikiPromptSectionBuilder(config));
api.registerMemoryCorpusSupplement(createWikiCorpusSupplement({
config,
appConfig: api.config
}));
registerMemoryWikiGatewayMethods({
api,
config,
appConfig: api.config
});
api.registerTool(createWikiStatusTool(config, api.config), { name: "wiki_status" });
api.registerTool(createWikiLintTool(config, api.config), { name: "wiki_lint" });
api.registerTool(createWikiApplyTool(config, api.config), { name: "wiki_apply" });
api.registerTool((ctx) => createWikiSearchTool(config, api.config, {
agentId: ctx.agentId,
agentSessionKey: ctx.sessionKey,
sandboxed: ctx.sandboxed
}), { name: "wiki_search" });
api.registerTool((ctx) => createWikiGetTool(config, api.config, {
agentId: ctx.agentId,
agentSessionKey: ctx.sessionKey,
sandboxed: ctx.sandboxed
}), { name: "wiki_get" });
api.registerCli(({ program }) => {
registerWikiCli(program, config, api.config);
}, { descriptors: [{
name: "wiki",
description: "Inspect and initialize the memory wiki vault",
hasSubcommands: true
}] });
}
});
//#endregion
export { memory_wiki_default as default };