@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.
- package/README.md +196 -84
- package/dist/ingest/dedup.d.ts +8 -0
- package/dist/ingest/dedup.d.ts.map +1 -1
- package/dist/ingest/dedup.js +21 -0
- package/dist/ingest/dedup.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts +14 -0
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +104 -0
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +14 -0
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +100 -0
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +14 -0
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +96 -0
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +22 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +68 -0
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +22 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +143 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +2 -0
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +15 -0
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts +2 -0
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +115 -12
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +1 -0
- package/dist/recall/engine.js.map +1 -1
- package/dist/skill/bundled-memory-guide.d.ts +6 -0
- package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
- package/dist/skill/bundled-memory-guide.js +95 -0
- package/dist/skill/bundled-memory-guide.js.map +1 -0
- package/dist/skill/evaluator.d.ts +31 -0
- package/dist/skill/evaluator.d.ts.map +1 -0
- package/dist/skill/evaluator.js +194 -0
- package/dist/skill/evaluator.js.map +1 -0
- package/dist/skill/evolver.d.ts +22 -0
- package/dist/skill/evolver.d.ts.map +1 -0
- package/dist/skill/evolver.js +193 -0
- package/dist/skill/evolver.js.map +1 -0
- package/dist/skill/generator.d.ts +25 -0
- package/dist/skill/generator.d.ts.map +1 -0
- package/dist/skill/generator.js +477 -0
- package/dist/skill/generator.js.map +1 -0
- package/dist/skill/installer.d.ts +16 -0
- package/dist/skill/installer.d.ts.map +1 -0
- package/dist/skill/installer.js +89 -0
- package/dist/skill/installer.js.map +1 -0
- package/dist/skill/upgrader.d.ts +19 -0
- package/dist/skill/upgrader.d.ts.map +1 -0
- package/dist/skill/upgrader.js +263 -0
- package/dist/skill/upgrader.js.map +1 -0
- package/dist/skill/validator.d.ts +29 -0
- package/dist/skill/validator.d.ts.map +1 -0
- package/dist/skill/validator.js +227 -0
- package/dist/skill/validator.js.map +1 -0
- package/dist/storage/sqlite.d.ts +75 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +417 -6
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +1549 -113
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +13 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +289 -4
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +489 -181
- package/package.json +1 -1
- package/skill/memos-memory-guide/SKILL.md +86 -0
- package/src/ingest/dedup.ts +29 -0
- package/src/ingest/providers/anthropic.ts +130 -0
- package/src/ingest/providers/bedrock.ts +126 -0
- package/src/ingest/providers/gemini.ts +124 -0
- package/src/ingest/providers/index.ts +86 -4
- package/src/ingest/providers/openai.ts +174 -0
- package/src/ingest/task-processor.ts +16 -0
- package/src/ingest/worker.ts +126 -21
- package/src/recall/engine.ts +1 -0
- package/src/skill/bundled-memory-guide.ts +91 -0
- package/src/skill/evaluator.ts +220 -0
- package/src/skill/evolver.ts +169 -0
- package/src/skill/generator.ts +506 -0
- package/src/skill/installer.ts +59 -0
- package/src/skill/upgrader.ts +257 -0
- package/src/skill/validator.ts +227 -0
- package/src/storage/sqlite.ts +508 -6
- package/src/types.ts +77 -0
- package/src/viewer/html.ts +1549 -113
- package/src/viewer/server.ts +285 -4
- package/skill/SKILL.md +0 -59
package/src/viewer/server.ts
CHANGED
|
@@ -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")) ||
|
|
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,
|
|
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
|
-
|
|
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
|