@memtensor/memos-local-openclaw-plugin 1.0.4-beta.8 → 1.0.4
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 +7 -0
- package/README.md +94 -27
- package/dist/capture/index.js +3 -1
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +132 -10
- package/dist/client/connector.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +251 -38
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +9 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +26 -2
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.js +2 -2
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +2 -1
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +56 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +58 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +295 -35
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +27 -8
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +796 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +11 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +456 -92
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +411 -52
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/src/capture/index.ts +4 -1
- package/src/client/connector.ts +136 -10
- package/src/config.ts +2 -1
- package/src/hub/server.ts +246 -38
- package/src/hub/user-manager.ts +42 -6
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +2 -2
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +2 -1
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +58 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/sqlite.ts +326 -40
- package/src/telemetry.ts +27 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +796 -289
- package/src/viewer/server.ts +430 -89
- package/telemetry.credentials.json +5 -0
package/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { IngestWorker } from "./src/ingest/worker";
|
|
|
19
19
|
import { RecallEngine } from "./src/recall/engine";
|
|
20
20
|
import { captureMessages, stripInboundMetadata } from "./src/capture";
|
|
21
21
|
import { DEFAULTS } from "./src/types";
|
|
22
|
+
import type { SearchHit } from "./src/types";
|
|
22
23
|
import { ViewerServer } from "./src/viewer/server";
|
|
23
24
|
import { HubServer } from "./src/hub/server";
|
|
24
25
|
import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
|
|
@@ -179,7 +180,7 @@ const memosLocalPlugin = {
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
182
|
-
const stateDir = api.resolvePath("~/.openclaw");
|
|
183
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR || api.resolvePath("~/.openclaw");
|
|
183
184
|
|
|
184
185
|
// Fallback: read config from file if not provided by OpenClaw
|
|
185
186
|
const configPath = path.join(stateDir, "state", "memos-local", "config.json");
|
|
@@ -318,6 +319,21 @@ const memosLocalPlugin = {
|
|
|
318
319
|
candidates: det.candidates,
|
|
319
320
|
filtered: det.hits ?? det.filtered ?? [],
|
|
320
321
|
});
|
|
322
|
+
} else if (det && det.local && det.hub) {
|
|
323
|
+
const localHits = det.local?.hits ?? [];
|
|
324
|
+
const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
|
|
325
|
+
score: h.score ?? 0,
|
|
326
|
+
role: h.source?.role ?? h.role ?? "assistant",
|
|
327
|
+
summary: h.summary ?? "",
|
|
328
|
+
original_excerpt: h.excerpt ?? h.summary ?? "",
|
|
329
|
+
origin: "hub-remote",
|
|
330
|
+
ownerName: h.ownerName ?? "",
|
|
331
|
+
groupName: h.groupName ?? "",
|
|
332
|
+
}));
|
|
333
|
+
outputText = JSON.stringify({
|
|
334
|
+
candidates: [...localHits, ...hubHits],
|
|
335
|
+
filtered: [...localHits, ...hubHits],
|
|
336
|
+
});
|
|
321
337
|
} else {
|
|
322
338
|
outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
|
|
323
339
|
}
|
|
@@ -370,27 +386,30 @@ const memosLocalPlugin = {
|
|
|
370
386
|
}),
|
|
371
387
|
}) as { memoryId?: string; visibility?: "public" | "group" };
|
|
372
388
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
389
|
+
const memoryId = response?.memoryId ?? `${chunk.id}-hub`;
|
|
390
|
+
|
|
391
|
+
// Hub role: full hub_memories row for local recall/embeddings. Client: metadata only (team_shared_chunks) for UI.
|
|
392
|
+
if (ctx.config.sharing?.role === "hub") {
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
395
|
+
store.upsertHubMemory({
|
|
396
|
+
id: memoryId,
|
|
397
|
+
sourceChunkId: chunk.id,
|
|
398
|
+
sourceUserId: hubClient.userId,
|
|
399
|
+
role: chunk.role,
|
|
400
|
+
content: chunk.content,
|
|
401
|
+
summary: chunk.summary ?? "",
|
|
402
|
+
kind: chunk.kind,
|
|
403
|
+
groupId,
|
|
404
|
+
visibility,
|
|
405
|
+
createdAt: existing?.createdAt ?? now,
|
|
406
|
+
updatedAt: now,
|
|
407
|
+
});
|
|
408
|
+
} else if (ctx.config.sharing?.enabled && hubClient.userId) {
|
|
409
|
+
store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: memoryId, visibility, groupId });
|
|
410
|
+
}
|
|
388
411
|
|
|
389
|
-
return {
|
|
390
|
-
memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
391
|
-
visibility,
|
|
392
|
-
groupId,
|
|
393
|
-
};
|
|
412
|
+
return { memoryId, visibility, groupId };
|
|
394
413
|
};
|
|
395
414
|
|
|
396
415
|
const unshareMemoryFromHub = async (
|
|
@@ -407,6 +426,7 @@ const memosLocalPlugin = {
|
|
|
407
426
|
body: JSON.stringify({ sourceChunkId: chunk.id }),
|
|
408
427
|
});
|
|
409
428
|
store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
|
|
429
|
+
store.deleteTeamSharedChunk(chunk.id);
|
|
410
430
|
};
|
|
411
431
|
|
|
412
432
|
// ─── Tool: memory_search ───
|
|
@@ -448,7 +468,10 @@ const memosLocalPlugin = {
|
|
|
448
468
|
};
|
|
449
469
|
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
450
470
|
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
451
|
-
|
|
471
|
+
let searchScope = resolveMemorySearchScope(rawScope);
|
|
472
|
+
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
473
|
+
searchScope = "all";
|
|
474
|
+
}
|
|
452
475
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
453
476
|
|
|
454
477
|
const agentId = currentAgentId;
|
|
@@ -464,6 +487,7 @@ const memosLocalPlugin = {
|
|
|
464
487
|
score: h.score,
|
|
465
488
|
summary: h.summary,
|
|
466
489
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
490
|
+
origin: h.origin || "local",
|
|
467
491
|
}));
|
|
468
492
|
|
|
469
493
|
if (result.hits.length === 0 && searchScope === "local") {
|
|
@@ -518,29 +542,79 @@ const memosLocalPlugin = {
|
|
|
518
542
|
role: h.source.role,
|
|
519
543
|
score: h.score,
|
|
520
544
|
summary: h.summary,
|
|
545
|
+
origin: h.origin || "local",
|
|
521
546
|
};
|
|
522
547
|
});
|
|
523
548
|
|
|
524
549
|
if (searchScope !== "local") {
|
|
525
550
|
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
551
|
+
|
|
552
|
+
let filteredHubHits = hub.hits;
|
|
553
|
+
if (hub.hits.length > 0) {
|
|
554
|
+
const hubCandidates = hub.hits.map((h, i) => ({
|
|
555
|
+
index: filteredHits.length + i + 1,
|
|
556
|
+
role: (h.source?.role || "assistant") as string,
|
|
557
|
+
content: (h.summary || h.excerpt || "").slice(0, 300),
|
|
558
|
+
time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
559
|
+
}));
|
|
560
|
+
const localCandidatesForMerge = filteredHits.map((h, i) => ({
|
|
561
|
+
index: i + 1,
|
|
562
|
+
role: h.source.role,
|
|
563
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
564
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
565
|
+
}));
|
|
566
|
+
const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
|
|
567
|
+
const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
|
|
568
|
+
if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
|
|
569
|
+
const relevantSet = new Set(mergedFilter.relevant);
|
|
570
|
+
const hubStartIdx = filteredHits.length + 1;
|
|
571
|
+
filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
|
|
572
|
+
filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
|
|
573
|
+
ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const originLabel = (h: SearchHit) => {
|
|
578
|
+
if (h.origin === "hub-memory") return " [团队缓存]";
|
|
579
|
+
if (h.origin === "local-shared") return " [本机共享]";
|
|
580
|
+
return "";
|
|
581
|
+
};
|
|
526
582
|
const localText = filteredHits.length > 0
|
|
527
583
|
? filteredHits.map((h, i) => {
|
|
528
584
|
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
529
|
-
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
585
|
+
return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
|
|
530
586
|
}).join("\n")
|
|
531
587
|
: "(none)";
|
|
532
|
-
const hubText =
|
|
533
|
-
?
|
|
588
|
+
const hubText = filteredHubHits.length > 0
|
|
589
|
+
? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
534
590
|
: "(none)";
|
|
535
591
|
|
|
592
|
+
const localDetailsFiltered = filteredHits.map((h) => {
|
|
593
|
+
let effectiveTaskId = h.taskId;
|
|
594
|
+
if (effectiveTaskId) {
|
|
595
|
+
const t = store.getTask(effectiveTaskId);
|
|
596
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
ref: h.ref,
|
|
600
|
+
chunkId: h.ref.chunkId,
|
|
601
|
+
taskId: effectiveTaskId,
|
|
602
|
+
skillId: h.skillId,
|
|
603
|
+
role: h.source.role,
|
|
604
|
+
score: h.score,
|
|
605
|
+
summary: h.summary,
|
|
606
|
+
origin: h.origin,
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
|
|
536
610
|
return {
|
|
537
611
|
content: [{
|
|
538
612
|
type: "text",
|
|
539
613
|
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
540
614
|
}],
|
|
541
615
|
details: {
|
|
542
|
-
local: { hits:
|
|
543
|
-
hub,
|
|
616
|
+
local: { hits: localDetailsFiltered, meta: result.meta },
|
|
617
|
+
hub: { ...hub, hits: filteredHubHits },
|
|
544
618
|
},
|
|
545
619
|
};
|
|
546
620
|
}
|
|
@@ -552,9 +626,15 @@ const memosLocalPlugin = {
|
|
|
552
626
|
};
|
|
553
627
|
}
|
|
554
628
|
|
|
629
|
+
const originTag = (o?: string) => {
|
|
630
|
+
if (o === "local-shared") return " [本机共享]";
|
|
631
|
+
if (o === "hub-memory") return " [团队缓存]";
|
|
632
|
+
if (o === "hub-remote") return " [团队]";
|
|
633
|
+
return "";
|
|
634
|
+
};
|
|
555
635
|
const lines = filteredHits.map((h, i) => {
|
|
556
636
|
const excerpt = h.original_excerpt;
|
|
557
|
-
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
637
|
+
const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
|
|
558
638
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
559
639
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
560
640
|
if (h.taskId) {
|
|
@@ -609,6 +689,7 @@ const memosLocalPlugin = {
|
|
|
609
689
|
score: h.score,
|
|
610
690
|
summary: h.summary,
|
|
611
691
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
692
|
+
origin: h.origin || "local",
|
|
612
693
|
};
|
|
613
694
|
}),
|
|
614
695
|
meta: result.meta,
|
|
@@ -1058,10 +1139,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1058
1139
|
};
|
|
1059
1140
|
}
|
|
1060
1141
|
|
|
1142
|
+
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1143
|
+
let footer = "\n\n---\n";
|
|
1144
|
+
|
|
1145
|
+
if (manifest && manifest.hasCompanionFiles) {
|
|
1146
|
+
const fileSummary = manifest.files
|
|
1147
|
+
.filter(f => f.type !== "eval")
|
|
1148
|
+
.map(f => `\`${f.relativePath}\``)
|
|
1149
|
+
.join(", ");
|
|
1150
|
+
footer += `**Companion files available:** ${fileSummary}\n`;
|
|
1151
|
+
footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
|
|
1152
|
+
footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
|
|
1153
|
+
if (manifest.installMode === "install_recommended") {
|
|
1154
|
+
footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
|
|
1155
|
+
}
|
|
1156
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1157
|
+
footer += `> Already installed at: ${manifest.installedPath}/\n`;
|
|
1158
|
+
}
|
|
1159
|
+
} else {
|
|
1160
|
+
footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1061
1163
|
return {
|
|
1062
1164
|
content: [{
|
|
1063
1165
|
type: "text",
|
|
1064
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}
|
|
1166
|
+
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1065
1167
|
}],
|
|
1066
1168
|
details: {
|
|
1067
1169
|
skillId: skill.id,
|
|
@@ -1069,6 +1171,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1069
1171
|
version: skill.version,
|
|
1070
1172
|
status: skill.status,
|
|
1071
1173
|
installed: skill.installed,
|
|
1174
|
+
companionFiles: manifest?.hasCompanionFiles ?? false,
|
|
1175
|
+
installMode: manifest?.installMode ?? "inline",
|
|
1072
1176
|
},
|
|
1073
1177
|
};
|
|
1074
1178
|
}),
|
|
@@ -1104,9 +1208,116 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1104
1208
|
{ name: "skill_install" },
|
|
1105
1209
|
);
|
|
1106
1210
|
|
|
1211
|
+
// ─── Tool: skill_files ───
|
|
1212
|
+
|
|
1213
|
+
api.registerTool(
|
|
1214
|
+
{
|
|
1215
|
+
name: "skill_files",
|
|
1216
|
+
label: "List Skill Companion Files",
|
|
1217
|
+
description:
|
|
1218
|
+
"List companion files (scripts, references, evals) for a skill. " +
|
|
1219
|
+
"Use this after skill_get to see what additional files are available. " +
|
|
1220
|
+
"Returns file names, sizes, and whether the skill recommends installation.",
|
|
1221
|
+
parameters: Type.Object({
|
|
1222
|
+
skillId: Type.String({ description: "The skill_id to inspect" }),
|
|
1223
|
+
}),
|
|
1224
|
+
execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
|
|
1225
|
+
const { skillId } = params as { skillId: string };
|
|
1226
|
+
ctx.log.debug(`skill_files called for skill=${skillId}`);
|
|
1227
|
+
|
|
1228
|
+
const manifest = skillInstaller.getCompanionManifest(skillId);
|
|
1229
|
+
if (!manifest) {
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{ type: "text", text: `Skill not found: ${skillId}` }],
|
|
1232
|
+
details: { error: "not_found" },
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (!manifest.hasCompanionFiles) {
|
|
1237
|
+
return {
|
|
1238
|
+
content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
|
|
1239
|
+
details: manifest,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
|
|
1244
|
+
if (manifest.scriptsCount > 0) {
|
|
1245
|
+
lines.push(`### Scripts (${manifest.scriptsCount})`);
|
|
1246
|
+
for (const f of manifest.files.filter(f => f.type === "script")) {
|
|
1247
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (manifest.referencesCount > 0) {
|
|
1251
|
+
lines.push(`\n### References (${manifest.referencesCount})`);
|
|
1252
|
+
for (const f of manifest.files.filter(f => f.type === "reference")) {
|
|
1253
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (manifest.evalsCount > 0) {
|
|
1257
|
+
lines.push(`\n### Evals (${manifest.evalsCount})`);
|
|
1258
|
+
for (const f of manifest.files.filter(f => f.type === "eval")) {
|
|
1259
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (manifest.installMode === "install_recommended") {
|
|
1264
|
+
lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
|
|
1265
|
+
}
|
|
1266
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1267
|
+
lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1272
|
+
details: manifest,
|
|
1273
|
+
};
|
|
1274
|
+
}),
|
|
1275
|
+
},
|
|
1276
|
+
{ name: "skill_files" },
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
// ─── Tool: skill_file_get ───
|
|
1280
|
+
|
|
1281
|
+
api.registerTool(
|
|
1282
|
+
{
|
|
1283
|
+
name: "skill_file_get",
|
|
1284
|
+
label: "Get Skill Companion File",
|
|
1285
|
+
description:
|
|
1286
|
+
"Read the content of a specific companion file (script, reference) from a skill. " +
|
|
1287
|
+
"Use after skill_files to retrieve a script or reference document. " +
|
|
1288
|
+
"Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
|
|
1289
|
+
parameters: Type.Object({
|
|
1290
|
+
skillId: Type.String({ description: "The skill_id" }),
|
|
1291
|
+
path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
|
|
1292
|
+
}),
|
|
1293
|
+
execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
|
|
1294
|
+
const { skillId, path: filePath } = params as { skillId: string; path: string };
|
|
1295
|
+
ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
|
|
1296
|
+
|
|
1297
|
+
const result = skillInstaller.readCompanionFile(skillId, filePath);
|
|
1298
|
+
if ("error" in result) {
|
|
1299
|
+
return {
|
|
1300
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
1301
|
+
details: result,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const ext = filePath.split(".").pop() || "";
|
|
1306
|
+
const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
|
|
1307
|
+
|
|
1308
|
+
return {
|
|
1309
|
+
content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
|
|
1310
|
+
details: { path: filePath, size: result.size },
|
|
1311
|
+
};
|
|
1312
|
+
}),
|
|
1313
|
+
},
|
|
1314
|
+
{ name: "skill_file_get" },
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1107
1317
|
// ─── Tool: memory_viewer ───
|
|
1108
1318
|
|
|
1109
|
-
const
|
|
1319
|
+
const gatewayPort = (api.config as any)?.gateway?.port ?? 18789;
|
|
1320
|
+
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
1110
1321
|
|
|
1111
1322
|
api.registerTool(
|
|
1112
1323
|
{
|
|
@@ -1613,11 +1824,73 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1613
1824
|
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
1614
1825
|
|
|
1615
1826
|
const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1827
|
+
|
|
1828
|
+
// Hub fallback helper: search team shared memories when local search has no relevant results
|
|
1829
|
+
const hubFallback = async (): Promise<SearchHit[]> => {
|
|
1830
|
+
if (!ctx.config?.sharing?.enabled) return [];
|
|
1831
|
+
try {
|
|
1832
|
+
const hubResult = await hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" });
|
|
1833
|
+
if (hubResult.hits.length === 0) return [];
|
|
1834
|
+
ctx.log.debug(`auto-recall: hub fallback returned ${hubResult.hits.length} hit(s)`);
|
|
1835
|
+
return hubResult.hits.map((h) => ({
|
|
1836
|
+
summary: h.summary,
|
|
1837
|
+
original_excerpt: h.excerpt || h.summary,
|
|
1838
|
+
ref: { sessionKey: "", chunkId: h.remoteHitId, turnId: "", seq: 0 },
|
|
1839
|
+
score: 0.9,
|
|
1840
|
+
taskId: null,
|
|
1841
|
+
skillId: null,
|
|
1842
|
+
origin: "hub-remote" as const,
|
|
1843
|
+
source: { ts: h.source.ts, role: h.source.role, sessionKey: "" },
|
|
1844
|
+
}));
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
ctx.log.debug(`auto-recall: hub fallback failed (${err})`);
|
|
1847
|
+
return [];
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
if (result.hits.length === 0) {
|
|
1852
|
+
// Local found nothing — try hub before giving up
|
|
1853
|
+
const hubHits = await hubFallback();
|
|
1854
|
+
if (hubHits.length > 0) {
|
|
1855
|
+
result.hits.push(...hubHits);
|
|
1856
|
+
ctx.log.debug(`auto-recall: local empty, using ${hubHits.length} hub hit(s)`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1616
1859
|
if (result.hits.length === 0) {
|
|
1617
|
-
ctx.log.debug("auto-recall: no candidates found");
|
|
1860
|
+
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1618
1861
|
const dur = performance.now() - recallT0;
|
|
1619
1862
|
store.recordToolCall("memory_search", dur, true);
|
|
1620
1863
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
|
|
1864
|
+
|
|
1865
|
+
// Even without memory hits, try skill recall
|
|
1866
|
+
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1867
|
+
if (skillAutoRecallEarly) {
|
|
1868
|
+
try {
|
|
1869
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1870
|
+
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1871
|
+
const topSkills = skillHits.slice(0, skillLimit);
|
|
1872
|
+
if (topSkills.length > 0) {
|
|
1873
|
+
const skillLines = topSkills.map((sc, i) => {
|
|
1874
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1875
|
+
let badge = "";
|
|
1876
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1877
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1878
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1879
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1880
|
+
});
|
|
1881
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1882
|
+
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1883
|
+
skillLines.join("\n\n") +
|
|
1884
|
+
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1885
|
+
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1886
|
+
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1887
|
+
return { prependContext: skillContext };
|
|
1888
|
+
}
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1621
1894
|
if (query.length > 50) {
|
|
1622
1895
|
const noRecallHint =
|
|
1623
1896
|
"## Memory system — ACTION REQUIRED\n\n" +
|
|
@@ -1646,22 +1919,36 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1646
1919
|
const indexSet = new Set(filterResult.relevant);
|
|
1647
1920
|
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
1648
1921
|
} else {
|
|
1649
|
-
ctx.log.debug("auto-recall: LLM filter returned no relevant hits");
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1922
|
+
ctx.log.debug("auto-recall: LLM filter returned no relevant local hits, trying hub fallback");
|
|
1923
|
+
const hubHits = await hubFallback();
|
|
1924
|
+
if (hubHits.length > 0) {
|
|
1925
|
+
ctx.log.debug(`auto-recall: hub fallback provided ${hubHits.length} hit(s) after local filter yielded 0`);
|
|
1926
|
+
filteredHits = hubHits;
|
|
1927
|
+
} else {
|
|
1928
|
+
const dur = performance.now() - recallT0;
|
|
1929
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1930
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1931
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
1932
|
+
filtered: []
|
|
1933
|
+
}), dur, true);
|
|
1934
|
+
if (query.length > 50) {
|
|
1935
|
+
const noRecallHint =
|
|
1936
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1937
|
+
"Auto-recall found no relevant results for a long query. " +
|
|
1938
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1939
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1940
|
+
return { prependContext: noRecallHint };
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1663
1943
|
}
|
|
1664
|
-
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (!sufficient && filteredHits.length > 0 && ctx.config?.sharing?.enabled) {
|
|
1948
|
+
const hubSupp = await hubFallback();
|
|
1949
|
+
if (hubSupp.length > 0) {
|
|
1950
|
+
ctx.log.debug(`auto-recall: local insufficient, supplementing with ${hubSupp.length} hub hit(s)`);
|
|
1951
|
+
filteredHits.push(...hubSupp);
|
|
1665
1952
|
}
|
|
1666
1953
|
}
|
|
1667
1954
|
|
|
@@ -1671,7 +1958,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1671
1958
|
|
|
1672
1959
|
const lines = filteredHits.map((h, i) => {
|
|
1673
1960
|
const excerpt = h.original_excerpt;
|
|
1674
|
-
const
|
|
1961
|
+
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
1962
|
+
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
1675
1963
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
1676
1964
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
1677
1965
|
if (h.taskId) {
|
|
@@ -1706,17 +1994,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1706
1994
|
lines.join("\n\n"),
|
|
1707
1995
|
];
|
|
1708
1996
|
if (tipsText) contextParts.push(tipsText);
|
|
1997
|
+
|
|
1998
|
+
// ─── Skill auto-recall ───
|
|
1999
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
2000
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
2001
|
+
let skillSection = "";
|
|
2002
|
+
|
|
2003
|
+
if (skillAutoRecall) {
|
|
2004
|
+
try {
|
|
2005
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
2006
|
+
|
|
2007
|
+
// Source 1: direct skill search based on user query
|
|
2008
|
+
try {
|
|
2009
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
2010
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
2011
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
2012
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Source 2: skills linked to tasks from memory hits
|
|
2020
|
+
const taskIds = new Set<string>();
|
|
2021
|
+
for (const h of filteredHits) {
|
|
2022
|
+
if (h.taskId) {
|
|
2023
|
+
const t = store.getTask(h.taskId);
|
|
2024
|
+
if (t && t.status !== "skipped") taskIds.add(h.taskId);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
for (const tid of taskIds) {
|
|
2028
|
+
const linked = store.getSkillsByTask(tid);
|
|
2029
|
+
for (const rs of linked) {
|
|
2030
|
+
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
2031
|
+
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
2037
|
+
|
|
2038
|
+
if (skillCandidates.length > 0) {
|
|
2039
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
2040
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
2041
|
+
let badge = "";
|
|
2042
|
+
if (manifest?.installed) badge = " [installed]";
|
|
2043
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
2044
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
2045
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
2046
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
2047
|
+
});
|
|
2048
|
+
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
2049
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
2050
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
2051
|
+
skillLines.join("\n\n");
|
|
2052
|
+
|
|
2053
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2054
|
+
try {
|
|
2055
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2056
|
+
} catch { /* best-effort */ }
|
|
2057
|
+
} else {
|
|
2058
|
+
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2059
|
+
}
|
|
2060
|
+
} catch (err) {
|
|
2061
|
+
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (skillSection) contextParts.push(skillSection);
|
|
1709
2066
|
const context = contextParts.join("\n");
|
|
1710
2067
|
|
|
1711
2068
|
const recallDur = performance.now() - recallT0;
|
|
1712
2069
|
store.recordToolCall("memory_search", recallDur, true);
|
|
1713
2070
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1714
|
-
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
1715
|
-
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
|
|
2071
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2072
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
|
|
1716
2073
|
}), recallDur, true);
|
|
1717
2074
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
1718
2075
|
|
|
1719
|
-
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
|
|
2076
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
|
|
1720
2077
|
|
|
1721
2078
|
if (!sufficient) {
|
|
1722
2079
|
const searchHint =
|
|
@@ -1943,6 +2300,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1943
2300
|
|
|
1944
2301
|
// ─── Memory Viewer (web UI) ───
|
|
1945
2302
|
|
|
2303
|
+
const derivedHubPort = gatewayPort + 11;
|
|
2304
|
+
|
|
1946
2305
|
const viewer = new ViewerServer({
|
|
1947
2306
|
store,
|
|
1948
2307
|
embedder,
|
|
@@ -1950,10 +2309,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1950
2309
|
log: ctx.log,
|
|
1951
2310
|
dataDir: stateDir,
|
|
1952
2311
|
ctx,
|
|
2312
|
+
defaultHubPort: derivedHubPort,
|
|
1953
2313
|
});
|
|
1954
|
-
|
|
1955
2314
|
const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
|
|
1956
|
-
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
|
|
2315
|
+
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder, defaultHubPort: derivedHubPort })
|
|
1957
2316
|
: null;
|
|
1958
2317
|
|
|
1959
2318
|
// ─── Service lifecycle ───
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "memos-local-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Local Memory",
|
|
4
|
-
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary,
|
|
4
|
+
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
|
|
5
5
|
"kind": "memory",
|
|
6
6
|
"version": "0.1.12",
|
|
7
7
|
"skills": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.4
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"prebuilds",
|
|
14
14
|
"scripts/postinstall.cjs",
|
|
15
15
|
"openclaw.plugin.json",
|
|
16
|
+
"telemetry.credentials.json",
|
|
16
17
|
"README.md",
|
|
17
18
|
".env.example"
|
|
18
19
|
],
|
|
Binary file
|
|
Binary file
|
|
Binary file
|