@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.
- package/.env.example +13 -5
- package/README.md +283 -91
- package/dist/capture/index.d.ts +5 -7
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +72 -43
- package/dist/capture/index.js.map +1 -1
- 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 +16 -0
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +214 -1
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +16 -5
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +210 -6
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +16 -0
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +202 -1
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +31 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +134 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +24 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +255 -1
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +65 -0
- package/dist/ingest/task-processor.d.ts.map +1 -0
- package/dist/ingest/task-processor.js +354 -0
- package/dist/ingest/task-processor.js.map +1 -0
- package/dist/ingest/worker.d.ts +3 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +131 -23
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts +1 -0
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +22 -11
- package/dist/recall/engine.js.map +1 -1
- package/dist/recall/mmr.d.ts.map +1 -1
- package/dist/recall/mmr.js +3 -1
- package/dist/recall/mmr.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 +141 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +664 -7
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -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 +2391 -159
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +16 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +346 -3
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +572 -89
- package/openclaw.plugin.json +20 -45
- package/package.json +3 -4
- package/skill/memos-memory-guide/SKILL.md +86 -0
- package/src/capture/index.ts +85 -45
- package/src/ingest/dedup.ts +29 -0
- package/src/ingest/providers/anthropic.ts +258 -1
- package/src/ingest/providers/bedrock.ts +256 -6
- package/src/ingest/providers/gemini.ts +252 -1
- package/src/ingest/providers/index.ts +156 -8
- package/src/ingest/providers/openai.ts +304 -1
- package/src/ingest/task-processor.ts +396 -0
- package/src/ingest/worker.ts +145 -34
- package/src/recall/engine.ts +23 -12
- package/src/recall/mmr.ts +3 -1
- 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 +802 -7
- package/src/types.ts +96 -0
- package/src/viewer/html.ts +2391 -159
- package/src/viewer/server.ts +346 -3
- package/SKILL.md +0 -43
- package/www/index.html +0 -632
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");
|
|
@@ -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")) ||
|
|
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
|
-
|
|
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.
|