Files
openclaw/extensions/memory-wiki/src/obsidian.ts
2026-04-06 04:56:52 +01:00

146 lines
3.8 KiB
TypeScript

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,
});
}