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
416 lines
12 KiB
TypeScript
416 lines
12 KiB
TypeScript
// Memory Wiki plugin module implements gateway behavior.
|
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/memory-host-core";
|
|
import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
|
|
import type { OpenClawConfig, OpenClawPluginApi } from "../api.js";
|
|
import { applyMemoryWikiMutation, normalizeMemoryWikiMutationInput } from "./apply.js";
|
|
import { compileMemoryWikiVault } from "./compile.js";
|
|
import {
|
|
WIKI_SEARCH_BACKENDS,
|
|
WIKI_SEARCH_CORPORA,
|
|
type ResolvedMemoryWikiConfig,
|
|
} from "./config.js";
|
|
import { listMemoryWikiImportInsights } from "./import-insights.js";
|
|
import { listMemoryWikiImportRuns } from "./import-runs.js";
|
|
import { ingestMemoryWikiSource } from "./ingest.js";
|
|
import { lintMemoryWikiVault } from "./lint.js";
|
|
import { listMemoryWikiPalace } from "./memory-palace.js";
|
|
import {
|
|
probeObsidianCli,
|
|
runObsidianCommand,
|
|
runObsidianDaily,
|
|
runObsidianOpen,
|
|
runObsidianSearch,
|
|
} from "./obsidian.js";
|
|
import { getMemoryWikiPage, searchMemoryWiki, WIKI_SEARCH_MODES } from "./query.js";
|
|
import { syncMemoryWikiImportedSources } from "./source-sync.js";
|
|
import { buildMemoryWikiDoctorReport, resolveMemoryWikiStatus } from "./status.js";
|
|
import { initializeMemoryWikiVault } from "./vault.js";
|
|
|
|
const READ_SCOPE = "operator.read" as const;
|
|
const WRITE_SCOPE = "operator.write" as const;
|
|
const ADMIN_SCOPE = "operator.admin" as const;
|
|
const LOCAL_FILE_INGEST_SCOPE = ADMIN_SCOPE;
|
|
type GatewayMethodContext = Parameters<
|
|
Parameters<OpenClawPluginApi["registerGatewayMethod"]>[1]
|
|
>[0];
|
|
type GatewayRespond = GatewayMethodContext["respond"];
|
|
|
|
function readStringParam(params: Record<string, unknown>, key: string): string | undefined;
|
|
function readStringParam(
|
|
params: Record<string, unknown>,
|
|
key: string,
|
|
options: { required: true },
|
|
): string;
|
|
function readStringParam(
|
|
params: Record<string, unknown>,
|
|
key: string,
|
|
options?: { required?: boolean },
|
|
): string | undefined {
|
|
const value = params[key];
|
|
if (typeof value === "string" && value.trim()) {
|
|
return value.trim();
|
|
}
|
|
if (options?.required) {
|
|
throw new Error(`${key} is required.`);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readEnumParam<T extends string>(
|
|
params: Record<string, unknown>,
|
|
key: string,
|
|
allowed: readonly T[],
|
|
): T | undefined {
|
|
const value = readStringParam(params, key);
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
if ((allowed as readonly string[]).includes(value)) {
|
|
return value as T;
|
|
}
|
|
throw new Error(`${key} must be one of: ${allowed.join(", ")}.`);
|
|
}
|
|
|
|
function respondError(respond: GatewayRespond, error: unknown) {
|
|
const message = formatErrorMessage(error);
|
|
respond(false, undefined, { code: "internal_error", message });
|
|
}
|
|
|
|
function resolveGatewayAgentId(
|
|
requestParams: Record<string, unknown>,
|
|
appConfig: OpenClawConfig | undefined,
|
|
): string | undefined {
|
|
return (
|
|
readStringParam(requestParams, "agentId") ??
|
|
(appConfig ? resolveDefaultAgentId(appConfig) : undefined)
|
|
);
|
|
}
|
|
|
|
async function syncImportedSourcesIfNeeded(
|
|
config: ResolvedMemoryWikiConfig,
|
|
appConfig?: OpenClawConfig,
|
|
) {
|
|
await syncMemoryWikiImportedSources({ config, appConfig });
|
|
}
|
|
|
|
export function registerMemoryWikiGatewayMethods(params: {
|
|
api: OpenClawPluginApi;
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
}) {
|
|
const { api, config, appConfig } = params;
|
|
|
|
api.registerGatewayMethod(
|
|
"wiki.status",
|
|
async ({ respond }) => {
|
|
try {
|
|
await syncImportedSourcesIfNeeded(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 !== undefined ? { limit } : {}));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"wiki.importInsights",
|
|
async ({ respond }) => {
|
|
try {
|
|
await syncImportedSourcesIfNeeded(config, appConfig);
|
|
respond(true, await listMemoryWikiImportInsights(config));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"wiki.palace",
|
|
async ({ respond }) => {
|
|
try {
|
|
await syncImportedSourcesIfNeeded(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(config, appConfig);
|
|
const status = await resolveMemoryWikiStatus(config, {
|
|
appConfig,
|
|
});
|
|
respond(true, buildMemoryWikiDoctorReport(status));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"wiki.compile",
|
|
async ({ respond }) => {
|
|
try {
|
|
await syncImportedSourcesIfNeeded(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(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(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(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(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 {
|
|
const query = readStringParam(requestParams, "query", { required: true });
|
|
respond(true, await runObsidianSearch({ config, query }));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_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 },
|
|
);
|
|
}
|