@memtensor/memos-local-openclaw-plugin 1.0.2-beta.6 → 1.0.2-beta.8

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.
@@ -186,8 +186,9 @@ export class Summarizer {
186
186
  return resultCleaned;
187
187
  }
188
188
 
189
- if (resultCleaned !== undefined) {
190
- this.log.warn(`summarize: result (${(resultCleaned as string).length}) >= input (${cleaned.length}), retrying`);
189
+ if (resultCleaned !== undefined && resultCleaned !== null) {
190
+ const len: number = (resultCleaned as string).length;
191
+ this.log.warn(`summarize: result (${len}) >= input (${cleaned.length}), retrying`);
191
192
  }
192
193
  } catch (err) {
193
194
  this.log.warn(`summarize primary failed: ${err}`);
@@ -282,6 +283,12 @@ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promis
282
283
  case "openai":
283
284
  case "openai_compatible":
284
285
  case "azure_openai":
286
+ case "zhipu":
287
+ case "siliconflow":
288
+ case "bailian":
289
+ case "cohere":
290
+ case "mistral":
291
+ case "voyage":
285
292
  return summarizeOpenAI(text, cfg, log);
286
293
  case "anthropic":
287
294
  return summarizeAnthropic(text, cfg, log);
@@ -299,6 +306,12 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr
299
306
  case "openai":
300
307
  case "openai_compatible":
301
308
  case "azure_openai":
309
+ case "zhipu":
310
+ case "siliconflow":
311
+ case "bailian":
312
+ case "cohere":
313
+ case "mistral":
314
+ case "voyage":
302
315
  return summarizeTaskOpenAI(text, cfg, log);
303
316
  case "anthropic":
304
317
  return summarizeTaskAnthropic(text, cfg, log);
@@ -316,6 +329,12 @@ function callGenerateTaskTitle(cfg: SummarizerConfig, text: string, log: Logger)
316
329
  case "openai":
317
330
  case "openai_compatible":
318
331
  case "azure_openai":
332
+ case "zhipu":
333
+ case "siliconflow":
334
+ case "bailian":
335
+ case "cohere":
336
+ case "mistral":
337
+ case "voyage":
319
338
  return generateTaskTitleOpenAI(text, cfg, log);
320
339
  case "anthropic":
321
340
  return generateTaskTitleAnthropic(text, cfg, log);
@@ -333,6 +352,12 @@ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessag
333
352
  case "openai":
334
353
  case "openai_compatible":
335
354
  case "azure_openai":
355
+ case "zhipu":
356
+ case "siliconflow":
357
+ case "bailian":
358
+ case "cohere":
359
+ case "mistral":
360
+ case "voyage":
336
361
  return judgeNewTopicOpenAI(currentContext, newMessage, cfg, log);
337
362
  case "anthropic":
338
363
  return judgeNewTopicAnthropic(currentContext, newMessage, cfg, log);
@@ -350,6 +375,12 @@ function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Ar
350
375
  case "openai":
351
376
  case "openai_compatible":
352
377
  case "azure_openai":
378
+ case "zhipu":
379
+ case "siliconflow":
380
+ case "bailian":
381
+ case "cohere":
382
+ case "mistral":
383
+ case "voyage":
353
384
  return filterRelevantOpenAI(query, candidates, cfg, log);
354
385
  case "anthropic":
355
386
  return filterRelevantAnthropic(query, candidates, cfg, log);
@@ -367,6 +398,12 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A
367
398
  case "openai":
368
399
  case "openai_compatible":
369
400
  case "azure_openai":
401
+ case "zhipu":
402
+ case "siliconflow":
403
+ case "bailian":
404
+ case "cohere":
405
+ case "mistral":
406
+ case "voyage":
370
407
  return judgeDedupOpenAI(newSummary, candidates, cfg, log);
371
408
  case "anthropic":
372
409
  return judgeDedupAnthropic(newSummary, candidates, cfg, log);
@@ -270,8 +270,8 @@ export class RecallEngine {
270
270
  ): Promise<number[]> {
271
271
  const candidateList = candidates.map((c, i) => ({
272
272
  index: i,
273
- content: `[${c.skill.name}] ${c.skill.description}`,
274
273
  role: "skill" as const,
274
+ content: `[${c.skill.name}] ${c.skill.description}`,
275
275
  }));
276
276
 
277
277
  try {
package/src/types.ts CHANGED
@@ -144,7 +144,13 @@ export type SummaryProvider =
144
144
  | "anthropic"
145
145
  | "gemini"
146
146
  | "azure_openai"
147
- | "bedrock";
147
+ | "bedrock"
148
+ | "zhipu"
149
+ | "siliconflow"
150
+ | "bailian"
151
+ | "cohere"
152
+ | "mistral"
153
+ | "voyage";
148
154
 
149
155
  export type EmbeddingProvider =
150
156
  | "openai"
@@ -4,7 +4,6 @@
4
4
  * - Stable users compare against latest tag only (semver gt).
5
5
  * - Beta users get optional stableChannel hint to install @latest when stable exists.
6
6
  */
7
- // @ts-ignore — semver has no bundled types
8
7
  import * as semver from "semver";
9
8
 
10
9
  export interface UpdateCheckResult {
@@ -586,7 +586,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
586
586
  [data-theme="light"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}
587
587
  .settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}
588
588
  .settings-saved.show{opacity:1}
589
- .model-health-bar{margin-bottom:20px;border-radius:var(--radius-lg);overflow:hidden}
589
+ .model-health-bar{margin-bottom:20px;border-radius:var(--radius-lg);overflow:visible}
590
590
  .mh-table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
591
591
  .mh-table th{text-align:left;padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;background:var(--bg);border-bottom:1px solid var(--border)}
592
592
  .mh-table td{padding:8px 12px;border-bottom:1px solid var(--border);vertical-align:middle}
@@ -604,7 +604,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
604
604
  .mh-badge.error{background:rgba(239,68,68,.1);color:#dc2626}
605
605
  .mh-badge.unknown{background:rgba(148,163,184,.1);color:#64748b}
606
606
  .mh-model-name{color:var(--text-muted);font-size:11px;font-family:var(--font-mono,'SFMono-Regular',Consolas,monospace)}
607
- .mh-err-text{font-size:11px;color:var(--rose);max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help}
607
+ .mh-err-text{font-size:11px;color:var(--rose);max-width:320px;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help}
608
+ #mhTooltip{display:none;position:fixed;min-width:280px;max-width:480px;max-height:300px;overflow-y:auto;padding:8px 10px;background:var(--bg-card,#1e1e2e);color:var(--text,#e2e8f0);border:1px solid var(--border,#333);border-radius:6px;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-all;box-shadow:0 4px 12px rgba(0,0,0,.25);z-index:10000;pointer-events:none}
608
609
  .mh-time{font-size:10px;color:var(--text-muted);white-space:nowrap}
609
610
  .mh-empty{padding:16px;font-size:12px;color:var(--text-muted);text-align:center}
610
611
  @keyframes healthPulse{0%,100%{opacity:1}50%{opacity:.4}}
@@ -1690,10 +1691,11 @@ const I18N={
1690
1691
  'skill.save.error':'Failed to save skill: ',
1691
1692
  'update.available':'New version available',
1692
1693
  'update.run':'Run',
1693
- 'update.btn':'Update now',
1694
- 'update.installing':'Installing update...',
1695
- 'update.success':'Update installed! Restarting...',
1694
+ 'update.btn':'Update',
1695
+ 'update.installing':'Installing...',
1696
+ 'update.success':'Updated!',
1696
1697
  'update.failed':'Update failed',
1698
+ 'update.restarting':'Restarting service...',
1697
1699
  'update.dismiss':'Dismiss'
1698
1700
  },
1699
1701
  zh:{
@@ -2011,10 +2013,11 @@ const I18N={
2011
2013
  'skill.save.error':'保存技能失败:',
2012
2014
  'update.available':'发现新版本',
2013
2015
  'update.run':'执行命令',
2014
- 'update.btn':'立即更新',
2015
- 'update.installing':'正在安装更新...',
2016
- 'update.success':'更新成功!正在重启...',
2016
+ 'update.btn':'更新',
2017
+ 'update.installing':'安装中...',
2018
+ 'update.success':'更新完成',
2017
2019
  'update.failed':'更新失败',
2020
+ 'update.restarting':'正在重启服务...',
2018
2021
  'update.dismiss':'关闭'
2019
2022
  }
2020
2023
  };
@@ -3008,7 +3011,14 @@ function classifyError(msg){
3008
3011
  if(msg.indexOf('ECONNREFUSED')>=0) return 'Connection refused';
3009
3012
  if(msg.indexOf('ENOTFOUND')>=0) return 'DNS resolution failed';
3010
3013
  if(msg.indexOf('403')>=0) return 'Forbidden (403)';
3011
- return msg;
3014
+ if(msg.indexOf('503')>=0||msg.indexOf('upstream connect error')>=0||msg.indexOf('Service Unavailable')>=0) return 'Service unavailable (503)';
3015
+ if(msg.indexOf('502')>=0||msg.indexOf('Bad Gateway')>=0) return 'Bad gateway (502)';
3016
+ if(msg.indexOf('500')>=0||msg.indexOf('Internal Server Error')>=0) return 'Server error (500)';
3017
+ if(msg.indexOf('404')>=0||msg.indexOf('Not Found')>=0) return 'Not found (404)';
3018
+ if(msg.indexOf('fetch failed')>=0||msg.indexOf('ETIMEDOUT')>=0) return 'Network error';
3019
+ if(msg.indexOf('Unknown')>=0&&msg.indexOf('provider')>=0) return 'Unknown provider';
3020
+ var m=msg.match(/\((\d{3})\)/); if(m) return 'HTTP error ('+m[1]+')';
3021
+ return msg.length>80?msg.substring(0,77)+'...':msg;
3012
3022
  }
3013
3023
 
3014
3024
  function shortenModel(s){return s?s.replace('openai_compatible/','').replace('openai/',''):'\u2014';}
@@ -3054,7 +3064,7 @@ async function loadModelHealth(){
3054
3064
  issue+=shortErr;
3055
3065
  if(m.consecutiveErrors>1) issue+=' ('+m.consecutiveErrors+'x)';
3056
3066
  }
3057
- if(issue) h+='<td><span class="mh-err-text" title="'+escapeHtml(m.lastErrorMessage||'')+'">'+escapeHtml(issue)+'</span></td>';
3067
+ if(issue) h+='<td><span class="mh-err-text" data-err="'+escapeHtml(m.lastErrorMessage||'')+'">'+escapeHtml(issue)+'</span></td>';
3058
3068
  else h+='<td><span style="color:var(--text-muted);font-size:11px">\u2014</span></td>';
3059
3069
 
3060
3070
  h+='<td style="text-align:right"><span class="mh-time">'+(ago||'\u2014')+'</span></td>';
@@ -3062,11 +3072,29 @@ async function loadModelHealth(){
3062
3072
  }
3063
3073
  h+='</tbody></table>';
3064
3074
  bar.innerHTML=h;
3075
+ initMhTooltips();
3065
3076
  }catch(e){
3066
3077
  bar.innerHTML='<div class="mh-empty">Failed to load model health</div>';
3067
3078
  }
3068
3079
  }
3069
3080
 
3081
+ function initMhTooltips(){
3082
+ var tip=document.getElementById('mhTooltip');
3083
+ if(!tip){tip=document.createElement('div');tip.id='mhTooltip';document.body.appendChild(tip);}
3084
+ document.querySelectorAll('.mh-err-text[data-err]').forEach(function(el){
3085
+ el.addEventListener('mouseenter',function(e){
3086
+ var msg=el.getAttribute('data-err');
3087
+ if(!msg)return;
3088
+ tip.textContent=msg;
3089
+ tip.style.display='block';
3090
+ var rect=el.getBoundingClientRect();
3091
+ tip.style.left=Math.max(0,Math.min(rect.left,window.innerWidth-490))+'px';
3092
+ tip.style.top=(rect.bottom+6)+'px';
3093
+ });
3094
+ el.addEventListener('mouseleave',function(){tip.style.display='none';});
3095
+ });
3096
+ }
3097
+
3070
3098
  function timeAgo(ts){
3071
3099
  var diff=Date.now()-ts;
3072
3100
  if(diff<60000) return 'just now';
@@ -4663,35 +4691,35 @@ function waitForGatewayAndReload(maxAttempts,attempt){
4663
4691
  attempt=attempt||0;
4664
4692
  if(attempt>=maxAttempts){window.location.reload();return;}
4665
4693
  setTimeout(function(){
4666
- fetch('/api/update-check').then(function(r){
4667
- if(r.ok) window.location.reload();
4668
- else waitForGatewayAndReload(maxAttempts,attempt+1);
4694
+ fetch('/api/auth/status').then(function(){
4695
+ window.location.reload();
4669
4696
  }).catch(function(){waitForGatewayAndReload(maxAttempts,attempt+1);});
4670
4697
  },3000);
4671
4698
  }
4672
- function doUpdateInstall(packageSpec,btnEl){
4699
+ function doUpdateInstall(packageSpec,btnEl,statusEl){
4673
4700
  btnEl.disabled=true;
4674
4701
  btnEl.textContent=t('update.installing');
4702
+ btnEl.style.cssText='background:rgba(99,102,241,.15);color:var(--pri);border:1px solid rgba(99,102,241,.3);border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;cursor:wait;white-space:nowrap';
4675
4703
  fetch('/api/update-install',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({packageSpec:packageSpec})})
4676
4704
  .then(function(r){return r.json()})
4677
4705
  .then(function(d){
4678
4706
  if(d.ok){
4679
4707
  btnEl.textContent=t('update.success');
4680
- btnEl.style.background='#22c55e';
4681
- btnEl.style.color='#fff';
4682
- waitForGatewayAndReload(20);
4708
+ btnEl.style.cssText='background:rgba(34,197,94,.15);color:#22c55e;border:1px solid rgba(34,197,94,.3);border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;cursor:default;white-space:nowrap';
4709
+ if(statusEl)statusEl.textContent=t('update.restarting');
4710
+ waitForGatewayAndReload(40);
4683
4711
  }else{
4684
- btnEl.textContent=t('update.failed')+': '+(d.error||'').slice(0,80);
4685
- btnEl.style.background='#ef4444';
4686
- btnEl.style.color='#fff';
4712
+ btnEl.textContent=t('update.btn');
4713
+ btnEl.style.cssText='background:none;border:1px solid currentColor;border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;color:inherit;cursor:pointer;white-space:nowrap;opacity:.85';
4687
4714
  btnEl.disabled=false;
4688
- setTimeout(function(){btnEl.textContent=t('update.btn');btnEl.style.background='';btnEl.style.color='';},5000);
4715
+ if(statusEl)statusEl.textContent=t('update.failed')+': '+(d.error||'').slice(0,60);
4716
+ setTimeout(function(){if(statusEl)statusEl.textContent='';},8000);
4689
4717
  }
4690
4718
  })
4691
- .catch(function(e){
4692
- btnEl.textContent=t('update.failed');
4719
+ .catch(function(){
4720
+ btnEl.textContent=t('update.btn');
4721
+ btnEl.style.cssText='background:none;border:1px solid currentColor;border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;color:inherit;cursor:pointer;white-space:nowrap;opacity:.85';
4693
4722
  btnEl.disabled=false;
4694
- setTimeout(function(){btnEl.textContent=t('update.btn');btnEl.style.background='';btnEl.style.color='';},5000);
4695
4723
  });
4696
4724
  }
4697
4725
  async function checkForUpdate(){
@@ -4700,25 +4728,33 @@ async function checkForUpdate(){
4700
4728
  if(!r.ok)return;
4701
4729
  const d=await r.json();
4702
4730
  if(!d.updateAvailable)return;
4703
- const pkgSpec=d.installCommand?d.installCommand.replace(/^openclaw plugins install\s+/,''):(d.packageName+'@'+d.latest);
4704
- const banner=document.createElement('div');
4731
+ const pkgSpec=d.installCommand?d.installCommand.replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/,''):(d.packageName+'@'+d.latest);
4732
+ var banner=document.createElement('div');
4705
4733
  banner.id='updateBanner';
4706
- 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)';
4707
- var leftSpan=document.createElement('span');
4708
- leftSpan.innerHTML='🔔 '+t('update.available')+': <b>v'+esc(d.current)+'</b> → <b>v'+esc(d.latest)+'</b>';
4734
+ banner.style.cssText='display:flex;align-items:center;gap:10px;padding:12px 20px;font-size:13px;font-weight:500;border-radius:10px;margin:0 32px;animation:slideIn .3s ease;background:rgba(245,158,11,.1);color:#d97706;border:1px solid rgba(245,158,11,.25)';
4735
+ var textNode=document.createElement('div');
4736
+ textNode.style.cssText='display:flex;align-items:center;gap:8px;flex-shrink:0';
4737
+ textNode.innerHTML='\u{1F4E6} '+t('update.available')+' <b style="margin:0 2px">v'+esc(d.current)+'</b> \u2192 <b style="margin:0 2px">v'+esc(d.latest)+'</b>';
4709
4738
  var btnUpdate=document.createElement('button');
4739
+ btnUpdate.className='emb-banner-btn';
4710
4740
  btnUpdate.textContent=t('update.btn');
4711
- btnUpdate.style.cssText='background:#fff;color:#d97706;border:none;padding:5px 16px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;margin-left:12px';
4712
- btnUpdate.onclick=function(){doUpdateInstall(pkgSpec,btnUpdate)};
4741
+ var statusDiv=document.createElement('div');
4742
+ statusDiv.style.cssText='font-size:11px;opacity:.8;flex-shrink:0';
4743
+ btnUpdate.onclick=function(){doUpdateInstall(pkgSpec,btnUpdate,statusDiv)};
4744
+ textNode.appendChild(btnUpdate);
4745
+ var spacer=document.createElement('div');
4746
+ spacer.style.cssText='flex:1';
4713
4747
  var btnClose=document.createElement('button');
4748
+ btnClose.className='emb-banner-close';
4714
4749
  btnClose.innerHTML='&times;';
4715
- btnClose.style.cssText='background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px';
4716
- btnClose.onclick=function(){banner.remove();document.body.style.paddingTop='';};
4717
- leftSpan.appendChild(btnUpdate);
4718
- banner.appendChild(leftSpan);
4750
+ btnClose.onclick=function(){banner.remove()};
4751
+ banner.appendChild(textNode);
4752
+ banner.appendChild(statusDiv);
4753
+ banner.appendChild(spacer);
4719
4754
  banner.appendChild(btnClose);
4720
- document.body.prepend(banner);
4721
- document.body.style.paddingTop='48px';
4755
+ var embBanner=document.querySelector('.emb-banner');
4756
+ if(embBanner&&embBanner.parentNode){embBanner.parentNode.insertBefore(banner,embBanner);}
4757
+ else{var ct=document.querySelector('.content-area')||document.querySelector('main')||document.body;if(ct.firstChild)ct.insertBefore(banner,ct.firstChild);else ct.appendChild(banner);}
4722
4758
  }catch(e){}
4723
4759
  }
4724
4760
 
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import os from "node:os";
2
3
  import crypto from "node:crypto";
3
4
  import { execSync, exec } from "node:child_process";
4
5
  import fs from "node:fs";
@@ -1177,34 +1178,119 @@ export class ViewerServer {
1177
1178
  req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
1178
1179
  req.on("end", () => {
1179
1180
  try {
1180
- const { packageSpec } = JSON.parse(body);
1181
- if (!packageSpec || typeof packageSpec !== "string") {
1181
+ const { packageSpec: rawSpec } = JSON.parse(body);
1182
+ if (!rawSpec || typeof rawSpec !== "string") {
1182
1183
  res.writeHead(400, { "Content-Type": "application/json" });
1183
1184
  res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
1184
1185
  return;
1185
1186
  }
1187
+ const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
1186
1188
  const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
1189
+ this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
1187
1190
  if (!allowed.test(packageSpec)) {
1191
+ this.log.warn(`update-install: rejected packageSpec="${packageSpec}" — does not match ${allowed}`);
1188
1192
  res.writeHead(400, { "Content-Type": "application/json" });
1189
- res.end(JSON.stringify({ ok: false, error: "Invalid package spec" }));
1193
+ res.end(JSON.stringify({ ok: false, error: `Invalid package spec: "${packageSpec}"` }));
1190
1194
  return;
1191
1195
  }
1192
- this.log.info(`update-install: installing ${packageSpec}...`);
1193
- exec(`npx openclaw plugins install ${packageSpec}`, { timeout: 120_000 }, (err, stdout, stderr) => {
1194
- if (err) {
1195
- this.log.warn(`update-install failed: ${err.message}\n${stderr}`);
1196
- this.jsonResponse(res, { ok: false, error: stderr || err.message });
1196
+
1197
+ const pkgPath = this.findPluginPackageJson();
1198
+ const pluginName = pkgPath
1199
+ ? (() => { try { return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name; } catch { return null; } })()
1200
+ : null;
1201
+ const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
1202
+ const extDir = path.join(os.homedir(), ".openclaw", "extensions", shortName);
1203
+ const tmpDir = path.join(os.tmpdir(), `openclaw-update-${Date.now()}`);
1204
+
1205
+ // Download via npm pack, extract, and replace extension dir.
1206
+ // Does NOT touch openclaw.json → no config watcher SIGUSR1.
1207
+ this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
1208
+ fs.mkdirSync(tmpDir, { recursive: true });
1209
+ exec(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
1210
+ if (packErr) {
1211
+ this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
1212
+ this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
1213
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1197
1214
  return;
1198
1215
  }
1199
- this.log.info(`update-install success: ${stdout}`);
1200
- this.jsonResponse(res, { ok: true, output: stdout });
1201
- this.log.info(`update-install: restarting gateway...`);
1202
- setTimeout(() => {
1203
- exec("npx openclaw gateway restart", { timeout: 30_000 }, (restartErr) => {
1204
- if (restartErr) this.log.warn(`gateway restart failed: ${restartErr.message}`);
1205
- else this.log.info("gateway restart initiated");
1216
+ const tgzFile = packOut.trim().split("\n").pop()!;
1217
+ const tgzPath = path.join(tmpDir, tgzFile);
1218
+ this.log.info(`update-install: downloaded ${tgzFile}, extracting...`);
1219
+
1220
+ const extractDir = path.join(tmpDir, "extract");
1221
+ fs.mkdirSync(extractDir, { recursive: true });
1222
+ exec(`tar -xzf ${tgzPath} -C ${extractDir}`, { timeout: 30_000 }, (tarErr) => {
1223
+ if (tarErr) {
1224
+ this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
1225
+ this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
1226
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1227
+ return;
1228
+ }
1229
+
1230
+ // npm pack extracts to a "package" subdirectory
1231
+ const srcDir = path.join(extractDir, "package");
1232
+ if (!fs.existsSync(srcDir)) {
1233
+ this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
1234
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1235
+ return;
1236
+ }
1237
+
1238
+ // Replace extension directory
1239
+ this.log.info(`update-install: replacing ${extDir}...`);
1240
+ try { fs.rmSync(extDir, { recursive: true, force: true }); } catch {}
1241
+ fs.mkdirSync(path.dirname(extDir), { recursive: true });
1242
+ fs.renameSync(srcDir, extDir);
1243
+
1244
+ // Install dependencies
1245
+ this.log.info(`update-install: installing dependencies...`);
1246
+ exec(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
1247
+ if (npmErr) {
1248
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1249
+ this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
1250
+ this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
1251
+ return;
1252
+ }
1253
+
1254
+ // Rebuild native modules (do not swallow errors)
1255
+ exec(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
1256
+ if (rebuildErr) {
1257
+ this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
1258
+ const stderr = String(rebuildStderr || "").trim();
1259
+ if (stderr) this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
1260
+ // Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
1261
+ }
1262
+
1263
+ // Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
1264
+ this.log.info(`update-install: running postinstall...`);
1265
+ exec(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => {
1266
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1267
+
1268
+ if (postErr) {
1269
+ this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
1270
+ const postStderrStr = String(postStderr || "").trim();
1271
+ if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
1272
+ // Still report success; plugin is updated, user can run postinstall manually if needed
1273
+ }
1274
+
1275
+ // Read new version
1276
+ let newVersion = "unknown";
1277
+ try {
1278
+ const newPkg = JSON.parse(fs.readFileSync(path.join(extDir, "package.json"), "utf-8"));
1279
+ newVersion = newPkg.version ?? newVersion;
1280
+ } catch {}
1281
+
1282
+ this.log.info(`update-install: success! Updated to ${newVersion}`);
1283
+ this.jsonResponse(res, { ok: true, version: newVersion });
1284
+
1285
+ // Trigger Gateway restart after response is sent
1286
+ setTimeout(() => {
1287
+ this.log.info(`update-install: triggering gateway restart...`);
1288
+ process.kill(process.pid, "SIGUSR1");
1289
+ }, 500);
1290
+ });
1291
+ });
1206
1292
  });
1207
- }, 1000);
1293
+ });
1208
1294
  });
1209
1295
  } catch (e) {
1210
1296
  res.writeHead(400, { "Content-Type": "application/json" });