@memtensor/memos-local-openclaw-plugin 0.1.3 → 0.1.5

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 (117) hide show
  1. package/.env.example +13 -5
  2. package/README.md +283 -91
  3. package/dist/capture/index.d.ts +5 -7
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +72 -43
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/ingest/dedup.d.ts +8 -0
  8. package/dist/ingest/dedup.d.ts.map +1 -1
  9. package/dist/ingest/dedup.js +21 -0
  10. package/dist/ingest/dedup.js.map +1 -1
  11. package/dist/ingest/providers/anthropic.d.ts +16 -0
  12. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  13. package/dist/ingest/providers/anthropic.js +214 -1
  14. package/dist/ingest/providers/anthropic.js.map +1 -1
  15. package/dist/ingest/providers/bedrock.d.ts +16 -5
  16. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  17. package/dist/ingest/providers/bedrock.js +210 -6
  18. package/dist/ingest/providers/bedrock.js.map +1 -1
  19. package/dist/ingest/providers/gemini.d.ts +16 -0
  20. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  21. package/dist/ingest/providers/gemini.js +202 -1
  22. package/dist/ingest/providers/gemini.js.map +1 -1
  23. package/dist/ingest/providers/index.d.ts +31 -0
  24. package/dist/ingest/providers/index.d.ts.map +1 -1
  25. package/dist/ingest/providers/index.js +134 -4
  26. package/dist/ingest/providers/index.js.map +1 -1
  27. package/dist/ingest/providers/openai.d.ts +24 -0
  28. package/dist/ingest/providers/openai.d.ts.map +1 -1
  29. package/dist/ingest/providers/openai.js +255 -1
  30. package/dist/ingest/providers/openai.js.map +1 -1
  31. package/dist/ingest/task-processor.d.ts +65 -0
  32. package/dist/ingest/task-processor.d.ts.map +1 -0
  33. package/dist/ingest/task-processor.js +354 -0
  34. package/dist/ingest/task-processor.js.map +1 -0
  35. package/dist/ingest/worker.d.ts +3 -1
  36. package/dist/ingest/worker.d.ts.map +1 -1
  37. package/dist/ingest/worker.js +131 -23
  38. package/dist/ingest/worker.js.map +1 -1
  39. package/dist/recall/engine.d.ts +1 -0
  40. package/dist/recall/engine.d.ts.map +1 -1
  41. package/dist/recall/engine.js +22 -11
  42. package/dist/recall/engine.js.map +1 -1
  43. package/dist/recall/mmr.d.ts.map +1 -1
  44. package/dist/recall/mmr.js +3 -1
  45. package/dist/recall/mmr.js.map +1 -1
  46. package/dist/skill/bundled-memory-guide.d.ts +6 -0
  47. package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
  48. package/dist/skill/bundled-memory-guide.js +95 -0
  49. package/dist/skill/bundled-memory-guide.js.map +1 -0
  50. package/dist/skill/evaluator.d.ts +31 -0
  51. package/dist/skill/evaluator.d.ts.map +1 -0
  52. package/dist/skill/evaluator.js +194 -0
  53. package/dist/skill/evaluator.js.map +1 -0
  54. package/dist/skill/evolver.d.ts +22 -0
  55. package/dist/skill/evolver.d.ts.map +1 -0
  56. package/dist/skill/evolver.js +193 -0
  57. package/dist/skill/evolver.js.map +1 -0
  58. package/dist/skill/generator.d.ts +25 -0
  59. package/dist/skill/generator.d.ts.map +1 -0
  60. package/dist/skill/generator.js +477 -0
  61. package/dist/skill/generator.js.map +1 -0
  62. package/dist/skill/installer.d.ts +16 -0
  63. package/dist/skill/installer.d.ts.map +1 -0
  64. package/dist/skill/installer.js +89 -0
  65. package/dist/skill/installer.js.map +1 -0
  66. package/dist/skill/upgrader.d.ts +19 -0
  67. package/dist/skill/upgrader.d.ts.map +1 -0
  68. package/dist/skill/upgrader.js +263 -0
  69. package/dist/skill/upgrader.js.map +1 -0
  70. package/dist/skill/validator.d.ts +29 -0
  71. package/dist/skill/validator.d.ts.map +1 -0
  72. package/dist/skill/validator.js +227 -0
  73. package/dist/skill/validator.js.map +1 -0
  74. package/dist/storage/sqlite.d.ts +141 -1
  75. package/dist/storage/sqlite.d.ts.map +1 -1
  76. package/dist/storage/sqlite.js +664 -7
  77. package/dist/storage/sqlite.js.map +1 -1
  78. package/dist/types.d.ts +93 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js +8 -0
  81. package/dist/types.js.map +1 -1
  82. package/dist/viewer/html.d.ts +1 -1
  83. package/dist/viewer/html.d.ts.map +1 -1
  84. package/dist/viewer/html.js +2391 -159
  85. package/dist/viewer/html.js.map +1 -1
  86. package/dist/viewer/server.d.ts +16 -0
  87. package/dist/viewer/server.d.ts.map +1 -1
  88. package/dist/viewer/server.js +346 -3
  89. package/dist/viewer/server.js.map +1 -1
  90. package/index.ts +572 -89
  91. package/openclaw.plugin.json +20 -45
  92. package/package.json +3 -4
  93. package/skill/memos-memory-guide/SKILL.md +86 -0
  94. package/src/capture/index.ts +85 -45
  95. package/src/ingest/dedup.ts +29 -0
  96. package/src/ingest/providers/anthropic.ts +258 -1
  97. package/src/ingest/providers/bedrock.ts +256 -6
  98. package/src/ingest/providers/gemini.ts +252 -1
  99. package/src/ingest/providers/index.ts +156 -8
  100. package/src/ingest/providers/openai.ts +304 -1
  101. package/src/ingest/task-processor.ts +396 -0
  102. package/src/ingest/worker.ts +145 -34
  103. package/src/recall/engine.ts +23 -12
  104. package/src/recall/mmr.ts +3 -1
  105. package/src/skill/bundled-memory-guide.ts +91 -0
  106. package/src/skill/evaluator.ts +220 -0
  107. package/src/skill/evolver.ts +169 -0
  108. package/src/skill/generator.ts +506 -0
  109. package/src/skill/installer.ts +59 -0
  110. package/src/skill/upgrader.ts +257 -0
  111. package/src/skill/validator.ts +227 -0
  112. package/src/storage/sqlite.ts +802 -7
  113. package/src/types.ts +96 -0
  114. package/src/viewer/html.ts +2391 -159
  115. package/src/viewer/server.ts +346 -3
  116. package/SKILL.md +0 -43
  117. package/www/index.html +0 -632
@@ -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");
@@ -155,12 +158,25 @@ export class ViewerServer {
155
158
 
156
159
  if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
157
160
  else if (p === "/api/stats") this.serveStats(res);
161
+ else if (p === "/api/metrics") this.serveMetrics(res, url);
162
+ else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
158
163
  else if (p === "/api/search") this.serveSearch(req, res, url);
164
+ else if (p === "/api/tasks" && req.method === "GET") this.serveTasks(res, url);
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);
159
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);
160
172
  else if (p.startsWith("/api/memory/") && req.method === "PUT") this.handleUpdate(req, res, p);
161
173
  else if (p.startsWith("/api/memory/") && req.method === "DELETE") this.handleDelete(res, p);
162
174
  else if (p === "/api/session" && req.method === "DELETE") this.handleDeleteSession(res, url);
163
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);
164
180
  else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
165
181
  else {
166
182
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -272,14 +288,14 @@ export class ViewerServer {
272
288
  // ─── Pages ───
273
289
 
274
290
  private serveViewer(res: http.ServerResponse): void {
275
- 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" });
276
292
  res.end(viewerHTML);
277
293
  }
278
294
 
279
295
  // ─── Data APIs ───
280
296
 
281
297
  private serveMemories(res: http.ServerResponse, url: URL): void {
282
- const limit = Math.min(Number(url.searchParams.get("limit")) || 30, 200);
298
+ const limit = Math.min(Number(url.searchParams.get("limit")) || 40, 200);
283
299
  const page = Math.max(1, Number(url.searchParams.get("page")) || 1);
284
300
  const offset = (page - 1) * limit;
285
301
  const session = url.searchParams.get("session") ?? undefined;
@@ -302,12 +318,93 @@ export class ViewerServer {
302
318
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
303
319
  const memories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
304
320
 
321
+ this.store.recordViewerEvent("list");
305
322
  this.jsonResponse(res, {
306
323
  memories, page, limit, total: totalRow.count,
307
324
  totalPages: Math.ceil(totalRow.count / limit),
308
325
  });
309
326
  }
310
327
 
328
+ private serveMetrics(res: http.ServerResponse, url: URL): void {
329
+ const days = Math.min(90, Math.max(7, Number(url.searchParams.get("days")) || 30));
330
+ const data = this.store.getMetrics(days);
331
+ this.jsonResponse(res, data);
332
+ }
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
+
340
+ private serveTasks(res: http.ServerResponse, url: URL): void {
341
+ this.store.recordViewerEvent("tasks_list");
342
+ const status = url.searchParams.get("status") ?? undefined;
343
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
344
+ const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
345
+ const { tasks, total } = this.store.listTasks({ status, limit, offset });
346
+
347
+ const items = tasks.map((t) => ({
348
+ id: t.id,
349
+ sessionKey: t.sessionKey,
350
+ title: t.title,
351
+ summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
352
+ status: t.status,
353
+ startedAt: t.startedAt,
354
+ endedAt: t.endedAt,
355
+ chunkCount: this.store.countChunksByTask(t.id),
356
+ }));
357
+
358
+ this.jsonResponse(res, { tasks: items, total, limit, offset });
359
+ }
360
+
361
+ private serveTaskDetail(res: http.ServerResponse, urlPath: string): void {
362
+ const taskId = urlPath.replace("/api/task/", "");
363
+ const task = this.store.getTask(taskId);
364
+ if (!task) {
365
+ res.writeHead(404, { "Content-Type": "application/json" });
366
+ res.end(JSON.stringify({ error: "Task not found" }));
367
+ return;
368
+ }
369
+
370
+ const chunks = this.store.getChunksByTask(taskId);
371
+ const chunkItems = chunks.map((c) => ({
372
+ id: c.id,
373
+ role: c.role,
374
+ content: c.content.length > 500 ? c.content.slice(0, 497) + "..." : c.content,
375
+ summary: c.summary,
376
+ createdAt: c.createdAt,
377
+ }));
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
+
393
+ this.jsonResponse(res, {
394
+ id: task.id,
395
+ sessionKey: task.sessionKey,
396
+ title: task.title,
397
+ summary: task.summary,
398
+ status: task.status,
399
+ startedAt: task.startedAt,
400
+ endedAt: task.endedAt,
401
+ chunks: chunkItems,
402
+ skillStatus: meta?.skill_status ?? null,
403
+ skillReason: meta?.skill_reason ?? null,
404
+ skillLinks,
405
+ });
406
+ }
407
+
311
408
  private serveStats(res: http.ServerResponse): void {
312
409
  const db = (this.store as any).db;
313
410
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
@@ -320,11 +417,22 @@ export class ViewerServer {
320
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",
321
418
  ).all() as any[];
322
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
+
323
429
  this.jsonResponse(res, {
324
430
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embeddings.count,
431
+ totalSkills: skillCount,
325
432
  embeddingProvider: this.embedder.provider,
326
433
  roleBreakdown: Object.fromEntries(roles.map((r: any) => [r.role, r.count])),
327
434
  kindBreakdown: Object.fromEntries(kinds.map((k: any) => [k.kind, k.count])),
435
+ dedupBreakdown,
328
436
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
329
437
  sessions: sessionList,
330
438
  });
@@ -385,6 +493,7 @@ export class ViewerServer {
385
493
  if (!seenIds.has(r.id)) { seenIds.add(r.id); merged.push(r); }
386
494
  }
387
495
 
496
+ this.store.recordViewerEvent("search");
388
497
  this.jsonResponse(res, {
389
498
  results: merged,
390
499
  query: q,
@@ -394,6 +503,136 @@ export class ViewerServer {
394
503
  });
395
504
  }
396
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
+
397
636
  // ─── CRUD ───
398
637
 
399
638
  private handleCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
@@ -407,6 +646,8 @@ export class ViewerServer {
407
646
  id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
408
647
  role: data.role || "user", content: data.content || "", kind: data.kind || "paragraph",
409
648
  summary: data.summary || data.content?.slice(0, 100) || "",
649
+ taskId: null, skillId: null, dedupStatus: "active", dedupTarget: null, dedupReason: null,
650
+ mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
410
651
  createdAt: now, updatedAt: now, embedding: null,
411
652
  });
412
653
  this.jsonResponse(res, { ok: true, id, message: "Memory created" });
@@ -417,6 +658,17 @@ export class ViewerServer {
417
658
  });
418
659
  }
419
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
+
420
672
  private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
421
673
  const chunkId = urlPath.replace("/api/memory/", "");
422
674
  this.readBody(req, (body) => {
@@ -446,11 +698,102 @@ export class ViewerServer {
446
698
  }
447
699
 
448
700
  private handleDeleteAll(res: http.ServerResponse): void {
449
- 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 });
450
714
  }
451
715
 
452
716
  // ─── Helpers ───
453
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
+
454
797
  private readBody(req: http.IncomingMessage, cb: (body: string) => void): void {
455
798
  let body = "";
456
799
  req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
package/SKILL.md DELETED
@@ -1,43 +0,0 @@
1
- # Memory Recall Skill — memos-local
2
-
3
- You have access to long-term conversation memory through four tools:
4
- `memory_search`, `memory_timeline`, `memory_get`, `memory_viewer` (returns the web dashboard URL).
5
-
6
- ## When to Search
7
-
8
- - **DO search** when the user asks about a previous conversation, a specific detail (command, error, parameter, decision) you don't have in the current context, or when the topic clearly references past interactions.
9
- - **DO NOT search** when the current context already contains enough information to answer accurately.
10
-
11
- ## Progressive Recall Chain
12
-
13
- Follow this 3-step chain. Stop as soon as you have sufficient evidence.
14
-
15
- ### Step 1 — `memory_search`
16
-
17
- Start with the default call (no extra parameters → top 6 results, minScore 0.45).
18
-
19
- - If results are insufficient, **refine the query first** (add entity names, exact commands, error keywords).
20
- - If still insufficient, increase `maxResults` to 12, then 20.
21
- - As a last resort, lower `minScore` to 0.35.
22
- - **Never repeat the exact same query with the same parameters** — vary query wording or adjust parameters.
23
-
24
- ### Step 2 — `memory_timeline`
25
-
26
- When a search hit looks relevant but the `original_excerpt` is too short to confirm, call `memory_timeline` with the hit's `ref` to get surrounding context (±2 turns by default).
27
-
28
- ### Step 3 — `memory_get`
29
-
30
- When you need the exact original text (to verify a command, a code snippet, an error stack), call `memory_get` with the `ref` and request up to 2000 characters (max 8000).
31
-
32
- ## How to Answer
33
-
34
- 1. **Evidence-based**: Only state facts backed by `original_excerpt`, timeline entries, or `memory_get` content. Do not fabricate details.
35
- 2. **Cite sources**: When referencing a memory, mention the approximate time and context (e.g., "In our conversation about deploying the API…").
36
- 3. **Acknowledge gaps**: If memory search returns no relevant results, say so honestly rather than guessing.
37
-
38
- ## Anti-Patterns to Avoid
39
-
40
- - Searching on every single turn (only search when needed).
41
- - Repeating the same failed query without modification.
42
- - Ignoring `original_excerpt` and only using `summary` — the excerpt is the primary evidence.
43
- - Making claims based solely on `summary` without checking the original text when details matter.