@memtensor/memos-local-openclaw-plugin 1.0.4-beta.6 → 1.0.4-beta.8
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/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +29 -3
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +29 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +231 -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 +170 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +36 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +226 -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 +17 -1
- 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 +48 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +922 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +31 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +129 -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 +8 -4
- 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 +203 -6
- 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 +1 -1
- 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 +14 -1
- 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/ensure-binding.d.ts.map +1 -1
- package/dist/storage/ensure-binding.js +3 -1
- package/dist/storage/ensure-binding.js.map +1 -1
- package/dist/storage/sqlite.d.ts +332 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +913 -4
- 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 +5 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +50 -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 +48 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +4299 -511
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +65 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1844 -90
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +767 -41
- package/openclaw.plugin.json +3 -2
- package/package.json +3 -3
- package/scripts/postinstall.cjs +282 -45
- package/skill/memos-memory-guide/SKILL.md +82 -20
- package/src/capture/index.ts +30 -2
- package/src/client/connector.ts +225 -0
- package/src/client/hub.ts +207 -0
- package/src/client/skill-sync.ts +216 -0
- package/src/config.ts +92 -3
- package/src/embedding/index.ts +21 -1
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +906 -0
- package/src/hub/user-manager.ts +143 -0
- package/src/index.ts +13 -5
- package/src/ingest/providers/index.ts +240 -6
- 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 +1 -1
- package/src/shared/llm-call.ts +18 -2
- 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/ensure-binding.ts +3 -1
- package/src/storage/sqlite.ts +1164 -4
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +58 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +43 -2
- package/src/viewer/html.ts +4299 -511
- package/src/viewer/server.ts +1688 -73
package/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from "fs";
|
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { buildContext } from "./src/config";
|
|
14
|
+
import type { HostModelsConfig } from "./src/openclaw-api";
|
|
14
15
|
import { ensureSqliteBinding } from "./src/storage/ensure-binding";
|
|
15
16
|
import { SqliteStore } from "./src/storage/sqlite";
|
|
16
17
|
import { Embedder } from "./src/embedding";
|
|
@@ -19,6 +20,10 @@ import { RecallEngine } from "./src/recall/engine";
|
|
|
19
20
|
import { captureMessages, stripInboundMetadata } from "./src/capture";
|
|
20
21
|
import { DEFAULTS } from "./src/types";
|
|
21
22
|
import { ViewerServer } from "./src/viewer/server";
|
|
23
|
+
import { HubServer } from "./src/hub/server";
|
|
24
|
+
import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
|
|
25
|
+
import { getHubStatus, connectToHub } from "./src/client/connector";
|
|
26
|
+
import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub, unpublishSkillBundleFromHub } from "./src/client/skill-sync";
|
|
22
27
|
import { SkillEvolver } from "./src/skill/evolver";
|
|
23
28
|
import { SkillInstaller } from "./src/skill/installer";
|
|
24
29
|
import { Summarizer } from "./src/ingest/providers";
|
|
@@ -173,19 +178,37 @@ const memosLocalPlugin = {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
|
|
181
|
+
let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
177
182
|
const stateDir = api.resolvePath("~/.openclaw");
|
|
183
|
+
|
|
184
|
+
// Fallback: read config from file if not provided by OpenClaw
|
|
185
|
+
const configPath = path.join(stateDir, "state", "memos-local", "config.json");
|
|
186
|
+
if (Object.keys(pluginCfg).length === 0 && fs.existsSync(configPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
189
|
+
pluginCfg = fileConfig;
|
|
190
|
+
api.logger.info(`memos-local: loaded config from ${configPath}`);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
api.logger.warn(`memos-local: failed to load config from ${configPath}: ${e}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Extract host model providers so OpenClawAPIClient can proxy completion/embedding
|
|
197
|
+
const hostModels: HostModelsConfig | undefined = api.config?.models?.providers
|
|
198
|
+
? { providers: api.config.models.providers as Record<string, import("./src/openclaw-api").HostModelProvider> }
|
|
199
|
+
: undefined;
|
|
200
|
+
|
|
178
201
|
const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, {
|
|
179
202
|
debug: (msg: string) => api.logger.info(`[debug] ${msg}`),
|
|
180
203
|
info: (msg: string) => api.logger.info(msg),
|
|
181
204
|
warn: (msg: string) => api.logger.warn(msg),
|
|
182
205
|
error: (msg: string) => api.logger.warn(`[error] ${msg}`),
|
|
183
|
-
});
|
|
206
|
+
}, hostModels);
|
|
184
207
|
|
|
185
208
|
ensureSqliteBinding(ctx.log);
|
|
186
209
|
|
|
187
210
|
const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
|
|
188
|
-
const embedder = new Embedder(ctx.config.embedding, ctx.log);
|
|
211
|
+
const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
|
|
189
212
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
190
213
|
const engine = new RecallEngine(store, embedder, ctx);
|
|
191
214
|
const evidenceTag = ctx.config.capture?.evidenceWrapperTag ?? DEFAULTS.evidenceWrapperTag;
|
|
@@ -250,7 +273,7 @@ const memosLocalPlugin = {
|
|
|
250
273
|
});
|
|
251
274
|
});
|
|
252
275
|
|
|
253
|
-
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
|
|
276
|
+
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI);
|
|
254
277
|
|
|
255
278
|
api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
|
|
256
279
|
|
|
@@ -303,6 +326,89 @@ const memosLocalPlugin = {
|
|
|
303
326
|
}
|
|
304
327
|
};
|
|
305
328
|
|
|
329
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
330
|
+
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
331
|
+
scope === "group" || scope === "all" ? scope : "local";
|
|
332
|
+
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
333
|
+
target === "hub" || target === "both" ? target : "agents";
|
|
334
|
+
const resolveMemoryUnshareTarget = (target?: string): "agents" | "hub" | "all" =>
|
|
335
|
+
target === "agents" || target === "hub" ? target : "all";
|
|
336
|
+
const resolveSkillPublishTarget = (target?: string, scope?: string): "agents" | "hub" => {
|
|
337
|
+
if (target === "hub") return "hub";
|
|
338
|
+
if (target === "agents") return "agents";
|
|
339
|
+
return scope === "public" || scope === "group" ? "hub" : "agents";
|
|
340
|
+
};
|
|
341
|
+
const resolveSkillHubVisibility = (visibility?: string, scope?: string): "public" | "group" =>
|
|
342
|
+
visibility === "group" || scope === "group" ? "group" : "public";
|
|
343
|
+
const resolveSkillUnpublishTarget = (target?: string): "agents" | "hub" | "all" =>
|
|
344
|
+
target === "hub" || target === "all" ? target : "agents";
|
|
345
|
+
|
|
346
|
+
const shareMemoryToHub = async (
|
|
347
|
+
chunkId: string,
|
|
348
|
+
input?: { visibility?: "public" | "group"; groupId?: string; hubAddress?: string; userToken?: string },
|
|
349
|
+
): Promise<{ memoryId: string; visibility: "public" | "group"; groupId: string | null }> => {
|
|
350
|
+
const chunk = store.getChunk(chunkId);
|
|
351
|
+
if (!chunk) {
|
|
352
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const visibility = input?.visibility === "group" ? "group" : "public";
|
|
356
|
+
const groupId = visibility === "group" ? (input?.groupId ?? null) : null;
|
|
357
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
358
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
359
|
+
method: "POST",
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
memory: {
|
|
362
|
+
sourceChunkId: chunk.id,
|
|
363
|
+
role: chunk.role,
|
|
364
|
+
content: chunk.content,
|
|
365
|
+
summary: chunk.summary,
|
|
366
|
+
kind: chunk.kind,
|
|
367
|
+
groupId,
|
|
368
|
+
visibility,
|
|
369
|
+
},
|
|
370
|
+
}),
|
|
371
|
+
}) as { memoryId?: string; visibility?: "public" | "group" };
|
|
372
|
+
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
375
|
+
store.upsertHubMemory({
|
|
376
|
+
id: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
377
|
+
sourceChunkId: chunk.id,
|
|
378
|
+
sourceUserId: hubClient.userId,
|
|
379
|
+
role: chunk.role,
|
|
380
|
+
content: chunk.content,
|
|
381
|
+
summary: chunk.summary ?? "",
|
|
382
|
+
kind: chunk.kind,
|
|
383
|
+
groupId,
|
|
384
|
+
visibility,
|
|
385
|
+
createdAt: existing?.createdAt ?? now,
|
|
386
|
+
updatedAt: now,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
391
|
+
visibility,
|
|
392
|
+
groupId,
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const unshareMemoryFromHub = async (
|
|
397
|
+
chunkId: string,
|
|
398
|
+
input?: { hubAddress?: string; userToken?: string },
|
|
399
|
+
): Promise<void> => {
|
|
400
|
+
const chunk = store.getChunk(chunkId);
|
|
401
|
+
if (!chunk) {
|
|
402
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
403
|
+
}
|
|
404
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
405
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
406
|
+
method: "POST",
|
|
407
|
+
body: JSON.stringify({ sourceChunkId: chunk.id }),
|
|
408
|
+
});
|
|
409
|
+
store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
|
|
410
|
+
};
|
|
411
|
+
|
|
306
412
|
// ─── Tool: memory_search ───
|
|
307
413
|
|
|
308
414
|
api.registerTool(
|
|
@@ -311,20 +417,43 @@ const memosLocalPlugin = {
|
|
|
311
417
|
label: "Memory Search",
|
|
312
418
|
description:
|
|
313
419
|
"Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"Pass only a short natural-language query (2-5 key words).",
|
|
420
|
+
"Use scope='local' for this agent plus local shared memories, or scope='group'/'all' to include Hub-shared memories. " +
|
|
421
|
+
"Supports optional maxResults, minScore, and role filtering when you need tighter control.",
|
|
317
422
|
parameters: Type.Object({
|
|
318
423
|
query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
|
|
424
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'local' (default), 'group', or 'all'. Use group/all to include Hub-shared memories." })),
|
|
425
|
+
maxResults: Type.Optional(Type.Number({ description: "Maximum results to return. Default 10, max 20." })),
|
|
426
|
+
minScore: Type.Optional(Type.Number({ description: "Minimum score threshold for local recall. Default 0.45, floor 0.35." })),
|
|
427
|
+
role: Type.Optional(Type.String({ description: "Optional local role filter: 'user', 'assistant', 'tool', or 'system'." })),
|
|
428
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
429
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
319
430
|
}),
|
|
320
431
|
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
321
|
-
const {
|
|
322
|
-
|
|
323
|
-
|
|
432
|
+
const {
|
|
433
|
+
query,
|
|
434
|
+
scope: rawScope,
|
|
435
|
+
maxResults,
|
|
436
|
+
minScore: rawMinScore,
|
|
437
|
+
role: rawRole,
|
|
438
|
+
hubAddress,
|
|
439
|
+
userToken,
|
|
440
|
+
} = params as {
|
|
441
|
+
query: string;
|
|
442
|
+
scope?: string;
|
|
443
|
+
maxResults?: number;
|
|
444
|
+
minScore?: number;
|
|
445
|
+
role?: string;
|
|
446
|
+
hubAddress?: string;
|
|
447
|
+
userToken?: string;
|
|
448
|
+
};
|
|
449
|
+
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
450
|
+
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
451
|
+
const searchScope = resolveMemorySearchScope(rawScope);
|
|
452
|
+
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
324
453
|
|
|
325
454
|
const agentId = currentAgentId;
|
|
326
|
-
const ownerFilter = [
|
|
327
|
-
const effectiveMaxResults =
|
|
455
|
+
const ownerFilter = [getCurrentOwner(), "public"];
|
|
456
|
+
const effectiveMaxResults = searchLimit;
|
|
328
457
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
329
458
|
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
330
459
|
ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
|
|
@@ -337,14 +466,13 @@ const memosLocalPlugin = {
|
|
|
337
466
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
338
467
|
}));
|
|
339
468
|
|
|
340
|
-
if (result.hits.length === 0) {
|
|
469
|
+
if (result.hits.length === 0 && searchScope === "local") {
|
|
341
470
|
return {
|
|
342
471
|
content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
|
|
343
472
|
details: { candidates: [], meta: result.meta },
|
|
344
473
|
};
|
|
345
474
|
}
|
|
346
475
|
|
|
347
|
-
// LLM relevance + sufficiency filtering
|
|
348
476
|
let filteredHits = result.hits;
|
|
349
477
|
let sufficient = false;
|
|
350
478
|
|
|
@@ -362,14 +490,61 @@ const memosLocalPlugin = {
|
|
|
362
490
|
const indexSet = new Set(filterResult.relevant);
|
|
363
491
|
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
364
492
|
ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
|
|
365
|
-
} else {
|
|
493
|
+
} else if (searchScope === "local") {
|
|
366
494
|
return {
|
|
367
495
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
368
496
|
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
369
497
|
};
|
|
498
|
+
} else {
|
|
499
|
+
filteredHits = [];
|
|
370
500
|
}
|
|
371
501
|
}
|
|
372
502
|
|
|
503
|
+
const beforeDedup = filteredHits.length;
|
|
504
|
+
filteredHits = deduplicateHits(filteredHits);
|
|
505
|
+
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
506
|
+
|
|
507
|
+
const localDetailsHits = filteredHits.map((h) => {
|
|
508
|
+
let effectiveTaskId = h.taskId;
|
|
509
|
+
if (effectiveTaskId) {
|
|
510
|
+
const t = store.getTask(effectiveTaskId);
|
|
511
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
ref: h.ref,
|
|
515
|
+
chunkId: h.ref.chunkId,
|
|
516
|
+
taskId: effectiveTaskId,
|
|
517
|
+
skillId: h.skillId,
|
|
518
|
+
role: h.source.role,
|
|
519
|
+
score: h.score,
|
|
520
|
+
summary: h.summary,
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (searchScope !== "local") {
|
|
525
|
+
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
526
|
+
const localText = filteredHits.length > 0
|
|
527
|
+
? filteredHits.map((h, i) => {
|
|
528
|
+
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
529
|
+
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
530
|
+
}).join("\n")
|
|
531
|
+
: "(none)";
|
|
532
|
+
const hubText = hub.hits.length > 0
|
|
533
|
+
? hub.hits.map((h, i) => `${i + 1}. [${h.ownerName}] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
534
|
+
: "(none)";
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
content: [{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
540
|
+
}],
|
|
541
|
+
details: {
|
|
542
|
+
local: { hits: localDetailsHits, meta: result.meta },
|
|
543
|
+
hub,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
373
548
|
if (filteredHits.length === 0) {
|
|
374
549
|
return {
|
|
375
550
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
@@ -377,10 +552,6 @@ const memosLocalPlugin = {
|
|
|
377
552
|
};
|
|
378
553
|
}
|
|
379
554
|
|
|
380
|
-
const beforeDedup = filteredHits.length;
|
|
381
|
-
filteredHits = deduplicateHits(filteredHits);
|
|
382
|
-
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
383
|
-
|
|
384
555
|
const lines = filteredHits.map((h, i) => {
|
|
385
556
|
const excerpt = h.original_excerpt;
|
|
386
557
|
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
@@ -473,7 +644,7 @@ const memosLocalPlugin = {
|
|
|
473
644
|
if (!anchorChunk) {
|
|
474
645
|
return {
|
|
475
646
|
content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
|
|
476
|
-
details: { error: "not_found" },
|
|
647
|
+
details: { error: "not_found", entries: [] },
|
|
477
648
|
};
|
|
478
649
|
}
|
|
479
650
|
|
|
@@ -522,7 +693,7 @@ const memosLocalPlugin = {
|
|
|
522
693
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
523
694
|
),
|
|
524
695
|
}),
|
|
525
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
696
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
|
|
526
697
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
527
698
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
528
699
|
|
|
@@ -629,6 +800,209 @@ const memosLocalPlugin = {
|
|
|
629
800
|
{ name: "task_summary" },
|
|
630
801
|
);
|
|
631
802
|
|
|
803
|
+
// ─── Tool: task_share ───
|
|
804
|
+
|
|
805
|
+
api.registerTool(
|
|
806
|
+
{
|
|
807
|
+
name: "task_share",
|
|
808
|
+
label: "Task Share",
|
|
809
|
+
description:
|
|
810
|
+
"Share one existing local task and its chunks to the configured hub. " +
|
|
811
|
+
"Minimal MVP path for validating team task sharing.",
|
|
812
|
+
parameters: Type.Object({
|
|
813
|
+
taskId: Type.String({ description: "Local task ID to share" }),
|
|
814
|
+
visibility: Type.Optional(Type.String({ description: "Share visibility: 'public' (default) or 'group'" })),
|
|
815
|
+
groupId: Type.Optional(Type.String({ description: "Optional group ID when visibility='group'" })),
|
|
816
|
+
}),
|
|
817
|
+
execute: trackTool("task_share", async (_toolCallId: any, params: any) => {
|
|
818
|
+
const { taskId, visibility: rawVisibility, groupId } = params as {
|
|
819
|
+
taskId: string;
|
|
820
|
+
visibility?: string;
|
|
821
|
+
groupId?: string;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const task = store.getTask(taskId);
|
|
825
|
+
if (!task) {
|
|
826
|
+
return {
|
|
827
|
+
content: [{ type: "text", text: `Task not found: ${taskId}` }],
|
|
828
|
+
details: { error: "not_found", taskId },
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const chunks = store.getChunksByTask(taskId);
|
|
833
|
+
if (chunks.length === 0) {
|
|
834
|
+
return {
|
|
835
|
+
content: [{ type: "text", text: `Task ${taskId} has no chunks to share.` }],
|
|
836
|
+
details: { error: "no_chunks", taskId },
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
841
|
+
const hubClient = await resolveHubClient(store, ctx);
|
|
842
|
+
const { v4: uuidv4 } = require("uuid");
|
|
843
|
+
const hubTaskId = uuidv4();
|
|
844
|
+
|
|
845
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
846
|
+
method: "POST",
|
|
847
|
+
body: JSON.stringify({
|
|
848
|
+
task: {
|
|
849
|
+
id: hubTaskId,
|
|
850
|
+
sourceTaskId: task.id,
|
|
851
|
+
sourceUserId: hubClient.userId,
|
|
852
|
+
title: task.title,
|
|
853
|
+
summary: task.summary,
|
|
854
|
+
groupId: visibility === "group" ? (groupId ?? null) : null,
|
|
855
|
+
visibility,
|
|
856
|
+
createdAt: task.startedAt,
|
|
857
|
+
updatedAt: task.updatedAt,
|
|
858
|
+
},
|
|
859
|
+
chunks: chunks.map((chunk) => ({
|
|
860
|
+
id: uuidv4(),
|
|
861
|
+
hubTaskId,
|
|
862
|
+
sourceTaskId: task.id,
|
|
863
|
+
sourceChunkId: chunk.id,
|
|
864
|
+
sourceUserId: hubClient.userId,
|
|
865
|
+
role: chunk.role,
|
|
866
|
+
content: chunk.content,
|
|
867
|
+
summary: chunk.summary,
|
|
868
|
+
kind: chunk.kind,
|
|
869
|
+
createdAt: chunk.createdAt,
|
|
870
|
+
})),
|
|
871
|
+
}),
|
|
872
|
+
}) as any;
|
|
873
|
+
|
|
874
|
+
store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId);
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
|
|
878
|
+
details: {
|
|
879
|
+
shared: true,
|
|
880
|
+
taskId: task.id,
|
|
881
|
+
visibility,
|
|
882
|
+
chunkCount: chunks.length,
|
|
883
|
+
hubUrl: hubClient.hubUrl,
|
|
884
|
+
response,
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}),
|
|
888
|
+
},
|
|
889
|
+
{ name: "task_share" },
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
// ─── Tool: task_unshare ───
|
|
893
|
+
|
|
894
|
+
api.registerTool(
|
|
895
|
+
{
|
|
896
|
+
name: "task_unshare",
|
|
897
|
+
label: "Task Unshare",
|
|
898
|
+
description: "Remove one previously shared task from the configured hub.",
|
|
899
|
+
parameters: Type.Object({
|
|
900
|
+
taskId: Type.String({ description: "Local task ID to unshare" }),
|
|
901
|
+
}),
|
|
902
|
+
execute: trackTool("task_unshare", async (_toolCallId: any, params: any) => {
|
|
903
|
+
const { taskId } = params as { taskId: string };
|
|
904
|
+
|
|
905
|
+
const task = store.getTask(taskId);
|
|
906
|
+
if (!task) {
|
|
907
|
+
return {
|
|
908
|
+
content: [{ type: "text", text: `Task not found: ${taskId}` }],
|
|
909
|
+
details: { error: "not_found", taskId },
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const hubClient = await resolveHubClient(store, ctx);
|
|
914
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
915
|
+
method: "POST",
|
|
916
|
+
body: JSON.stringify({
|
|
917
|
+
sourceUserId: hubClient.userId,
|
|
918
|
+
sourceTaskId: task.id,
|
|
919
|
+
}),
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
store.unmarkTaskShared(task.id);
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
content: [{ type: "text", text: `Unshared task "${task.title}" from the hub.` }],
|
|
926
|
+
details: {
|
|
927
|
+
unshared: true,
|
|
928
|
+
taskId: task.id,
|
|
929
|
+
hubUrl: hubClient.hubUrl,
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}),
|
|
933
|
+
},
|
|
934
|
+
{ name: "task_unshare" },
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
api.registerTool(
|
|
938
|
+
{
|
|
939
|
+
name: "network_memory_detail",
|
|
940
|
+
label: "Network Memory Detail",
|
|
941
|
+
description: "Fetch the full detail for a Hub search hit returned by memory_search(scope=group|all).",
|
|
942
|
+
parameters: Type.Object({
|
|
943
|
+
remoteHitId: Type.String({ description: "The remoteHitId returned by a Hub search hit" }),
|
|
944
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
945
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
946
|
+
}),
|
|
947
|
+
execute: trackTool("network_memory_detail", async (_toolCallId: any, params: any) => {
|
|
948
|
+
const { remoteHitId, hubAddress, userToken } = params as {
|
|
949
|
+
remoteHitId: string;
|
|
950
|
+
hubAddress?: string;
|
|
951
|
+
userToken?: string;
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const detail = await hubGetMemoryDetail(store, ctx, { remoteHitId, hubAddress, userToken });
|
|
955
|
+
return {
|
|
956
|
+
content: [{
|
|
957
|
+
type: "text",
|
|
958
|
+
text: `## Shared Memory Detail
|
|
959
|
+
|
|
960
|
+
${detail.summary}
|
|
961
|
+
|
|
962
|
+
${detail.content}`,
|
|
963
|
+
}],
|
|
964
|
+
details: detail,
|
|
965
|
+
};
|
|
966
|
+
}),
|
|
967
|
+
},
|
|
968
|
+
{ name: "network_memory_detail" },
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
api.registerTool(
|
|
972
|
+
{
|
|
973
|
+
name: "network_team_info",
|
|
974
|
+
label: "Network Team Info",
|
|
975
|
+
description:
|
|
976
|
+
"Show current Hub connection status, signed-in user, role, and group memberships. " +
|
|
977
|
+
"Use this as a preflight check before any Hub share/unshare or Hub pull operation.",
|
|
978
|
+
parameters: Type.Object({}),
|
|
979
|
+
execute: trackTool("network_team_info", async () => {
|
|
980
|
+
const status = await getHubStatus(store, ctx.config);
|
|
981
|
+
if (!status.connected || !status.user) {
|
|
982
|
+
return {
|
|
983
|
+
content: [{ type: "text", text: "Hub is not connected." }],
|
|
984
|
+
details: status,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const groupNames = status.user.groups.map((group) => group.name);
|
|
989
|
+
return {
|
|
990
|
+
content: [{
|
|
991
|
+
type: "text",
|
|
992
|
+
text: `## Team Connection
|
|
993
|
+
|
|
994
|
+
User: ${status.user.username}
|
|
995
|
+
Role: ${status.user.role}
|
|
996
|
+
Hub: ${status.hubUrl ?? "(unknown)"}
|
|
997
|
+
Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
998
|
+
}],
|
|
999
|
+
details: status,
|
|
1000
|
+
};
|
|
1001
|
+
}),
|
|
1002
|
+
},
|
|
1003
|
+
{ name: "network_team_info" },
|
|
1004
|
+
);
|
|
1005
|
+
|
|
632
1006
|
// ─── Tool: skill_get ───
|
|
633
1007
|
|
|
634
1008
|
api.registerTool(
|
|
@@ -776,12 +1150,13 @@ const memosLocalPlugin = {
|
|
|
776
1150
|
api.registerTool(
|
|
777
1151
|
{
|
|
778
1152
|
name: "memory_write_public",
|
|
779
|
-
label: "Write
|
|
1153
|
+
label: "Write Local Shared Memory",
|
|
780
1154
|
description:
|
|
781
|
-
"Write a piece of information to
|
|
782
|
-
"Use this
|
|
1155
|
+
"Write a piece of information to local shared memory for all agents in this OpenClaw workspace. " +
|
|
1156
|
+
"Use this when you are creating a new shared note from scratch. This does not publish to Hub. " +
|
|
1157
|
+
"If you already have a memory chunk and want to expose it, use memory_share instead.",
|
|
783
1158
|
parameters: Type.Object({
|
|
784
|
-
content: Type.String({ description: "The content to write to
|
|
1159
|
+
content: Type.String({ description: "The content to write to local shared memory" }),
|
|
785
1160
|
summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
|
|
786
1161
|
}),
|
|
787
1162
|
execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
|
|
@@ -826,7 +1201,7 @@ const memosLocalPlugin = {
|
|
|
826
1201
|
}
|
|
827
1202
|
|
|
828
1203
|
return {
|
|
829
|
-
content: [{ type: "text", text: `
|
|
1204
|
+
content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
|
|
830
1205
|
details: { chunkId, owner: "public" },
|
|
831
1206
|
};
|
|
832
1207
|
}),
|
|
@@ -834,6 +1209,164 @@ const memosLocalPlugin = {
|
|
|
834
1209
|
{ name: "memory_write_public" },
|
|
835
1210
|
);
|
|
836
1211
|
|
|
1212
|
+
api.registerTool(
|
|
1213
|
+
{
|
|
1214
|
+
name: "memory_share",
|
|
1215
|
+
label: "Share Memory",
|
|
1216
|
+
description:
|
|
1217
|
+
"Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
|
|
1218
|
+
"Use this only for an existing chunkId. Use target='agents' for local multi-agent sharing, target='hub' for team sharing, or target='both' for both. " +
|
|
1219
|
+
"If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
|
|
1220
|
+
parameters: Type.Object({
|
|
1221
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
|
|
1222
|
+
target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
|
|
1223
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target includes hub: 'public' (default) or 'group'" })),
|
|
1224
|
+
groupId: Type.Optional(Type.String({ description: "Optional Hub group ID when visibility='group'" })),
|
|
1225
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1226
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1227
|
+
}),
|
|
1228
|
+
execute: trackTool("memory_share", async (_toolCallId: any, params: any) => {
|
|
1229
|
+
const {
|
|
1230
|
+
chunkId,
|
|
1231
|
+
target: rawTarget,
|
|
1232
|
+
visibility: rawVisibility,
|
|
1233
|
+
groupId,
|
|
1234
|
+
hubAddress,
|
|
1235
|
+
userToken,
|
|
1236
|
+
} = params as {
|
|
1237
|
+
chunkId: string;
|
|
1238
|
+
target?: string;
|
|
1239
|
+
visibility?: string;
|
|
1240
|
+
groupId?: string;
|
|
1241
|
+
hubAddress?: string;
|
|
1242
|
+
userToken?: string;
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
const chunk = store.getChunk(chunkId);
|
|
1246
|
+
if (!chunk) {
|
|
1247
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const target = resolveMemoryShareTarget(rawTarget);
|
|
1251
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
1252
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1253
|
+
const messages: string[] = [];
|
|
1254
|
+
|
|
1255
|
+
if (target === "agents" || target === "both") {
|
|
1256
|
+
const local = store.markMemorySharedLocally(chunkId);
|
|
1257
|
+
if (!local.ok) {
|
|
1258
|
+
return { content: [{ type: "text", text: `Failed to share memory ${chunkId} to local agents.` }], details: { error: local.reason ?? "local_share_failed", chunkId, target } };
|
|
1259
|
+
}
|
|
1260
|
+
details.local = {
|
|
1261
|
+
shared: true,
|
|
1262
|
+
owner: local.owner,
|
|
1263
|
+
originalOwner: local.originalOwner ?? null,
|
|
1264
|
+
};
|
|
1265
|
+
messages.push("shared to local agents");
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (target === "hub" || target === "both") {
|
|
1269
|
+
const hub = await shareMemoryToHub(chunkId, { visibility, groupId, hubAddress, userToken });
|
|
1270
|
+
details.hub = {
|
|
1271
|
+
shared: true,
|
|
1272
|
+
memoryId: hub.memoryId,
|
|
1273
|
+
visibility: hub.visibility,
|
|
1274
|
+
groupId: hub.groupId,
|
|
1275
|
+
};
|
|
1276
|
+
messages.push(`shared to Hub (${hub.visibility})`);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return {
|
|
1280
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1281
|
+
details,
|
|
1282
|
+
};
|
|
1283
|
+
}),
|
|
1284
|
+
},
|
|
1285
|
+
{ name: "memory_share" },
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
api.registerTool(
|
|
1289
|
+
{
|
|
1290
|
+
name: "memory_unshare",
|
|
1291
|
+
label: "Unshare Memory",
|
|
1292
|
+
description:
|
|
1293
|
+
"Remove sharing from an existing memory. Use target='agents' to stop local multi-agent sharing, target='hub' to remove it from Hub, or target='all' (default) to remove both. " +
|
|
1294
|
+
"privateOwner is only needed for older public memories that were never tracked with an original owner.",
|
|
1295
|
+
parameters: Type.Object({
|
|
1296
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to unshare" }),
|
|
1297
|
+
target: Type.Optional(Type.String({ description: "Unshare target: 'agents', 'hub', or 'all' (default)" })),
|
|
1298
|
+
privateOwner: Type.Optional(Type.String({ description: "Optional owner to restore when converting a public memory back to private and no original owner was tracked" })),
|
|
1299
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1300
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1301
|
+
}),
|
|
1302
|
+
execute: trackTool("memory_unshare", async (_toolCallId: any, params: any) => {
|
|
1303
|
+
const {
|
|
1304
|
+
chunkId,
|
|
1305
|
+
target: rawTarget,
|
|
1306
|
+
privateOwner,
|
|
1307
|
+
hubAddress,
|
|
1308
|
+
userToken,
|
|
1309
|
+
} = params as {
|
|
1310
|
+
chunkId: string;
|
|
1311
|
+
target?: string;
|
|
1312
|
+
privateOwner?: string;
|
|
1313
|
+
hubAddress?: string;
|
|
1314
|
+
userToken?: string;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const chunk = store.getChunk(chunkId);
|
|
1318
|
+
if (!chunk) {
|
|
1319
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const target = resolveMemoryUnshareTarget(rawTarget);
|
|
1323
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1324
|
+
const messages: string[] = [];
|
|
1325
|
+
|
|
1326
|
+
if (target === "agents" || target === "all") {
|
|
1327
|
+
const local = store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1328
|
+
if (!local.ok) {
|
|
1329
|
+
return {
|
|
1330
|
+
content: [{
|
|
1331
|
+
type: "text",
|
|
1332
|
+
text: local.reason === "original_owner_missing"
|
|
1333
|
+
? `Cannot restore memory "${chunk.summary || chunk.id}" to a private owner automatically. Pass privateOwner to unshare it locally.`
|
|
1334
|
+
: `Failed to stop local sharing for memory ${chunkId}.`,
|
|
1335
|
+
}],
|
|
1336
|
+
details: { error: local.reason ?? "local_unshare_failed", chunkId, target },
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
details.local = {
|
|
1340
|
+
shared: false,
|
|
1341
|
+
owner: local.owner,
|
|
1342
|
+
};
|
|
1343
|
+
messages.push("removed from local agent sharing");
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (target === "hub" || target === "all") {
|
|
1347
|
+
try {
|
|
1348
|
+
await unshareMemoryFromHub(chunkId, { hubAddress, userToken });
|
|
1349
|
+
details.hub = { shared: false };
|
|
1350
|
+
messages.push("removed from Hub");
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1353
|
+
if (target === "all" && msg.includes("hub client connection is not configured")) {
|
|
1354
|
+
details.hub = { shared: false, skipped: true, reason: "hub_not_configured" };
|
|
1355
|
+
} else {
|
|
1356
|
+
throw err;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return {
|
|
1362
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1363
|
+
details,
|
|
1364
|
+
};
|
|
1365
|
+
}),
|
|
1366
|
+
},
|
|
1367
|
+
{ name: "memory_unshare" },
|
|
1368
|
+
);
|
|
1369
|
+
|
|
837
1370
|
// ─── Tool: skill_search ───
|
|
838
1371
|
|
|
839
1372
|
api.registerTool(
|
|
@@ -841,16 +1374,42 @@ const memosLocalPlugin = {
|
|
|
841
1374
|
name: "skill_search",
|
|
842
1375
|
label: "Skill Search",
|
|
843
1376
|
description:
|
|
844
|
-
"Search available skills by natural language.
|
|
845
|
-
"Use when you need a capability or guide and don't have a matching skill at hand.",
|
|
1377
|
+
"Search available skills by natural language. Use scope='mix' (default) for this agent plus local shared skills, 'self' for this agent only, 'public' for local shared skills only, or 'group'/'all' to include Hub skills as well. " +
|
|
1378
|
+
"Use this when you need a capability or guide and don't have a matching skill at hand.",
|
|
846
1379
|
parameters: Type.Object({
|
|
847
1380
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
848
|
-
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default, self
|
|
1381
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
849
1382
|
}),
|
|
850
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1383
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
851
1384
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
852
1385
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
853
|
-
const currentOwner =
|
|
1386
|
+
const currentOwner = getCurrentOwner();
|
|
1387
|
+
|
|
1388
|
+
if (rawScope === "group" || rawScope === "all") {
|
|
1389
|
+
const [localHits, hub] = await Promise.all([
|
|
1390
|
+
engine.searchSkills(skillQuery, "mix" as any, currentOwner),
|
|
1391
|
+
hubSearchSkills(store, ctx, { query: skillQuery, maxResults: 10 }).catch(() => ({ hits: [] })),
|
|
1392
|
+
]);
|
|
1393
|
+
|
|
1394
|
+
if (localHits.length === 0 && hub.hits.length === 0) {
|
|
1395
|
+
return {
|
|
1396
|
+
content: [{ type: "text", text: `No relevant skills found for: "${skillQuery}" (scope: ${rawScope})` }],
|
|
1397
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: [] }, hub },
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const localText = localHits.length > 0
|
|
1402
|
+
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
|
|
1403
|
+
: "(none)";
|
|
1404
|
+
const hubText = hub.hits.length > 0
|
|
1405
|
+
? 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")
|
|
1406
|
+
: "(none)";
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
|
|
1410
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: localHits }, hub },
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
854
1413
|
|
|
855
1414
|
const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
|
|
856
1415
|
|
|
@@ -862,7 +1421,7 @@ const memosLocalPlugin = {
|
|
|
862
1421
|
}
|
|
863
1422
|
|
|
864
1423
|
const text = hits.map((h, i) =>
|
|
865
|
-
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (
|
|
1424
|
+
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
|
|
866
1425
|
).join("\n");
|
|
867
1426
|
|
|
868
1427
|
return {
|
|
@@ -880,20 +1439,54 @@ const memosLocalPlugin = {
|
|
|
880
1439
|
{
|
|
881
1440
|
name: "skill_publish",
|
|
882
1441
|
label: "Publish Skill",
|
|
883
|
-
description:
|
|
1442
|
+
description:
|
|
1443
|
+
"Share a skill with local agents or publish it to the Hub. " +
|
|
1444
|
+
"Use target='agents' for local sharing, or target='hub' with visibility='public'/'group' for Hub publishing. " +
|
|
1445
|
+
"The old scope parameter is still accepted for backward compatibility.",
|
|
884
1446
|
parameters: Type.Object({
|
|
885
1447
|
skillId: Type.String({ description: "The skill ID to publish" }),
|
|
1448
|
+
target: Type.Optional(Type.String({ description: "Publish target: 'agents' (default) or 'hub'." })),
|
|
1449
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target='hub': 'public' (default) or 'group'." })),
|
|
1450
|
+
scope: Type.Optional(Type.String({ description: "Deprecated alias: omit for local agents, or use 'public' / 'group' to publish to Hub." })),
|
|
1451
|
+
groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
|
|
1452
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1453
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
886
1454
|
}),
|
|
887
1455
|
execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
|
|
888
|
-
const {
|
|
1456
|
+
const {
|
|
1457
|
+
skillId: pubSkillId,
|
|
1458
|
+
target: rawTarget,
|
|
1459
|
+
visibility: rawVisibility,
|
|
1460
|
+
scope,
|
|
1461
|
+
groupId,
|
|
1462
|
+
hubAddress,
|
|
1463
|
+
userToken,
|
|
1464
|
+
} = params as {
|
|
1465
|
+
skillId: string;
|
|
1466
|
+
target?: string;
|
|
1467
|
+
visibility?: string;
|
|
1468
|
+
scope?: string;
|
|
1469
|
+
groupId?: string;
|
|
1470
|
+
hubAddress?: string;
|
|
1471
|
+
userToken?: string;
|
|
1472
|
+
};
|
|
889
1473
|
const skill = store.getSkill(pubSkillId);
|
|
890
1474
|
if (!skill) {
|
|
891
1475
|
return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
|
|
892
1476
|
}
|
|
1477
|
+
const target = resolveSkillPublishTarget(rawTarget, scope);
|
|
1478
|
+
const visibility = resolveSkillHubVisibility(rawVisibility, scope);
|
|
1479
|
+
if (target === "hub") {
|
|
1480
|
+
const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility, groupId, hubAddress, userToken });
|
|
1481
|
+
return {
|
|
1482
|
+
content: [{ type: "text", text: `Skill "${skill.name}" shared to Hub (${published.visibility}).` }],
|
|
1483
|
+
details: { skillId: pubSkillId, name: skill.name, target, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
893
1486
|
store.setSkillVisibility(pubSkillId, "public");
|
|
894
1487
|
return {
|
|
895
|
-
content: [{ type: "text", text: `Skill "${skill.name}" is now
|
|
896
|
-
details: { skillId: pubSkillId, name: skill.name, visibility: "public" },
|
|
1488
|
+
content: [{ type: "text", text: `Skill "${skill.name}" is now shared with local agents.` }],
|
|
1489
|
+
details: { skillId: pubSkillId, name: skill.name, target, visibility: "public", publishedToHub: false },
|
|
897
1490
|
};
|
|
898
1491
|
}),
|
|
899
1492
|
},
|
|
@@ -906,26 +1499,75 @@ const memosLocalPlugin = {
|
|
|
906
1499
|
{
|
|
907
1500
|
name: "skill_unpublish",
|
|
908
1501
|
label: "Unpublish Skill",
|
|
909
|
-
description:
|
|
1502
|
+
description:
|
|
1503
|
+
"Stop sharing a skill with local agents, remove it from the Hub, or do both. " +
|
|
1504
|
+
"Use target='agents' (default), 'hub', or 'all'.",
|
|
910
1505
|
parameters: Type.Object({
|
|
911
1506
|
skillId: Type.String({ description: "The skill ID to unpublish" }),
|
|
1507
|
+
target: Type.Optional(Type.String({ description: "Unpublish target: 'agents' (default), 'hub', or 'all'." })),
|
|
1508
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1509
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
912
1510
|
}),
|
|
913
1511
|
execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
|
|
914
|
-
const { skillId: unpubSkillId } = params as { skillId: string };
|
|
1512
|
+
const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
|
|
915
1513
|
const skill = store.getSkill(unpubSkillId);
|
|
916
1514
|
if (!skill) {
|
|
917
1515
|
return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
|
|
918
1516
|
}
|
|
919
|
-
|
|
1517
|
+
const resolvedTarget = resolveSkillUnpublishTarget(target);
|
|
1518
|
+
const messages: string[] = [];
|
|
1519
|
+
const details: Record<string, unknown> = { skillId: unpubSkillId, name: skill.name, target: resolvedTarget };
|
|
1520
|
+
if (resolvedTarget === "hub" || resolvedTarget === "all") {
|
|
1521
|
+
try {
|
|
1522
|
+
await unpublishSkillBundleFromHub(store, ctx, { skillId: unpubSkillId, hubAddress, userToken });
|
|
1523
|
+
details.hub = { unpublished: true };
|
|
1524
|
+
messages.push("removed from Hub sharing");
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1527
|
+
if (resolvedTarget === "all" && msg.includes("hub client connection is not configured")) {
|
|
1528
|
+
details.hub = { unpublished: false, skipped: true, reason: "hub_not_configured" };
|
|
1529
|
+
} else {
|
|
1530
|
+
throw err;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (resolvedTarget === "agents" || resolvedTarget === "all") {
|
|
1535
|
+
store.setSkillVisibility(unpubSkillId, "private");
|
|
1536
|
+
details.local = { visibility: "private" };
|
|
1537
|
+
messages.push("limited to this agent");
|
|
1538
|
+
}
|
|
920
1539
|
return {
|
|
921
|
-
content: [{ type: "text", text: `Skill "${skill.name}"
|
|
922
|
-
details
|
|
1540
|
+
content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
|
|
1541
|
+
details,
|
|
923
1542
|
};
|
|
924
1543
|
}),
|
|
925
1544
|
},
|
|
926
1545
|
{ name: "skill_unpublish" },
|
|
927
1546
|
);
|
|
928
1547
|
|
|
1548
|
+
api.registerTool(
|
|
1549
|
+
{
|
|
1550
|
+
name: "network_skill_pull",
|
|
1551
|
+
label: "Network Skill Pull",
|
|
1552
|
+
description: "Download a published Hub skill bundle and restore it into local managed skills.",
|
|
1553
|
+
parameters: Type.Object({
|
|
1554
|
+
skillId: Type.String({ description: "The Hub skill ID to pull" }),
|
|
1555
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1556
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1557
|
+
}),
|
|
1558
|
+
execute: trackTool("network_skill_pull", async (_toolCallId: any, params: any) => {
|
|
1559
|
+
const { skillId, hubAddress, userToken } = params as { skillId: string; hubAddress?: string; userToken?: string };
|
|
1560
|
+
const payload = await fetchHubSkillBundle(store, ctx, { skillId, hubAddress, userToken });
|
|
1561
|
+
const restored = restoreSkillBundleFromHub(store, ctx, payload);
|
|
1562
|
+
return {
|
|
1563
|
+
content: [{ type: "text", text: `Pulled Hub skill "${restored.localName}" into local storage.` }],
|
|
1564
|
+
details: { pulled: true, hubSkillId: skillId, localSkillId: restored.localSkillId, localName: restored.localName, dirPath: restored.dirPath },
|
|
1565
|
+
};
|
|
1566
|
+
}),
|
|
1567
|
+
},
|
|
1568
|
+
{ name: "network_skill_pull" },
|
|
1569
|
+
);
|
|
1570
|
+
|
|
929
1571
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
930
1572
|
|
|
931
1573
|
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
@@ -1231,11 +1873,74 @@ const memosLocalPlugin = {
|
|
|
1231
1873
|
worker.enqueue(captured);
|
|
1232
1874
|
telemetry.trackMemoryIngested(captured.length);
|
|
1233
1875
|
}
|
|
1876
|
+
|
|
1877
|
+
// Incremental push: sync new chunks for already-shared tasks
|
|
1878
|
+
syncSharedTasksIncremental().catch((err) => {
|
|
1879
|
+
ctx.log.warn(`incremental sync failed: ${err}`);
|
|
1880
|
+
});
|
|
1234
1881
|
} catch (err) {
|
|
1235
1882
|
api.logger.warn(`memos-local: capture failed: ${String(err)}`);
|
|
1236
1883
|
}
|
|
1237
1884
|
});
|
|
1238
1885
|
|
|
1886
|
+
async function syncSharedTasksIncremental(): Promise<void> {
|
|
1887
|
+
if (!ctx.config.sharing?.enabled || ctx.config.sharing.role !== "client") return;
|
|
1888
|
+
const shared = store.listLocalSharedTasks();
|
|
1889
|
+
if (shared.length === 0) return;
|
|
1890
|
+
|
|
1891
|
+
let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
|
|
1892
|
+
try {
|
|
1893
|
+
hubClient = await resolveHubClient(store, ctx);
|
|
1894
|
+
} catch {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const { v4: uuidv4 } = require("uuid");
|
|
1898
|
+
|
|
1899
|
+
for (const entry of shared) {
|
|
1900
|
+
const task = store.getTask(entry.taskId);
|
|
1901
|
+
if (!task) continue;
|
|
1902
|
+
const chunks = store.getChunksByTask(entry.taskId);
|
|
1903
|
+
if (chunks.length <= entry.syncedChunks) continue;
|
|
1904
|
+
|
|
1905
|
+
const newChunks = chunks.slice(entry.syncedChunks);
|
|
1906
|
+
ctx.log.info(`incremental sync: task=${entry.taskId} pushing ${newChunks.length} new chunk(s)`);
|
|
1907
|
+
|
|
1908
|
+
try {
|
|
1909
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1910
|
+
method: "POST",
|
|
1911
|
+
body: JSON.stringify({
|
|
1912
|
+
task: {
|
|
1913
|
+
id: entry.hubTaskId,
|
|
1914
|
+
sourceTaskId: entry.taskId,
|
|
1915
|
+
sourceUserId: hubClient.userId,
|
|
1916
|
+
title: task.title,
|
|
1917
|
+
summary: task.summary,
|
|
1918
|
+
groupId: entry.visibility === "group" ? entry.groupId ?? null : null,
|
|
1919
|
+
visibility: entry.visibility,
|
|
1920
|
+
createdAt: task.startedAt ?? task.updatedAt ?? Date.now(),
|
|
1921
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1922
|
+
},
|
|
1923
|
+
chunks: newChunks.map((chunk) => ({
|
|
1924
|
+
id: uuidv4(),
|
|
1925
|
+
hubTaskId: entry.hubTaskId,
|
|
1926
|
+
sourceTaskId: entry.taskId,
|
|
1927
|
+
sourceChunkId: chunk.id,
|
|
1928
|
+
sourceUserId: hubClient.userId,
|
|
1929
|
+
role: chunk.role,
|
|
1930
|
+
content: chunk.content,
|
|
1931
|
+
summary: chunk.summary,
|
|
1932
|
+
kind: chunk.kind,
|
|
1933
|
+
createdAt: chunk.createdAt,
|
|
1934
|
+
})),
|
|
1935
|
+
}),
|
|
1936
|
+
});
|
|
1937
|
+
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1239
1944
|
// ─── Memory Viewer (web UI) ───
|
|
1240
1945
|
|
|
1241
1946
|
const viewer = new ViewerServer({
|
|
@@ -1247,11 +1952,30 @@ const memosLocalPlugin = {
|
|
|
1247
1952
|
ctx,
|
|
1248
1953
|
});
|
|
1249
1954
|
|
|
1955
|
+
const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
|
|
1956
|
+
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
|
|
1957
|
+
: null;
|
|
1958
|
+
|
|
1250
1959
|
// ─── Service lifecycle ───
|
|
1251
1960
|
|
|
1252
1961
|
api.registerService({
|
|
1253
1962
|
id: "memos-local-openclaw-plugin",
|
|
1254
1963
|
start: async () => {
|
|
1964
|
+
if (hubServer) {
|
|
1965
|
+
const hubUrl = await hubServer.start();
|
|
1966
|
+
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
|
|
1970
|
+
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
1971
|
+
try {
|
|
1972
|
+
const session = await connectToHub(store, ctx.config, ctx.log);
|
|
1973
|
+
api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
api.logger.warn(`memos-local: Hub connection failed: ${err}`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1255
1979
|
try {
|
|
1256
1980
|
const viewerUrl = await viewer.start();
|
|
1257
1981
|
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
@@ -1277,7 +2001,9 @@ const memosLocalPlugin = {
|
|
|
1277
2001
|
);
|
|
1278
2002
|
},
|
|
1279
2003
|
stop: async () => {
|
|
2004
|
+
await worker.flush();
|
|
1280
2005
|
await telemetry.shutdown();
|
|
2006
|
+
await hubServer?.stop();
|
|
1281
2007
|
viewer.stop();
|
|
1282
2008
|
store.close();
|
|
1283
2009
|
api.logger.info("memos-local: stopped");
|