From 111b8cc68a244c8a516542ae8b85c9878995d505 Mon Sep 17 00:00:00 2001 From: fancyboi999 <135568692+fancyboi999@users.noreply.github.com> Date: Wed, 6 May 2026 13:40:22 +0800 Subject: [PATCH 1/2] fix(memos-local-openclaw): ship compiled JS for OpenClaw plugin loader (#1619) The npm package previously published only TypeScript source, so OpenClaw 2026.5.4's plugin loader rejected it with "expected ./dist/index.js, ./dist/index.mjs, ./dist/index.cjs". - tsconfig: rootDir=., include root index.ts, emit ESM (ES2022) - package.json / openclaw.plugin.json: entry -> dist/index.js, files include dist, prepublishOnly does a clean tsc build - scripts/fix-esm-imports.cjs: append .js suffix to relative imports in dist/ so Node's native ESM loader can resolve them (moduleResolution: bundler omits suffixes during emit) - src/shared/plugin-root.ts: marker-based plugin-root lookup, replacing fragile __dirname/../.. paths that no longer match the new dist/src/* layout (4 call sites unified) - src/openclaw-sdk.d.ts: dev-time type shim for openclaw/plugin-sdk - index.ts: add type annotations + null guard so the previously uncompiled root entry passes strict-mode tsc Verified: tsc compiles clean, fix-esm-imports rewrites 32 files, Node 25 ESM loader resolves dist/index.js -> exports {id, register}, which matches OpenClaw's plugin contract. --- apps/memos-local-openclaw/index.ts | 12 ++--- .../memos-local-openclaw/openclaw.plugin.json | 2 +- apps/memos-local-openclaw/package.json | 12 ++--- .../scripts/fix-esm-imports.cjs | 51 +++++++++++++++++++ .../src/openclaw-sdk.d.ts | 13 +++++ .../src/shared/plugin-root.ts | 28 ++++++++++ .../src/skill/bundled-memory-guide.ts | 3 +- .../src/storage/ensure-binding.ts | 6 ++- apps/memos-local-openclaw/src/telemetry.ts | 3 +- .../memos-local-openclaw/src/viewer/server.ts | 18 +++---- apps/memos-local-openclaw/tsconfig.json | 10 ++-- 11 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 apps/memos-local-openclaw/scripts/fix-esm-imports.cjs create mode 100644 apps/memos-local-openclaw/src/openclaw-sdk.d.ts create mode 100644 apps/memos-local-openclaw/src/shared/plugin-root.ts diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 5e2245198..8d6577589 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -527,7 +527,7 @@ const memosLocalPlugin = { // ─── Tool: memory_search ─── api.registerTool( - (context) => ({ + (context: any) => ({ name: "memory_search", label: "Memory Search", description: @@ -758,7 +758,7 @@ const memosLocalPlugin = { // ─── Tool: memory_timeline ─── api.registerTool( - (context) => ({ + (context: any) => ({ name: "memory_timeline", label: "Memory Timeline", description: @@ -819,7 +819,7 @@ const memosLocalPlugin = { // ─── Tool: memory_get ─── api.registerTool( - (context) => ({ + (context: any) => ({ name: "memory_get", label: "Memory Get", description: @@ -1124,7 +1124,7 @@ ${detail.content}`, }; } - const groupNames = status.user.groups.map((group) => group.name); + const groupNames = (status.user.groups ?? []).map((group: { name: string }) => group.name); return { content: [{ type: "text", @@ -1382,7 +1382,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`, const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10); api.registerTool( - (context) => ({ + (context: any) => ({ name: "memory_viewer", label: "Open Memory Viewer", description: @@ -1646,7 +1646,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`, // ─── Tool: skill_search ─── api.registerTool( - (context) => ({ + (context: any) => ({ name: "skill_search", label: "Skill Search", description: diff --git a/apps/memos-local-openclaw/openclaw.plugin.json b/apps/memos-local-openclaw/openclaw.plugin.json index bb828c19f..ebc480a98 100644 --- a/apps/memos-local-openclaw/openclaw.plugin.json +++ b/apps/memos-local-openclaw/openclaw.plugin.json @@ -33,5 +33,5 @@ "If better-sqlite3 fails to build, ensure you have C++ build tools: xcode-select --install (macOS) or build-essential (Linux)" ] }, - "extensions": ["./index.ts"] + "extensions": ["./dist/index.js"] } diff --git a/apps/memos-local-openclaw/package.json b/apps/memos-local-openclaw/package.json index ca245051d..b032d2073 100644 --- a/apps/memos-local-openclaw/package.json +++ b/apps/memos-local-openclaw/package.json @@ -3,10 +3,10 @@ "version": "1.0.9-beta.1", "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval", "type": "module", - "main": "index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ - "index.ts", - "src", + "dist", "skill", "prebuilds", "scripts/native-binding.cjs", @@ -19,7 +19,7 @@ "openclaw": { "id": "memos-local-openclaw-plugin", "extensions": [ - "./index.ts" + "./dist/index.js" ], "skills": [ "skill/memos-memory-guide" @@ -27,14 +27,14 @@ "installDependencies": true }, "scripts": { - "build": "tsc", + "build": "tsc && node scripts/fix-esm-imports.cjs", "dev": "tsc --watch", "lint": "eslint src --ext .ts", "test": "vitest run", "test:watch": "vitest", "test:accuracy": "tsx scripts/run-accuracy-test.ts", "postinstall": "node scripts/postinstall.cjs", - "prepublishOnly": "echo 'Source-only publish — no build needed.'" + "prepublishOnly": "rm -rf dist && tsc && node scripts/fix-esm-imports.cjs" }, "keywords": [ "openclaw", diff --git a/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs b/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs new file mode 100644 index 000000000..b7351bfe0 --- /dev/null +++ b/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +"use strict"; + +/** + * Post-build: append `.js` (or `/index.js`) to every relative ESM import in dist/. + * + * Why: tsconfig uses `moduleResolution: "bundler"` so source files don't need + * `.js` suffixes — but Node's native ESM loader requires explicit suffixes. + * OpenClaw's plugin loader runs the emitted JS through the standard loader, + * so we rewrite the imports here instead of polluting source with `.js`. + */ + +const fs = require("fs"); +const path = require("path"); + +const DIST = path.resolve(__dirname, "..", "dist"); + +// Matches: import ... from "X" | export ... from "X" | import("X") +const IMPORT_RE = /(\bfrom\s*['"]|\bimport\s*\(\s*['"])(\.[^'"]+)(['"])/g; + +function rewriteSpec(spec, fileDir) { + if (/\.(m?js|cjs|json)$/.test(spec)) return spec; + const abs = path.resolve(fileDir, spec); + if (fs.existsSync(abs + ".js")) return spec + ".js"; + if (fs.existsSync(path.join(abs, "index.js"))) return spec.replace(/\/?$/, "/index.js"); + return spec; +} + +let touched = 0; +function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { walk(full); continue; } + if (!entry.name.endsWith(".js")) continue; + const src = fs.readFileSync(full, "utf8"); + let changed = false; + const out = src.replace(IMPORT_RE, (m, head, spec, tail) => { + const next = rewriteSpec(spec, path.dirname(full)); + if (next !== spec) { changed = true; return head + next + tail; } + return m; + }); + if (changed) { fs.writeFileSync(full, out); touched++; } + } +} + +if (!fs.existsSync(DIST)) { + console.error("[fix-esm-imports] dist/ not found — run tsc first"); + process.exit(1); +} +walk(DIST); +console.log(`[fix-esm-imports] rewrote imports in ${touched} file(s)`); diff --git a/apps/memos-local-openclaw/src/openclaw-sdk.d.ts b/apps/memos-local-openclaw/src/openclaw-sdk.d.ts new file mode 100644 index 000000000..ab3ded05f --- /dev/null +++ b/apps/memos-local-openclaw/src/openclaw-sdk.d.ts @@ -0,0 +1,13 @@ +declare module "openclaw/plugin-sdk" { + export interface OpenClawPluginApi { + registerTool(...args: any[]): void; + registerHook(...args: any[]): void; + registerMemoryCapability(...args: any[]): void; + registerService(...args: any[]): void; + getConfig(): any; + getLogger(): any; + config: any; + logger: any; + [key: string]: any; + } +} diff --git a/apps/memos-local-openclaw/src/shared/plugin-root.ts b/apps/memos-local-openclaw/src/shared/plugin-root.ts new file mode 100644 index 000000000..4821baf7f --- /dev/null +++ b/apps/memos-local-openclaw/src/shared/plugin-root.ts @@ -0,0 +1,28 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Resolve the plugin's installation root by walking up from the caller's file + * until a `package.json` whose name matches `memos-local-openclaw-plugin` is found. + * + * Necessary because the build emits to `dist/` with `rootDir: "."`, so + * compiled files live one extra level deep than their sources. Hard-coded + * `../../` paths break across dev (src/) vs published (dist/src/) layouts. + */ +export function findPluginRoot(importMetaUrl: string): string { + let dir = path.dirname(fileURLToPath(importMetaUrl)); + for (let i = 0; i < 8; i++) { + const pkgPath = path.join(dir, "package.json"); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + if (typeof pkg.name === "string" && pkg.name.includes("memos-local")) return dir; + } catch { /* keep walking */ } + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error(`findPluginRoot: could not locate plugin root from ${importMetaUrl}`); +} diff --git a/apps/memos-local-openclaw/src/skill/bundled-memory-guide.ts b/apps/memos-local-openclaw/src/skill/bundled-memory-guide.ts index 3efb787d1..51b332536 100644 --- a/apps/memos-local-openclaw/src/skill/bundled-memory-guide.ts +++ b/apps/memos-local-openclaw/src/skill/bundled-memory-guide.ts @@ -4,6 +4,7 @@ */ import * as fs from "fs"; import * as path from "path"; +import { findPluginRoot } from "../shared/plugin-root"; -const skillPath = path.join(__dirname, "..", "..", "skill", "memos-memory-guide", "SKILL.md"); +const skillPath = path.join(findPluginRoot(import.meta.url), "skill", "memos-memory-guide", "SKILL.md"); export const MEMORY_GUIDE_SKILL_MD: string = fs.readFileSync(skillPath, "utf-8"); diff --git a/apps/memos-local-openclaw/src/storage/ensure-binding.ts b/apps/memos-local-openclaw/src/storage/ensure-binding.ts index 29fbd4e96..595e099e5 100644 --- a/apps/memos-local-openclaw/src/storage/ensure-binding.ts +++ b/apps/memos-local-openclaw/src/storage/ensure-binding.ts @@ -2,6 +2,10 @@ import { existsSync, mkdirSync, copyFileSync } from "fs"; import { execSync } from "child_process"; import path from "path"; import { createRequire } from "module"; +import { fileURLToPath } from "node:url"; +import { findPluginRoot } from "../shared/plugin-root"; + +const __filename = fileURLToPath(import.meta.url); /** * Ensure the better-sqlite3 native binary is available. @@ -19,7 +23,7 @@ export function ensureSqliteBinding(log?: { info: (msg: string) => void; warn: ( if (existsSync(bindingPath)) return; const platform = `${process.platform}-${process.arch}`; - const pluginRoot = path.resolve(__dirname, "..", ".."); + const pluginRoot = findPluginRoot(import.meta.url); const prebuildSrc = path.join(pluginRoot, "prebuilds", platform, "better_sqlite3.node"); if (existsSync(prebuildSrc)) { diff --git a/apps/memos-local-openclaw/src/telemetry.ts b/apps/memos-local-openclaw/src/telemetry.ts index c11484e9c..49ebe978b 100644 --- a/apps/memos-local-openclaw/src/telemetry.ts +++ b/apps/memos-local-openclaw/src/telemetry.ts @@ -13,6 +13,7 @@ import * as path from "path"; import * as os from "os"; import { v4 as uuidv4 } from "uuid"; import type { Logger } from "./types"; +import { findPluginRoot } from "./shared/plugin-root"; export interface TelemetryConfig { enabled?: boolean; @@ -27,7 +28,7 @@ function loadTelemetryCredentials(pluginDir?: string): { endpoint: string; pid: }; } const bases = pluginDir ? [pluginDir, path.join(pluginDir, "src")] : []; - if (typeof __dirname === "string") bases.push(path.resolve(__dirname, ".."), __dirname); + try { bases.push(findPluginRoot(import.meta.url)); } catch { /* not in a memos-local install */ } const candidates = bases.map(b => path.join(b, "telemetry.credentials.json")); for (const credPath of candidates) { try { diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts index 852f3eda0..e1a6f5c42 100644 --- a/apps/memos-local-openclaw/src/viewer/server.ts +++ b/apps/memos-local-openclaw/src/viewer/server.ts @@ -5,6 +5,7 @@ import { execSync, exec, execFile } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; +import { findPluginRoot } from "../shared/plugin-root"; import type { SqliteStore } from "../storage/sqlite"; import type { Embedder } from "../embedding"; import { Summarizer, modelHealth } from "../ingest/providers"; @@ -121,7 +122,7 @@ export class ViewerServer { private static readonly SESSION_TTL = 24 * 60 * 60 * 1000; private static readonly PLUGIN_VERSION: string = (() => { try { - const pkgPath = path.resolve(__dirname, "../../package.json"); + const pkgPath = path.join(findPluginRoot(import.meta.url), "package.json"); return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version ?? "unknown"; } catch { return "unknown"; @@ -3554,18 +3555,11 @@ export class ViewerServer { } private findPluginPackageJson(): string | null { - let dir = __dirname; - for (let i = 0; i < 6; i++) { - const candidate = path.join(dir, "package.json"); - if (fs.existsSync(candidate)) { - try { - const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8")); - if (pkg.name && pkg.name.includes("memos-local")) return candidate; - } catch { /* skip */ } - } - dir = path.dirname(dir); + try { + return path.join(findPluginRoot(import.meta.url), "package.json"); + } catch { + return null; } - return null; } private async handleUpdateCheck(res: http.ServerResponse): Promise { diff --git a/apps/memos-local-openclaw/tsconfig.json b/apps/memos-local-openclaw/tsconfig.json index 53c08a1d2..ade8cf721 100644 --- a/apps/memos-local-openclaw/tsconfig.json +++ b/apps/memos-local-openclaw/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "target": "ES2022", - "module": "CommonJS", + "module": "ESNext", "lib": ["ES2022"], "outDir": "dist", - "rootDir": "src", + "rootDir": ".", "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -13,8 +13,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "moduleResolution": "node" + "moduleResolution": "bundler" }, - "include": ["src"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] + "include": ["index.ts", "src/**/*.ts"], + "exclude": ["node_modules", "dist", "tests", "scripts", "**/*.test.ts"] } From 4b3db4e464efd20ff0e8f5c8b1f026ea49f3b6af Mon Sep 17 00:00:00 2001 From: fancyboi999 <135568692+fancyboi999@users.noreply.github.com> Date: Wed, 6 May 2026 13:59:22 +0800 Subject: [PATCH 2/2] fix(memos-local-openclaw): address Copilot review on plugin-root and import rewrite - plugin-root.ts: tighten the matching predicate from substring `includes("memos-local")` to an exact-match Set ({memos-local-openclaw-plugin, @memtensor/memos-local-openclaw-plugin}). In monorepo layouts the sibling `memos-local-plugin` could otherwise be selected as the root by accident. - fix-esm-imports.cjs: extend regex to also rewrite side-effect imports (`import "./polyfills";`). Verified with a negative test that bare identifiers like `important` are not falsely matched. Re-verified end-to-end: tsc clean, 32 files rewritten, Node 25 ESM loader resolves dist/index.js -> exports {id, register}. --- .../scripts/fix-esm-imports.cjs | 11 ++++++++-- .../src/shared/plugin-root.ts | 20 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs b/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs index b7351bfe0..77af87736 100644 --- a/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs +++ b/apps/memos-local-openclaw/scripts/fix-esm-imports.cjs @@ -15,8 +15,15 @@ const path = require("path"); const DIST = path.resolve(__dirname, "..", "dist"); -// Matches: import ... from "X" | export ... from "X" | import("X") -const IMPORT_RE = /(\bfrom\s*['"]|\bimport\s*\(\s*['"])(\.[^'"]+)(['"])/g; +// Matches relative module specifiers in: +// import ... from "X" / export ... from "X" (static import / re-export) +// import("X") (dynamic import) +// import "X" (side-effect import) +// +// `\bimport\s+['"]` requires whitespace between the keyword and the quote so +// it cannot collide with identifiers like `important`. Specifiers must start +// with `.` so absolute and bare-package imports are left untouched. +const IMPORT_RE = /(\bfrom\s*['"]|\bimport\s*\(\s*['"]|\bimport\s+['"])(\.[^'"]+)(['"])/g; function rewriteSpec(spec, fileDir) { if (/\.(m?js|cjs|json)$/.test(spec)) return spec; diff --git a/apps/memos-local-openclaw/src/shared/plugin-root.ts b/apps/memos-local-openclaw/src/shared/plugin-root.ts index 4821baf7f..12076358d 100644 --- a/apps/memos-local-openclaw/src/shared/plugin-root.ts +++ b/apps/memos-local-openclaw/src/shared/plugin-root.ts @@ -2,9 +2,25 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; +/** + * Exact package names recognized as this plugin's installation root. + * + * Kept narrow (not a substring match) so monorepo layouts — where sibling + * packages such as `memos-local-plugin` live next to `memos-local-openclaw` — + * can never accidentally lock onto the wrong root. + * + * The unscoped form covers historical / local checkouts; the scoped form is + * the published name on npm. + */ +const PLUGIN_PACKAGE_NAMES = new Set([ + "memos-local-openclaw-plugin", + "@memtensor/memos-local-openclaw-plugin", +]); + /** * Resolve the plugin's installation root by walking up from the caller's file - * until a `package.json` whose name matches `memos-local-openclaw-plugin` is found. + * until a `package.json` whose `name` matches one of {@link PLUGIN_PACKAGE_NAMES} + * is found. * * Necessary because the build emits to `dist/` with `rootDir: "."`, so * compiled files live one extra level deep than their sources. Hard-coded @@ -17,7 +33,7 @@ export function findPluginRoot(importMetaUrl: string): string { if (fs.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); - if (typeof pkg.name === "string" && pkg.name.includes("memos-local")) return dir; + if (typeof pkg.name === "string" && PLUGIN_PACKAGE_NAMES.has(pkg.name)) return dir; } catch { /* keep walking */ } } const parent = path.dirname(dir);