Skip to content

Commit 05dc915

Browse files
authored
feat: Sync memos-local-openclaw from native_memos (#1279)
…emos, v1.0.3 - Version: 1.0.3 - Sync memos-local-openclaw from native_memos - Includes: update-check, accuracy tests, test-agent-isolation, viewer/server & html updates, ingest/recall/storage improvements, better-sqlite3 Node 25+ compatibility Made-with: Cursor ## Description Please include a summary of the change, the problem it solves, the implementation approach, and relevant context. List any dependencies required for this change. Related Issue (Required): Fixes @issue_number ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (does not change functionality, e.g. code style improvements, linting) - [ ] Documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - [ ] Unit Test - [ ] Test Script Or Test Steps (please provide) - [ ] Pipeline Automated API Test (please provide) ## Checklist - [x] I have performed a self-review of my own code | 我已自行检查了自己的代码 - [x] I have commented my code in hard-to-understand areas | 我已在难以理解的地方对代码进行了注释 - [x] I have added tests that prove my fix is effective or that my feature works | 我已添加测试以证明我的修复有效或功能正常 - [x] I have created related documentation issue/PR in [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) (if applicable) | 我已在 [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) 中创建了相关的文档 issue/PR(如果适用) - [x] I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用) - [x] I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人 ## Reviewer Checklist - [ ] closes #xxxx (Replace xxxx with the GitHub issue number) - [ ] Made sure Checks passed - [ ] Tests have been provided
2 parents 834e8c8 + e7908f4 commit 05dc915

31 files changed

+5316
-997
lines changed

apps/memos-local-openclaw/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Thumbs.db
1313

1414
# Generated / non-essential
1515
package-lock.json
16+
.installed-version
1617
www/
1718
docs/
1819
ppt/

apps/memos-local-openclaw/index.ts

Lines changed: 179 additions & 162 deletions
Large diffs are not rendered by default.

apps/memos-local-openclaw/openclaw.plugin.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary, memory_timeline, memory_viewer for layered retrieval.",
55
"kind": "memory",
66
"version": "0.1.11",
7+
"skills": [
8+
"skill/memos-memory-guide"
9+
],
710
"homepage": "https://github.com/MemTensor/MemOS/tree/main/apps/memos-local-openclaw",
811
"configSchema": {
912
"type": "object",

apps/memos-local-openclaw/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@memtensor/memos-local-openclaw-plugin",
3-
"version": "1.0.0",
4-
"description": "MemOS Local memory plugin for OpenClaw full-write, hybrid-recall, progressive retrieval",
3+
"version": "1.0.3",
4+
"description": "MemOS Local memory plugin for OpenClaw \u2014 full-write, hybrid-recall, progressive retrieval",
55
"type": "module",
66
"main": "index.ts",
77
"types": "dist/index.d.ts",
@@ -21,6 +21,9 @@
2121
"extensions": [
2222
"./index.ts"
2323
],
24+
"skills": [
25+
"skill/memos-memory-guide"
26+
],
2427
"installDependencies": true
2528
},
2629
"scripts": {
@@ -29,6 +32,7 @@
2932
"lint": "eslint src --ext .ts",
3033
"test": "vitest run",
3134
"test:watch": "vitest",
35+
"test:accuracy": "tsx scripts/run-accuracy-test.ts",
3236
"postinstall": "node scripts/postinstall.cjs",
3337
"prepublishOnly": "npm run build"
3438
},
@@ -49,14 +53,16 @@
4953
"better-sqlite3": "^12.6.2",
5054
"posthog-node": "^5.28.0",
5155
"puppeteer": "^24.38.0",
56+
"semver": "^7.7.4",
5257
"uuid": "^10.0.0"
5358
},
5459
"devDependencies": {
5560
"@types/better-sqlite3": "^7.6.12",
5661
"@types/node": "^22.10.0",
62+
"@types/semver": "^7.7.1",
5763
"@types/uuid": "^10.0.0",
5864
"tsx": "^4.21.0",
5965
"typescript": "^5.7.0",
6066
"vitest": "^2.1.0"
6167
}
62-
}
68+
}

apps/memos-local-openclaw/scripts/postinstall.cjs

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,78 @@ ${CYAN}${BOLD}┌─────────────────────
3333
log(`Plugin dir: ${DIM}${pluginDir}${RESET}`);
3434
log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
3535

36+
/* ═══════════════════════════════════════════════════════════
37+
* Pre-phase: Clean stale build artifacts on upgrade
38+
* When openclaw re-installs a new version over an existing
39+
* extensions dir, old dist/node_modules can conflict.
40+
* We nuke them so npm install gets a clean slate, but
41+
* preserve user data (.env, data/).
42+
* ═══════════════════════════════════════════════════════════ */
43+
44+
function cleanStaleArtifacts() {
45+
const isExtensionsDir = pluginDir.includes(path.join(".openclaw", "extensions"));
46+
if (!isExtensionsDir) return;
47+
48+
const pkgPath = path.join(pluginDir, "package.json");
49+
if (!fs.existsSync(pkgPath)) return;
50+
51+
let installedVer = "unknown";
52+
try {
53+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
54+
installedVer = pkg.version || "unknown";
55+
} catch { /* ignore */ }
56+
57+
const markerPath = path.join(pluginDir, ".installed-version");
58+
let prevVer = "";
59+
try { prevVer = fs.readFileSync(markerPath, "utf-8").trim(); } catch { /* first install */ }
60+
61+
if (prevVer === installedVer) {
62+
log(`Version unchanged (${installedVer}), skipping artifact cleanup.`);
63+
return;
64+
}
65+
66+
if (prevVer) {
67+
log(`Upgrade detected: ${DIM}${prevVer}${RESET}${GREEN}${installedVer}${RESET}`);
68+
} else {
69+
log(`Fresh install: ${GREEN}${installedVer}${RESET}`);
70+
}
71+
72+
const dirsToClean = ["dist", "node_modules"];
73+
let cleaned = 0;
74+
for (const dir of dirsToClean) {
75+
const full = path.join(pluginDir, dir);
76+
if (fs.existsSync(full)) {
77+
try {
78+
fs.rmSync(full, { recursive: true, force: true });
79+
ok(`Cleaned stale ${dir}/`);
80+
cleaned++;
81+
} catch (e) {
82+
warn(`Could not remove ${dir}/: ${e.message}`);
83+
}
84+
}
85+
}
86+
87+
const filesToClean = ["package-lock.json"];
88+
for (const f of filesToClean) {
89+
const full = path.join(pluginDir, f);
90+
if (fs.existsSync(full)) {
91+
try { fs.unlinkSync(full); ok(`Removed stale ${f}`); cleaned++; } catch { /* ignore */ }
92+
}
93+
}
94+
95+
try { fs.writeFileSync(markerPath, installedVer + "\n", "utf-8"); } catch { /* ignore */ }
96+
97+
if (cleaned > 0) {
98+
ok(`Cleaned ${cleaned} stale artifact(s). Fresh install will follow.`);
99+
}
100+
}
101+
102+
try {
103+
cleanStaleArtifacts();
104+
} catch (e) {
105+
warn(`Artifact cleanup error: ${e.message}`);
106+
}
107+
36108
/* ═══════════════════════════════════════════════════════════
37109
* Phase 0: Ensure all dependencies are installed
38110
* ═══════════════════════════════════════════════════════════ */
@@ -102,6 +174,7 @@ function cleanupLegacy() {
102174
if (!fs.existsSync(extDir)) { log("No extensions directory found, skipping."); return; }
103175

104176
const legacyDirs = [
177+
path.join(extDir, "memos-local"),
105178
path.join(extDir, "memos-lite"),
106179
path.join(extDir, "memos-lite-openclaw-plugin"),
107180
path.join(extDir, "node_modules", "@memtensor", "memos-lite-openclaw-plugin"),
@@ -127,7 +200,7 @@ function cleanupLegacy() {
127200
const cfg = JSON.parse(raw);
128201
const entries = cfg?.plugins?.entries;
129202
if (entries) {
130-
const oldKeys = ["memos-lite", "memos-lite-openclaw-plugin"];
203+
const oldKeys = ["memos-local", "memos-lite", "memos-lite-openclaw-plugin"];
131204
let cfgChanged = false;
132205

133206
for (const oldKey of oldKeys) {
@@ -146,17 +219,29 @@ function cleanupLegacy() {
146219
const newEntry = entries["memos-local-openclaw-plugin"];
147220
if (newEntry && typeof newEntry.source === "string") {
148221
const oldSource = newEntry.source;
149-
if (oldSource.includes("memos-lite")) {
222+
if (oldSource.includes("memos-lite") || (oldSource.includes("memos-local") && !oldSource.includes("memos-local-openclaw-plugin"))) {
150223
newEntry.source = oldSource
151224
.replace(/memos-lite-openclaw-plugin/g, "memos-local-openclaw-plugin")
152-
.replace(/memos-lite/g, "memos-local");
225+
.replace(/memos-lite/g, "memos-local-openclaw-plugin")
226+
.replace(/\/memos-local\//g, "/memos-local-openclaw-plugin/")
227+
.replace(/\/memos-local$/g, "/memos-local-openclaw-plugin");
153228
if (newEntry.source !== oldSource) {
154229
log(`Updated source path: ${DIM}${oldSource}${RESET}${GREEN}${newEntry.source}${RESET}`);
155230
cfgChanged = true;
156231
}
157232
}
158233
}
159234

235+
const slots = cfg?.plugins?.slots;
236+
if (slots && typeof slots.memory === "string") {
237+
const oldSlotNames = ["memos-local", "memos-lite", "memos-lite-openclaw-plugin"];
238+
if (oldSlotNames.includes(slots.memory)) {
239+
log(`Migrated plugins.slots.memory: ${DIM}${slots.memory}${RESET}${GREEN}memos-local-openclaw-plugin${RESET}`);
240+
slots.memory = "memos-local-openclaw-plugin";
241+
cfgChanged = true;
242+
}
243+
}
244+
160245
if (cfgChanged) {
161246
const backup = cfgPath + ".bak-" + Date.now();
162247
fs.copyFileSync(cfgPath, backup);
@@ -185,10 +270,77 @@ try {
185270
}
186271

187272
/* ═══════════════════════════════════════════════════════════
188-
* Phase 2: Verify better-sqlite3 native module
273+
* Phase 2: Install bundled skill (memos-memory-guide)
274+
* ═══════════════════════════════════════════════════════════ */
275+
276+
function installBundledSkill() {
277+
phase(2, "安装记忆技能 / Install memory skill");
278+
279+
const home = process.env.HOME || process.env.USERPROFILE || "";
280+
if (!home) { warn("Cannot determine HOME directory, skipping skill install."); return; }
281+
282+
const skillSrc = path.join(pluginDir, "skill", "memos-memory-guide", "SKILL.md");
283+
if (!fs.existsSync(skillSrc)) {
284+
warn("Bundled SKILL.md not found, skipping skill install.");
285+
return;
286+
}
287+
288+
let pluginVersion = "0.0.0";
289+
try {
290+
const pkg = JSON.parse(fs.readFileSync(path.join(pluginDir, "package.json"), "utf-8"));
291+
pluginVersion = pkg.version || pluginVersion;
292+
} catch { /* ignore */ }
293+
294+
const skillContent = fs.readFileSync(skillSrc, "utf-8");
295+
const targets = [
296+
path.join(home, ".openclaw", "workspace", "skills", "memos-memory-guide"),
297+
path.join(home, ".openclaw", "skills", "memos-memory-guide"),
298+
];
299+
300+
const meta = JSON.stringify({ ownerId: "memos-local-openclaw-plugin", slug: "memos-memory-guide", version: pluginVersion, publishedAt: Date.now() });
301+
const origin = JSON.stringify({ version: 1, registry: "memos-local-openclaw-plugin", slug: "memos-memory-guide", installedVersion: pluginVersion, installedAt: Date.now() });
302+
303+
for (const dest of targets) {
304+
try {
305+
fs.mkdirSync(dest, { recursive: true });
306+
fs.writeFileSync(path.join(dest, "SKILL.md"), skillContent, "utf-8");
307+
fs.writeFileSync(path.join(dest, "_meta.json"), meta, "utf-8");
308+
const clawHubDir = path.join(dest, ".clawhub");
309+
fs.mkdirSync(clawHubDir, { recursive: true });
310+
fs.writeFileSync(path.join(clawHubDir, "origin.json"), origin, "utf-8");
311+
ok(`Skill installed → ${DIM}${dest}${RESET}`);
312+
} catch (e) {
313+
warn(`Could not install skill to ${dest}: ${e.message}`);
314+
}
315+
}
316+
317+
// Register in skills-lock.json so OpenClaw Dashboard can discover it
318+
const lockPath = path.join(home, ".openclaw", "workspace", "skills-lock.json");
319+
try {
320+
let lockData = { version: 1, skills: {} };
321+
if (fs.existsSync(lockPath)) {
322+
lockData = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
323+
}
324+
if (!lockData.skills) lockData.skills = {};
325+
lockData.skills["memos-memory-guide"] = { source: "memos-local-openclaw-plugin", sourceType: "plugin", computedHash: "" };
326+
fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + "\n", "utf-8");
327+
ok("Registered in skills-lock.json");
328+
} catch (e) {
329+
warn(`Could not update skills-lock.json: ${e.message}`);
330+
}
331+
}
332+
333+
try {
334+
installBundledSkill();
335+
} catch (e) {
336+
warn(`Skill install error: ${e.message}`);
337+
}
338+
339+
/* ═══════════════════════════════════════════════════════════
340+
* Phase 3: Verify better-sqlite3 native module
189341
* ═══════════════════════════════════════════════════════════ */
190342

191-
phase(2, "检查 better-sqlite3 原生模块 / Check native module");
343+
phase(3, "检查 better-sqlite3 原生模块 / Check native module");
192344

193345
const sqliteModulePath = path.join(pluginDir, "node_modules", "better-sqlite3");
194346

0 commit comments

Comments
 (0)