@memtensor/memos-local-openclaw-plugin 1.0.7 → 1.0.8-beta.2

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.
@@ -315,6 +315,7 @@ export class ViewerServer {
315
315
  else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
316
316
  else if (p === "/api/search") this.serveSearch(req, res, url);
317
317
  else if (p === "/api/tasks" && req.method === "GET") this.serveTasks(res, url);
318
+ else if (p === "/api/task-search" && req.method === "GET") this.serveTaskSearch(res, url);
318
319
  else if (p.match(/^\/api\/task\/[^/]+\/retry-skill$/) && req.method === "POST") this.handleTaskRetrySkill(req, res, p);
319
320
  else if (p.startsWith("/api/task/") && req.method === "DELETE") this.handleTaskDelete(res, p);
320
321
  else if (p.startsWith("/api/task/") && req.method === "PUT") this.handleTaskUpdate(req, res, p);
@@ -323,6 +324,8 @@ export class ViewerServer {
323
324
  else if (p.match(/^\/api\/skill\/[^/]+\/download$/) && req.method === "GET") this.serveSkillDownload(res, p);
324
325
  else if (p.match(/^\/api\/skill\/[^/]+\/files$/) && req.method === "GET") this.serveSkillFiles(res, p);
325
326
  else if (p.match(/^\/api\/skill\/[^/]+\/visibility$/) && req.method === "PUT") this.handleSkillVisibility(req, res, p);
327
+ else if (p.match(/^\/api\/skill\/[^/]+\/disable$/) && req.method === "PUT") this.handleSkillDisable(res, p);
328
+ else if (p.match(/^\/api\/skill\/[^/]+\/enable$/) && req.method === "PUT") this.handleSkillEnable(res, p);
326
329
  else if (p.startsWith("/api/skill/") && req.method === "DELETE") this.handleSkillDelete(res, p);
327
330
  else if (p.startsWith("/api/skill/") && req.method === "PUT") this.handleSkillUpdate(req, res, p);
328
331
  else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
@@ -608,9 +611,10 @@ export class ViewerServer {
608
611
  this.store.recordViewerEvent("tasks_list");
609
612
  const status = url.searchParams.get("status") ?? undefined;
610
613
  const owner = url.searchParams.get("owner") ?? undefined;
614
+ const session = url.searchParams.get("session") ?? undefined;
611
615
  const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
612
616
  const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
613
- const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
617
+ const { tasks, total } = this.store.listTasks({ status, limit, offset, owner, session });
614
618
 
615
619
  const db = (this.store as any).db;
616
620
  const items = tasks.map((t) => {
@@ -631,9 +635,65 @@ export class ViewerServer {
631
635
  };
632
636
  });
633
637
 
638
+ this.backfillTaskEmbeddings(items);
634
639
  this.jsonResponse(res, { tasks: items, total, limit, offset });
635
640
  }
636
641
 
642
+ private async serveTaskSearch(res: http.ServerResponse, url: URL): Promise<void> {
643
+ const q = (url.searchParams.get("q") ?? "").trim();
644
+ if (!q) { this.jsonResponse(res, { tasks: [], total: 0 }); return; }
645
+
646
+ const owner = url.searchParams.get("owner") ?? undefined;
647
+ const maxResults = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 20));
648
+
649
+ const scoreMap = new Map<string, number>();
650
+
651
+ if (this.embedder) {
652
+ try {
653
+ const [queryVec] = await this.embedder.embed([q]);
654
+ const allEmb = this.store.getTaskEmbeddings(owner);
655
+ for (const { taskId, vector } of allEmb) {
656
+ let dot = 0, normA = 0, normB = 0;
657
+ for (let i = 0; i < queryVec.length && i < vector.length; i++) {
658
+ dot += queryVec[i] * vector[i];
659
+ normA += queryVec[i] * queryVec[i];
660
+ normB += vector[i] * vector[i];
661
+ }
662
+ const sim = normA > 0 && normB > 0 ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0;
663
+ if (sim > 0.3) scoreMap.set(taskId, sim);
664
+ }
665
+ } catch { /* embedding unavailable, fall through to FTS */ }
666
+ }
667
+
668
+ const ftsResults = this.store.taskFtsSearch(q, maxResults, owner);
669
+ for (const { taskId, score } of ftsResults) {
670
+ const existing = scoreMap.get(taskId) ?? 0;
671
+ scoreMap.set(taskId, Math.max(existing, score * 0.8));
672
+ }
673
+
674
+ const sorted = [...scoreMap.entries()]
675
+ .sort((a, b) => b[1] - a[1])
676
+ .slice(0, maxResults);
677
+
678
+ const db = (this.store as any).db;
679
+ const tasks = sorted.map(([taskId, score]) => {
680
+ const t = this.store.getTask(taskId);
681
+ if (!t) return null;
682
+ const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(taskId) as { skill_status: string | null; owner: string | null } | undefined;
683
+ return {
684
+ id: t.id, sessionKey: t.sessionKey, title: t.title,
685
+ summary: t.summary ?? "", status: t.status,
686
+ startedAt: t.startedAt, endedAt: t.endedAt,
687
+ chunkCount: this.store.countChunksByTask(t.id),
688
+ skillStatus: meta?.skill_status ?? null,
689
+ owner: meta?.owner ?? "agent:main",
690
+ score,
691
+ };
692
+ }).filter(Boolean);
693
+
694
+ this.jsonResponse(res, { tasks, total: tasks.length });
695
+ }
696
+
637
697
  private serveTaskDetail(res: http.ServerResponse, urlPath: string): void {
638
698
  const taskId = urlPath.replace("/api/task/", "");
639
699
  const task = this.store.getTask(taskId);
@@ -685,7 +745,7 @@ export class ViewerServer {
685
745
 
686
746
  private serveStats(res: http.ServerResponse, url?: URL): void {
687
747
  const emptyStats = {
688
- totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
748
+ totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0, totalTasks: 0,
689
749
  embeddingProvider: this.embedder?.provider ?? "none",
690
750
  dedupBreakdown: {},
691
751
  timeRange: { earliest: null, latest: null },
@@ -728,9 +788,30 @@ export class ViewerServer {
728
788
  }
729
789
  const sessionList = db.prepare(sessionQuery).all(...sessionParams) as any[];
730
790
 
791
+ let taskSessionList: Array<{ session_key: string; count: number; earliest: number | null; latest: number | null }> = [];
792
+ try {
793
+ taskSessionList = db.prepare(
794
+ "SELECT session_key, COUNT(*) as count, MIN(started_at) as earliest, MAX(COALESCE(updated_at, started_at)) as latest FROM tasks GROUP BY session_key ORDER BY latest DESC",
795
+ ).all() as any[];
796
+ } catch { /* tasks table may not exist yet */ }
797
+
798
+ let skillSessionList: Array<{ session_key: string; count: number; earliest: number | null; latest: number | null }> = [];
799
+ try {
800
+ skillSessionList = db.prepare(
801
+ `SELECT t.session_key as session_key, COUNT(DISTINCT ts.skill_id) as count,
802
+ MIN(t.started_at) as earliest, MAX(COALESCE(t.updated_at, t.started_at)) as latest
803
+ FROM task_skills ts JOIN tasks t ON t.id = ts.task_id
804
+ GROUP BY t.session_key
805
+ ORDER BY latest DESC`,
806
+ ).all() as any[];
807
+ } catch { /* task_skills may not exist yet */ }
808
+
731
809
  let skillCount = 0;
732
810
  try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
733
811
 
812
+ let taskCount = 0;
813
+ try { taskCount = (db.prepare("SELECT COUNT(*) as count FROM tasks").get() as any).count; } catch { /* table may not exist yet */ }
814
+
734
815
  let dedupBreakdown: Record<string, number> = {};
735
816
  try {
736
817
  const dedupRows = db.prepare("SELECT dedup_status, COUNT(*) as count FROM chunks GROUP BY dedup_status").all() as any[];
@@ -739,7 +820,15 @@ export class ViewerServer {
739
820
 
740
821
  let owners: string[] = [];
741
822
  try {
742
- const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all() as any[];
823
+ const ownerRows = db.prepare(`
824
+ SELECT DISTINCT owner FROM (
825
+ SELECT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%'
826
+ UNION
827
+ SELECT owner FROM tasks WHERE owner IS NOT NULL AND owner LIKE 'agent:%'
828
+ UNION
829
+ SELECT owner FROM skills WHERE owner IS NOT NULL AND owner LIKE 'agent:%'
830
+ ) ORDER BY owner
831
+ `).all() as any[];
743
832
  owners = ownerRows.map((o: any) => o.owner);
744
833
  } catch { /* column may not exist yet */ }
745
834
 
@@ -751,11 +840,13 @@ export class ViewerServer {
751
840
 
752
841
  this.jsonResponse(res, {
753
842
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
754
- totalSkills: skillCount,
843
+ totalSkills: skillCount, totalTasks: taskCount,
755
844
  embeddingProvider: this.embedder.provider,
756
845
  dedupBreakdown,
757
846
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
758
847
  sessions: sessionList,
848
+ taskSessions: taskSessionList,
849
+ skillSessions: skillSessionList,
759
850
  owners,
760
851
  currentAgentOwner,
761
852
  });
@@ -865,7 +956,9 @@ export class ViewerServer {
865
956
  private serveSkills(res: http.ServerResponse, url: URL): void {
866
957
  const status = url.searchParams.get("status") ?? undefined;
867
958
  const visibility = url.searchParams.get("visibility") ?? undefined;
868
- let skills = this.store.listSkills({ status });
959
+ const session = url.searchParams.get("session") ?? undefined;
960
+ const owner = url.searchParams.get("owner") ?? undefined;
961
+ let skills = this.store.listSkills({ status, session, owner });
869
962
  if (visibility) {
870
963
  skills = skills.filter(s => s.visibility === visibility);
871
964
  }
@@ -1104,6 +1197,27 @@ export class ViewerServer {
1104
1197
  });
1105
1198
  }
1106
1199
 
1200
+ private embedTaskInBackground(taskId: string, text: string): void {
1201
+ if (!this.embedder || !text.trim()) return;
1202
+ this.embedder.embed([text]).then((vecs: number[][]) => {
1203
+ if (vecs.length > 0) this.store.upsertTaskEmbedding(taskId, vecs[0]);
1204
+ }).catch(() => {});
1205
+ }
1206
+
1207
+ private backfillTaskEmbeddings(tasks: Array<{ id: string; summary: string; title: string }>): void {
1208
+ if (!this.embedder) return;
1209
+ const db = (this.store as any).db;
1210
+ for (const t of tasks) {
1211
+ try {
1212
+ const exists = db.prepare("SELECT 1 FROM task_embeddings WHERE task_id = ?").get(t.id);
1213
+ if (!exists) {
1214
+ const text = `${t.title ?? ""}: ${t.summary ?? ""}`.trim();
1215
+ if (text.length > 1) this.embedTaskInBackground(t.id, text);
1216
+ }
1217
+ } catch { /* best-effort */ }
1218
+ }
1219
+ }
1220
+
1107
1221
  private handleTaskDelete(res: http.ServerResponse, urlPath: string): void {
1108
1222
  const taskId = urlPath.replace("/api/task/", "");
1109
1223
  const deleted = this.store.deleteTask(taskId);
@@ -1118,12 +1232,15 @@ export class ViewerServer {
1118
1232
  const data = JSON.parse(body);
1119
1233
  const task = this.store.getTask(taskId);
1120
1234
  if (!task) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Task not found" })); return; }
1235
+ const newTitle = data.title ?? task.title;
1236
+ const newSummary = data.summary ?? task.summary;
1121
1237
  this.store.updateTask(taskId, {
1122
- title: data.title ?? task.title,
1123
- summary: data.summary ?? task.summary,
1238
+ title: newTitle,
1239
+ summary: newSummary,
1124
1240
  status: data.status ?? task.status,
1125
1241
  endedAt: task.endedAt ?? undefined,
1126
1242
  });
1243
+ this.embedTaskInBackground(taskId, `${newTitle ?? ""}: ${newSummary ?? ""}`);
1127
1244
  this.jsonResponse(res, { ok: true, taskId });
1128
1245
  } catch (err) {
1129
1246
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -1180,6 +1297,58 @@ export class ViewerServer {
1180
1297
  });
1181
1298
  }
1182
1299
 
1300
+ private async handleSkillDisable(res: http.ServerResponse, urlPath: string): Promise<void> {
1301
+ const skillId = urlPath.split("/")[3];
1302
+ const skill = this.store.getSkill(skillId);
1303
+ if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
1304
+ if (skill.status === "archived") { this.jsonResponse(res, { ok: true, skillId, message: "already disabled" }); return; }
1305
+
1306
+ try {
1307
+ if (skill.visibility === "public") {
1308
+ this.store.setSkillVisibility(skillId, "private");
1309
+ }
1310
+ const hub = this.resolveHubConnection();
1311
+ if (hub) {
1312
+ await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
1313
+ method: "POST",
1314
+ body: JSON.stringify({ sourceSkillId: skillId }),
1315
+ }).catch(() => {});
1316
+ }
1317
+ } catch (_) {}
1318
+
1319
+ try {
1320
+ const workspaceSkillsDir = path.join(this.dataDir, "workspace", "skills");
1321
+ const installedDir = path.join(workspaceSkillsDir, skill.name);
1322
+ if (fs.existsSync(installedDir)) {
1323
+ fs.rmSync(installedDir, { recursive: true, force: true });
1324
+ }
1325
+ } catch (_) {}
1326
+
1327
+ this.store.disableSkill(skillId);
1328
+ this.jsonResponse(res, { ok: true, skillId });
1329
+ }
1330
+
1331
+ private handleSkillEnable(res: http.ServerResponse, urlPath: string): void {
1332
+ const skillId = urlPath.split("/")[3];
1333
+ const skill = this.store.getSkill(skillId);
1334
+ if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
1335
+ if (skill.status !== "archived") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Only disabled (archived) skills can be enabled" })); return; }
1336
+
1337
+ this.store.enableSkill(skillId);
1338
+
1339
+ if (this.embedder) {
1340
+ const sv = this.store.getLatestSkillVersion(skillId);
1341
+ if (sv) {
1342
+ const text = `${skill.name}: ${skill.description}`;
1343
+ this.embedder.embed([text]).then((vecs: number[][]) => {
1344
+ if (vecs.length > 0) this.store.upsertSkillEmbedding(skillId, vecs[0]);
1345
+ }).catch(() => {});
1346
+ }
1347
+ }
1348
+
1349
+ this.jsonResponse(res, { ok: true, skillId });
1350
+ }
1351
+
1183
1352
  // ─── CRUD ───
1184
1353
 
1185
1354
  private serveMemoryDetail(res: http.ServerResponse, urlPath: string): void {
@@ -3258,7 +3427,8 @@ export class ViewerServer {
3258
3427
  const providerCfg = providerKey
3259
3428
  ? raw?.models?.providers?.[providerKey]
3260
3429
  : Object.values(raw?.models?.providers ?? {})[0] as Record<string, unknown> | undefined;
3261
- if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
3430
+ const resolvedKey = ViewerServer.resolveApiKeyValue(providerCfg?.apiKey);
3431
+ if (!providerCfg || !providerCfg.baseUrl || !resolvedKey) {
3262
3432
  this.jsonResponse(res, { available: false });
3263
3433
  return;
3264
3434
  }
@@ -3268,6 +3438,17 @@ export class ViewerServer {
3268
3438
  }
3269
3439
  }
3270
3440
 
3441
+ private static resolveApiKeyValue(
3442
+ input: unknown,
3443
+ ): string | undefined {
3444
+ if (!input) return undefined;
3445
+ if (typeof input === "string") return input;
3446
+ if (typeof input === "object" && input !== null && (input as any).source === "env") {
3447
+ return process.env[(input as any).id];
3448
+ }
3449
+ return undefined;
3450
+ }
3451
+
3271
3452
  private findPluginPackageJson(): string | null {
3272
3453
  let dir = __dirname;
3273
3454
  for (let i = 0; i < 6; i++) {
@@ -1,5 +0,0 @@
1
- {
2
- "endpoint": "https://proj-xtrace-e218d9316b328f196a3c640cc7ca84-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/rum/web/v2?workspace=default-cms-1026429231103299-cn-hangzhou&service_id=a3u72ukxmr@066657d42a13a9a9f337f",
3
- "pid": "a3u72ukxmr@066657d42a13a9a9f337f",
4
- "env": "prod"
5
- }