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

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.
Files changed (87) hide show
  1. package/dist/capture/index.js +52 -8
  2. package/dist/capture/index.js.map +1 -1
  3. package/dist/ingest/chunker.d.ts +3 -4
  4. package/dist/ingest/chunker.d.ts.map +1 -1
  5. package/dist/ingest/chunker.js +19 -24
  6. package/dist/ingest/chunker.js.map +1 -1
  7. package/dist/ingest/providers/anthropic.d.ts +3 -1
  8. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  9. package/dist/ingest/providers/anthropic.js +90 -51
  10. package/dist/ingest/providers/anthropic.js.map +1 -1
  11. package/dist/ingest/providers/bedrock.d.ts +3 -1
  12. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  13. package/dist/ingest/providers/bedrock.js +90 -51
  14. package/dist/ingest/providers/bedrock.js.map +1 -1
  15. package/dist/ingest/providers/gemini.d.ts +3 -1
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +88 -51
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +3 -1
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +70 -30
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +3 -1
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +91 -51
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +1 -0
  28. package/dist/ingest/task-processor.d.ts.map +1 -1
  29. package/dist/ingest/task-processor.js +33 -9
  30. package/dist/ingest/task-processor.js.map +1 -1
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +29 -13
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/recall/engine.d.ts.map +1 -1
  35. package/dist/recall/engine.js +19 -14
  36. package/dist/recall/engine.js.map +1 -1
  37. package/dist/skill/bundled-memory-guide.d.ts +1 -5
  38. package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
  39. package/dist/skill/bundled-memory-guide.js +38 -88
  40. package/dist/skill/bundled-memory-guide.js.map +1 -1
  41. package/dist/skill/evaluator.js +1 -1
  42. package/dist/storage/sqlite.d.ts +1 -2
  43. package/dist/storage/sqlite.d.ts.map +1 -1
  44. package/dist/storage/sqlite.js +90 -17
  45. package/dist/storage/sqlite.js.map +1 -1
  46. package/dist/tools/memory-get.d.ts.map +1 -1
  47. package/dist/tools/memory-get.js +1 -3
  48. package/dist/tools/memory-get.js.map +1 -1
  49. package/dist/types.d.ts +2 -2
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/update-check.d.ts +21 -0
  54. package/dist/update-check.d.ts.map +1 -0
  55. package/dist/update-check.js +111 -0
  56. package/dist/update-check.js.map +1 -0
  57. package/dist/viewer/html.d.ts +1 -1
  58. package/dist/viewer/html.d.ts.map +1 -1
  59. package/dist/viewer/html.js +608 -234
  60. package/dist/viewer/html.js.map +1 -1
  61. package/dist/viewer/server.d.ts +2 -1
  62. package/dist/viewer/server.d.ts.map +1 -1
  63. package/dist/viewer/server.js +201 -90
  64. package/dist/viewer/server.js.map +1 -1
  65. package/index.ts +206 -198
  66. package/openclaw.plugin.json +3 -0
  67. package/package.json +6 -1
  68. package/scripts/postinstall.cjs +69 -2
  69. package/skill/memos-memory-guide/SKILL.md +73 -36
  70. package/src/capture/index.ts +52 -8
  71. package/src/ingest/chunker.ts +22 -30
  72. package/src/ingest/providers/anthropic.ts +100 -53
  73. package/src/ingest/providers/bedrock.ts +101 -53
  74. package/src/ingest/providers/gemini.ts +100 -53
  75. package/src/ingest/providers/index.ts +81 -35
  76. package/src/ingest/providers/openai.ts +101 -53
  77. package/src/ingest/task-processor.ts +29 -8
  78. package/src/ingest/worker.ts +31 -13
  79. package/src/recall/engine.ts +20 -13
  80. package/src/skill/bundled-memory-guide.ts +5 -87
  81. package/src/skill/evaluator.ts +1 -1
  82. package/src/storage/sqlite.ts +93 -21
  83. package/src/tools/memory-get.ts +1 -4
  84. package/src/types.ts +2 -9
  85. package/src/update-check.ts +96 -0
  86. package/src/viewer/html.ts +607 -233
  87. package/src/viewer/server.ts +152 -82
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import crypto from "node:crypto";
3
- import { execSync } from "node:child_process";
3
+ import { execSync, exec } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import readline from "node:readline";
@@ -48,6 +48,14 @@ export class ViewerServer {
48
48
  private readonly ctx?: PluginContext;
49
49
 
50
50
  private static readonly SESSION_TTL = 24 * 60 * 60 * 1000;
51
+ private static readonly PLUGIN_VERSION: string = (() => {
52
+ try {
53
+ const pkgPath = path.resolve(__dirname, "../../package.json");
54
+ return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
55
+ } catch {
56
+ return "unknown";
57
+ }
58
+ })();
51
59
  private resetToken: string;
52
60
  private migrationRunning = false;
53
61
  private migrationAbort = false;
@@ -67,8 +75,8 @@ export class ViewerServer {
67
75
 
68
76
  private ppRunning = false;
69
77
  private ppAbort = false;
70
- private ppState: { running: boolean; done: boolean; stopped: boolean; processed: number; total: number; tasksCreated: number; skillsCreated: number; errors: number } =
71
- { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
78
+ private ppState: { running: boolean; done: boolean; stopped: boolean; processed: number; total: number; tasksCreated: number; skillsCreated: number; errors: number; skippedSessions: number; totalSessions: number } =
79
+ { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
72
80
  private ppSSEClients: http.ServerResponse[] = [];
73
81
 
74
82
  constructor(opts: ViewerServerOptions) {
@@ -227,7 +235,6 @@ export class ViewerServer {
227
235
  else if (p.startsWith("/api/skill/") && req.method === "DELETE") this.handleSkillDelete(res, p);
228
236
  else if (p.startsWith("/api/skill/") && req.method === "PUT") this.handleSkillUpdate(req, res, p);
229
237
  else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
230
- else if (p === "/api/memory" && req.method === "POST") this.handleCreate(req, res);
231
238
  else if (p.startsWith("/api/memory/") && req.method === "GET") this.serveMemoryDetail(res, p);
232
239
  else if (p.startsWith("/api/memory/") && req.method === "PUT") this.handleUpdate(req, res, p);
233
240
  else if (p.startsWith("/api/memory/") && req.method === "DELETE") this.handleDelete(res, p);
@@ -241,6 +248,7 @@ export class ViewerServer {
241
248
  else if (p === "/api/model-health" && req.method === "GET") this.serveModelHealth(res);
242
249
  else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
243
250
  else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
251
+ else if (p === "/api/update-install" && req.method === "POST") this.handleUpdateInstall(req, res);
244
252
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
245
253
  else if (p === "/api/cleanup-polluted" && req.method === "POST") this.handleCleanupPolluted(res);
246
254
  else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
@@ -363,7 +371,7 @@ export class ViewerServer {
363
371
 
364
372
  private serveViewer(res: http.ServerResponse): void {
365
373
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0" });
366
- res.end(viewerHTML);
374
+ res.end(viewerHTML(ViewerServer.PLUGIN_VERSION));
367
375
  }
368
376
 
369
377
  // ─── Data APIs ───
@@ -374,7 +382,6 @@ export class ViewerServer {
374
382
  const offset = (page - 1) * limit;
375
383
  const session = url.searchParams.get("session") ?? undefined;
376
384
  const role = url.searchParams.get("role") ?? undefined;
377
- const kind = url.searchParams.get("kind") ?? undefined;
378
385
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
379
386
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
380
387
  const owner = url.searchParams.get("owner") ?? undefined;
@@ -385,7 +392,6 @@ export class ViewerServer {
385
392
  const params: any[] = [];
386
393
  if (session) { conditions.push("session_key = ?"); params.push(session); }
387
394
  if (role) { conditions.push("role = ?"); params.push(role); }
388
- if (kind) { conditions.push("kind = ?"); params.push(kind); }
389
395
  if (owner) { conditions.push("owner = ?"); params.push(owner); }
390
396
  if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
391
397
  if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
@@ -393,9 +399,14 @@ export class ViewerServer {
393
399
  const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
394
400
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
395
401
  const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
402
+ const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
396
403
  const memories = rawMemories.map((m: any) => {
397
404
  if (m.role === "user" && m.content) {
398
- return { ...m, content: stripInboundMetadata(m.content) };
405
+ m = { ...m, content: stripInboundMetadata(m.content) };
406
+ }
407
+ if (m.merge_count > 0) {
408
+ const sources = findMergeSources.all(m.id) as Array<{ id: string; summary: string; role: string }>;
409
+ m.merge_sources = sources;
399
410
  }
400
411
  return m;
401
412
  });
@@ -433,7 +444,7 @@ export class ViewerServer {
433
444
  id: t.id,
434
445
  sessionKey: t.sessionKey,
435
446
  title: t.title,
436
- summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
447
+ summary: t.summary ?? "",
437
448
  status: t.status,
438
449
  startedAt: t.startedAt,
439
450
  endedAt: t.endedAt,
@@ -456,8 +467,7 @@ export class ViewerServer {
456
467
 
457
468
  const chunks = this.store.getChunksByTask(taskId);
458
469
  const chunkItems = chunks.map((c) => {
459
- let text = c.role === "user" ? stripInboundMetadata(c.content) : c.content;
460
- if (text.length > 500) text = text.slice(0, 497) + "...";
470
+ const text = c.role === "user" ? stripInboundMetadata(c.content) : c.content;
461
471
  return { id: c.id, role: c.role, content: text, summary: c.summary, createdAt: c.createdAt };
462
472
  });
463
473
 
@@ -494,7 +504,7 @@ export class ViewerServer {
494
504
  const emptyStats = {
495
505
  totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
496
506
  embeddingProvider: this.embedder?.provider ?? "none",
497
- roleBreakdown: {}, kindBreakdown: {}, dedupBreakdown: {},
507
+ dedupBreakdown: {},
498
508
  timeRange: { earliest: null, latest: null },
499
509
  sessions: [],
500
510
  };
@@ -508,7 +518,6 @@ export class ViewerServer {
508
518
  const db = (this.store as any).db;
509
519
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
510
520
  const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get() as any;
511
- const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as any[];
512
521
  const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE dedup_status = 'active'").get() as any;
513
522
  const MIN_VALID_TS = 1704067200000; // 2024-01-01
514
523
  if (timeRange.earliest != null && timeRange.earliest < MIN_VALID_TS) {
@@ -520,7 +529,6 @@ export class ViewerServer {
520
529
  }
521
530
  let embCount = 0;
522
531
  try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
523
- const kinds = db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as any[];
524
532
  const sessionList = db.prepare(
525
533
  "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",
526
534
  ).all() as any[];
@@ -544,8 +552,6 @@ export class ViewerServer {
544
552
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
545
553
  totalSkills: skillCount,
546
554
  embeddingProvider: this.embedder.provider,
547
- roleBreakdown: Object.fromEntries(roles.map((r: any) => [r.role, r.count])),
548
- kindBreakdown: Object.fromEntries(kinds.map((k: any) => [k.kind, k.count])),
549
555
  dedupBreakdown,
550
556
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
551
557
  sessions: sessionList,
@@ -562,45 +568,70 @@ export class ViewerServer {
562
568
  if (!q.trim()) { this.jsonResponse(res, { results: [], query: q }); return; }
563
569
 
564
570
  const role = url.searchParams.get("role") ?? undefined;
565
- const kind = url.searchParams.get("kind") ?? undefined;
571
+ const session = url.searchParams.get("session") ?? undefined;
572
+ const owner = url.searchParams.get("owner") ?? undefined;
566
573
  const dateFrom = url.searchParams.get("dateFrom") ?? undefined;
567
574
  const dateTo = url.searchParams.get("dateTo") ?? undefined;
568
575
 
569
576
  const passesFilter = (r: any): boolean => {
570
577
  if (role && r.role !== role) return false;
571
- if (kind && r.kind !== kind) return false;
578
+ if (session && r.session_key !== session) return false;
579
+ if (owner && r.owner !== owner) return false;
572
580
  if (dateFrom && r.created_at < new Date(dateFrom).getTime()) return false;
573
581
  if (dateTo && r.created_at > new Date(dateTo).getTime()) return false;
574
582
  return true;
575
583
  };
576
584
 
585
+ const ftsFilters: string[] = [];
586
+ const likeFilters: string[] = [];
587
+ const sqlParams: any[] = [];
588
+ if (session) { ftsFilters.push("c.session_key = ?"); likeFilters.push("session_key = ?"); sqlParams.push(session); }
589
+ if (owner) { ftsFilters.push("c.owner = ?"); likeFilters.push("owner = ?"); sqlParams.push(owner); }
590
+ const ftsWhere = ftsFilters.length > 0 ? " AND " + ftsFilters.join(" AND ") : "";
591
+ const likeWhere = likeFilters.length > 0 ? " AND " + likeFilters.join(" AND ") : "";
592
+
577
593
  const db = (this.store as any).db;
578
594
  let ftsResults: any[] = [];
579
595
  try {
580
596
  ftsResults = db.prepare(
581
- "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100",
582
- ).all(q).filter(passesFilter);
597
+ `SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?${ftsWhere} ORDER BY rank LIMIT 100`,
598
+ ).all(q, ...sqlParams).filter(passesFilter);
583
599
  } catch { /* FTS syntax error, fall through */ }
584
600
  if (ftsResults.length === 0) {
585
- ftsResults = db.prepare(
586
- "SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100",
587
- ).all(`%${q}%`, `%${q}%`).filter(passesFilter);
601
+ try {
602
+ ftsResults = db.prepare(
603
+ `SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)${likeWhere} ORDER BY created_at DESC LIMIT 100`,
604
+ ).all(`%${q}%`, `%${q}%`, ...sqlParams).filter(passesFilter);
605
+ } catch (err) {
606
+ this.log.warn(`LIKE search failed: ${err}`);
607
+ }
588
608
  }
589
609
 
590
610
  const SEMANTIC_THRESHOLD = 0.64;
611
+ const VECTOR_TIMEOUT_MS = 8000;
591
612
  let vectorResults: any[] = [];
592
613
  let scoreMap = new Map<string, number>();
593
614
  try {
594
- const queryVec = await this.embedder.embedQuery(q);
595
- const hits = vectorSearch(this.store, queryVec, 40);
596
- scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
597
- const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
598
- if (hitIds.size > 0) {
599
- const placeholders = [...hitIds].map(() => "?").join(",");
600
- const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})`).all(...hitIds).filter(passesFilter);
601
- rows.forEach((r: any) => { r._vscore = scoreMap.get(r.id) ?? 0; });
602
- rows.sort((a: any, b: any) => (b._vscore ?? 0) - (a._vscore ?? 0));
603
- vectorResults = rows;
615
+ const vecPromise = (async () => {
616
+ const queryVec = await this.embedder.embedQuery(q);
617
+ return vectorSearch(this.store, queryVec, 40);
618
+ })();
619
+ const hits = await Promise.race([
620
+ vecPromise,
621
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), VECTOR_TIMEOUT_MS)),
622
+ ]);
623
+ if (hits) {
624
+ scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
625
+ const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
626
+ if (hitIds.size > 0) {
627
+ const placeholders = [...hitIds].map(() => "?").join(",");
628
+ const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})${likeWhere}`).all(...hitIds, ...sqlParams).filter(passesFilter);
629
+ rows.forEach((r: any) => { r._vscore = scoreMap.get(r.id) ?? 0; });
630
+ rows.sort((a: any, b: any) => (b._vscore ?? 0) - (a._vscore ?? 0));
631
+ vectorResults = rows;
632
+ }
633
+ } else {
634
+ this.log.warn("Vector search timed out, returning FTS results only");
604
635
  }
605
636
  } catch (err) {
606
637
  this.log.warn(`Vector search failed (falling back to FTS only): ${err}`);
@@ -885,35 +916,6 @@ export class ViewerServer {
885
916
 
886
917
  // ─── CRUD ───
887
918
 
888
- private handleCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
889
- this.readBody(req, (body) => {
890
- try {
891
- const data = JSON.parse(body);
892
- if (!data.content || typeof data.content !== "string" || !data.content.trim()) {
893
- res.writeHead(400, { "Content-Type": "application/json" });
894
- res.end(JSON.stringify({ error: "content is required and must be a non-empty string" }));
895
- return;
896
- }
897
- const { v4: uuidv4 } = require("uuid");
898
- const id = uuidv4();
899
- const now = Date.now();
900
- this.store.insertChunk({
901
- id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
902
- role: data.role || "user", content: data.content, kind: data.kind || "paragraph",
903
- summary: data.summary || data.content.slice(0, 100),
904
- taskId: null, skillId: null, owner: data.owner || "agent:main",
905
- dedupStatus: "active", dedupTarget: null, dedupReason: null,
906
- mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
907
- createdAt: now, updatedAt: now, embedding: null,
908
- });
909
- this.jsonResponse(res, { ok: true, id, message: "Memory created" });
910
- } catch (err) {
911
- res.writeHead(400, { "Content-Type": "application/json" });
912
- res.end(JSON.stringify({ error: String(err) }));
913
- }
914
- });
915
- }
916
-
917
919
  private serveMemoryDetail(res: http.ServerResponse, urlPath: string): void {
918
920
  const chunkId = urlPath.replace("/api/memory/", "");
919
921
  const chunk = this.store.getChunk(chunkId);
@@ -938,7 +940,7 @@ export class ViewerServer {
938
940
  res.end(JSON.stringify({ error: "content must be a non-empty string" }));
939
941
  return;
940
942
  }
941
- const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind, owner: data.owner });
943
+ const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, owner: data.owner });
942
944
  if (ok) this.jsonResponse(res, { ok: true, message: "Memory updated" });
943
945
  else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
944
946
  } catch (err) {
@@ -1149,20 +1151,20 @@ export class ViewerServer {
1149
1151
  this.jsonResponse(res, { updateAvailable: false, current });
1150
1152
  return;
1151
1153
  }
1152
- const npmResp = await fetch(`https://registry.npmjs.org/${name}/latest`, {
1153
- signal: AbortSignal.timeout(6_000),
1154
- });
1155
- if (!npmResp.ok) {
1156
- this.jsonResponse(res, { updateAvailable: false, current });
1154
+ const { computeUpdateCheck } = await import("../update-check");
1155
+ const result = await computeUpdateCheck(name, current, fetch, 6_000);
1156
+ if (!result) {
1157
+ this.jsonResponse(res, { updateAvailable: false, current, packageName: name });
1157
1158
  return;
1158
1159
  }
1159
- const data = await npmResp.json() as { version?: string };
1160
- const latest = data.version ?? current;
1161
1160
  this.jsonResponse(res, {
1162
- updateAvailable: latest !== current,
1163
- current,
1164
- latest,
1165
- packageName: name,
1161
+ updateAvailable: result.updateAvailable,
1162
+ current: result.current,
1163
+ latest: result.latest,
1164
+ packageName: result.packageName,
1165
+ channel: result.channel,
1166
+ installCommand: result.installCommand,
1167
+ stableChannel: result.stableChannel,
1166
1168
  });
1167
1169
  } catch (e) {
1168
1170
  this.log.warn(`handleUpdateCheck error: ${e}`);
@@ -1170,6 +1172,47 @@ export class ViewerServer {
1170
1172
  }
1171
1173
  }
1172
1174
 
1175
+ private handleUpdateInstall(req: http.IncomingMessage, res: http.ServerResponse): void {
1176
+ let body = "";
1177
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
1178
+ req.on("end", () => {
1179
+ try {
1180
+ const { packageSpec } = JSON.parse(body);
1181
+ if (!packageSpec || typeof packageSpec !== "string") {
1182
+ res.writeHead(400, { "Content-Type": "application/json" });
1183
+ res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
1184
+ return;
1185
+ }
1186
+ const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
1187
+ if (!allowed.test(packageSpec)) {
1188
+ res.writeHead(400, { "Content-Type": "application/json" });
1189
+ res.end(JSON.stringify({ ok: false, error: "Invalid package spec" }));
1190
+ return;
1191
+ }
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 });
1197
+ return;
1198
+ }
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");
1206
+ });
1207
+ }, 1000);
1208
+ });
1209
+ } catch (e) {
1210
+ res.writeHead(400, { "Content-Type": "application/json" });
1211
+ res.end(JSON.stringify({ ok: false, error: String(e) }));
1212
+ }
1213
+ });
1214
+ }
1215
+
1173
1216
  private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<number | undefined> {
1174
1217
  if (provider === "local") {
1175
1218
  return 384;
@@ -1403,10 +1446,18 @@ export class ViewerServer {
1403
1446
  }
1404
1447
 
1405
1448
  let importedSessions: string[] = [];
1449
+ let importedChunkCount = 0;
1406
1450
  try {
1407
1451
  if (this.store) {
1408
1452
  importedSessions = this.store.getDistinctSessionKeys()
1409
1453
  .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
1454
+ if (importedSessions.length > 0) {
1455
+ const placeholders = importedSessions.map(() => "?").join(",");
1456
+ const row = (this.store as any).db.prepare(
1457
+ `SELECT COUNT(*) as cnt FROM chunks WHERE session_key IN (${placeholders})`
1458
+ ).get(...importedSessions) as { cnt: number };
1459
+ importedChunkCount = row?.cnt ?? 0;
1460
+ }
1410
1461
  }
1411
1462
  } catch (storeErr) {
1412
1463
  this.log.warn(`migrate/scan: store query failed: ${storeErr}`);
@@ -1421,6 +1472,7 @@ export class ViewerServer {
1421
1472
  hasSummarizer,
1422
1473
  hasImportedData: importedSessions.length > 0,
1423
1474
  importedSessionCount: importedSessions.length,
1475
+ importedChunkCount,
1424
1476
  });
1425
1477
  } catch (e) {
1426
1478
  this.log.warn(`migrate/scan error: ${e}`);
@@ -1552,11 +1604,14 @@ export class ViewerServer {
1552
1604
  } else {
1553
1605
  this.broadcastSSE("done", { ok: true });
1554
1606
  }
1555
- for (const c of this.migrationSSEClients) {
1556
- try { c.end(); } catch { /* ignore */ }
1557
- }
1558
- this.migrationSSEClients = [];
1559
1607
  this.migrationAbort = false;
1608
+ const clientsToClose = [...this.migrationSSEClients];
1609
+ this.migrationSSEClients = [];
1610
+ setTimeout(() => {
1611
+ for (const c of clientsToClose) {
1612
+ try { c.end(); } catch { /* ignore */ }
1613
+ }
1614
+ }, 500);
1560
1615
  });
1561
1616
  });
1562
1617
  }
@@ -1987,7 +2042,7 @@ export class ViewerServer {
1987
2042
  res.on("close", () => { this.ppSSEClients = this.ppSSEClients.filter(c => c !== res); });
1988
2043
 
1989
2044
  this.ppAbort = false;
1990
- this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
2045
+ this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
1991
2046
 
1992
2047
  const send = (event: string, data: unknown) => {
1993
2048
  this.broadcastPPSSE(event, data);
@@ -2004,9 +2059,12 @@ export class ViewerServer {
2004
2059
  } else {
2005
2060
  this.broadcastPPSSE("done", { ...this.ppState });
2006
2061
  }
2007
- for (const c of this.ppSSEClients) { try { c.end(); } catch { /* */ } }
2008
- this.ppSSEClients = [];
2009
2062
  this.ppAbort = false;
2063
+ const ppClientsToClose = [...this.ppSSEClients];
2064
+ this.ppSSEClients = [];
2065
+ setTimeout(() => {
2066
+ for (const c of ppClientsToClose) { try { c.end(); } catch { /* */ } }
2067
+ }, 500);
2010
2068
  });
2011
2069
  });
2012
2070
  }
@@ -2038,7 +2096,13 @@ export class ViewerServer {
2038
2096
  }
2039
2097
 
2040
2098
  private handlePostprocessStatus(res: http.ServerResponse): void {
2041
- this.jsonResponse(res, this.ppState);
2099
+ let existingTasks = 0;
2100
+ let existingSkills = 0;
2101
+ try {
2102
+ existingTasks = (this.store as any).db.prepare("SELECT COUNT(*) as c FROM tasks").get()?.c ?? 0;
2103
+ existingSkills = this.store.countSkills("active");
2104
+ } catch { /* */ }
2105
+ this.jsonResponse(res, { ...this.ppState, existingTasks, existingSkills });
2042
2106
  }
2043
2107
 
2044
2108
  private broadcastPPSSE(event: string, data: unknown): void {
@@ -2088,12 +2152,18 @@ export class ViewerServer {
2088
2152
  }
2089
2153
 
2090
2154
  this.ppState.total = pendingItems.length;
2155
+ this.ppState.skippedSessions = skippedCount;
2156
+ this.ppState.totalSessions = importSessions.length;
2157
+ const existingTaskCount = (this.store as any).db.prepare("SELECT COUNT(*) as c FROM tasks WHERE session_key IN (" + importSessions.map(() => "?").join(",") + ")").get(...importSessions)?.c ?? 0;
2158
+ const existingSkillCount = this.store.countSkills("active");
2091
2159
  send("info", {
2092
2160
  totalSessions: importSessions.length,
2093
2161
  alreadyProcessed: skippedCount,
2094
2162
  pending: pendingItems.length,
2095
2163
  agents: Array.from(agentGroups.keys()),
2096
2164
  concurrency,
2165
+ existingTasks: existingTaskCount,
2166
+ existingSkills: existingSkillCount,
2097
2167
  });
2098
2168
  send("progress", { processed: 0, total: pendingItems.length });
2099
2169