@memtensor/memos-local-openclaw-plugin 1.0.2 → 1.0.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -21
- package/dist/client/connector.d.ts +26 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +127 -0
- package/dist/client/connector.js.map +1 -0
- package/dist/client/hub.d.ts +61 -0
- package/dist/client/hub.d.ts.map +1 -0
- package/dist/client/hub.js +148 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +29 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +216 -0
- package/dist/client/skill-sync.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +70 -3
- package/dist/config.js.map +1 -1
- package/dist/embedding/index.d.ts +4 -2
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +21 -4
- package/dist/embedding/index.js.map +1 -1
- package/dist/hub/auth.d.ts +19 -0
- package/dist/hub/auth.d.ts.map +1 -0
- package/dist/hub/auth.js +70 -0
- package/dist/hub/auth.js.map +1 -0
- package/dist/hub/server.d.ts +41 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +742 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +28 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +112 -0
- package/dist/hub/user-manager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +10 -2
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +242 -8
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +1 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.js +1 -1
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/openclaw-api.d.ts +53 -0
- package/dist/openclaw-api.d.ts.map +1 -0
- package/dist/openclaw-api.js +189 -0
- package/dist/openclaw-api.js.map +1 -0
- package/dist/recall/engine.js +2 -2
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +4 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +15 -0
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.contract.d.ts +2 -0
- package/dist/sharing/types.contract.d.ts.map +1 -0
- package/dist/sharing/types.contract.js +3 -0
- package/dist/sharing/types.contract.js.map +1 -0
- package/dist/sharing/types.d.ts +80 -0
- package/dist/sharing/types.d.ts.map +1 -0
- package/dist/sharing/types.js +3 -0
- package/dist/sharing/types.js.map +1 -0
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +2 -2
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +4 -4
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.js +1 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.js +1 -1
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +294 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +902 -8
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +48 -7
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/network-memory-detail.d.ts +4 -0
- package/dist/tools/network-memory-detail.d.ts.map +1 -0
- package/dist/tools/network-memory-detail.js +34 -0
- package/dist/tools/network-memory-detail.js.map +1 -0
- package/dist/types.d.ts +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +0 -1
- package/dist/update-check.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2396 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +43 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1180 -33
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +445 -25
- package/openclaw.plugin.json +2 -1
- package/package.json +2 -1
- package/scripts/postinstall.cjs +282 -45
- package/skill/memos-memory-guide/SKILL.md +26 -2
- package/src/client/connector.ts +124 -0
- package/src/client/hub.ts +189 -0
- package/src/client/skill-sync.ts +202 -0
- package/src/config.ts +92 -3
- package/src/embedding/index.ts +25 -3
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +734 -0
- package/src/hub/user-manager.ts +126 -0
- package/src/index.ts +7 -4
- package/src/ingest/providers/index.ts +279 -8
- package/src/ingest/providers/openai.ts +1 -1
- package/src/ingest/task-processor.ts +1 -1
- package/src/openclaw-api.ts +287 -0
- package/src/recall/engine.ts +2 -2
- package/src/shared/llm-call.ts +19 -1
- package/src/sharing/types.contract.ts +40 -0
- package/src/sharing/types.ts +102 -0
- package/src/skill/evaluator.ts +3 -2
- package/src/skill/generator.ts +6 -4
- package/src/skill/upgrader.ts +1 -1
- package/src/skill/validator.ts +1 -1
- package/src/storage/sqlite.ts +1167 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +57 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +48 -2
- package/src/update-check.ts +0 -1
- package/src/viewer/html.ts +2396 -289
- package/src/viewer/server.ts +1087 -34
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
import { buildContext } from "./src/config";
|
|
13
|
+
import type { HostModelsConfig } from "./src/openclaw-api";
|
|
13
14
|
import { SqliteStore } from "./src/storage/sqlite";
|
|
14
15
|
import { Embedder } from "./src/embedding";
|
|
15
16
|
import { IngestWorker } from "./src/ingest/worker";
|
|
@@ -17,6 +18,10 @@ import { RecallEngine } from "./src/recall/engine";
|
|
|
17
18
|
import { captureMessages, stripInboundMetadata } from "./src/capture";
|
|
18
19
|
import { DEFAULTS } from "./src/types";
|
|
19
20
|
import { ViewerServer } from "./src/viewer/server";
|
|
21
|
+
import { HubServer } from "./src/hub/server";
|
|
22
|
+
import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
|
|
23
|
+
import { getHubStatus, connectToHub } from "./src/client/connector";
|
|
24
|
+
import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
|
|
20
25
|
import { SkillEvolver } from "./src/skill/evolver";
|
|
21
26
|
import { SkillInstaller } from "./src/skill/installer";
|
|
22
27
|
import { Summarizer } from "./src/ingest/providers";
|
|
@@ -130,13 +135,16 @@ const memosLocalPlugin = {
|
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
if (!sqliteReady) {
|
|
133
|
-
const
|
|
138
|
+
const nodeVer = process.version;
|
|
139
|
+
const nodeMajor = parseInt(process.versions?.node?.split(".")[0] ?? "0", 10);
|
|
140
|
+
const isNode25Plus = nodeMajor >= 25;
|
|
141
|
+
const lines = [
|
|
134
142
|
"",
|
|
135
143
|
"╔══════════════════════════════════════════════════════════════╗",
|
|
136
144
|
"║ MemOS Local Memory — better-sqlite3 native module missing ║",
|
|
137
145
|
"╠══════════════════════════════════════════════════════════════╣",
|
|
138
146
|
"║ ║",
|
|
139
|
-
"║ Auto-rebuild failed. Run
|
|
147
|
+
"║ Auto-rebuild failed (Node " + nodeVer + "). Run manually: ║",
|
|
140
148
|
"║ ║",
|
|
141
149
|
`║ cd ${pluginDir}`,
|
|
142
150
|
"║ npm rebuild better-sqlite3 ║",
|
|
@@ -145,28 +153,51 @@ const memosLocalPlugin = {
|
|
|
145
153
|
"║ If rebuild fails, install build tools first: ║",
|
|
146
154
|
"║ macOS: xcode-select --install ║",
|
|
147
155
|
"║ Linux: sudo apt install build-essential python3 ║",
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
""
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
];
|
|
157
|
+
if (isNode25Plus) {
|
|
158
|
+
lines.push("║ ║");
|
|
159
|
+
lines.push("║ Node 25+ has no prebuild: build tools required, or use ║");
|
|
160
|
+
lines.push("║ Node LTS (20/22): nvm install 22 && nvm use 22 ║");
|
|
161
|
+
}
|
|
162
|
+
lines.push("║ ║");
|
|
163
|
+
lines.push("╚══════════════════════════════════════════════════════════════╝");
|
|
164
|
+
lines.push("");
|
|
165
|
+
api.logger.warn(lines.join("\n"));
|
|
153
166
|
throw new Error(
|
|
154
|
-
`better-sqlite3 native module not found. Auto-rebuild failed. Fix: cd ${pluginDir} && npm rebuild better-sqlite3
|
|
167
|
+
`better-sqlite3 native module not found (Node ${nodeVer}). Auto-rebuild failed. Fix: install build tools, then cd ${pluginDir} && npm rebuild better-sqlite3. Or use Node LTS (20/22).`
|
|
155
168
|
);
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
|
|
159
|
-
|
|
172
|
+
let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
160
173
|
const stateDir = api.resolvePath("~/.openclaw");
|
|
174
|
+
|
|
175
|
+
// Fallback: read config from file if not provided by OpenClaw
|
|
176
|
+
const configPath = path.join(stateDir, "state", "memos-local", "config.json");
|
|
177
|
+
if (Object.keys(pluginCfg).length === 0 && fs.existsSync(configPath)) {
|
|
178
|
+
try {
|
|
179
|
+
const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
180
|
+
pluginCfg = fileConfig;
|
|
181
|
+
api.logger.info(`memos-local: loaded config from ${configPath}`);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
api.logger.warn(`memos-local: failed to load config from ${configPath}: ${e}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Extract host model providers so OpenClawAPIClient can proxy completion/embedding
|
|
188
|
+
const hostModels: HostModelsConfig | undefined = api.config?.models?.providers
|
|
189
|
+
? { providers: api.config.models.providers as Record<string, import("./src/openclaw-api").HostModelProvider> }
|
|
190
|
+
: undefined;
|
|
191
|
+
|
|
161
192
|
const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, {
|
|
162
193
|
debug: (msg: string) => api.logger.info(`[debug] ${msg}`),
|
|
163
194
|
info: (msg: string) => api.logger.info(msg),
|
|
164
195
|
warn: (msg: string) => api.logger.warn(msg),
|
|
165
196
|
error: (msg: string) => api.logger.warn(`[error] ${msg}`),
|
|
166
|
-
});
|
|
197
|
+
}, hostModels);
|
|
167
198
|
|
|
168
199
|
const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
|
|
169
|
-
const embedder = new Embedder(ctx.config.embedding, ctx.log);
|
|
200
|
+
const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
|
|
170
201
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
171
202
|
const engine = new RecallEngine(store, embedder, ctx);
|
|
172
203
|
const evidenceTag = ctx.config.capture?.evidenceWrapperTag ?? DEFAULTS.evidenceWrapperTag;
|
|
@@ -230,7 +261,7 @@ const memosLocalPlugin = {
|
|
|
230
261
|
});
|
|
231
262
|
});
|
|
232
263
|
|
|
233
|
-
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
|
|
264
|
+
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI);
|
|
234
265
|
|
|
235
266
|
api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
|
|
236
267
|
|
|
@@ -288,6 +319,10 @@ const memosLocalPlugin = {
|
|
|
288
319
|
const { query } = params as { query: string };
|
|
289
320
|
const role = undefined;
|
|
290
321
|
const minScore = undefined;
|
|
322
|
+
const searchScope = "local";
|
|
323
|
+
const searchLimit = 10;
|
|
324
|
+
const hubAddress: string | undefined = undefined;
|
|
325
|
+
const userToken: string | undefined = undefined;
|
|
291
326
|
|
|
292
327
|
const agentId = currentAgentId;
|
|
293
328
|
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
@@ -311,7 +346,6 @@ const memosLocalPlugin = {
|
|
|
311
346
|
};
|
|
312
347
|
}
|
|
313
348
|
|
|
314
|
-
// LLM relevance + sufficiency filtering
|
|
315
349
|
let filteredHits = result.hits;
|
|
316
350
|
let sufficient = false;
|
|
317
351
|
|
|
@@ -337,6 +371,51 @@ const memosLocalPlugin = {
|
|
|
337
371
|
}
|
|
338
372
|
}
|
|
339
373
|
|
|
374
|
+
const beforeDedup = filteredHits.length;
|
|
375
|
+
filteredHits = deduplicateHits(filteredHits);
|
|
376
|
+
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
377
|
+
|
|
378
|
+
const localDetailsHits = filteredHits.map((h) => {
|
|
379
|
+
let effectiveTaskId = h.taskId;
|
|
380
|
+
if (effectiveTaskId) {
|
|
381
|
+
const t = store.getTask(effectiveTaskId);
|
|
382
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
ref: h.ref,
|
|
386
|
+
chunkId: h.ref.chunkId,
|
|
387
|
+
taskId: effectiveTaskId,
|
|
388
|
+
skillId: h.skillId,
|
|
389
|
+
role: h.source.role,
|
|
390
|
+
score: h.score,
|
|
391
|
+
summary: h.summary,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (searchScope !== "local") {
|
|
396
|
+
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
397
|
+
const localText = filteredHits.length > 0
|
|
398
|
+
? filteredHits.map((h, i) => {
|
|
399
|
+
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
400
|
+
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
401
|
+
}).join("\n")
|
|
402
|
+
: "(none)";
|
|
403
|
+
const hubText = hub.hits.length > 0
|
|
404
|
+
? hub.hits.map((h, i) => `${i + 1}. [${h.ownerName}] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
405
|
+
: "(none)";
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
content: [{
|
|
409
|
+
type: "text",
|
|
410
|
+
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
411
|
+
}],
|
|
412
|
+
details: {
|
|
413
|
+
local: { hits: localDetailsHits, meta: result.meta },
|
|
414
|
+
hub,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
340
419
|
if (filteredHits.length === 0) {
|
|
341
420
|
return {
|
|
342
421
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
@@ -344,10 +423,6 @@ const memosLocalPlugin = {
|
|
|
344
423
|
};
|
|
345
424
|
}
|
|
346
425
|
|
|
347
|
-
const beforeDedup = filteredHits.length;
|
|
348
|
-
filteredHits = deduplicateHits(filteredHits);
|
|
349
|
-
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
350
|
-
|
|
351
426
|
const lines = filteredHits.map((h, i) => {
|
|
352
427
|
const excerpt = h.original_excerpt;
|
|
353
428
|
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
@@ -440,7 +515,7 @@ const memosLocalPlugin = {
|
|
|
440
515
|
if (!anchorChunk) {
|
|
441
516
|
return {
|
|
442
517
|
content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
|
|
443
|
-
details: { error: "not_found" },
|
|
518
|
+
details: { error: "not_found", entries: [] },
|
|
444
519
|
};
|
|
445
520
|
}
|
|
446
521
|
|
|
@@ -489,7 +564,7 @@ const memosLocalPlugin = {
|
|
|
489
564
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
490
565
|
),
|
|
491
566
|
}),
|
|
492
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
567
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
|
|
493
568
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
494
569
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
495
570
|
|
|
@@ -596,6 +671,207 @@ const memosLocalPlugin = {
|
|
|
596
671
|
{ name: "task_summary" },
|
|
597
672
|
);
|
|
598
673
|
|
|
674
|
+
// ─── Tool: task_share ───
|
|
675
|
+
|
|
676
|
+
api.registerTool(
|
|
677
|
+
{
|
|
678
|
+
name: "task_share",
|
|
679
|
+
label: "Task Share",
|
|
680
|
+
description:
|
|
681
|
+
"Share one existing local task and its chunks to the configured hub. " +
|
|
682
|
+
"Minimal MVP path for validating team task sharing.",
|
|
683
|
+
parameters: Type.Object({
|
|
684
|
+
taskId: Type.String({ description: "Local task ID to share" }),
|
|
685
|
+
visibility: Type.Optional(Type.String({ description: "Share visibility: 'public' (default) or 'group'" })),
|
|
686
|
+
groupId: Type.Optional(Type.String({ description: "Optional group ID when visibility='group'" })),
|
|
687
|
+
}),
|
|
688
|
+
execute: trackTool("task_share", async (_toolCallId: any, params: any) => {
|
|
689
|
+
const { taskId, visibility: rawVisibility, groupId } = params as {
|
|
690
|
+
taskId: string;
|
|
691
|
+
visibility?: string;
|
|
692
|
+
groupId?: string;
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const task = store.getTask(taskId);
|
|
696
|
+
if (!task) {
|
|
697
|
+
return {
|
|
698
|
+
content: [{ type: "text", text: `Task not found: ${taskId}` }],
|
|
699
|
+
details: { error: "not_found", taskId },
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const chunks = store.getChunksByTask(taskId);
|
|
704
|
+
if (chunks.length === 0) {
|
|
705
|
+
return {
|
|
706
|
+
content: [{ type: "text", text: `Task ${taskId} has no chunks to share.` }],
|
|
707
|
+
details: { error: "no_chunks", taskId },
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
712
|
+
const hubClient = await resolveHubClient(store, ctx);
|
|
713
|
+
const { v4: uuidv4 } = require("uuid");
|
|
714
|
+
const hubTaskId = uuidv4();
|
|
715
|
+
|
|
716
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
717
|
+
method: "POST",
|
|
718
|
+
body: JSON.stringify({
|
|
719
|
+
task: {
|
|
720
|
+
id: hubTaskId,
|
|
721
|
+
sourceTaskId: task.id,
|
|
722
|
+
sourceUserId: hubClient.userId,
|
|
723
|
+
title: task.title,
|
|
724
|
+
summary: task.summary,
|
|
725
|
+
groupId: visibility === "group" ? (groupId ?? null) : null,
|
|
726
|
+
visibility,
|
|
727
|
+
createdAt: task.startedAt,
|
|
728
|
+
updatedAt: task.updatedAt,
|
|
729
|
+
},
|
|
730
|
+
chunks: chunks.map((chunk) => ({
|
|
731
|
+
id: uuidv4(),
|
|
732
|
+
hubTaskId,
|
|
733
|
+
sourceTaskId: task.id,
|
|
734
|
+
sourceChunkId: chunk.id,
|
|
735
|
+
sourceUserId: hubClient.userId,
|
|
736
|
+
role: chunk.role,
|
|
737
|
+
content: chunk.content,
|
|
738
|
+
summary: chunk.summary,
|
|
739
|
+
kind: chunk.kind,
|
|
740
|
+
createdAt: chunk.createdAt,
|
|
741
|
+
})),
|
|
742
|
+
}),
|
|
743
|
+
}) as any;
|
|
744
|
+
|
|
745
|
+
store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId);
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
|
|
749
|
+
details: {
|
|
750
|
+
shared: true,
|
|
751
|
+
taskId: task.id,
|
|
752
|
+
visibility,
|
|
753
|
+
chunkCount: chunks.length,
|
|
754
|
+
hubUrl: hubClient.hubUrl,
|
|
755
|
+
response,
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}),
|
|
759
|
+
},
|
|
760
|
+
{ name: "task_share" },
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
// ─── Tool: task_unshare ───
|
|
764
|
+
|
|
765
|
+
api.registerTool(
|
|
766
|
+
{
|
|
767
|
+
name: "task_unshare",
|
|
768
|
+
label: "Task Unshare",
|
|
769
|
+
description: "Remove one previously shared task from the configured hub.",
|
|
770
|
+
parameters: Type.Object({
|
|
771
|
+
taskId: Type.String({ description: "Local task ID to unshare" }),
|
|
772
|
+
}),
|
|
773
|
+
execute: trackTool("task_unshare", async (_toolCallId: any, params: any) => {
|
|
774
|
+
const { taskId } = params as { taskId: string };
|
|
775
|
+
|
|
776
|
+
const task = store.getTask(taskId);
|
|
777
|
+
if (!task) {
|
|
778
|
+
return {
|
|
779
|
+
content: [{ type: "text", text: `Task not found: ${taskId}` }],
|
|
780
|
+
details: { error: "not_found", taskId },
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const hubClient = await resolveHubClient(store, ctx);
|
|
785
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
786
|
+
method: "POST",
|
|
787
|
+
body: JSON.stringify({
|
|
788
|
+
sourceUserId: hubClient.userId,
|
|
789
|
+
sourceTaskId: task.id,
|
|
790
|
+
}),
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
store.unmarkTaskShared(task.id);
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
content: [{ type: "text", text: `Unshared task "${task.title}" from the hub.` }],
|
|
797
|
+
details: {
|
|
798
|
+
unshared: true,
|
|
799
|
+
taskId: task.id,
|
|
800
|
+
hubUrl: hubClient.hubUrl,
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}),
|
|
804
|
+
},
|
|
805
|
+
{ name: "task_unshare" },
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
api.registerTool(
|
|
809
|
+
{
|
|
810
|
+
name: "network_memory_detail",
|
|
811
|
+
label: "Network Memory Detail",
|
|
812
|
+
description: "Fetch the full detail for a Hub search hit returned by memory_search(scope=group|all).",
|
|
813
|
+
parameters: Type.Object({
|
|
814
|
+
remoteHitId: Type.String({ description: "The remoteHitId returned by a Hub search hit" }),
|
|
815
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
816
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
817
|
+
}),
|
|
818
|
+
execute: trackTool("network_memory_detail", async (_toolCallId: any, params: any) => {
|
|
819
|
+
const { remoteHitId, hubAddress, userToken } = params as {
|
|
820
|
+
remoteHitId: string;
|
|
821
|
+
hubAddress?: string;
|
|
822
|
+
userToken?: string;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const detail = await hubGetMemoryDetail(store, ctx, { remoteHitId, hubAddress, userToken });
|
|
826
|
+
return {
|
|
827
|
+
content: [{
|
|
828
|
+
type: "text",
|
|
829
|
+
text: `## Shared Memory Detail
|
|
830
|
+
|
|
831
|
+
${detail.summary}
|
|
832
|
+
|
|
833
|
+
${detail.content}`,
|
|
834
|
+
}],
|
|
835
|
+
details: detail,
|
|
836
|
+
};
|
|
837
|
+
}),
|
|
838
|
+
},
|
|
839
|
+
{ name: "network_memory_detail" },
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
api.registerTool(
|
|
843
|
+
{
|
|
844
|
+
name: "network_team_info",
|
|
845
|
+
label: "Network Team Info",
|
|
846
|
+
description: "Show current Hub connection status, signed-in user, role, and group memberships.",
|
|
847
|
+
parameters: Type.Object({}),
|
|
848
|
+
execute: trackTool("network_team_info", async () => {
|
|
849
|
+
const status = await getHubStatus(store, ctx.config);
|
|
850
|
+
if (!status.connected || !status.user) {
|
|
851
|
+
return {
|
|
852
|
+
content: [{ type: "text", text: "Hub is not connected." }],
|
|
853
|
+
details: status,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const groupNames = status.user.groups.map((group) => group.name);
|
|
858
|
+
return {
|
|
859
|
+
content: [{
|
|
860
|
+
type: "text",
|
|
861
|
+
text: `## Team Connection
|
|
862
|
+
|
|
863
|
+
User: ${status.user.username}
|
|
864
|
+
Role: ${status.user.role}
|
|
865
|
+
Hub: ${status.hubUrl ?? "(unknown)"}
|
|
866
|
+
Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
867
|
+
}],
|
|
868
|
+
details: status,
|
|
869
|
+
};
|
|
870
|
+
}),
|
|
871
|
+
},
|
|
872
|
+
{ name: "network_team_info" },
|
|
873
|
+
);
|
|
874
|
+
|
|
599
875
|
// ─── Tool: skill_get ───
|
|
600
876
|
|
|
601
877
|
api.registerTool(
|
|
@@ -807,17 +1083,43 @@ const memosLocalPlugin = {
|
|
|
807
1083
|
name: "skill_search",
|
|
808
1084
|
label: "Skill Search",
|
|
809
1085
|
description:
|
|
810
|
-
"Search available skills by natural language. Searches
|
|
1086
|
+
"Search available skills by natural language. Searches local skills by default, or local + Hub skills when scope=group/all. " +
|
|
811
1087
|
"Use when you need a capability or guide and don't have a matching skill at hand.",
|
|
812
1088
|
parameters: Type.Object({
|
|
813
1089
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
814
|
-
scope: Type.Optional(Type.String({ description: "Search scope: 'mix'
|
|
1090
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'mix'/'self'/'public' for local search, or 'group'/'all' for local + Hub search" })),
|
|
815
1091
|
}),
|
|
816
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1092
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
817
1093
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
818
1094
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
819
1095
|
const currentOwner = `agent:${currentAgentId}`;
|
|
820
1096
|
|
|
1097
|
+
if (rawScope === "group" || rawScope === "all") {
|
|
1098
|
+
const [localHits, hub] = await Promise.all([
|
|
1099
|
+
engine.searchSkills(skillQuery, "mix" as any, currentOwner),
|
|
1100
|
+
hubSearchSkills(store, ctx, { query: skillQuery, maxResults: 10 }).catch(() => ({ hits: [] })),
|
|
1101
|
+
]);
|
|
1102
|
+
|
|
1103
|
+
if (localHits.length === 0 && hub.hits.length === 0) {
|
|
1104
|
+
return {
|
|
1105
|
+
content: [{ type: "text", text: `No relevant skills found for: "${skillQuery}" (scope: ${rawScope})` }],
|
|
1106
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: [] }, hub },
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const localText = localHits.length > 0
|
|
1111
|
+
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (public)" : ""}`).join("\n")
|
|
1112
|
+
: "(none)";
|
|
1113
|
+
const hubText = hub.hits.length > 0
|
|
1114
|
+
? hub.hits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
|
|
1115
|
+
: "(none)";
|
|
1116
|
+
|
|
1117
|
+
return {
|
|
1118
|
+
content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
|
|
1119
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: localHits }, hub },
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
821
1123
|
const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
|
|
822
1124
|
|
|
823
1125
|
if (hits.length === 0) {
|
|
@@ -849,17 +1151,28 @@ const memosLocalPlugin = {
|
|
|
849
1151
|
description: "Make a skill public so other agents can discover and install it via skill_search.",
|
|
850
1152
|
parameters: Type.Object({
|
|
851
1153
|
skillId: Type.String({ description: "The skill ID to publish" }),
|
|
1154
|
+
scope: Type.Optional(Type.String({ description: "Publish scope: omit for local public, or use 'public' / 'group' to publish to Hub" })),
|
|
1155
|
+
groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
|
|
1156
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1157
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
852
1158
|
}),
|
|
853
1159
|
execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
|
|
854
|
-
const { skillId: pubSkillId } = params as { skillId: string };
|
|
1160
|
+
const { skillId: pubSkillId, scope, groupId, hubAddress, userToken } = params as { skillId: string; scope?: string; groupId?: string; hubAddress?: string; userToken?: string };
|
|
855
1161
|
const skill = store.getSkill(pubSkillId);
|
|
856
1162
|
if (!skill) {
|
|
857
1163
|
return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
|
|
858
1164
|
}
|
|
1165
|
+
if (scope === "public" || scope === "group") {
|
|
1166
|
+
const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility: scope, groupId, hubAddress, userToken });
|
|
1167
|
+
return {
|
|
1168
|
+
content: [{ type: "text", text: `Skill "${skill.name}" published to hub (${published.visibility}).` }],
|
|
1169
|
+
details: { skillId: pubSkillId, name: skill.name, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
859
1172
|
store.setSkillVisibility(pubSkillId, "public");
|
|
860
1173
|
return {
|
|
861
1174
|
content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
|
|
862
|
-
details: { skillId: pubSkillId, name: skill.name, visibility: "public" },
|
|
1175
|
+
details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
|
|
863
1176
|
};
|
|
864
1177
|
}),
|
|
865
1178
|
},
|
|
@@ -892,6 +1205,29 @@ const memosLocalPlugin = {
|
|
|
892
1205
|
{ name: "skill_unpublish" },
|
|
893
1206
|
);
|
|
894
1207
|
|
|
1208
|
+
api.registerTool(
|
|
1209
|
+
{
|
|
1210
|
+
name: "network_skill_pull",
|
|
1211
|
+
label: "Network Skill Pull",
|
|
1212
|
+
description: "Download a published Hub skill bundle and restore it into local managed skills.",
|
|
1213
|
+
parameters: Type.Object({
|
|
1214
|
+
skillId: Type.String({ description: "The Hub skill ID to pull" }),
|
|
1215
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1216
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1217
|
+
}),
|
|
1218
|
+
execute: trackTool("network_skill_pull", async (_toolCallId: any, params: any) => {
|
|
1219
|
+
const { skillId, hubAddress, userToken } = params as { skillId: string; hubAddress?: string; userToken?: string };
|
|
1220
|
+
const payload = await fetchHubSkillBundle(store, ctx, { skillId, hubAddress, userToken });
|
|
1221
|
+
const restored = restoreSkillBundleFromHub(store, ctx, payload);
|
|
1222
|
+
return {
|
|
1223
|
+
content: [{ type: "text", text: `Pulled Hub skill "${restored.localName}" into local storage.` }],
|
|
1224
|
+
details: { pulled: true, hubSkillId: skillId, localSkillId: restored.localSkillId, localName: restored.localName, dirPath: restored.dirPath },
|
|
1225
|
+
};
|
|
1226
|
+
}),
|
|
1227
|
+
},
|
|
1228
|
+
{ name: "network_skill_pull" },
|
|
1229
|
+
);
|
|
1230
|
+
|
|
895
1231
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
896
1232
|
|
|
897
1233
|
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
@@ -1194,13 +1530,76 @@ const memosLocalPlugin = {
|
|
|
1194
1530
|
|
|
1195
1531
|
if (captured.length > 0) {
|
|
1196
1532
|
worker.enqueue(captured);
|
|
1197
|
-
telemetry.trackMemoryIngested(
|
|
1533
|
+
telemetry.trackMemoryIngested(captured.length);
|
|
1198
1534
|
}
|
|
1535
|
+
|
|
1536
|
+
// Incremental push: sync new chunks for already-shared tasks
|
|
1537
|
+
syncSharedTasksIncremental().catch((err) => {
|
|
1538
|
+
ctx.log.warn(`incremental sync failed: ${err}`);
|
|
1539
|
+
});
|
|
1199
1540
|
} catch (err) {
|
|
1200
1541
|
api.logger.warn(`memos-local: capture failed: ${String(err)}`);
|
|
1201
1542
|
}
|
|
1202
1543
|
});
|
|
1203
1544
|
|
|
1545
|
+
async function syncSharedTasksIncremental(): Promise<void> {
|
|
1546
|
+
if (!ctx.config.sharing?.enabled || ctx.config.sharing.role !== "client") return;
|
|
1547
|
+
const shared = store.listLocalSharedTasks();
|
|
1548
|
+
if (shared.length === 0) return;
|
|
1549
|
+
|
|
1550
|
+
let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
|
|
1551
|
+
try {
|
|
1552
|
+
hubClient = await resolveHubClient(store, ctx);
|
|
1553
|
+
} catch {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const { v4: uuidv4 } = require("uuid");
|
|
1557
|
+
|
|
1558
|
+
for (const entry of shared) {
|
|
1559
|
+
const task = store.getTask(entry.taskId);
|
|
1560
|
+
if (!task) continue;
|
|
1561
|
+
const chunks = store.getChunksByTask(entry.taskId);
|
|
1562
|
+
if (chunks.length <= entry.syncedChunks) continue;
|
|
1563
|
+
|
|
1564
|
+
const newChunks = chunks.slice(entry.syncedChunks);
|
|
1565
|
+
ctx.log.info(`incremental sync: task=${entry.taskId} pushing ${newChunks.length} new chunk(s)`);
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1569
|
+
method: "POST",
|
|
1570
|
+
body: JSON.stringify({
|
|
1571
|
+
task: {
|
|
1572
|
+
id: entry.hubTaskId,
|
|
1573
|
+
sourceTaskId: entry.taskId,
|
|
1574
|
+
sourceUserId: hubClient.userId,
|
|
1575
|
+
title: task.title,
|
|
1576
|
+
summary: task.summary,
|
|
1577
|
+
groupId: entry.visibility === "group" ? entry.groupId ?? null : null,
|
|
1578
|
+
visibility: entry.visibility,
|
|
1579
|
+
createdAt: task.startedAt ?? task.updatedAt ?? Date.now(),
|
|
1580
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1581
|
+
},
|
|
1582
|
+
chunks: newChunks.map((chunk) => ({
|
|
1583
|
+
id: uuidv4(),
|
|
1584
|
+
hubTaskId: entry.hubTaskId,
|
|
1585
|
+
sourceTaskId: entry.taskId,
|
|
1586
|
+
sourceChunkId: chunk.id,
|
|
1587
|
+
sourceUserId: hubClient.userId,
|
|
1588
|
+
role: chunk.role,
|
|
1589
|
+
content: chunk.content,
|
|
1590
|
+
summary: chunk.summary,
|
|
1591
|
+
kind: chunk.kind,
|
|
1592
|
+
createdAt: chunk.createdAt,
|
|
1593
|
+
})),
|
|
1594
|
+
}),
|
|
1595
|
+
});
|
|
1596
|
+
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1204
1603
|
// ─── Memory Viewer (web UI) ───
|
|
1205
1604
|
|
|
1206
1605
|
const viewer = new ViewerServer({
|
|
@@ -1212,11 +1611,30 @@ const memosLocalPlugin = {
|
|
|
1212
1611
|
ctx,
|
|
1213
1612
|
});
|
|
1214
1613
|
|
|
1614
|
+
const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
|
|
1615
|
+
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
|
|
1616
|
+
: null;
|
|
1617
|
+
|
|
1215
1618
|
// ─── Service lifecycle ───
|
|
1216
1619
|
|
|
1217
1620
|
api.registerService({
|
|
1218
1621
|
id: "memos-local-openclaw-plugin",
|
|
1219
1622
|
start: async () => {
|
|
1623
|
+
if (hubServer) {
|
|
1624
|
+
const hubUrl = await hubServer.start();
|
|
1625
|
+
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
|
|
1629
|
+
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
1630
|
+
try {
|
|
1631
|
+
const session = await connectToHub(store, ctx.config, ctx.log);
|
|
1632
|
+
api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
api.logger.warn(`memos-local: Hub connection failed: ${err}`);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1220
1638
|
try {
|
|
1221
1639
|
const viewerUrl = await viewer.start();
|
|
1222
1640
|
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
@@ -1242,7 +1660,9 @@ const memosLocalPlugin = {
|
|
|
1242
1660
|
);
|
|
1243
1661
|
},
|
|
1244
1662
|
stop: async () => {
|
|
1663
|
+
await worker.flush();
|
|
1245
1664
|
await telemetry.shutdown();
|
|
1665
|
+
await hubServer?.stop();
|
|
1246
1666
|
viewer.stop();
|
|
1247
1667
|
store.close();
|
|
1248
1668
|
api.logger.info("memos-local: stopped");
|
package/openclaw.plugin.json
CHANGED
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.4-beta.0",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/better-sqlite3": "^7.6.12",
|
|
60
60
|
"@types/node": "^22.10.0",
|
|
61
|
+
"@types/semver": "^7.7.1",
|
|
61
62
|
"@types/uuid": "^10.0.0",
|
|
62
63
|
"tsx": "^4.21.0",
|
|
63
64
|
"typescript": "^5.7.0",
|