Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 3 additions & 81 deletions apps/memos-local-openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SkillInstaller } from "./src/skill/installer";
import { Summarizer } from "./src/ingest/providers";
import { MEMORY_GUIDE_SKILL_MD } from "./src/skill/bundled-memory-guide";
import { Telemetry } from "./src/telemetry";
import { ensureBetterSqlite3Available } from "./src/runtime/sqlite-bootstrap";


/** Remove near-duplicate hits based on summary word overlap (>70%). Keeps first (highest-scored) hit. */
Expand Down Expand Up @@ -75,86 +76,7 @@ const memosLocalPlugin = {
configSchema: pluginConfigSchema,

register(api: OpenClawPluginApi) {
// ─── Ensure better-sqlite3 native module is available ───
const pluginDir = path.dirname(new URL(import.meta.url).pathname);
let sqliteReady = false;

function trySqliteLoad(): boolean {
try {
const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
if (!resolved.startsWith(pluginDir)) {
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
return false;
}
require(resolved);
return true;
} catch {
return false;
}
}

sqliteReady = trySqliteLoad();

if (!sqliteReady) {
api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`);

try {
const { spawnSync } = require("child_process");
const rebuildResult = spawnSync("npm", ["rebuild", "better-sqlite3"], {
cwd: pluginDir,
stdio: "pipe",
shell: true,
timeout: 120_000,
});

const stdout = rebuildResult.stdout?.toString() || "";
const stderr = rebuildResult.stderr?.toString() || "";
if (stdout) api.logger.info(`memos-local: rebuild stdout: ${stdout.slice(0, 500)}`);
if (stderr) api.logger.warn(`memos-local: rebuild stderr: ${stderr.slice(0, 500)}`);

if (rebuildResult.status === 0) {
Object.keys(require.cache)
.filter(k => k.includes("better-sqlite3") || k.includes("better_sqlite3"))
.forEach(k => delete require.cache[k]);
sqliteReady = trySqliteLoad();
if (sqliteReady) {
api.logger.info("memos-local: better-sqlite3 auto-rebuild succeeded!");
} else {
api.logger.warn("memos-local: rebuild exited 0 but module still not loadable from plugin dir");
}
} else {
api.logger.warn(`memos-local: rebuild exited with code ${rebuildResult.status}`);
}
} catch (rebuildErr) {
api.logger.warn(`memos-local: auto-rebuild error: ${rebuildErr}`);
}

if (!sqliteReady) {
const msg = [
"",
"╔══════════════════════════════════════════════════════════════╗",
"║ MemOS Local Memory — better-sqlite3 native module missing ║",
"╠══════════════════════════════════════════════════════════════╣",
"║ ║",
"║ Auto-rebuild failed. Run these commands manually: ║",
"║ ║",
`║ cd ${pluginDir}`,
"║ npm rebuild better-sqlite3 ║",
"║ openclaw gateway stop && openclaw gateway start ║",
"║ ║",
"║ If rebuild fails, install build tools first: ║",
"║ macOS: xcode-select --install ║",
"║ Linux: sudo apt install build-essential python3 ║",
"║ ║",
"╚══════════════════════════════════════════════════════════════╝",
"",
].join("\n");
api.logger.warn(msg);
throw new Error(
`better-sqlite3 native module not found. Auto-rebuild failed. Fix: cd ${pluginDir} && npm rebuild better-sqlite3`
);
}
}
const pluginDir = ensureBetterSqlite3Available(api, import.meta.url);

const pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
const stateDir = api.resolvePath("~/.openclaw");
Expand All @@ -178,7 +100,7 @@ const memosLocalPlugin = {

let pluginVersion = "0.0.0";
try {
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf-8"));
const pkg = JSON.parse(fs.readFileSync(path.join(pluginDir, "package.json"), "utf-8"));
pluginVersion = pkg.version ?? pluginVersion;
} catch {}
const telemetry = new Telemetry(ctx.config.telemetry ?? {}, stateDir, pluginVersion, ctx.log);
Expand Down
226 changes: 226 additions & 0 deletions apps/memos-local-openclaw/src/runtime/sqlite-bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { spawnSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import { createRequire } from "module";
import { fileURLToPath } from "url";

type Logger = {
info: (message: string) => void;
warn: (message: string) => void;
};

type BootstrapApi = {
logger: Logger;
};

type RebuildResult = {
status: number | null;
stdout?: string | Buffer | null;
stderr?: string | Buffer | null;
};

export type BetterSqliteRuntime = {
resolveFromPluginDir: (pluginDir: string) => string;
load: (resolvedPath: string) => void;
rebuild: (pluginDir: string) => RebuildResult;
clearCache: () => void;
};

function fileUrlToPathWithWindowsFallback(
importMetaUrl: string,
platform: NodeJS.Platform,
): string {
const nativePath = fileURLToPath(importMetaUrl);
if (platform !== "win32" || process.platform === "win32" || !/^\/[A-Za-z]:/.test(nativePath)) {
return nativePath;
}

const parsedUrl = new URL(importMetaUrl);
if (parsedUrl.protocol !== "file:") {
return nativePath;
}

if (parsedUrl.hostname) {
const uncPath = decodeURIComponent(parsedUrl.pathname || "").replace(/\//g, "\\");
return `\\\\${parsedUrl.hostname}${uncPath}`;
}

let pathname = decodeURIComponent(parsedUrl.pathname || "");
if (/^\/[A-Za-z]:/.test(pathname)) {
pathname = pathname.slice(1);
}

return pathname.replace(/\//g, "\\");
}

export function getPluginDirFromImportMeta(
importMetaUrl: string,
platform: NodeJS.Platform = process.platform,
): string {
const pathApi = platform === "win32" ? path.win32 : path.posix;
return pathApi.dirname(fileUrlToPathWithWindowsFallback(importMetaUrl, platform));
}

function canonicalizePath(fsPath: string, platform: NodeJS.Platform): string {
const pathApi = platform === "win32" ? path.win32 : path.posix;

let resolved = pathApi.resolve(fsPath);
if (platform === process.platform) {
try {
resolved = fs.realpathSync.native?.(fsPath) ?? fs.realpathSync(fsPath);
} catch {
resolved = pathApi.resolve(fsPath);
}
}

if (platform === "win32") {
return resolved.replace(/\//g, "\\").toLowerCase();
}

return resolved;
}

export function isPathWithinDir(
candidatePath: string,
baseDir: string,
platform: NodeJS.Platform = process.platform,
): boolean {
const pathApi = platform === "win32" ? path.win32 : path.posix;
const candidate = canonicalizePath(candidatePath, platform);
const base = canonicalizePath(baseDir, platform);
const relative = pathApi.relative(base, candidate);

return (
relative === "" ||
(!(relative === ".." || relative.startsWith(`..${pathApi.sep}`)) && !pathApi.isAbsolute(relative))
);
}

function createBetterSqliteRuntime(importMetaUrl: string): BetterSqliteRuntime {
const runtimeRequire = createRequire(importMetaUrl);

return {
resolveFromPluginDir(pluginDir: string): string {
return runtimeRequire.resolve("better-sqlite3", { paths: [pluginDir] });
},
load(resolvedPath: string): void {
runtimeRequire(resolvedPath);
},
rebuild(pluginDir: string): RebuildResult {
return spawnSync("npm", ["rebuild", "better-sqlite3"], {
cwd: pluginDir,
stdio: "pipe",
shell: true,
timeout: 120_000,
});
},
clearCache(): void {
Object.keys(runtimeRequire.cache)
.filter((cacheKey) => cacheKey.includes("better-sqlite3") || cacheKey.includes("better_sqlite3"))
.forEach((cacheKey) => delete runtimeRequire.cache[cacheKey]);
},
};
}

function tryLoadBetterSqlite(
api: BootstrapApi,
pluginDir: string,
platform: NodeJS.Platform,
runtime: BetterSqliteRuntime,
): boolean {
try {
const resolved = runtime.resolveFromPluginDir(pluginDir);
if (!isPathWithinDir(resolved, pluginDir, platform)) {
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
return false;
}

runtime.load(resolved);
return true;
} catch {
return false;
}
}

function formatOutputSnippet(output: string | Buffer | null | undefined): string {
if (!output) {
return "";
}

return output.toString().slice(0, 500);
}

export function ensureBetterSqlite3Available(
api: BootstrapApi,
importMetaUrl: string,
options: {
platform?: NodeJS.Platform;
runtime?: BetterSqliteRuntime;
} = {},
): string {
const platform = options.platform ?? process.platform;
const pluginDir = getPluginDirFromImportMeta(importMetaUrl, platform);
const runtime = options.runtime ?? createBetterSqliteRuntime(importMetaUrl);

let sqliteReady = tryLoadBetterSqlite(api, pluginDir, platform, runtime);
if (sqliteReady) {
return pluginDir;
}

api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`);

try {
const rebuildResult = runtime.rebuild(pluginDir);
const stdout = formatOutputSnippet(rebuildResult.stdout);
const stderr = formatOutputSnippet(rebuildResult.stderr);

if (stdout) {
api.logger.info(`memos-local: rebuild stdout: ${stdout}`);
}
if (stderr) {
api.logger.warn(`memos-local: rebuild stderr: ${stderr}`);
}

if (rebuildResult.status === 0) {
runtime.clearCache();
sqliteReady = tryLoadBetterSqlite(api, pluginDir, platform, runtime);
if (sqliteReady) {
api.logger.info("memos-local: better-sqlite3 auto-rebuild succeeded!");
} else {
api.logger.warn("memos-local: rebuild exited 0 but module still not loadable from plugin dir");
}
} else {
api.logger.warn(`memos-local: rebuild exited with code ${rebuildResult.status}`);
}
} catch (rebuildErr) {
api.logger.warn(`memos-local: auto-rebuild error: ${rebuildErr}`);
}

if (sqliteReady) {
return pluginDir;
}

const msg = [
"",
"╔══════════════════════════════════════════════════════════════╗",
"║ MemOS Local Memory — better-sqlite3 native module missing ║",
"╠══════════════════════════════════════════════════════════════╣",
"║ ║",
"║ Auto-rebuild failed. Run these commands manually: ║",
"║ ║",
`║ cd ${pluginDir}`,
"║ npm rebuild better-sqlite3 ║",
"║ openclaw gateway stop && openclaw gateway start ║",
"║ ║",
"║ If rebuild fails, install build tools first: ║",
"║ macOS: xcode-select --install ║",
"║ Linux: sudo apt install build-essential python3 ║",
"║ ║",
"╚══════════════════════════════════════════════════════════════╝",
"",
].join("\n");
api.logger.warn(msg);
throw new Error(
`better-sqlite3 native module not found. Auto-rebuild failed. Fix: cd ${pluginDir} && npm rebuild better-sqlite3`,
);
}
Loading
Loading