@memtensor/memos-local-openclaw-plugin 0.1.4 → 0.1.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 (104) hide show
  1. package/README.md +196 -84
  2. package/dist/ingest/dedup.d.ts +8 -0
  3. package/dist/ingest/dedup.d.ts.map +1 -1
  4. package/dist/ingest/dedup.js +21 -0
  5. package/dist/ingest/dedup.js.map +1 -1
  6. package/dist/ingest/providers/anthropic.d.ts +14 -0
  7. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  8. package/dist/ingest/providers/anthropic.js +104 -0
  9. package/dist/ingest/providers/anthropic.js.map +1 -1
  10. package/dist/ingest/providers/bedrock.d.ts +14 -0
  11. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  12. package/dist/ingest/providers/bedrock.js +100 -0
  13. package/dist/ingest/providers/bedrock.js.map +1 -1
  14. package/dist/ingest/providers/gemini.d.ts +14 -0
  15. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  16. package/dist/ingest/providers/gemini.js +96 -0
  17. package/dist/ingest/providers/gemini.js.map +1 -1
  18. package/dist/ingest/providers/index.d.ts +22 -0
  19. package/dist/ingest/providers/index.d.ts.map +1 -1
  20. package/dist/ingest/providers/index.js +68 -0
  21. package/dist/ingest/providers/index.js.map +1 -1
  22. package/dist/ingest/providers/openai.d.ts +22 -0
  23. package/dist/ingest/providers/openai.d.ts.map +1 -1
  24. package/dist/ingest/providers/openai.js +143 -0
  25. package/dist/ingest/providers/openai.js.map +1 -1
  26. package/dist/ingest/task-processor.d.ts +2 -0
  27. package/dist/ingest/task-processor.d.ts.map +1 -1
  28. package/dist/ingest/task-processor.js +15 -0
  29. package/dist/ingest/task-processor.js.map +1 -1
  30. package/dist/ingest/worker.d.ts +2 -0
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +115 -12
  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 +1 -0
  36. package/dist/recall/engine.js.map +1 -1
  37. package/dist/skill/bundled-memory-guide.d.ts +6 -0
  38. package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
  39. package/dist/skill/bundled-memory-guide.js +95 -0
  40. package/dist/skill/bundled-memory-guide.js.map +1 -0
  41. package/dist/skill/evaluator.d.ts +31 -0
  42. package/dist/skill/evaluator.d.ts.map +1 -0
  43. package/dist/skill/evaluator.js +194 -0
  44. package/dist/skill/evaluator.js.map +1 -0
  45. package/dist/skill/evolver.d.ts +22 -0
  46. package/dist/skill/evolver.d.ts.map +1 -0
  47. package/dist/skill/evolver.js +193 -0
  48. package/dist/skill/evolver.js.map +1 -0
  49. package/dist/skill/generator.d.ts +25 -0
  50. package/dist/skill/generator.d.ts.map +1 -0
  51. package/dist/skill/generator.js +477 -0
  52. package/dist/skill/generator.js.map +1 -0
  53. package/dist/skill/installer.d.ts +16 -0
  54. package/dist/skill/installer.d.ts.map +1 -0
  55. package/dist/skill/installer.js +89 -0
  56. package/dist/skill/installer.js.map +1 -0
  57. package/dist/skill/upgrader.d.ts +19 -0
  58. package/dist/skill/upgrader.d.ts.map +1 -0
  59. package/dist/skill/upgrader.js +263 -0
  60. package/dist/skill/upgrader.js.map +1 -0
  61. package/dist/skill/validator.d.ts +29 -0
  62. package/dist/skill/validator.d.ts.map +1 -0
  63. package/dist/skill/validator.js +227 -0
  64. package/dist/skill/validator.js.map +1 -0
  65. package/dist/storage/sqlite.d.ts +75 -1
  66. package/dist/storage/sqlite.d.ts.map +1 -1
  67. package/dist/storage/sqlite.js +417 -6
  68. package/dist/storage/sqlite.js.map +1 -1
  69. package/dist/types.d.ts +78 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js +6 -0
  72. package/dist/types.js.map +1 -1
  73. package/dist/viewer/html.d.ts +1 -1
  74. package/dist/viewer/html.d.ts.map +1 -1
  75. package/dist/viewer/html.js +1549 -113
  76. package/dist/viewer/html.js.map +1 -1
  77. package/dist/viewer/server.d.ts +13 -0
  78. package/dist/viewer/server.d.ts.map +1 -1
  79. package/dist/viewer/server.js +289 -4
  80. package/dist/viewer/server.js.map +1 -1
  81. package/index.ts +489 -181
  82. package/package.json +1 -1
  83. package/skill/memos-memory-guide/SKILL.md +86 -0
  84. package/src/ingest/dedup.ts +29 -0
  85. package/src/ingest/providers/anthropic.ts +130 -0
  86. package/src/ingest/providers/bedrock.ts +126 -0
  87. package/src/ingest/providers/gemini.ts +124 -0
  88. package/src/ingest/providers/index.ts +86 -4
  89. package/src/ingest/providers/openai.ts +174 -0
  90. package/src/ingest/task-processor.ts +16 -0
  91. package/src/ingest/worker.ts +126 -21
  92. package/src/recall/engine.ts +1 -0
  93. package/src/skill/bundled-memory-guide.ts +91 -0
  94. package/src/skill/evaluator.ts +220 -0
  95. package/src/skill/evolver.ts +169 -0
  96. package/src/skill/generator.ts +506 -0
  97. package/src/skill/installer.ts +59 -0
  98. package/src/skill/upgrader.ts +257 -0
  99. package/src/skill/validator.ts +227 -0
  100. package/src/storage/sqlite.ts +508 -6
  101. package/src/types.ts +77 -0
  102. package/src/viewer/html.ts +1549 -113
  103. package/src/viewer/server.ts +285 -4
  104. package/skill/SKILL.md +0 -59
@@ -1,5 +1,6 @@
1
1
  import http from "node:http";
2
2
  import crypto from "node:crypto";
3
+ import { execSync } from "node:child_process";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import type { SqliteStore } from "../storage/sqlite";
@@ -27,6 +28,7 @@ export class ViewerServer {
27
28
  private readonly embedder: Embedder;
28
29
  private readonly port: number;
29
30
  private readonly log: Logger;
31
+ private readonly dataDir: string;
30
32
  private readonly authFile: string;
31
33
  private readonly auth: AuthState;
32
34
 
@@ -38,6 +40,7 @@ export class ViewerServer {
38
40
  this.embedder = opts.embedder;
39
41
  this.port = opts.port;
40
42
  this.log = opts.log;
43
+ this.dataDir = opts.dataDir;
41
44
  this.authFile = path.join(opts.dataDir, "viewer-auth.json");
42
45
  this.auth = { passwordHash: null, sessions: new Map() };
43
46
  this.resetToken = crypto.randomBytes(16).toString("hex");
@@ -156,14 +159,24 @@ export class ViewerServer {
156
159
  if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
157
160
  else if (p === "/api/stats") this.serveStats(res);
158
161
  else if (p === "/api/metrics") this.serveMetrics(res, url);
162
+ else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
159
163
  else if (p === "/api/search") this.serveSearch(req, res, url);
160
164
  else if (p === "/api/tasks" && req.method === "GET") this.serveTasks(res, url);
161
165
  else if (p.startsWith("/api/task/") && req.method === "GET") this.serveTaskDetail(res, p);
166
+ else if (p === "/api/skills" && req.method === "GET") this.serveSkills(res, url);
167
+ else if (p.match(/^\/api\/skill\/[^/]+\/download$/) && req.method === "GET") this.serveSkillDownload(res, p);
168
+ else if (p.match(/^\/api\/skill\/[^/]+\/files$/) && req.method === "GET") this.serveSkillFiles(res, p);
169
+ else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
162
170
  else if (p === "/api/memory" && req.method === "POST") this.handleCreate(req, res);
171
+ else if (p.startsWith("/api/memory/") && req.method === "GET") this.serveMemoryDetail(res, p);
163
172
  else if (p.startsWith("/api/memory/") && req.method === "PUT") this.handleUpdate(req, res, p);
164
173
  else if (p.startsWith("/api/memory/") && req.method === "DELETE") this.handleDelete(res, p);
165
174
  else if (p === "/api/session" && req.method === "DELETE") this.handleDeleteSession(res, url);
166
175
  else if (p === "/api/memories" && req.method === "DELETE") this.handleDeleteAll(res);
176
+ else if (p === "/api/logs" && req.method === "GET") this.serveLogs(res, url);
177
+ else if (p === "/api/log-tools" && req.method === "GET") this.serveLogTools(res);
178
+ else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
179
+ else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
167
180
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
168
181
  else {
169
182
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -275,14 +288,14 @@ export class ViewerServer {
275
288
  // ─── Pages ───
276
289
 
277
290
  private serveViewer(res: http.ServerResponse): void {
278
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
291
+ 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" });
279
292
  res.end(viewerHTML);
280
293
  }
281
294
 
282
295
  // ─── Data APIs ───
283
296
 
284
297
  private serveMemories(res: http.ServerResponse, url: URL): void {
285
- const limit = Math.min(Number(url.searchParams.get("limit")) || 30, 200);
298
+ const limit = Math.min(Number(url.searchParams.get("limit")) || 40, 200);
286
299
  const page = Math.max(1, Number(url.searchParams.get("page")) || 1);
287
300
  const offset = (page - 1) * limit;
288
301
  const session = url.searchParams.get("session") ?? undefined;
@@ -318,6 +331,12 @@ export class ViewerServer {
318
331
  this.jsonResponse(res, data);
319
332
  }
320
333
 
334
+ private serveToolMetrics(res: http.ServerResponse, url: URL): void {
335
+ const minutes = Math.min(1440, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
336
+ const data = this.store.getToolMetrics(minutes);
337
+ this.jsonResponse(res, data);
338
+ }
339
+
321
340
  private serveTasks(res: http.ServerResponse, url: URL): void {
322
341
  this.store.recordViewerEvent("tasks_list");
323
342
  const status = url.searchParams.get("status") ?? undefined;
@@ -357,6 +376,20 @@ export class ViewerServer {
357
376
  createdAt: c.createdAt,
358
377
  }));
359
378
 
379
+ const relatedSkills = this.store.getSkillsByTask(taskId);
380
+ const skillLinks = relatedSkills.map((rs) => ({
381
+ skillId: rs.skill.id,
382
+ skillName: rs.skill.name,
383
+ relation: rs.relation,
384
+ versionAt: rs.versionAt,
385
+ status: rs.skill.status,
386
+ qualityScore: rs.skill.qualityScore,
387
+ }));
388
+
389
+ const db = (this.store as any).db;
390
+ const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
391
+ { skill_status: string | null; skill_reason: string | null } | undefined;
392
+
360
393
  this.jsonResponse(res, {
361
394
  id: task.id,
362
395
  sessionKey: task.sessionKey,
@@ -366,6 +399,9 @@ export class ViewerServer {
366
399
  startedAt: task.startedAt,
367
400
  endedAt: task.endedAt,
368
401
  chunks: chunkItems,
402
+ skillStatus: meta?.skill_status ?? null,
403
+ skillReason: meta?.skill_reason ?? null,
404
+ skillLinks,
369
405
  });
370
406
  }
371
407
 
@@ -381,11 +417,22 @@ export class ViewerServer {
381
417
  "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",
382
418
  ).all() as any[];
383
419
 
420
+ let skillCount = 0;
421
+ try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
422
+
423
+ let dedupBreakdown: Record<string, number> = {};
424
+ try {
425
+ const dedupRows = db.prepare("SELECT dedup_status, COUNT(*) as count FROM chunks GROUP BY dedup_status").all() as any[];
426
+ dedupBreakdown = Object.fromEntries(dedupRows.map((d: any) => [d.dedup_status ?? "active", d.count]));
427
+ } catch { /* column may not exist yet */ }
428
+
384
429
  this.jsonResponse(res, {
385
430
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embeddings.count,
431
+ totalSkills: skillCount,
386
432
  embeddingProvider: this.embedder.provider,
387
433
  roleBreakdown: Object.fromEntries(roles.map((r: any) => [r.role, r.count])),
388
434
  kindBreakdown: Object.fromEntries(kinds.map((k: any) => [k.kind, k.count])),
435
+ dedupBreakdown,
389
436
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
390
437
  sessions: sessionList,
391
438
  });
@@ -456,6 +503,136 @@ export class ViewerServer {
456
503
  });
457
504
  }
458
505
 
506
+ // ─── Skills API ───
507
+
508
+ private serveSkills(res: http.ServerResponse, url: URL): void {
509
+ const status = url.searchParams.get("status") ?? undefined;
510
+ const skills = this.store.listSkills({ status });
511
+ this.jsonResponse(res, { skills });
512
+ }
513
+
514
+ private serveSkillDetail(res: http.ServerResponse, urlPath: string): void {
515
+ const skillId = urlPath.replace("/api/skill/", "");
516
+ const skill = this.store.getSkill(skillId);
517
+ if (!skill) {
518
+ res.writeHead(404, { "Content-Type": "application/json" });
519
+ res.end(JSON.stringify({ error: "Skill not found" }));
520
+ return;
521
+ }
522
+
523
+ const versions = this.store.getSkillVersions(skillId);
524
+ const relatedTasks = this.store.getTasksBySkill(skillId);
525
+ const files = fs.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
526
+
527
+ this.jsonResponse(res, {
528
+ skill,
529
+ versions: versions.map(v => ({
530
+ id: v.id,
531
+ version: v.version,
532
+ content: v.content,
533
+ changelog: v.changelog,
534
+ changeSummary: v.changeSummary,
535
+ upgradeType: v.upgradeType,
536
+ sourceTaskId: v.sourceTaskId,
537
+ metrics: v.metrics,
538
+ qualityScore: v.qualityScore,
539
+ createdAt: v.createdAt,
540
+ })),
541
+ relatedTasks: relatedTasks.map(rt => ({
542
+ task: {
543
+ id: rt.task.id,
544
+ title: rt.task.title,
545
+ status: rt.task.status,
546
+ startedAt: rt.task.startedAt,
547
+ },
548
+ relation: rt.relation,
549
+ })),
550
+ files,
551
+ });
552
+ }
553
+
554
+ private serveSkillFiles(res: http.ServerResponse, urlPath: string): void {
555
+ const skillId = urlPath.replace("/api/skill/", "").replace("/files", "");
556
+ const skill = this.store.getSkill(skillId);
557
+ if (!skill) {
558
+ res.writeHead(404, { "Content-Type": "application/json" });
559
+ res.end(JSON.stringify({ error: "Skill not found" }));
560
+ return;
561
+ }
562
+
563
+ if (!fs.existsSync(skill.dirPath)) {
564
+ this.jsonResponse(res, { files: [], error: "Skill directory not found" });
565
+ return;
566
+ }
567
+
568
+ const files = this.walkDir(skill.dirPath, skill.dirPath);
569
+ this.jsonResponse(res, { files });
570
+ }
571
+
572
+ private walkDir(dir: string, root: string): Array<{ path: string; type: string; size: number }> {
573
+ const results: Array<{ path: string; type: string; size: number }> = [];
574
+ try {
575
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
576
+ for (const entry of entries) {
577
+ const fullPath = path.join(dir, entry.name);
578
+ const relPath = path.relative(root, fullPath);
579
+ if (entry.isDirectory()) {
580
+ results.push(...this.walkDir(fullPath, root));
581
+ } else {
582
+ const stat = fs.statSync(fullPath);
583
+ const ext = path.extname(entry.name).toLowerCase();
584
+ let type = "file";
585
+ if (entry.name === "SKILL.md") type = "skill";
586
+ else if ([".sh", ".py", ".ts", ".js"].includes(ext)) type = "script";
587
+ else if ([".md", ".txt", ".json"].includes(ext)) type = "reference";
588
+ results.push({ path: relPath, type, size: stat.size });
589
+ }
590
+ }
591
+ } catch { /* directory may not exist */ }
592
+ return results;
593
+ }
594
+
595
+ private serveSkillDownload(res: http.ServerResponse, urlPath: string): void {
596
+ const skillId = urlPath.replace("/api/skill/", "").replace("/download", "");
597
+ const skill = this.store.getSkill(skillId);
598
+ if (!skill) {
599
+ res.writeHead(404, { "Content-Type": "application/json" });
600
+ res.end(JSON.stringify({ error: "Skill not found" }));
601
+ return;
602
+ }
603
+
604
+ if (!fs.existsSync(skill.dirPath)) {
605
+ res.writeHead(404, { "Content-Type": "application/json" });
606
+ res.end(JSON.stringify({ error: "Skill directory not found" }));
607
+ return;
608
+ }
609
+
610
+ const zipName = `${skill.name}-v${skill.version}.zip`;
611
+ const tmpPath = path.join(require("os").tmpdir(), zipName);
612
+
613
+ try {
614
+ try { fs.unlinkSync(tmpPath); } catch { /* no-op */ }
615
+ execSync(
616
+ `cd "${path.dirname(skill.dirPath)}" && zip -r "${tmpPath}" "${path.basename(skill.dirPath)}"`,
617
+ { timeout: 15_000 },
618
+ );
619
+
620
+ const data = fs.readFileSync(tmpPath);
621
+ res.writeHead(200, {
622
+ "Content-Type": "application/zip",
623
+ "Content-Disposition": `attachment; filename="${zipName}"`,
624
+ "Content-Length": String(data.length),
625
+ });
626
+ res.end(data);
627
+
628
+ try { fs.unlinkSync(tmpPath); } catch { /* cleanup */ }
629
+ } catch (err) {
630
+ this.log.error(`Skill download zip failed: ${err}`);
631
+ res.writeHead(500, { "Content-Type": "application/json" });
632
+ res.end(JSON.stringify({ error: `Failed to create zip: ${err}` }));
633
+ }
634
+ }
635
+
459
636
  // ─── CRUD ───
460
637
 
461
638
  private handleCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
@@ -469,7 +646,9 @@ export class ViewerServer {
469
646
  id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
470
647
  role: data.role || "user", content: data.content || "", kind: data.kind || "paragraph",
471
648
  summary: data.summary || data.content?.slice(0, 100) || "",
472
- taskId: null, createdAt: now, updatedAt: now, embedding: null,
649
+ taskId: null, skillId: null, dedupStatus: "active", dedupTarget: null, dedupReason: null,
650
+ mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
651
+ createdAt: now, updatedAt: now, embedding: null,
473
652
  });
474
653
  this.jsonResponse(res, { ok: true, id, message: "Memory created" });
475
654
  } catch (err) {
@@ -479,6 +658,17 @@ export class ViewerServer {
479
658
  });
480
659
  }
481
660
 
661
+ private serveMemoryDetail(res: http.ServerResponse, urlPath: string): void {
662
+ const chunkId = urlPath.replace("/api/memory/", "");
663
+ const chunk = this.store.getChunk(chunkId);
664
+ if (!chunk) {
665
+ res.writeHead(404, { "Content-Type": "application/json" });
666
+ res.end(JSON.stringify({ error: "Not found" }));
667
+ return;
668
+ }
669
+ this.jsonResponse(res, { memory: chunk });
670
+ }
671
+
482
672
  private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
483
673
  const chunkId = urlPath.replace("/api/memory/", "");
484
674
  this.readBody(req, (body) => {
@@ -508,11 +698,102 @@ export class ViewerServer {
508
698
  }
509
699
 
510
700
  private handleDeleteAll(res: http.ServerResponse): void {
511
- this.jsonResponse(res, { ok: true, deleted: this.store.deleteAll() });
701
+ const result = this.store.deleteAll();
702
+ // Clean up skills-store directory
703
+ const skillsStoreDir = path.join(this.dataDir, "skills-store");
704
+ try {
705
+ if (fs.existsSync(skillsStoreDir)) {
706
+ fs.rmSync(skillsStoreDir, { recursive: true });
707
+ fs.mkdirSync(skillsStoreDir, { recursive: true });
708
+ this.log.info("Cleared skills-store directory");
709
+ }
710
+ } catch (err) {
711
+ this.log.warn(`Failed to clear skills-store: ${err}`);
712
+ }
713
+ this.jsonResponse(res, { ok: true, deleted: result });
512
714
  }
513
715
 
514
716
  // ─── Helpers ───
515
717
 
718
+ // ─── Config API ───
719
+
720
+ private getOpenClawConfigPath(): string {
721
+ const home = process.env.HOME || process.env.USERPROFILE || "";
722
+ return path.join(home, ".openclaw", "openclaw.json");
723
+ }
724
+
725
+ private serveConfig(res: http.ServerResponse): void {
726
+ try {
727
+ const cfgPath = this.getOpenClawConfigPath();
728
+ if (!fs.existsSync(cfgPath)) {
729
+ this.jsonResponse(res, {});
730
+ return;
731
+ }
732
+ const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
733
+ const pluginEntry = raw?.plugins?.entries?.["memos-local"]?.config ?? {};
734
+ const result: Record<string, unknown> = { ...pluginEntry };
735
+ if (raw?.plugins?.entries?.["memos-local"]?.config?.viewerPort == null) {
736
+ const topLevel = raw?.plugins?.entries?.["memos-local"] ?? {};
737
+ if (topLevel.viewerPort) result.viewerPort = topLevel.viewerPort;
738
+ }
739
+ this.jsonResponse(res, result);
740
+ } catch (e) {
741
+ this.log.warn(`serveConfig error: ${e}`);
742
+ this.jsonResponse(res, {});
743
+ }
744
+ }
745
+
746
+ private handleSaveConfig(req: http.IncomingMessage, res: http.ServerResponse): void {
747
+ this.readBody(req, (body) => {
748
+ try {
749
+ const newCfg = JSON.parse(body);
750
+ const cfgPath = this.getOpenClawConfigPath();
751
+ let raw: Record<string, unknown> = {};
752
+ if (fs.existsSync(cfgPath)) {
753
+ raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
754
+ }
755
+
756
+ if (!raw.plugins) raw.plugins = {};
757
+ const plugins = raw.plugins as Record<string, unknown>;
758
+ if (!plugins.entries) plugins.entries = {};
759
+ const entries = plugins.entries as Record<string, unknown>;
760
+ if (!entries["memos-local"]) entries["memos-local"] = { enabled: true };
761
+ const entry = entries["memos-local"] as Record<string, unknown>;
762
+ if (!entry.config) entry.config = {};
763
+ const config = entry.config as Record<string, unknown>;
764
+
765
+ if (newCfg.embedding) config.embedding = newCfg.embedding;
766
+ if (newCfg.summarizer) config.summarizer = newCfg.summarizer;
767
+ if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
768
+ if (newCfg.viewerPort) config.viewerPort = newCfg.viewerPort;
769
+
770
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
771
+ fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
772
+ this.log.info("Plugin config updated via Viewer");
773
+ this.jsonResponse(res, { ok: true });
774
+ } catch (e) {
775
+ this.log.warn(`handleSaveConfig error: ${e}`);
776
+ res.writeHead(500, { "Content-Type": "application/json" });
777
+ res.end(JSON.stringify({ error: String(e) }));
778
+ }
779
+ });
780
+ }
781
+
782
+ private serveLogs(res: http.ServerResponse, url: URL): void {
783
+ const limit = Math.min(Number(url.searchParams.get("limit") ?? 20), 200);
784
+ const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0));
785
+ const tool = url.searchParams.get("tool") || undefined;
786
+ const { logs, total } = this.store.getApiLogs(limit, offset, tool);
787
+ const page = Math.floor(offset / limit) + 1;
788
+ const totalPages = Math.ceil(total / limit);
789
+ this.jsonResponse(res, { logs, total, page, totalPages, limit, offset });
790
+ }
791
+
792
+ private serveLogTools(res: http.ServerResponse): void {
793
+ const tools = this.store.getApiLogToolNames();
794
+ this.jsonResponse(res, { tools });
795
+ }
796
+
516
797
  private readBody(req: http.IncomingMessage, cb: (body: string) => void): void {
517
798
  let body = "";
518
799
  req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
package/skill/SKILL.md DELETED
@@ -1,59 +0,0 @@
1
- ---
2
- name: memos-local
3
- description: "Local long-term conversation memory (MemOS). All past conversations are recorded and searchable. Use this skill whenever the conversation involves user-specific information such as identity, preferences, project details, past decisions, or personal facts. Also use it when you cannot fully answer a question or would otherwise need to ask the user for more details — always search memory first, because the answer may already exist in a previous conversation. Similarly, when the user references something from the past (e.g. 'last time', 'as before', 'continue'), search memory to retrieve the relevant context. Start with memory_search for lightweight hits, then drill down with memory_get, task_summary, or memory_timeline as needed."
4
- metadata: { "openclaw": { "emoji": "🧠" } }
5
- ---
6
-
7
- # MemOS — Long-Term Memory (Local)
8
-
9
- You have a local long-term memory that records conversation history. When you need historical information, use the tools below to search and drill down, aiming to get **enough to answer** with the **shortest context**.
10
-
11
- ## Tools
12
-
13
- | Tool | What it does |
14
- | ----------------- | ------------------------------------------------------------------------ |
15
- | `memory_search` | Lightweight search of conversation history. Returns hit summaries + IDs/refs (information may be truncated/compressed). |
16
- | `memory_get` | Fetch the **full original text** of a hit by `chunkId` (use when the summary is insufficient / the original is longer but you only need this one entry). |
17
- | `task_summary` | Get the **full task-level context summary** for the task a hit belongs to by `taskId` (use when you judge that more key information may be in other turns of the same task). |
18
- | `memory_timeline` | Expand the context before and after a hit using its `ref` (use when you need the cause-and-effect / chronological details of a conversation). |
19
- | `memory_viewer` | Returns the Memory Viewer URL (http://127.0.0.1:18799). |
20
-
21
- ## When to Trigger a Search (Trigger)
22
-
23
- Trigger a search when any of the following applies:
24
-
25
- * The current context is insufficient to answer definitively (missing key parameters/links/paths/versions/decisions, etc.)
26
- * You are about to ask the user for information (try searching memory first)
27
- * The user references history ("last time / before / continue / as previously planned")
28
- * You need user-specific information (preferences, identity, project config, directory structure, deployment method, etc.)
29
-
30
- ## Layered Retrieval Strategy (Minimum Information → Progressive Completion)
31
-
32
- ```text
33
- Step 1: memory_search(query="keywords")
34
- - Goal: quickly get 1-6 most relevant hits (summary + chunkId/taskId/ref)
35
-
36
- Step 2: Sufficiency check
37
- - If hit summaries are enough to support the answer → answer directly, stop drilling down
38
- - If summary is insufficient/truncated but "looks useful" → memory_get(chunkId)
39
-
40
- Step 3: Related but missing broader context → task_summary(taskId)
41
- - Applies when: you judge that missing information may be in other turns of the same task
42
- - Note: conversations are split into tasks by topic/time; each hit typically belongs to a taskId
43
- - Example: search hits "steps to apply for GPT key", but the summary lacks the website link/prerequisites
44
- → calling task_summary retrieves the more complete context and details for that task
45
-
46
- Step 4: Still need cause-and-effect / chronological details → memory_timeline(ref)
47
- - Applies when: you need to expand several turns before and after a hit to fill in details and context
48
-
49
- Step 5: Still insufficient
50
- - Identify the specific missing fields and ask the user a minimal follow-up question
51
- ```
52
-
53
- ## Rules
54
-
55
- * **If you don't know the answer, or you are about to ask the user for clarification/details, you MUST call `memory_search` first.** The user may have already provided this information in a previous conversation. Never say "I don't know" or ask the user without searching memory first.
56
- * Always `memory_search` first, then decide whether to use `memory_get` / `task_summary` / `memory_timeline`
57
- * `memory_get` is for "summary isn't enough but this hit is very likely the answer" — avoid pulling in excessive irrelevant context
58
- * `task_summary` is for "hit is relevant, but the answer may be scattered across other parts of the same task" — use the task-level summary to fill in at once
59
- * `memory_timeline` should only be used when you genuinely need surrounding context — avoid unnecessarily expanding the context window