@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.
- package/.env.example +4 -0
- package/index.ts +74 -49
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +59 -25
- package/skill/memos-memory-guide/SKILL.md +5 -2
- package/src/ingest/providers/index.ts +14 -1
- package/src/recall/engine.ts +1 -1
- package/src/shared/llm-call.ts +14 -1
- package/src/storage/sqlite.ts +150 -6
- package/src/viewer/html.ts +779 -220
- package/src/viewer/server.ts +189 -8
- 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/telemetry.credentials.json +0 -5
package/.env.example
CHANGED
|
@@ -18,6 +18,10 @@ SUMMARIZER_TEMPERATURE=0
|
|
|
18
18
|
# Port for the web-based Memory Viewer (default: 18799)
|
|
19
19
|
# VIEWER_PORT=18799
|
|
20
20
|
|
|
21
|
+
# ─── Tavily Search (optional) ───
|
|
22
|
+
# API key for Tavily web search (get from https://app.tavily.com)
|
|
23
|
+
# TAVILY_API_KEY=tvly-your-tavily-api-key
|
|
24
|
+
|
|
21
25
|
# ─── Telemetry (opt-out) ───
|
|
22
26
|
# Anonymous usage analytics to help improve the plugin.
|
|
23
27
|
# No memory content, queries, or personal data is ever sent — only tool names, latencies, and version info.
|
package/index.ts
CHANGED
|
@@ -53,6 +53,45 @@ function deduplicateHits<T extends { summary: string }>(hits: T[]): T[] {
|
|
|
53
53
|
return kept;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const NEW_SESSION_PROMPT_RE = /A new session was started via \/new or \/reset\./i;
|
|
57
|
+
const INTERNAL_CONTEXT_RE = /OpenClaw runtime context \(internal\):[\s\S]*/i;
|
|
58
|
+
const CONTINUE_PROMPT_RE = /^Continue where you left off\.[\s\S]*/i;
|
|
59
|
+
|
|
60
|
+
function normalizeAutoRecallQuery(rawPrompt: string): string {
|
|
61
|
+
let query = rawPrompt.trim();
|
|
62
|
+
|
|
63
|
+
const senderTag = "Sender (untrusted metadata):";
|
|
64
|
+
const senderPos = query.indexOf(senderTag);
|
|
65
|
+
if (senderPos !== -1) {
|
|
66
|
+
const afterSender = query.slice(senderPos);
|
|
67
|
+
const fenceStart = afterSender.indexOf("```json");
|
|
68
|
+
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
69
|
+
if (fenceEnd > 0) {
|
|
70
|
+
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
71
|
+
} else {
|
|
72
|
+
const firstDblNl = afterSender.indexOf("\n\n");
|
|
73
|
+
if (firstDblNl > 0) {
|
|
74
|
+
query = afterSender.slice(firstDblNl + 2).trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
query = stripInboundMetadata(query);
|
|
80
|
+
query = query.replace(/<[^>]+>/g, "").trim();
|
|
81
|
+
|
|
82
|
+
if (NEW_SESSION_PROMPT_RE.test(query)) {
|
|
83
|
+
query = query.replace(NEW_SESSION_PROMPT_RE, "").trim();
|
|
84
|
+
query = query.replace(/^(Execute|Run) your Session Startup sequence[^\n]*\n?/im, "").trim();
|
|
85
|
+
query = query.replace(/^Current time:[^\n]*(\n|$)/im, "").trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
query = query.replace(INTERNAL_CONTEXT_RE, "").trim();
|
|
89
|
+
query = query.replace(CONTINUE_PROMPT_RE, "").trim();
|
|
90
|
+
|
|
91
|
+
return query;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
56
95
|
const pluginConfigSchema = {
|
|
57
96
|
type: "object" as const,
|
|
58
97
|
additionalProperties: true,
|
|
@@ -278,7 +317,7 @@ const memosLocalPlugin = {
|
|
|
278
317
|
const raw = fs.readFileSync(openclawJsonPath, "utf-8");
|
|
279
318
|
const cfg = JSON.parse(raw);
|
|
280
319
|
const allow: string[] | undefined = cfg?.tools?.allow;
|
|
281
|
-
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")) {
|
|
320
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins") && !allow.includes("*")) {
|
|
282
321
|
const lastEntry = JSON.stringify(allow[allow.length - 1]);
|
|
283
322
|
const patched = raw.replace(
|
|
284
323
|
new RegExp(`(${lastEntry})(\\s*\\])`),
|
|
@@ -307,6 +346,7 @@ const memosLocalPlugin = {
|
|
|
307
346
|
// Current agent ID — updated by hooks, read by tools for owner isolation.
|
|
308
347
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
309
348
|
let currentAgentId = "main";
|
|
349
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
310
350
|
|
|
311
351
|
// ─── Check allowPromptInjection policy ───
|
|
312
352
|
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
@@ -354,7 +394,6 @@ const memosLocalPlugin = {
|
|
|
354
394
|
}
|
|
355
395
|
};
|
|
356
396
|
|
|
357
|
-
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
358
397
|
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
359
398
|
scope === "group" || scope === "all" ? scope : "local";
|
|
360
399
|
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
@@ -445,7 +484,7 @@ const memosLocalPlugin = {
|
|
|
445
484
|
// ─── Tool: memory_search ───
|
|
446
485
|
|
|
447
486
|
api.registerTool(
|
|
448
|
-
{
|
|
487
|
+
(context) => ({
|
|
449
488
|
name: "memory_search",
|
|
450
489
|
label: "Memory Search",
|
|
451
490
|
description:
|
|
@@ -461,7 +500,7 @@ const memosLocalPlugin = {
|
|
|
461
500
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
462
501
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
463
502
|
}),
|
|
464
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any
|
|
503
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
465
504
|
const {
|
|
466
505
|
query,
|
|
467
506
|
scope: rawScope,
|
|
@@ -482,9 +521,6 @@ const memosLocalPlugin = {
|
|
|
482
521
|
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
483
522
|
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
484
523
|
let searchScope = resolveMemorySearchScope(rawScope);
|
|
485
|
-
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
486
|
-
searchScope = "all";
|
|
487
|
-
}
|
|
488
524
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
489
525
|
|
|
490
526
|
const agentId = context?.agentId ?? currentAgentId;
|
|
@@ -504,7 +540,7 @@ const memosLocalPlugin = {
|
|
|
504
540
|
|
|
505
541
|
// Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
|
|
506
542
|
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
507
|
-
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
543
|
+
const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
|
|
508
544
|
|
|
509
545
|
const rawLocalCandidates = localHits.map((h) => ({
|
|
510
546
|
chunkId: h.ref.chunkId,
|
|
@@ -669,14 +705,14 @@ const memosLocalPlugin = {
|
|
|
669
705
|
},
|
|
670
706
|
};
|
|
671
707
|
}),
|
|
672
|
-
},
|
|
708
|
+
}),
|
|
673
709
|
{ name: "memory_search" },
|
|
674
710
|
);
|
|
675
711
|
|
|
676
712
|
// ─── Tool: memory_timeline ───
|
|
677
713
|
|
|
678
714
|
api.registerTool(
|
|
679
|
-
{
|
|
715
|
+
(context) => ({
|
|
680
716
|
name: "memory_timeline",
|
|
681
717
|
label: "Memory Timeline",
|
|
682
718
|
description:
|
|
@@ -686,7 +722,7 @@ const memosLocalPlugin = {
|
|
|
686
722
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
687
723
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
688
724
|
}),
|
|
689
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any
|
|
725
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
690
726
|
const agentId = context?.agentId ?? currentAgentId;
|
|
691
727
|
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
692
728
|
const { chunkId, window: win } = params as {
|
|
@@ -730,14 +766,14 @@ const memosLocalPlugin = {
|
|
|
730
766
|
details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
|
|
731
767
|
};
|
|
732
768
|
}),
|
|
733
|
-
},
|
|
769
|
+
}),
|
|
734
770
|
{ name: "memory_timeline" },
|
|
735
771
|
);
|
|
736
772
|
|
|
737
773
|
// ─── Tool: memory_get ───
|
|
738
774
|
|
|
739
775
|
api.registerTool(
|
|
740
|
-
{
|
|
776
|
+
(context) => ({
|
|
741
777
|
name: "memory_get",
|
|
742
778
|
label: "Memory Get",
|
|
743
779
|
description:
|
|
@@ -748,7 +784,7 @@ const memosLocalPlugin = {
|
|
|
748
784
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
749
785
|
),
|
|
750
786
|
}),
|
|
751
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any
|
|
787
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
752
788
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
753
789
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
754
790
|
|
|
@@ -774,7 +810,7 @@ const memosLocalPlugin = {
|
|
|
774
810
|
},
|
|
775
811
|
};
|
|
776
812
|
}),
|
|
777
|
-
},
|
|
813
|
+
}),
|
|
778
814
|
{ name: "memory_get" },
|
|
779
815
|
);
|
|
780
816
|
|
|
@@ -1115,6 +1151,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1115
1151
|
};
|
|
1116
1152
|
}
|
|
1117
1153
|
|
|
1154
|
+
const disabledWarning = skill.status === "archived"
|
|
1155
|
+
? "\n\n> **Warning:** This skill is currently **disabled** (archived). Its content is shown for reference only — it will not be used in search or auto-recall.\n\n"
|
|
1156
|
+
: "";
|
|
1157
|
+
|
|
1118
1158
|
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1119
1159
|
let footer = "\n\n---\n";
|
|
1120
1160
|
|
|
@@ -1139,7 +1179,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1139
1179
|
return {
|
|
1140
1180
|
content: [{
|
|
1141
1181
|
type: "text",
|
|
1142
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1182
|
+
text: `## Skill: ${skill.name} (v${skill.version})${disabledWarning}\n\n${sv.content}${footer}`,
|
|
1143
1183
|
}],
|
|
1144
1184
|
details: {
|
|
1145
1185
|
skillId: skill.id,
|
|
@@ -1296,7 +1336,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1296
1336
|
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
1297
1337
|
|
|
1298
1338
|
api.registerTool(
|
|
1299
|
-
{
|
|
1339
|
+
(context) => ({
|
|
1300
1340
|
name: "memory_viewer",
|
|
1301
1341
|
label: "Open Memory Viewer",
|
|
1302
1342
|
description:
|
|
@@ -1304,10 +1344,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1304
1344
|
"or access their stored memories, or asks where the memory dashboard is. " +
|
|
1305
1345
|
"Returns the URL the user can open in their browser.",
|
|
1306
1346
|
parameters: Type.Object({}),
|
|
1307
|
-
execute: trackTool("memory_viewer", async () => {
|
|
1347
|
+
execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
|
|
1308
1348
|
ctx.log.debug(`memory_viewer called`);
|
|
1309
1349
|
telemetry.trackViewerOpened();
|
|
1310
|
-
const
|
|
1350
|
+
const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
|
|
1351
|
+
const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
|
|
1311
1352
|
return {
|
|
1312
1353
|
content: [
|
|
1313
1354
|
{
|
|
@@ -1328,7 +1369,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1328
1369
|
details: { viewerUrl: url },
|
|
1329
1370
|
};
|
|
1330
1371
|
}),
|
|
1331
|
-
},
|
|
1372
|
+
}),
|
|
1332
1373
|
{ name: "memory_viewer" },
|
|
1333
1374
|
);
|
|
1334
1375
|
|
|
@@ -1401,9 +1442,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1401
1442
|
name: "memory_share",
|
|
1402
1443
|
label: "Share Memory",
|
|
1403
1444
|
description:
|
|
1404
|
-
"Share an existing memory
|
|
1405
|
-
"
|
|
1406
|
-
"
|
|
1445
|
+
"Share an existing stored memory (requires a real chunkId from the database) to the Hub team, or to both targets. " +
|
|
1446
|
+
"If you want to share content from the conversation, please first retrieve the memories related to that content to obtain the correct chunkId(s), then proceed with the sharing. " +
|
|
1447
|
+
"target='agents' (default): when retrieved memories would clearly help other agents in the same OpenClaw workspace, you may share proactively without asking the user. " +
|
|
1448
|
+
"target='hub' or 'both': do not share to the team Hub without explicit user consent when the content would benefit collaborators—explain briefly, ask first, and only call hub/both after they agree (Hub must be configured). " +
|
|
1449
|
+
"To create a brand-new shared note with no existing chunk, use memory_write_public.",
|
|
1407
1450
|
parameters: Type.Object({
|
|
1408
1451
|
chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
|
|
1409
1452
|
target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
|
|
@@ -1557,7 +1600,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1557
1600
|
// ─── Tool: skill_search ───
|
|
1558
1601
|
|
|
1559
1602
|
api.registerTool(
|
|
1560
|
-
{
|
|
1603
|
+
(context) => ({
|
|
1561
1604
|
name: "skill_search",
|
|
1562
1605
|
label: "Skill Search",
|
|
1563
1606
|
description:
|
|
@@ -1567,10 +1610,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1567
1610
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1568
1611
|
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1569
1612
|
}),
|
|
1570
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any
|
|
1613
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1571
1614
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1572
1615
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1573
|
-
const
|
|
1616
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
1617
|
+
const currentOwner = `agent:${agentId}`;
|
|
1574
1618
|
|
|
1575
1619
|
if (rawScope === "group" || rawScope === "all") {
|
|
1576
1620
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1632,7 +1676,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1632
1676
|
details: { query: skillQuery, scope, hits },
|
|
1633
1677
|
};
|
|
1634
1678
|
}),
|
|
1635
|
-
},
|
|
1679
|
+
}),
|
|
1636
1680
|
{ name: "skill_search" },
|
|
1637
1681
|
);
|
|
1638
1682
|
|
|
@@ -1777,7 +1821,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1777
1821
|
if (!allowPromptInjection) return {};
|
|
1778
1822
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1779
1823
|
|
|
1780
|
-
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
1824
|
+
const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
|
|
1781
1825
|
currentAgentId = recallAgentId;
|
|
1782
1826
|
const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
|
|
1783
1827
|
ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
|
|
@@ -1789,24 +1833,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1789
1833
|
const rawPrompt = event.prompt;
|
|
1790
1834
|
ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
|
|
1791
1835
|
|
|
1792
|
-
|
|
1793
|
-
const senderTag = "Sender (untrusted metadata):";
|
|
1794
|
-
const senderPos = rawPrompt.indexOf(senderTag);
|
|
1795
|
-
if (senderPos !== -1) {
|
|
1796
|
-
const afterSender = rawPrompt.slice(senderPos);
|
|
1797
|
-
const fenceStart = afterSender.indexOf("```json");
|
|
1798
|
-
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
1799
|
-
if (fenceEnd > 0) {
|
|
1800
|
-
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
1801
|
-
} else {
|
|
1802
|
-
const firstDblNl = afterSender.indexOf("\n\n");
|
|
1803
|
-
if (firstDblNl > 0) {
|
|
1804
|
-
query = afterSender.slice(firstDblNl + 2).trim();
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
query = stripInboundMetadata(query);
|
|
1809
|
-
query = query.replace(/<[^>]+>/g, "").trim();
|
|
1836
|
+
const query = normalizeAutoRecallQuery(rawPrompt);
|
|
1810
1837
|
recallQuery = query;
|
|
1811
1838
|
|
|
1812
1839
|
if (query.length < 2) {
|
|
@@ -1988,7 +2015,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1988
2015
|
try {
|
|
1989
2016
|
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
1990
2017
|
|
|
1991
|
-
// Source 1: direct skill search based on user query
|
|
1992
2018
|
try {
|
|
1993
2019
|
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1994
2020
|
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
@@ -2000,7 +2026,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2000
2026
|
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2001
2027
|
}
|
|
2002
2028
|
|
|
2003
|
-
// Source 2: skills linked to tasks from memory hits
|
|
2004
2029
|
const taskIds = new Set<string>();
|
|
2005
2030
|
for (const h of filteredHits) {
|
|
2006
2031
|
if (h.taskId) {
|
|
@@ -2090,7 +2115,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2090
2115
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
2091
2116
|
|
|
2092
2117
|
try {
|
|
2093
|
-
const captureAgentId = hookCtx?.agentId ?? "main";
|
|
2118
|
+
const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
|
|
2094
2119
|
currentAgentId = captureAgentId;
|
|
2095
2120
|
const captureOwner = `agent:${captureAgentId}`;
|
|
2096
2121
|
const sessionKey = hookCtx?.sessionKey ?? "default";
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "MemOS Local Memory",
|
|
4
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
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.8-beta.2",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skill/memos-memory-guide"
|
|
9
9
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8-beta.2",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
],
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=18.0.0 <25.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@huggingface/transformers": "^3.8.0",
|
|
52
52
|
"@sinclair/typebox": "^0.34.48",
|
|
53
|
-
"better-sqlite3": "^12.6.
|
|
53
|
+
"better-sqlite3": "^12.6.3",
|
|
54
|
+
"posthog-node": "^5.28.0",
|
|
54
55
|
"puppeteer": "^24.38.0",
|
|
55
56
|
"semver": "^7.7.4",
|
|
56
57
|
"uuid": "^10.0.0"
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -30,6 +30,9 @@ function normalizePathForMatch(p) {
|
|
|
30
30
|
return path.resolve(p).replace(/^\\\\\?\\/, "").replace(/\\/g, "/").toLowerCase();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const nodeVersion = process.version;
|
|
34
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
35
|
+
|
|
33
36
|
console.log(`
|
|
34
37
|
${CYAN}${BOLD}┌──────────────────────────────────────────────────┐
|
|
35
38
|
│ MemOS Local Memory — postinstall setup │
|
|
@@ -37,7 +40,13 @@ ${CYAN}${BOLD}┌─────────────────────
|
|
|
37
40
|
`);
|
|
38
41
|
|
|
39
42
|
log(`Plugin dir: ${DIM}${pluginDir}${RESET}`);
|
|
40
|
-
log(`Node: ${
|
|
43
|
+
log(`Node: ${GREEN}${nodeVersion}${RESET} Platform: ${process.platform}-${process.arch}`);
|
|
44
|
+
|
|
45
|
+
if (nodeMajor >= 25) {
|
|
46
|
+
warn(`Node.js ${nodeVersion} detected. This version may have compatibility issues with native modules.`);
|
|
47
|
+
log(`Recommended: Use Node.js LTS (v20 or v22) for best compatibility.`);
|
|
48
|
+
log(`You can use nvm to switch versions: ${CYAN}nvm use 22${RESET}`);
|
|
49
|
+
}
|
|
41
50
|
|
|
42
51
|
/* ═══════════════════════════════════════════════════════════
|
|
43
52
|
* Pre-phase: Clean stale build artifacts on upgrade
|
|
@@ -61,21 +70,30 @@ function cleanStaleArtifacts() {
|
|
|
61
70
|
installedVer = pkg.version || "unknown";
|
|
62
71
|
} catch { /* ignore */ }
|
|
63
72
|
|
|
73
|
+
const nodeMajor = process.versions.node.split(".")[0];
|
|
74
|
+
const currentFingerprint = `${installedVer}+node${nodeMajor}`;
|
|
75
|
+
|
|
64
76
|
const markerPath = path.join(pluginDir, ".installed-version");
|
|
65
|
-
let
|
|
66
|
-
try {
|
|
77
|
+
let prevFingerprint = "";
|
|
78
|
+
try { prevFingerprint = fs.readFileSync(markerPath, "utf-8").trim(); } catch { /* first install */ }
|
|
79
|
+
|
|
80
|
+
const writeMarker = () => {
|
|
81
|
+
try { fs.writeFileSync(markerPath, currentFingerprint + "\n", "utf-8"); } catch { /* ignore */ }
|
|
82
|
+
};
|
|
67
83
|
|
|
68
|
-
if (
|
|
69
|
-
log(`Version unchanged (${
|
|
84
|
+
if (prevFingerprint === currentFingerprint) {
|
|
85
|
+
log(`Version unchanged (${currentFingerprint}), skipping artifact cleanup.`);
|
|
70
86
|
return;
|
|
71
87
|
}
|
|
72
88
|
|
|
73
|
-
if (
|
|
74
|
-
log(`
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
if (!prevFingerprint) {
|
|
90
|
+
log(`Fresh install: ${GREEN}${currentFingerprint}${RESET}`);
|
|
91
|
+
writeMarker();
|
|
92
|
+
return;
|
|
77
93
|
}
|
|
78
94
|
|
|
95
|
+
log(`Environment changed: ${DIM}${prevFingerprint}${RESET} → ${GREEN}${currentFingerprint}${RESET}`);
|
|
96
|
+
|
|
79
97
|
const dirsToClean = ["dist", "node_modules"];
|
|
80
98
|
let cleaned = 0;
|
|
81
99
|
for (const dir of dirsToClean) {
|
|
@@ -99,7 +117,7 @@ function cleanStaleArtifacts() {
|
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
|
|
120
|
+
writeMarker();
|
|
103
121
|
|
|
104
122
|
if (cleaned > 0) {
|
|
105
123
|
ok(`Cleaned ${cleaned} stale artifact(s). Fresh install will follow.`);
|
|
@@ -418,23 +436,39 @@ if (sqliteBindingsExist()) {
|
|
|
418
436
|
else { fail(`Rebuild completed but bindings still missing (${elapsed}s).`); fail(`Looked in: ${sqliteModulePath}/build/`); }
|
|
419
437
|
console.log(`
|
|
420
438
|
${YELLOW}${BOLD} ╔══════════════════════════════════════════════════════════════╗
|
|
421
|
-
║ ✖ better-sqlite3 native module build failed
|
|
439
|
+
║ ✖ better-sqlite3 native module build failed ║
|
|
422
440
|
╠══════════════════════════════════════════════════════════════╣${RESET}
|
|
423
|
-
${YELLOW} ║${RESET}
|
|
424
|
-
${YELLOW} ║${RESET} This plugin requires C/C++ build tools to compile
|
|
425
|
-
${YELLOW} ║${RESET} the SQLite native module on first install.
|
|
426
|
-
${YELLOW} ║${RESET}
|
|
427
|
-
${YELLOW} ║${RESET} ${BOLD}Install build tools:${RESET}
|
|
428
|
-
${YELLOW} ║${RESET}
|
|
429
|
-
${YELLOW} ║${RESET} ${CYAN}macOS:${RESET} xcode-select --install
|
|
430
|
-
${YELLOW} ║${RESET} ${CYAN}Ubuntu:${RESET} sudo apt install build-essential python3
|
|
431
|
-
${YELLOW} ║${RESET} ${CYAN}Windows:${RESET} npm install -g windows-build-tools
|
|
432
|
-
${YELLOW} ║${RESET}
|
|
433
|
-
|
|
441
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
442
|
+
${YELLOW} ║${RESET} This plugin requires C/C++ build tools to compile ${YELLOW}║${RESET}
|
|
443
|
+
${YELLOW} ║${RESET} the SQLite native module on first install. ${YELLOW}║${RESET}
|
|
444
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
445
|
+
${YELLOW} ║${RESET} ${BOLD}Install build tools:${RESET} ${YELLOW}║${RESET}
|
|
446
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
447
|
+
${YELLOW} ║${RESET} ${CYAN}macOS:${RESET} xcode-select --install ${YELLOW}║${RESET}
|
|
448
|
+
${YELLOW} ║${RESET} ${CYAN}Ubuntu:${RESET} sudo apt install build-essential python3 ${YELLOW}║${RESET}
|
|
449
|
+
${YELLOW} ║${RESET} ${CYAN}Windows:${RESET} npm install -g windows-build-tools ${YELLOW}║${RESET}
|
|
450
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}`);
|
|
451
|
+
|
|
452
|
+
if (nodeMajor >= 25) {
|
|
453
|
+
console.log(`${YELLOW} ║${RESET} ${BOLD}${RED}Node.js v25+ compatibility issue detected:${RESET} ${YELLOW}║${RESET}
|
|
454
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
455
|
+
${YELLOW} ║${RESET} better-sqlite3 may not have prebuilt binaries for Node 25. ${YELLOW}║${RESET}
|
|
456
|
+
${YELLOW} ║${RESET} ${BOLD}Recommended solutions:${RESET} ${YELLOW}║${RESET}
|
|
457
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
458
|
+
${YELLOW} ║${RESET} 1. Use Node.js LTS (v20 or v22): ${YELLOW}║${RESET}
|
|
459
|
+
${YELLOW} ║${RESET} ${GREEN}nvm install 22 && nvm use 22${RESET} ${YELLOW}║${RESET}
|
|
460
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
461
|
+
${YELLOW} ║${RESET} 2. Or use MemOS Cloud version instead: ${YELLOW}║${RESET}
|
|
462
|
+
${YELLOW} ║${RESET} ${CYAN}https://github.com/MemTensor/MemOS/tree/main/apps/memos-cloud${RESET}
|
|
463
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(`${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
467
|
+
${YELLOW} ║${RESET} ${BOLD}Then retry:${RESET} ${YELLOW}║${RESET}
|
|
434
468
|
${YELLOW} ║${RESET} ${GREEN}cd ${pluginDir}${RESET}
|
|
435
|
-
${YELLOW} ║${RESET} ${GREEN}npm rebuild better-sqlite3${RESET}
|
|
436
|
-
${YELLOW} ║${RESET} ${GREEN}openclaw gateway stop && openclaw gateway start${RESET}
|
|
437
|
-
${YELLOW} ║${RESET}
|
|
469
|
+
${YELLOW} ║${RESET} ${GREEN}npm rebuild better-sqlite3${RESET} ${YELLOW}║${RESET}
|
|
470
|
+
${YELLOW} ║${RESET} ${GREEN}openclaw gateway stop && openclaw gateway start${RESET} ${YELLOW}║${RESET}
|
|
471
|
+
${YELLOW} ║${RESET} ${YELLOW}║${RESET}
|
|
438
472
|
${YELLOW}${BOLD} ╚══════════════════════════════════════════════════════════════╝${RESET}
|
|
439
473
|
`);
|
|
440
474
|
}
|
|
@@ -73,8 +73,11 @@ Two sharing planes exist and must not be confused:
|
|
|
73
73
|
### memory_share
|
|
74
74
|
|
|
75
75
|
- **What it does:** Share an existing memory either with local OpenClaw agents, to the team, or to both.
|
|
76
|
-
- **When to call:**
|
|
77
|
-
-
|
|
76
|
+
- **When to call:**
|
|
77
|
+
- If you want to share conversation content to team or hub, first retrieve memories related to that content to obtain the right `chunkId`(s), then share.
|
|
78
|
+
- `target='agents'` (default): When those memories would clearly help other agents in the same workspace, you may share proactively without asking the user.
|
|
79
|
+
- `target='hub'` or `'both'`: Only after explicit user consent when the content would benefit collaborators—explain briefly, ask first, then call `hub`/`both` (Hub must be configured). Never silently Hub-share.
|
|
80
|
+
- **Do not use when:** You are creating a brand-new shared note with **no** existing chunk—use `memory_write_public` instead.
|
|
78
81
|
- **Parameters:**
|
|
79
82
|
- `chunkId` (string, **required**) — Existing memory chunk ID.
|
|
80
83
|
- `target` (string, optional) — `'agents'` (default), `'hub'`, or `'both'`.
|
|
@@ -8,6 +8,19 @@ import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic,
|
|
|
8
8
|
import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
|
|
9
9
|
import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a SecretInput (string | SecretRef) to a plain string.
|
|
13
|
+
* Supports env-sourced SecretRef from OpenClaw's credential system.
|
|
14
|
+
*/
|
|
15
|
+
function resolveApiKey(
|
|
16
|
+
input: string | { source: string; provider?: string; id: string } | undefined,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
if (!input) return undefined;
|
|
19
|
+
if (typeof input === "string") return input;
|
|
20
|
+
if (input.source === "env") return process.env[input.id];
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Detect provider type from provider key name or base URL.
|
|
13
26
|
*/
|
|
@@ -68,7 +81,7 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
|
68
81
|
if (!providerCfg) return undefined;
|
|
69
82
|
|
|
70
83
|
const baseUrl: string | undefined = providerCfg.baseUrl;
|
|
71
|
-
const apiKey
|
|
84
|
+
const apiKey = resolveApiKey(providerCfg.apiKey);
|
|
72
85
|
if (!baseUrl || !apiKey) return undefined;
|
|
73
86
|
|
|
74
87
|
const provider = detectProvider(providerKey, baseUrl);
|
package/src/recall/engine.ts
CHANGED
|
@@ -77,7 +77,7 @@ export class RecallEngine {
|
|
|
77
77
|
}
|
|
78
78
|
const shortTerms = [...new Set([...spaceSplit, ...cjkBigrams])];
|
|
79
79
|
const patternHits = shortTerms.length > 0
|
|
80
|
-
? this.store.patternSearch(shortTerms, { limit: candidatePool })
|
|
80
|
+
? this.store.patternSearch(shortTerms, { limit: candidatePool, ownerFilter })
|
|
81
81
|
: [];
|
|
82
82
|
const patternRanked = patternHits.map((h, i) => ({
|
|
83
83
|
id: h.chunkId,
|
package/src/shared/llm-call.ts
CHANGED
|
@@ -2,6 +2,19 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import type { SummarizerConfig, SummaryProvider, Logger, PluginContext, OpenClawAPI } from "../types";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a SecretInput (string | SecretRef) to a plain string.
|
|
7
|
+
* Supports env-sourced SecretRef from OpenClaw's credential system.
|
|
8
|
+
*/
|
|
9
|
+
function resolveApiKey(
|
|
10
|
+
input: string | { source: string; provider?: string; id: string } | undefined,
|
|
11
|
+
): string | undefined {
|
|
12
|
+
if (!input) return undefined;
|
|
13
|
+
if (typeof input === "string") return input;
|
|
14
|
+
if (input.source === "env") return process.env[input.id];
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
19
|
* Detect provider type from provider key name or base URL.
|
|
7
20
|
*/
|
|
@@ -56,7 +69,7 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
|
|
|
56
69
|
if (!providerCfg) return undefined;
|
|
57
70
|
|
|
58
71
|
const baseUrl: string | undefined = providerCfg.baseUrl;
|
|
59
|
-
const apiKey
|
|
72
|
+
const apiKey = resolveApiKey(providerCfg.apiKey);
|
|
60
73
|
if (!baseUrl || !apiKey) return undefined;
|
|
61
74
|
|
|
62
75
|
const provider = detectProvider(providerKey, baseUrl);
|