@memtensor/memos-local-openclaw-plugin 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,64 @@ function warn(msg) { console.log(`${YELLOW}[memos-local]${RESET} ${msg}`); }
18
18
  function ok(msg) { console.log(`${GREEN}[memos-local]${RESET} ${msg}`); }
19
19
  function fail(msg) { console.log(`${RED}[memos-local]${RESET} ${msg}`); }
20
20
 
21
+ const pluginDir = path.resolve(__dirname, "..");
22
+
23
+ console.log(`
24
+ ${CYAN}${BOLD}┌──────────────────────────────────────────────────┐
25
+ │ MemOS Local Memory — postinstall │
26
+ └──────────────────────────────────────────────────┘${RESET}
27
+ `);
28
+
29
+ log(`Plugin dir: ${DIM}${pluginDir}${RESET}`);
30
+ log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
31
+
32
+ /* ═══════════════════════════════════════════════════════════
33
+ * Phase 0: Ensure all dependencies are installed
34
+ * ═══════════════════════════════════════════════════════════ */
35
+
36
+ function ensureDependencies() {
37
+ const coreDeps = ["@sinclair/typebox", "uuid", "posthog-node", "@huggingface/transformers"];
38
+ const missing = [];
39
+ for (const dep of coreDeps) {
40
+ try {
41
+ require.resolve(dep, { paths: [pluginDir] });
42
+ } catch {
43
+ missing.push(dep);
44
+ }
45
+ }
46
+
47
+ if (missing.length === 0) {
48
+ ok("All dependencies present.");
49
+ return;
50
+ }
51
+
52
+ warn(`Missing dependencies: ${missing.join(", ")}`);
53
+ log("Running: npm install --omit=dev (this may take a moment)...");
54
+
55
+ const startMs = Date.now();
56
+ const result = spawnSync("npm", ["install", "--omit=dev"], {
57
+ cwd: pluginDir,
58
+ stdio: "inherit",
59
+ shell: true,
60
+ timeout: 120_000,
61
+ });
62
+ const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
63
+
64
+ if (result.status === 0) {
65
+ ok(`Dependencies installed (${elapsed}s).`);
66
+ } else {
67
+ fail(`npm install exited with code ${result.status} (${elapsed}s).`);
68
+ warn("Some features may not work. Try running manually:");
69
+ warn(` cd ${pluginDir} && npm install --omit=dev`);
70
+ }
71
+ }
72
+
73
+ try {
74
+ ensureDependencies();
75
+ } catch (e) {
76
+ warn(`Dependency check skipped: ${e.message}`);
77
+ }
78
+
21
79
  /* ═══════════════════════════════════════════════════════════
22
80
  * Phase 1: Clean up legacy plugin versions
23
81
  * ═══════════════════════════════════════════════════════════ */
@@ -52,7 +110,6 @@ function cleanupLegacy() {
52
110
  }
53
111
  }
54
112
 
55
- // Clean up openclaw.json config — migrate old plugin entries
56
113
  const cfgPath = path.join(ocHome, "openclaw.json");
57
114
  if (fs.existsSync(cfgPath)) {
58
115
  try {
@@ -67,9 +124,8 @@ function cleanupLegacy() {
67
124
  if (entries[oldKey]) {
68
125
  const oldEntry = entries[oldKey];
69
126
  if (!entries["memos-local-openclaw-plugin"]) {
70
- // Migrate: copy old config to new key
71
127
  entries["memos-local-openclaw-plugin"] = oldEntry;
72
- log(`Migrated config: ${DIM}${oldKey}${RESET} ${GREEN}memos-local-openclaw-plugin${RESET}`);
128
+ log(`Migrated config: ${DIM}${oldKey}${RESET} -> ${GREEN}memos-local-openclaw-plugin${RESET}`);
73
129
  }
74
130
  delete entries[oldKey];
75
131
  cfgChanged = true;
@@ -77,7 +133,6 @@ function cleanupLegacy() {
77
133
  }
78
134
  }
79
135
 
80
- // Fix source path if it points to old directory names
81
136
  const newEntry = entries["memos-local-openclaw-plugin"];
82
137
  if (newEntry && typeof newEntry.source === "string") {
83
138
  const oldSource = newEntry.source;
@@ -86,14 +141,13 @@ function cleanupLegacy() {
86
141
  .replace(/memos-lite-openclaw-plugin/g, "memos-local-openclaw-plugin")
87
142
  .replace(/memos-lite/g, "memos-local");
88
143
  if (newEntry.source !== oldSource) {
89
- log(`Updated source path: ${DIM}${oldSource}${RESET} ${GREEN}${newEntry.source}${RESET}`);
144
+ log(`Updated source path: ${DIM}${oldSource}${RESET} -> ${GREEN}${newEntry.source}${RESET}`);
90
145
  cfgChanged = true;
91
146
  }
92
147
  }
93
148
  }
94
149
 
95
150
  if (cfgChanged) {
96
- // Write back with backup
97
151
  const backup = cfgPath + ".bak-" + Date.now();
98
152
  fs.copyFileSync(cfgPath, backup);
99
153
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
@@ -127,18 +181,18 @@ log("Checking better-sqlite3 native module...");
127
181
  try {
128
182
  require("better-sqlite3");
129
183
  ok("better-sqlite3 is ready.");
184
+ console.log(`\n${GREEN}${BOLD}[memos-local] Setup complete!${RESET} Restart the gateway: ${CYAN}openclaw gateway stop && openclaw gateway start${RESET}\n`);
130
185
  process.exit(0);
131
186
  } catch (_) {
132
187
  warn("better-sqlite3 native bindings not found, attempting rebuild...");
133
188
  }
134
189
 
135
- log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
136
190
  log("Running: npm rebuild better-sqlite3 (this may take 30-60 seconds)...");
137
191
 
138
192
  const startMs = Date.now();
139
193
 
140
194
  const result = spawnSync("npm", ["rebuild", "better-sqlite3"], {
141
- cwd: path.resolve(__dirname, ".."),
195
+ cwd: pluginDir,
142
196
  stdio: "inherit",
143
197
  shell: true,
144
198
  timeout: 180_000,
@@ -151,6 +205,7 @@ if (result.status === 0) {
151
205
  delete require.cache[require.resolve("better-sqlite3")];
152
206
  require("better-sqlite3");
153
207
  ok(`better-sqlite3 rebuilt successfully (${elapsed}s).`);
208
+ console.log(`\n${GREEN}${BOLD}[memos-local] Setup complete!${RESET} Restart the gateway: ${CYAN}openclaw gateway stop && openclaw gateway start${RESET}\n`);
154
209
  process.exit(0);
155
210
  } catch (_) {
156
211
  fail(`Rebuild completed but module still cannot load (${elapsed}s).`);
@@ -159,7 +214,6 @@ if (result.status === 0) {
159
214
  fail(`Rebuild failed with exit code ${result.status} (${elapsed}s).`);
160
215
  }
161
216
 
162
- const pluginDir = path.resolve(__dirname, "..");
163
217
  console.log(`
164
218
  ${YELLOW}${BOLD}╔══════════════════════════════════════════════════════════════╗
165
219
  ║ better-sqlite3 native module build failed ║
@@ -176,6 +230,7 @@ ${YELLOW}║${RESET}
176
230
  ${YELLOW}║${RESET} Then retry: ${YELLOW}║${RESET}
177
231
  ${YELLOW}║${RESET} ${GREEN}cd ${pluginDir}${RESET}
178
232
  ${YELLOW}║${RESET} ${GREEN}npm rebuild better-sqlite3${RESET} ${YELLOW}║${RESET}
233
+ ${YELLOW}║${RESET} ${GREEN}openclaw gateway stop && openclaw gateway start${RESET} ${YELLOW}║${RESET}
179
234
  ${YELLOW}${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}
180
235
  `);
181
236
 
@@ -2868,17 +2868,22 @@ async function loadAll(){
2868
2868
  }
2869
2869
 
2870
2870
  async function loadStats(){
2871
- const r=await fetch('/api/stats');
2872
- const d=await r.json();
2871
+ let d;
2872
+ try{
2873
+ const r=await fetch('/api/stats');
2874
+ d=await r.json();
2875
+ }catch(e){ d={}; }
2876
+ if(!d||typeof d!=='object') d={};
2877
+ const tm=d.totalMemories||0;
2873
2878
  const dedupB=d.dedupBreakdown||{};
2874
- const activeCount=dedupB.active||d.totalMemories;
2879
+ const activeCount=dedupB.active||tm;
2875
2880
  const inactiveCount=(dedupB.duplicate||0)+(dedupB.merged||0);
2876
- document.getElementById('statTotal').textContent=d.totalMemories;
2881
+ document.getElementById('statTotal').textContent=tm;
2877
2882
  if(inactiveCount>0){
2878
2883
  document.getElementById('statTotal').title=activeCount+' '+t('stat.active')+', '+inactiveCount+' '+t('stat.deduped');
2879
2884
  }
2880
- document.getElementById('statSessions').textContent=d.totalSessions;
2881
- document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;
2885
+ document.getElementById('statSessions').textContent=d.totalSessions||0;
2886
+ document.getElementById('statEmbeddings').textContent=d.totalEmbeddings||0;
2882
2887
  let days=0;
2883
2888
  if(d.timeRange&&d.timeRange.earliest!=null&&d.timeRange.latest!=null){
2884
2889
  let e=Number(d.timeRange.earliest), l=Number(d.timeRange.latest);
@@ -2900,7 +2905,7 @@ async function loadStats(){
2900
2905
  }
2901
2906
 
2902
2907
  const sl=document.getElementById('sessionList');
2903
- sl.innerHTML='<div class="session-item'+(activeSession===null?' active':'')+'" onclick="filterSession(null)"><span>'+t('sidebar.allsessions')+'</span><span class="count">'+d.totalMemories+'</span></div>';
2908
+ sl.innerHTML='<div class="session-item'+(activeSession===null?' active':'')+'" onclick="filterSession(null)"><span>'+t('sidebar.allsessions')+'</span><span class="count">'+tm+'</span></div>';
2904
2909
  (d.sessions||[]).forEach(s=>{
2905
2910
  const isActive=activeSession===s.session_key;
2906
2911
  const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;
@@ -2927,16 +2932,23 @@ async function loadMemories(page){
2927
2932
  if(page) currentPage=page;
2928
2933
  const list=document.getElementById('memoryList');
2929
2934
  list.innerHTML='<div class="spinner"></div>';
2930
- const p=getFilterParams();
2931
- p.set('limit',PAGE_SIZE);
2932
- p.set('page',currentPage);
2933
- const r=await fetch('/api/memories?'+p.toString());
2934
- const d=await r.json();
2935
- totalPages=d.totalPages||1;
2936
- totalCount=d.total||0;
2937
- document.getElementById('searchMeta').textContent=totalCount+t('search.meta.total');
2938
- renderMemories(d.memories||[]);
2939
- renderPagination();
2935
+ try{
2936
+ const p=getFilterParams();
2937
+ p.set('limit',PAGE_SIZE);
2938
+ p.set('page',currentPage);
2939
+ const r=await fetch('/api/memories?'+p.toString());
2940
+ const d=await r.json();
2941
+ totalPages=d.totalPages||1;
2942
+ totalCount=d.total||0;
2943
+ document.getElementById('searchMeta').textContent=totalCount+t('search.meta.total');
2944
+ renderMemories(d.memories||[]);
2945
+ renderPagination();
2946
+ }catch(e){
2947
+ list.innerHTML='';
2948
+ totalPages=1;totalCount=0;
2949
+ renderMemories([]);
2950
+ renderPagination();
2951
+ }
2940
2952
  }
2941
2953
 
2942
2954
  async function doSearch(q){
@@ -3261,11 +3273,11 @@ async function migrateScan(){
3261
3273
 
3262
3274
  try{
3263
3275
  const r=await fetch('/api/migrate/scan');
3264
- if(!r.ok){ const err=await r.text(); throw new Error(err); }
3265
- const d=await r.json();
3276
+ const d=await r.json().catch(()=>({}));
3277
+ if(d.error && !d.sqliteFiles) throw new Error(d.error);
3266
3278
  migrateScanData=d;
3267
3279
 
3268
- const files=d.sqliteFiles||[];
3280
+ const files=Array.isArray(d.sqliteFiles)?d.sqliteFiles:[];
3269
3281
  const sess=d.sessions||{count:0,messages:0};
3270
3282
  const sqliteTotal=files.reduce((s,f)=>s+f.chunks,0);
3271
3283
  document.getElementById('migrateSqliteCount').textContent=sqliteTotal;
@@ -447,36 +447,55 @@ export class ViewerServer {
447
447
  }
448
448
 
449
449
  private serveStats(res: http.ServerResponse): void {
450
- const db = (this.store as any).db;
451
- const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
452
- const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get() as any;
453
- const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as any[];
454
- const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks").get() as any;
455
- const embeddings = db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any;
456
- const kinds = db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as any[];
457
- const sessionList = db.prepare(
458
- "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC",
459
- ).all() as any[];
460
-
461
- let skillCount = 0;
462
- try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
463
-
464
- let dedupBreakdown: Record<string, number> = {};
450
+ const emptyStats = {
451
+ totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
452
+ embeddingProvider: this.embedder?.provider ?? "none",
453
+ roleBreakdown: {}, kindBreakdown: {}, dedupBreakdown: {},
454
+ timeRange: { earliest: null, latest: null },
455
+ sessions: [],
456
+ };
457
+
458
+ if (!this.store || !(this.store as any).db) {
459
+ this.jsonResponse(res, emptyStats);
460
+ return;
461
+ }
462
+
465
463
  try {
466
- const dedupRows = db.prepare("SELECT dedup_status, COUNT(*) as count FROM chunks GROUP BY dedup_status").all() as any[];
467
- dedupBreakdown = Object.fromEntries(dedupRows.map((d: any) => [d.dedup_status ?? "active", d.count]));
468
- } catch { /* column may not exist yet */ }
464
+ const db = (this.store as any).db;
465
+ const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
466
+ const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get() as any;
467
+ const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as any[];
468
+ const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks").get() as any;
469
+ let embCount = 0;
470
+ try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
471
+ const kinds = db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as any[];
472
+ const sessionList = db.prepare(
473
+ "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC",
474
+ ).all() as any[];
475
+
476
+ let skillCount = 0;
477
+ try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
478
+
479
+ let dedupBreakdown: Record<string, number> = {};
480
+ try {
481
+ const dedupRows = db.prepare("SELECT dedup_status, COUNT(*) as count FROM chunks GROUP BY dedup_status").all() as any[];
482
+ dedupBreakdown = Object.fromEntries(dedupRows.map((d: any) => [d.dedup_status ?? "active", d.count]));
483
+ } catch { /* column may not exist yet */ }
469
484
 
470
- this.jsonResponse(res, {
471
- totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embeddings.count,
472
- totalSkills: skillCount,
473
- embeddingProvider: this.embedder.provider,
474
- roleBreakdown: Object.fromEntries(roles.map((r: any) => [r.role, r.count])),
475
- kindBreakdown: Object.fromEntries(kinds.map((k: any) => [k.kind, k.count])),
476
- dedupBreakdown,
477
- timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
478
- sessions: sessionList,
479
- });
485
+ this.jsonResponse(res, {
486
+ totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
487
+ totalSkills: skillCount,
488
+ embeddingProvider: this.embedder.provider,
489
+ roleBreakdown: Object.fromEntries(roles.map((r: any) => [r.role, r.count])),
490
+ kindBreakdown: Object.fromEntries(kinds.map((k: any) => [k.kind, k.count])),
491
+ dedupBreakdown,
492
+ timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
493
+ sessions: sessionList,
494
+ });
495
+ } catch (e) {
496
+ this.log.warn(`stats error: ${e}`);
497
+ this.jsonResponse(res, emptyStats);
498
+ }
480
499
  }
481
500
 
482
501
  private async serveSearch(_req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void> {
@@ -943,8 +962,17 @@ export class ViewerServer {
943
962
  });
944
963
  } catch (e) {
945
964
  this.log.warn(`migrate/scan error: ${e}`);
946
- res.writeHead(500, { "Content-Type": "application/json" });
947
- res.end(JSON.stringify({ error: String(e) }));
965
+ this.jsonResponse(res, {
966
+ sqliteFiles: [],
967
+ sessions: { count: 0, messages: 0 },
968
+ totalItems: 0,
969
+ configReady: false,
970
+ hasEmbedding: false,
971
+ hasSummarizer: false,
972
+ hasImportedData: false,
973
+ importedSessionCount: 0,
974
+ error: String(e),
975
+ });
948
976
  }
949
977
  }
950
978