@memtensor/memos-local-openclaw-plugin 1.0.1 → 1.0.2-beta.2

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.
@@ -33,6 +33,78 @@ ${CYAN}${BOLD}┌─────────────────────
33
33
  log(`Plugin dir: ${DIM}${pluginDir}${RESET}`);
34
34
  log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
35
35
 
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
+
36
108
  /* ═══════════════════════════════════════════════════════════
37
109
  * Phase 0: Ensure all dependencies are installed
38
110
  * ═══════════════════════════════════════════════════════════ */
@@ -1615,7 +1615,10 @@ const I18N={
1615
1615
  'skill.cancel':'Cancel',
1616
1616
  'skill.delete.confirm':'Are you sure you want to delete this skill? This will also remove all associated files and cannot be undone.',
1617
1617
  'skill.delete.error':'Failed to delete skill: ',
1618
- 'skill.save.error':'Failed to save skill: '
1618
+ 'skill.save.error':'Failed to save skill: ',
1619
+ 'update.available':'New version available',
1620
+ 'update.run':'Run',
1621
+ 'update.dismiss':'Dismiss'
1619
1622
  },
1620
1623
  zh:{
1621
1624
  'title':'OpenClaw 记忆',
@@ -1921,7 +1924,10 @@ const I18N={
1921
1924
  'skill.cancel':'取消',
1922
1925
  'skill.delete.confirm':'确定要删除此技能吗?关联的文件也会被删除,此操作不可撤销。',
1923
1926
  'skill.delete.error':'删除技能失败:',
1924
- 'skill.save.error':'保存技能失败:'
1927
+ 'skill.save.error':'保存技能失败:',
1928
+ 'update.available':'发现新版本',
1929
+ 'update.run':'执行命令',
1930
+ 'update.dismiss':'关闭'
1925
1931
  }
1926
1932
  };
1927
1933
  const LANG_KEY='memos-viewer-lang';
@@ -3277,6 +3283,7 @@ async function loadAll(){
3277
3283
  await Promise.all([loadStats(),loadMemories()]);
3278
3284
  checkMigrateStatus();
3279
3285
  connectPPSSE();
3286
+ checkForUpdate();
3280
3287
  }
3281
3288
 
3282
3289
  async function loadStats(){
@@ -4213,6 +4220,22 @@ function initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const
4213
4220
  function toggleViewerTheme(){const el=document.documentElement;const cur=el.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(VIEWER_THEME_KEY,next);}
4214
4221
  initViewerTheme();
4215
4222
 
4223
+ /* ─── Update check ─── */
4224
+ async function checkForUpdate(){
4225
+ try{
4226
+ const r=await fetch('/api/update-check');
4227
+ if(!r.ok)return;
4228
+ const d=await r.json();
4229
+ if(!d.updateAvailable)return;
4230
+ const banner=document.createElement('div');
4231
+ banner.id='updateBanner';
4232
+ banner.style.cssText='position:fixed;top:0;left:0;right:0;z-index:9999;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:space-between;font-size:14px;box-shadow:0 2px 8px rgba(0,0,0,.25)';
4233
+ banner.innerHTML='<span>🔔 '+t('update.available')+': <b>v'+esc(d.current)+'</b> → <b>v'+esc(d.latest)+'</b> — '+t('update.run')+': <code style="background:rgba(0,0,0,.2);padding:2px 8px;border-radius:4px;margin:0 4px">openclaw plugins install '+esc(d.packageName)+'</code></span><button onclick="this.parentElement.remove();document.body.style.paddingTop=\\'\\';" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px">&times;</button>';
4234
+ document.body.prepend(banner);
4235
+ document.body.style.paddingTop='48px';
4236
+ }catch(e){}
4237
+ }
4238
+
4216
4239
  /* ─── Init ─── */
4217
4240
  document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
4218
4241
  document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});
@@ -217,6 +217,7 @@ export class ViewerServer {
217
217
  else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
218
218
  else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
219
219
  else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
220
+ else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
220
221
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
221
222
  else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
222
223
  else if (p === "/api/migrate/start" && req.method === "POST") this.handleMigrateStart(req, res);
@@ -1080,6 +1081,56 @@ export class ViewerServer {
1080
1081
  }
1081
1082
  }
1082
1083
 
1084
+ private findPluginPackageJson(): string | null {
1085
+ let dir = __dirname;
1086
+ for (let i = 0; i < 6; i++) {
1087
+ const candidate = path.join(dir, "package.json");
1088
+ if (fs.existsSync(candidate)) {
1089
+ try {
1090
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
1091
+ if (pkg.name && pkg.name.includes("memos-local")) return candidate;
1092
+ } catch { /* skip */ }
1093
+ }
1094
+ dir = path.dirname(dir);
1095
+ }
1096
+ return null;
1097
+ }
1098
+
1099
+ private async handleUpdateCheck(res: http.ServerResponse): Promise<void> {
1100
+ try {
1101
+ const pkgPath = this.findPluginPackageJson();
1102
+ if (!pkgPath) {
1103
+ this.jsonResponse(res, { updateAvailable: false, error: "package.json not found" });
1104
+ return;
1105
+ }
1106
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1107
+ const current = pkg.version as string;
1108
+ const name = pkg.name as string;
1109
+ if (!current || !name) {
1110
+ this.jsonResponse(res, { updateAvailable: false, current });
1111
+ return;
1112
+ }
1113
+ const npmResp = await fetch(`https://registry.npmjs.org/${name}/latest`, {
1114
+ signal: AbortSignal.timeout(6_000),
1115
+ });
1116
+ if (!npmResp.ok) {
1117
+ this.jsonResponse(res, { updateAvailable: false, current });
1118
+ return;
1119
+ }
1120
+ const data = await npmResp.json() as { version?: string };
1121
+ const latest = data.version ?? current;
1122
+ this.jsonResponse(res, {
1123
+ updateAvailable: latest !== current,
1124
+ current,
1125
+ latest,
1126
+ packageName: name,
1127
+ });
1128
+ } catch (e) {
1129
+ this.log.warn(`handleUpdateCheck error: ${e}`);
1130
+ this.jsonResponse(res, { updateAvailable: false, error: String(e) });
1131
+ }
1132
+ }
1133
+
1083
1134
  private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
1084
1135
  if (provider === "local") {
1085
1136
  return;