mirror of https://github.com/openclaw/openclaw.git
feat(memory-wiki): add bridge sync and obsidian cli adapter
This commit is contained in:
parent
82710f2add
commit
fbbe2a1675
|
|
@ -2,6 +2,7 @@ export {
|
|||
buildPluginConfigSchema,
|
||||
definePluginEntry,
|
||||
type AnyAgentTool,
|
||||
type OpenClawConfig,
|
||||
type OpenClawPluginApi,
|
||||
type OpenClawPluginConfigSchema,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ export default definePluginEntry({
|
|||
register(api) {
|
||||
const config = resolveMemoryWikiConfig(api.pluginConfig);
|
||||
|
||||
api.registerTool(createWikiStatusTool(config), { name: "wiki_status" });
|
||||
api.registerTool(createWikiSearchTool(config), { name: "wiki_search" });
|
||||
api.registerTool(createWikiGetTool(config), { name: "wiki_get" });
|
||||
api.registerTool(createWikiStatusTool(config, api.config), { name: "wiki_status" });
|
||||
api.registerTool(createWikiSearchTool(config, api.config), { name: "wiki_search" });
|
||||
api.registerTool(createWikiGetTool(config, api.config), { name: "wiki_get" });
|
||||
api.registerCli(
|
||||
({ program }) => {
|
||||
registerWikiCli(program, config);
|
||||
registerWikiCli(program, config, api.config);
|
||||
},
|
||||
{
|
||||
descriptors: [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ description: Maintain an Obsidian-friendly memory wiki vault with wikilinks, fro
|
|||
Use this skill when the memory-wiki vault render mode is `obsidian` or the user wants the wiki to play nicely with Obsidian.
|
||||
|
||||
- Start from `openclaw wiki status` to confirm the vault mode and whether the official Obsidian CLI is available.
|
||||
- Use `openclaw wiki obsidian status` before shelling out, then prefer the dedicated helpers like `openclaw wiki obsidian search`, `openclaw wiki obsidian open`, `openclaw wiki obsidian command`, and `openclaw wiki obsidian daily`.
|
||||
- Prefer `[[Wikilinks]]`, stable filenames, and frontmatter that works with Obsidian dashboards and Dataview-style queries.
|
||||
- Keep generated sections deterministic so Obsidian users can safely add handwritten notes around them.
|
||||
- If the official Obsidian CLI is enabled, probe it before depending on it. Do not assume the app is installed, running, or configured.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Use this skill when working inside a memory-wiki vault.
|
|||
- Prefer `wiki_status` first when you need to understand the vault mode, path, or Obsidian CLI availability.
|
||||
- Use `wiki_search` to discover candidate pages, then `wiki_get` to inspect the exact page before editing or citing it.
|
||||
- Use `openclaw wiki ingest`, `openclaw wiki compile`, and `openclaw wiki lint` as the default maintenance loop.
|
||||
- In `bridge` mode, run `openclaw wiki bridge import` before relying on search results if you need the latest public memory-core artifacts pulled in.
|
||||
- Keep generated sections inside managed markers. Do not overwrite human note blocks.
|
||||
- Treat raw sources, memory artifacts, and daily notes as evidence. Do not let wiki pages become the only source of truth for new claims.
|
||||
- Keep page identity stable. Favor updating existing entities and concepts over spawning duplicates with slightly different names.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import { syncMemoryWikiBridgeSources } from "./bridge.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("syncMemoryWikiBridgeSources", () => {
|
||||
it("imports public memory-core artifacts and stays idempotent across reruns", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-bridge-ws-"));
|
||||
const vaultDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-bridge-vault-"));
|
||||
tempDirs.push(workspaceDir, vaultDir);
|
||||
|
||||
await fs.mkdir(path.join(workspaceDir, "memory", "dreaming"), { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# Durable Memory\n", "utf8");
|
||||
await fs.writeFile(
|
||||
path.join(workspaceDir, "memory", "2026-04-05.md"),
|
||||
"# Daily Note\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(workspaceDir, "memory", "dreaming", "2026-04-05.md"),
|
||||
"# Dream Report\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{
|
||||
vaultMode: "bridge",
|
||||
vault: { path: vaultDir },
|
||||
bridge: {
|
||||
enabled: true,
|
||||
readMemoryCore: true,
|
||||
indexMemoryRoot: true,
|
||||
indexDailyNotes: true,
|
||||
indexDreamReports: true,
|
||||
},
|
||||
},
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
const appConfig: OpenClawConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
enabled: true,
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, workspace: workspaceDir }],
|
||||
},
|
||||
};
|
||||
|
||||
const first = await syncMemoryWikiBridgeSources({ config, appConfig });
|
||||
|
||||
expect(first.workspaces).toBe(1);
|
||||
expect(first.artifactCount).toBe(3);
|
||||
expect(first.importedCount).toBe(3);
|
||||
expect(first.updatedCount).toBe(0);
|
||||
expect(first.skippedCount).toBe(0);
|
||||
expect(first.pagePaths).toHaveLength(3);
|
||||
|
||||
const sourcePages = await fs.readdir(path.join(vaultDir, "sources"));
|
||||
expect(sourcePages.filter((name) => name.startsWith("bridge-"))).toHaveLength(3);
|
||||
|
||||
const memoryPage = await fs.readFile(path.join(vaultDir, first.pagePaths[0] ?? ""), "utf8");
|
||||
expect(memoryPage).toContain("sourceType: memory-bridge");
|
||||
expect(memoryPage).toContain("## Bridge Source");
|
||||
|
||||
const second = await syncMemoryWikiBridgeSources({ config, appConfig });
|
||||
|
||||
expect(second.importedCount).toBe(0);
|
||||
expect(second.updatedCount).toBe(0);
|
||||
expect(second.skippedCount).toBe(3);
|
||||
|
||||
const logLines = (await fs.readFile(path.join(vaultDir, ".openclaw-wiki", "log.jsonl"), "utf8"))
|
||||
.trim()
|
||||
.split("\n");
|
||||
expect(logLines).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns a no-op result outside bridge mode", async () => {
|
||||
const vaultDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-isolated-"));
|
||||
tempDirs.push(vaultDir);
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: vaultDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
|
||||
const result = await syncMemoryWikiBridgeSources({ config });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
importedCount: 0,
|
||||
updatedCount: 0,
|
||||
skippedCount: 0,
|
||||
artifactCount: 0,
|
||||
workspaces: 0,
|
||||
pagePaths: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
resolveMemoryCorePluginConfig,
|
||||
resolveMemoryDreamingWorkspaces,
|
||||
} from "openclaw/plugin-sdk/memory-host-status";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { appendMemoryWikiLog } from "./log.js";
|
||||
import { renderMarkdownFence, renderWikiMarkdown, slugifyWikiSegment } from "./markdown.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
type BridgeArtifact = {
|
||||
workspaceDir: string;
|
||||
relativePath: string;
|
||||
absolutePath: string;
|
||||
};
|
||||
|
||||
export type BridgeMemoryWikiResult = {
|
||||
importedCount: number;
|
||||
updatedCount: number;
|
||||
skippedCount: number;
|
||||
artifactCount: number;
|
||||
workspaces: number;
|
||||
pagePaths: string[];
|
||||
};
|
||||
|
||||
async function pathExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function listMarkdownFilesRecursive(rootDir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await listMarkdownFilesRecursive(fullPath)));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function resolveArtifactKey(absolutePath: string): Promise<string> {
|
||||
const canonicalPath = await fs.realpath(absolutePath).catch(() => path.resolve(absolutePath));
|
||||
return process.platform === "win32" ? canonicalPath.toLowerCase() : canonicalPath;
|
||||
}
|
||||
|
||||
async function collectWorkspaceArtifacts(
|
||||
workspaceDir: string,
|
||||
bridgeConfig: ResolvedMemoryWikiConfig["bridge"],
|
||||
): Promise<BridgeArtifact[]> {
|
||||
const artifacts: BridgeArtifact[] = [];
|
||||
if (bridgeConfig.indexMemoryRoot) {
|
||||
for (const relPath of ["MEMORY.md", "memory.md"]) {
|
||||
const absolutePath = path.join(workspaceDir, relPath);
|
||||
if (await pathExists(absolutePath)) {
|
||||
artifacts.push({
|
||||
workspaceDir,
|
||||
relativePath: relPath,
|
||||
absolutePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bridgeConfig.indexDailyNotes) {
|
||||
const memoryDir = path.join(workspaceDir, "memory");
|
||||
const files = await listMarkdownFilesRecursive(memoryDir);
|
||||
for (const absolutePath of files) {
|
||||
const relativePath = path.relative(workspaceDir, absolutePath).replace(/\\/g, "/");
|
||||
if (!relativePath.startsWith("memory/dreaming/")) {
|
||||
artifacts.push({
|
||||
workspaceDir,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bridgeConfig.indexDreamReports) {
|
||||
const dreamingDir = path.join(workspaceDir, "memory", "dreaming");
|
||||
const files = await listMarkdownFilesRecursive(dreamingDir);
|
||||
for (const absolutePath of files) {
|
||||
const relativePath = path.relative(workspaceDir, absolutePath).replace(/\\/g, "/");
|
||||
artifacts.push({
|
||||
workspaceDir,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const deduped = new Map<string, BridgeArtifact>();
|
||||
for (const artifact of artifacts) {
|
||||
deduped.set(await resolveArtifactKey(artifact.absolutePath), artifact);
|
||||
}
|
||||
return [...deduped.values()];
|
||||
}
|
||||
|
||||
function resolveBridgeTitle(relativePath: string, agentIds: string[]): string {
|
||||
const base = relativePath
|
||||
.replace(/\.md$/i, "")
|
||||
.replace(/^memory\//, "")
|
||||
.replace(/\//g, " / ");
|
||||
if (agentIds.length === 0) {
|
||||
return `Memory Bridge: ${base}`;
|
||||
}
|
||||
return `Memory Bridge (${agentIds.join(", ")}): ${base}`;
|
||||
}
|
||||
|
||||
function resolveBridgePagePath(params: { workspaceDir: string; relativePath: string }): {
|
||||
pageId: string;
|
||||
pagePath: string;
|
||||
workspaceSlug: string;
|
||||
artifactSlug: string;
|
||||
} {
|
||||
const workspaceBaseSlug = slugifyWikiSegment(path.basename(params.workspaceDir));
|
||||
const workspaceHash = createHash("sha1").update(path.resolve(params.workspaceDir)).digest("hex");
|
||||
const artifactBaseSlug = slugifyWikiSegment(
|
||||
params.relativePath.replace(/\.md$/i, "").replace(/\//g, "-"),
|
||||
);
|
||||
const artifactHash = createHash("sha1").update(params.relativePath).digest("hex");
|
||||
const workspaceSlug = `${workspaceBaseSlug}-${workspaceHash.slice(0, 8)}`;
|
||||
const artifactSlug = `${artifactBaseSlug}-${artifactHash.slice(0, 8)}`;
|
||||
return {
|
||||
pageId: `source.bridge.${workspaceSlug}.${artifactSlug}`,
|
||||
pagePath: path
|
||||
.join("sources", `bridge-${workspaceSlug}-${artifactSlug}.md`)
|
||||
.replace(/\\/g, "/"),
|
||||
workspaceSlug,
|
||||
artifactSlug,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeBridgeSourcePage(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
artifact: BridgeArtifact;
|
||||
agentIds: string[];
|
||||
}): Promise<{ pagePath: string; changed: boolean; created: boolean }> {
|
||||
const { pageId, pagePath } = resolveBridgePagePath({
|
||||
workspaceDir: params.artifact.workspaceDir,
|
||||
relativePath: params.artifact.relativePath,
|
||||
});
|
||||
const title = resolveBridgeTitle(params.artifact.relativePath, params.agentIds);
|
||||
const pageAbsPath = path.join(params.config.vault.path, pagePath);
|
||||
const created = !(await pathExists(pageAbsPath));
|
||||
const raw = await fs.readFile(params.artifact.absolutePath, "utf8");
|
||||
const stats = await fs.stat(params.artifact.absolutePath);
|
||||
const sourceUpdatedAt = stats.mtime.toISOString();
|
||||
const rendered = renderWikiMarkdown({
|
||||
frontmatter: {
|
||||
pageType: "source",
|
||||
id: pageId,
|
||||
title,
|
||||
sourceType: "memory-bridge",
|
||||
sourcePath: params.artifact.absolutePath,
|
||||
bridgeRelativePath: params.artifact.relativePath,
|
||||
bridgeWorkspaceDir: params.artifact.workspaceDir,
|
||||
bridgeAgentIds: params.agentIds,
|
||||
status: "active",
|
||||
updatedAt: sourceUpdatedAt,
|
||||
},
|
||||
body: [
|
||||
`# ${title}`,
|
||||
"",
|
||||
"## Bridge Source",
|
||||
`- Workspace: \`${params.artifact.workspaceDir}\``,
|
||||
`- Relative path: \`${params.artifact.relativePath}\``,
|
||||
`- Agents: ${params.agentIds.length > 0 ? params.agentIds.join(", ") : "unknown"}`,
|
||||
`- Updated: ${sourceUpdatedAt}`,
|
||||
"",
|
||||
"## Content",
|
||||
renderMarkdownFence(raw, "markdown"),
|
||||
"",
|
||||
"## Notes",
|
||||
"<!-- openclaw:human:start -->",
|
||||
"<!-- openclaw:human:end -->",
|
||||
"",
|
||||
].join("\n"),
|
||||
});
|
||||
const existing = await fs.readFile(pageAbsPath, "utf8").catch(() => "");
|
||||
if (existing === rendered) {
|
||||
return { pagePath, changed: false, created };
|
||||
}
|
||||
await fs.writeFile(pageAbsPath, rendered, "utf8");
|
||||
return { pagePath, changed: true, created };
|
||||
}
|
||||
|
||||
export async function syncMemoryWikiBridgeSources(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
}): Promise<BridgeMemoryWikiResult> {
|
||||
await initializeMemoryWikiVault(params.config);
|
||||
if (
|
||||
params.config.vaultMode !== "bridge" ||
|
||||
!params.config.bridge.enabled ||
|
||||
!params.config.bridge.readMemoryCore ||
|
||||
!params.appConfig
|
||||
) {
|
||||
return {
|
||||
importedCount: 0,
|
||||
updatedCount: 0,
|
||||
skippedCount: 0,
|
||||
artifactCount: 0,
|
||||
workspaces: 0,
|
||||
pagePaths: [],
|
||||
};
|
||||
}
|
||||
|
||||
const memoryPluginConfig = resolveMemoryCorePluginConfig(params.appConfig);
|
||||
if (!memoryPluginConfig) {
|
||||
return {
|
||||
importedCount: 0,
|
||||
updatedCount: 0,
|
||||
skippedCount: 0,
|
||||
artifactCount: 0,
|
||||
workspaces: 0,
|
||||
pagePaths: [],
|
||||
};
|
||||
}
|
||||
|
||||
const workspaces = resolveMemoryDreamingWorkspaces(params.appConfig);
|
||||
const results: Array<{ pagePath: string; changed: boolean; created: boolean }> = [];
|
||||
let artifactCount = 0;
|
||||
for (const workspace of workspaces) {
|
||||
const artifacts = await collectWorkspaceArtifacts(workspace.workspaceDir, params.config.bridge);
|
||||
artifactCount += artifacts.length;
|
||||
for (const artifact of artifacts) {
|
||||
results.push(
|
||||
await writeBridgeSourcePage({
|
||||
config: params.config,
|
||||
artifact,
|
||||
agentIds: workspace.agentIds,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const importedCount = results.filter((result) => result.changed && result.created).length;
|
||||
const updatedCount = results.filter((result) => result.changed && !result.created).length;
|
||||
const skippedCount = results.filter((result) => !result.changed).length;
|
||||
const pagePaths = results
|
||||
.map((result) => result.pagePath)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
if (importedCount > 0 || updatedCount > 0) {
|
||||
await appendMemoryWikiLog(params.config.vault.path, {
|
||||
type: "ingest",
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
sourceType: "memory-bridge",
|
||||
workspaces: workspaces.length,
|
||||
artifactCount,
|
||||
importedCount,
|
||||
updatedCount,
|
||||
skippedCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
importedCount,
|
||||
updatedCount,
|
||||
skippedCount,
|
||||
artifactCount,
|
||||
workspaces: workspaces.length,
|
||||
pagePaths,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
import type { Command } from "commander";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import { syncMemoryWikiBridgeSources } from "./bridge.js";
|
||||
import { compileMemoryWikiVault } from "./compile.js";
|
||||
import type { MemoryWikiPluginConfig, ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
import { ingestMemoryWikiSource } from "./ingest.js";
|
||||
import { lintMemoryWikiVault } from "./lint.js";
|
||||
import {
|
||||
probeObsidianCli,
|
||||
runObsidianCommand,
|
||||
runObsidianDaily,
|
||||
runObsidianOpen,
|
||||
runObsidianSearch,
|
||||
} from "./obsidian.js";
|
||||
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
|
||||
import { renderMemoryWikiStatus, resolveMemoryWikiStatus } from "./status.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
|
@ -40,15 +49,50 @@ type WikiGetCommandOptions = {
|
|||
lines?: number;
|
||||
};
|
||||
|
||||
type WikiBridgeImportCommandOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type WikiObsidianSearchCommandOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type WikiObsidianOpenCommandOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type WikiObsidianCommandCommandOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type WikiObsidianDailyCommandOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
function isResolvedMemoryWikiConfig(
|
||||
config: MemoryWikiPluginConfig | ResolvedMemoryWikiConfig | undefined,
|
||||
): config is ResolvedMemoryWikiConfig {
|
||||
return Boolean(
|
||||
config &&
|
||||
"vaultMode" in config &&
|
||||
"vault" in config &&
|
||||
"bridge" in config &&
|
||||
"obsidian" in config &&
|
||||
"unsafeLocal" in config,
|
||||
);
|
||||
}
|
||||
|
||||
function writeOutput(output: string, writer: Pick<NodeJS.WriteStream, "write"> = process.stdout) {
|
||||
writer.write(output.endsWith("\n") ? output : `${output}\n`);
|
||||
}
|
||||
|
||||
export async function runWikiStatus(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
await syncMemoryWikiBridgeSources({ config: params.config, appConfig: params.appConfig });
|
||||
const status = await resolveMemoryWikiStatus(params.config);
|
||||
writeOutput(
|
||||
params.json ? JSON.stringify(status, null, 2) : renderMemoryWikiStatus(status),
|
||||
|
|
@ -72,9 +116,11 @@ export async function runWikiInit(params: {
|
|||
|
||||
export async function runWikiCompile(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
await syncMemoryWikiBridgeSources({ config: params.config, appConfig: params.appConfig });
|
||||
const result = await compileMemoryWikiVault(params.config);
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
|
|
@ -85,9 +131,11 @@ export async function runWikiCompile(params: {
|
|||
|
||||
export async function runWikiLint(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
await syncMemoryWikiBridgeSources({ config: params.config, appConfig: params.appConfig });
|
||||
const result = await lintMemoryWikiVault(params.config);
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
|
|
@ -117,11 +165,13 @@ export async function runWikiIngest(params: {
|
|||
|
||||
export async function runWikiSearch(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
query: string;
|
||||
maxResults?: number;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
await syncMemoryWikiBridgeSources({ config: params.config, appConfig: params.appConfig });
|
||||
const results = await searchMemoryWiki({
|
||||
config: params.config,
|
||||
query: params.query,
|
||||
|
|
@ -143,12 +193,14 @@ export async function runWikiSearch(params: {
|
|||
|
||||
export async function runWikiGet(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
lookup: string;
|
||||
fromLine?: number;
|
||||
lineCount?: number;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
await syncMemoryWikiBridgeSources({ config: params.config, appConfig: params.appConfig });
|
||||
const result = await getMemoryWikiPage({
|
||||
config: params.config,
|
||||
lookup: params.lookup,
|
||||
|
|
@ -162,8 +214,99 @@ export async function runWikiGet(params: {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function registerWikiCli(program: Command, pluginConfig?: MemoryWikiPluginConfig) {
|
||||
const config = resolveMemoryWikiConfig(pluginConfig);
|
||||
export async function runWikiBridgeImport(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const result = await syncMemoryWikiBridgeSources({
|
||||
config: params.config,
|
||||
appConfig: params.appConfig,
|
||||
});
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: `Bridge import synced ${result.artifactCount} artifacts across ${result.workspaces} workspaces (${result.importedCount} new, ${result.updatedCount} updated, ${result.skippedCount} unchanged).`;
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiObsidianStatus(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const result = await probeObsidianCli();
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: result.available
|
||||
? `Obsidian CLI available at ${result.command}`
|
||||
: "Obsidian CLI is not available on PATH.";
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiObsidianSearch(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
query: string;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const result = await runObsidianSearch({ config: params.config, query: params.query });
|
||||
const summary = params.json ? JSON.stringify(result, null, 2) : result.stdout.trim();
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiObsidianOpenCli(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
vaultPath: string;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const result = await runObsidianOpen({ config: params.config, vaultPath: params.vaultPath });
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: result.stdout.trim() || "Opened in Obsidian.";
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiObsidianCommandCli(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
id: string;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const result = await runObsidianCommand({ config: params.config, id: params.id });
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: result.stdout.trim() || "Command sent to Obsidian.";
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runWikiObsidianDailyCli(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
json?: boolean;
|
||||
stdout?: Pick<NodeJS.WriteStream, "write">;
|
||||
}) {
|
||||
const result = await runObsidianDaily({ config: params.config });
|
||||
const summary = params.json
|
||||
? JSON.stringify(result, null, 2)
|
||||
: result.stdout.trim() || "Opened today's daily note.";
|
||||
writeOutput(summary, params.stdout);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function registerWikiCli(
|
||||
program: Command,
|
||||
pluginConfig?: MemoryWikiPluginConfig | ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
) {
|
||||
const config = isResolvedMemoryWikiConfig(pluginConfig)
|
||||
? pluginConfig
|
||||
: resolveMemoryWikiConfig(pluginConfig);
|
||||
const wiki = program.command("wiki").description("Inspect and initialize the memory wiki vault");
|
||||
|
||||
wiki
|
||||
|
|
@ -171,7 +314,7 @@ export function registerWikiCli(program: Command, pluginConfig?: MemoryWikiPlugi
|
|||
.description("Show wiki vault status")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: WikiStatusCommandOptions) => {
|
||||
await runWikiStatus({ config, json: opts.json });
|
||||
await runWikiStatus({ config, appConfig, json: opts.json });
|
||||
});
|
||||
|
||||
wiki
|
||||
|
|
@ -187,7 +330,7 @@ export function registerWikiCli(program: Command, pluginConfig?: MemoryWikiPlugi
|
|||
.description("Refresh generated wiki indexes")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: WikiCompileCommandOptions) => {
|
||||
await runWikiCompile({ config, json: opts.json });
|
||||
await runWikiCompile({ config, appConfig, json: opts.json });
|
||||
});
|
||||
|
||||
wiki
|
||||
|
|
@ -195,7 +338,7 @@ export function registerWikiCli(program: Command, pluginConfig?: MemoryWikiPlugi
|
|||
.description("Lint the wiki vault and write a report")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: WikiLintCommandOptions) => {
|
||||
await runWikiLint({ config, json: opts.json });
|
||||
await runWikiLint({ config, appConfig, json: opts.json });
|
||||
});
|
||||
|
||||
wiki
|
||||
|
|
@ -217,6 +360,7 @@ export function registerWikiCli(program: Command, pluginConfig?: MemoryWikiPlugi
|
|||
.action(async (query: string, opts: WikiSearchCommandOptions) => {
|
||||
await runWikiSearch({
|
||||
config,
|
||||
appConfig,
|
||||
query,
|
||||
maxResults: opts.maxResults,
|
||||
json: opts.json,
|
||||
|
|
@ -233,10 +377,62 @@ export function registerWikiCli(program: Command, pluginConfig?: MemoryWikiPlugi
|
|||
.action(async (lookup: string, opts: WikiGetCommandOptions) => {
|
||||
await runWikiGet({
|
||||
config,
|
||||
appConfig,
|
||||
lookup,
|
||||
fromLine: opts.from,
|
||||
lineCount: opts.lines,
|
||||
json: opts.json,
|
||||
});
|
||||
});
|
||||
|
||||
const bridge = wiki
|
||||
.command("bridge")
|
||||
.description("Import public memory-core artifacts into the wiki vault");
|
||||
bridge
|
||||
.command("import")
|
||||
.description("Sync bridge-backed memory-core artifacts into wiki source pages")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: WikiBridgeImportCommandOptions) => {
|
||||
await runWikiBridgeImport({ config, appConfig, json: opts.json });
|
||||
});
|
||||
|
||||
const obsidian = wiki.command("obsidian").description("Run official Obsidian CLI helpers");
|
||||
obsidian
|
||||
.command("status")
|
||||
.description("Probe the Obsidian CLI")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: WikiStatusCommandOptions) => {
|
||||
await runWikiObsidianStatus({ config, json: opts.json });
|
||||
});
|
||||
obsidian
|
||||
.command("search")
|
||||
.description("Search the current Obsidian vault")
|
||||
.argument("<query>", "Search query")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (query: string, opts: WikiObsidianSearchCommandOptions) => {
|
||||
await runWikiObsidianSearch({ config, query, json: opts.json });
|
||||
});
|
||||
obsidian
|
||||
.command("open")
|
||||
.description("Open a file in Obsidian by vault-relative path")
|
||||
.argument("<path>", "Vault-relative path")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (vaultPath: string, opts: WikiObsidianOpenCommandOptions) => {
|
||||
await runWikiObsidianOpenCli({ config, vaultPath, json: opts.json });
|
||||
});
|
||||
obsidian
|
||||
.command("command")
|
||||
.description("Execute an Obsidian command palette command by id")
|
||||
.argument("<id>", "Obsidian command id")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (id: string, opts: WikiObsidianCommandCommandOptions) => {
|
||||
await runWikiObsidianCommandCli({ config, id, json: opts.json });
|
||||
});
|
||||
obsidian
|
||||
.command("daily")
|
||||
.description("Open today's daily note in Obsidian")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: WikiObsidianDailyCommandOptions) => {
|
||||
await runWikiObsidianDailyCli({ config, json: opts.json });
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
import { runObsidianDaily, runObsidianSearch } from "./obsidian.js";
|
||||
|
||||
describe("runObsidianSearch", () => {
|
||||
it("builds the official obsidian cli argv with the configured vault name", async () => {
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{
|
||||
obsidian: {
|
||||
enabled: true,
|
||||
useOfficialCli: true,
|
||||
vaultName: "OpenClaw Wiki",
|
||||
},
|
||||
},
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
const calls: Array<{ command: string; argv: string[] }> = [];
|
||||
const exec: NonNullable<
|
||||
NonNullable<Parameters<typeof runObsidianSearch>[0]["deps"]>["exec"]
|
||||
> = async (command, argv) => {
|
||||
calls.push({ command, argv: [...argv] });
|
||||
return { stdout: "search output\n", stderr: "" };
|
||||
};
|
||||
|
||||
const result = await runObsidianSearch({
|
||||
config,
|
||||
query: "agent memory",
|
||||
deps: {
|
||||
exec,
|
||||
resolveCommand: async () => "/usr/local/bin/obsidian",
|
||||
},
|
||||
});
|
||||
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
command: "/usr/local/bin/obsidian",
|
||||
argv: ["vault=OpenClaw Wiki", "search", "query=agent memory"],
|
||||
},
|
||||
]);
|
||||
expect(result.stdout).toBe("search output\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runObsidianDaily", () => {
|
||||
it("fails cleanly when the obsidian cli is not installed", async () => {
|
||||
const config = resolveMemoryWikiConfig(undefined, { homedir: "/Users/tester" });
|
||||
|
||||
await expect(
|
||||
runObsidianDaily({
|
||||
config,
|
||||
deps: {
|
||||
resolveCommand: async () => null,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("Obsidian CLI is not available on PATH.");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export type ObsidianCliProbe = {
|
||||
available: boolean;
|
||||
command: string | null;
|
||||
};
|
||||
|
||||
export type ObsidianCliResult = {
|
||||
command: string;
|
||||
argv: string[];
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
type ObsidianCliDeps = {
|
||||
exec?: typeof execFileAsync;
|
||||
resolveCommand?: (command: string) => Promise<string | null>;
|
||||
};
|
||||
|
||||
async function isExecutableFile(inputPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(inputPath, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveCommandOnPath(command: string): Promise<string | null> {
|
||||
const pathValue = process.env.PATH ?? "";
|
||||
const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
|
||||
const windowsExts =
|
||||
process.platform === "win32"
|
||||
? (process.env.PATHEXT?.split(";").filter(Boolean) ?? [".EXE", ".CMD", ".BAT"])
|
||||
: [""];
|
||||
|
||||
if (command.includes(path.sep)) {
|
||||
return (await isExecutableFile(command)) ? command : null;
|
||||
}
|
||||
|
||||
for (const dir of pathEntries) {
|
||||
for (const extension of windowsExts) {
|
||||
const candidate = path.join(dir, extension ? `${command}${extension}` : command);
|
||||
if (await isExecutableFile(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildVaultPrefix(config: ResolvedMemoryWikiConfig): string[] {
|
||||
return config.obsidian.vaultName ? [`vault=${config.obsidian.vaultName}`] : [];
|
||||
}
|
||||
|
||||
export async function probeObsidianCli(
|
||||
deps?: Pick<ObsidianCliDeps, "resolveCommand">,
|
||||
): Promise<ObsidianCliProbe> {
|
||||
const resolveCommand = deps?.resolveCommand ?? resolveCommandOnPath;
|
||||
const command = await resolveCommand("obsidian");
|
||||
return {
|
||||
available: command !== null,
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runObsidianCli(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
subcommand: string;
|
||||
args?: string[];
|
||||
deps?: ObsidianCliDeps;
|
||||
}): Promise<ObsidianCliResult> {
|
||||
const resolveCommand = params.deps?.resolveCommand ?? resolveCommandOnPath;
|
||||
const exec = params.deps?.exec ?? execFileAsync;
|
||||
const probe = await probeObsidianCli({ resolveCommand });
|
||||
if (!probe.command) {
|
||||
throw new Error("Obsidian CLI is not available on PATH.");
|
||||
}
|
||||
const argv = [...buildVaultPrefix(params.config), params.subcommand, ...(params.args ?? [])];
|
||||
const { stdout, stderr } = await exec(probe.command, argv, { encoding: "utf8" });
|
||||
return {
|
||||
command: probe.command,
|
||||
argv,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runObsidianSearch(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
query: string;
|
||||
deps?: ObsidianCliDeps;
|
||||
}) {
|
||||
return await runObsidianCli({
|
||||
config: params.config,
|
||||
subcommand: "search",
|
||||
args: [`query=${params.query}`],
|
||||
deps: params.deps,
|
||||
});
|
||||
}
|
||||
|
||||
export async function runObsidianOpen(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
vaultPath: string;
|
||||
deps?: ObsidianCliDeps;
|
||||
}) {
|
||||
return await runObsidianCli({
|
||||
config: params.config,
|
||||
subcommand: "open",
|
||||
args: [`path=${params.vaultPath}`],
|
||||
deps: params.deps,
|
||||
});
|
||||
}
|
||||
|
||||
export async function runObsidianCommand(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
id: string;
|
||||
deps?: ObsidianCliDeps;
|
||||
}) {
|
||||
return await runObsidianCli({
|
||||
config: params.config,
|
||||
subcommand: "command",
|
||||
args: [`id=${params.id}`],
|
||||
deps: params.deps,
|
||||
});
|
||||
}
|
||||
|
||||
export async function runObsidianDaily(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
deps?: ObsidianCliDeps;
|
||||
}) {
|
||||
return await runObsidianCli({
|
||||
config: params.config,
|
||||
subcommand: "daily",
|
||||
deps: params.deps,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { inferWikiPageKind, type WikiPageKind } from "./markdown.js";
|
||||
import { probeObsidianCli } from "./obsidian.js";
|
||||
|
||||
export type MemoryWikiStatusWarning = {
|
||||
code:
|
||||
|
|
@ -49,39 +49,6 @@ async function pathExists(inputPath: string): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
async function isExecutableFile(inputPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(inputPath, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCommandOnPath(command: string): Promise<string | null> {
|
||||
const pathValue = process.env.PATH ?? "";
|
||||
const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
|
||||
const windowsExts =
|
||||
process.platform === "win32"
|
||||
? (process.env.PATHEXT?.split(";").filter(Boolean) ?? [".EXE", ".CMD", ".BAT"])
|
||||
: [""];
|
||||
|
||||
if (command.includes(path.sep)) {
|
||||
return (await isExecutableFile(command)) ? command : null;
|
||||
}
|
||||
|
||||
for (const dir of pathEntries) {
|
||||
for (const extension of windowsExts) {
|
||||
const candidate = path.join(dir, extension ? `${command}${extension}` : command);
|
||||
if (await isExecutableFile(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function collectPageCounts(vaultPath: string): Promise<Record<WikiPageKind, number>> {
|
||||
const counts: Record<WikiPageKind, number> = {
|
||||
entity: 0,
|
||||
|
|
@ -172,9 +139,8 @@ export async function resolveMemoryWikiStatus(
|
|||
deps?: ResolveMemoryWikiStatusDeps,
|
||||
): Promise<MemoryWikiStatus> {
|
||||
const exists = deps?.pathExists ?? pathExists;
|
||||
const resolveCommand = deps?.resolveCommand ?? resolveCommandOnPath;
|
||||
const vaultExists = await exists(config.vault.path);
|
||||
const obsidianCommand = await resolveCommand("obsidian");
|
||||
const obsidianProbe = await probeObsidianCli({ resolveCommand: deps?.resolveCommand });
|
||||
const pageCounts = vaultExists
|
||||
? await collectPageCounts(config.vault.path)
|
||||
: {
|
||||
|
|
@ -194,15 +160,15 @@ export async function resolveMemoryWikiStatus(
|
|||
obsidianCli: {
|
||||
enabled: config.obsidian.enabled,
|
||||
requested: config.obsidian.enabled && config.obsidian.useOfficialCli,
|
||||
available: obsidianCommand !== null,
|
||||
command: obsidianCommand,
|
||||
available: obsidianProbe.available,
|
||||
command: obsidianProbe.command,
|
||||
},
|
||||
unsafeLocal: {
|
||||
allowPrivateMemoryCoreAccess: config.unsafeLocal.allowPrivateMemoryCoreAccess,
|
||||
pathCount: config.unsafeLocal.paths.length,
|
||||
},
|
||||
pageCounts,
|
||||
warnings: buildWarnings({ config, vaultExists, obsidianCommand }),
|
||||
warnings: buildWarnings({ config, vaultExists, obsidianCommand: obsidianProbe.command }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Type } from "@sinclair/typebox";
|
||||
import type { AnyAgentTool } from "../api.js";
|
||||
import type { AnyAgentTool, OpenClawConfig } from "../api.js";
|
||||
import { syncMemoryWikiBridgeSources } from "./bridge.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
|
||||
import { renderMemoryWikiStatus, resolveMemoryWikiStatus } from "./status.js";
|
||||
|
|
@ -21,7 +22,14 @@ const WikiGetSchema = Type.Object(
|
|||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export function createWikiStatusTool(config: ResolvedMemoryWikiConfig): AnyAgentTool {
|
||||
async function syncBridgeIfNeeded(config: ResolvedMemoryWikiConfig, appConfig?: OpenClawConfig) {
|
||||
await syncMemoryWikiBridgeSources({ config, appConfig });
|
||||
}
|
||||
|
||||
export function createWikiStatusTool(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
): AnyAgentTool {
|
||||
return {
|
||||
name: "wiki_status",
|
||||
label: "Wiki Status",
|
||||
|
|
@ -29,6 +37,7 @@ export function createWikiStatusTool(config: ResolvedMemoryWikiConfig): AnyAgent
|
|||
"Inspect the current memory wiki vault mode, health, and Obsidian CLI availability.",
|
||||
parameters: WikiStatusSchema,
|
||||
execute: async () => {
|
||||
await syncBridgeIfNeeded(config, appConfig);
|
||||
const status = await resolveMemoryWikiStatus(config);
|
||||
return {
|
||||
content: [{ type: "text", text: renderMemoryWikiStatus(status) }],
|
||||
|
|
@ -38,7 +47,10 @@ export function createWikiStatusTool(config: ResolvedMemoryWikiConfig): AnyAgent
|
|||
};
|
||||
}
|
||||
|
||||
export function createWikiSearchTool(config: ResolvedMemoryWikiConfig): AnyAgentTool {
|
||||
export function createWikiSearchTool(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
): AnyAgentTool {
|
||||
return {
|
||||
name: "wiki_search",
|
||||
label: "Wiki Search",
|
||||
|
|
@ -46,6 +58,7 @@ export function createWikiSearchTool(config: ResolvedMemoryWikiConfig): AnyAgent
|
|||
parameters: WikiSearchSchema,
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const params = rawParams as { query: string; maxResults?: number };
|
||||
await syncBridgeIfNeeded(config, appConfig);
|
||||
const results = await searchMemoryWiki({
|
||||
config,
|
||||
query: params.query,
|
||||
|
|
@ -68,7 +81,10 @@ export function createWikiSearchTool(config: ResolvedMemoryWikiConfig): AnyAgent
|
|||
};
|
||||
}
|
||||
|
||||
export function createWikiGetTool(config: ResolvedMemoryWikiConfig): AnyAgentTool {
|
||||
export function createWikiGetTool(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
): AnyAgentTool {
|
||||
return {
|
||||
name: "wiki_get",
|
||||
label: "Wiki Get",
|
||||
|
|
@ -76,6 +92,7 @@ export function createWikiGetTool(config: ResolvedMemoryWikiConfig): AnyAgentToo
|
|||
parameters: WikiGetSchema,
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const params = rawParams as { lookup: string; fromLine?: number; lineCount?: number };
|
||||
await syncBridgeIfNeeded(config, appConfig);
|
||||
const result = await getMemoryWikiPage({
|
||||
config,
|
||||
lookup: params.lookup,
|
||||
|
|
|
|||
Loading…
Reference in New Issue