@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10

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.
Files changed (98) hide show
  1. package/.env.example +7 -0
  2. package/README.md +24 -24
  3. package/dist/capture/index.d.ts +1 -1
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +34 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +5 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +173 -14
  10. package/dist/client/connector.js.map +1 -1
  11. package/dist/client/hub.d.ts.map +1 -1
  12. package/dist/client/hub.js +22 -0
  13. package/dist/client/hub.js.map +1 -1
  14. package/dist/client/skill-sync.d.ts +7 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -1
  16. package/dist/client/skill-sync.js +10 -0
  17. package/dist/client/skill-sync.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +9 -11
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +7 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +301 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +3 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +18 -1
  28. package/dist/hub/user-manager.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +7 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest/providers/index.d.ts.map +1 -1
  33. package/dist/ingest/providers/index.js +37 -6
  34. package/dist/ingest/providers/index.js.map +1 -1
  35. package/dist/recall/engine.d.ts.map +1 -1
  36. package/dist/recall/engine.js +91 -1
  37. package/dist/recall/engine.js.map +1 -1
  38. package/dist/shared/llm-call.d.ts +1 -0
  39. package/dist/shared/llm-call.d.ts.map +1 -1
  40. package/dist/shared/llm-call.js +82 -8
  41. package/dist/shared/llm-call.js.map +1 -1
  42. package/dist/sharing/types.d.ts +1 -1
  43. package/dist/sharing/types.d.ts.map +1 -1
  44. package/dist/skill/evolver.d.ts +2 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +3 -0
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/storage/ensure-binding.d.ts +12 -0
  49. package/dist/storage/ensure-binding.d.ts.map +1 -0
  50. package/dist/storage/ensure-binding.js +53 -0
  51. package/dist/storage/ensure-binding.js.map +1 -0
  52. package/dist/storage/sqlite.d.ts +74 -20
  53. package/dist/storage/sqlite.d.ts.map +1 -1
  54. package/dist/storage/sqlite.js +301 -207
  55. package/dist/storage/sqlite.js.map +1 -1
  56. package/dist/telemetry.d.ts +12 -5
  57. package/dist/telemetry.d.ts.map +1 -1
  58. package/dist/telemetry.js +156 -40
  59. package/dist/telemetry.js.map +1 -1
  60. package/dist/tools/memory-search.d.ts +3 -1
  61. package/dist/tools/memory-search.d.ts.map +1 -1
  62. package/dist/tools/memory-search.js +3 -1
  63. package/dist/tools/memory-search.js.map +1 -1
  64. package/dist/types.d.ts +1 -2
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/viewer/html.d.ts.map +1 -1
  68. package/dist/viewer/html.js +2991 -1041
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +32 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +1122 -261
  73. package/dist/viewer/server.js.map +1 -1
  74. package/index.ts +384 -43
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +3 -2
  77. package/scripts/postinstall.cjs +1 -1
  78. package/skill/memos-memory-guide/SKILL.md +64 -26
  79. package/src/capture/index.ts +37 -1
  80. package/src/client/connector.ts +173 -16
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +9 -11
  84. package/src/hub/server.ts +285 -98
  85. package/src/hub/user-manager.ts +20 -3
  86. package/src/index.ts +10 -2
  87. package/src/ingest/providers/index.ts +41 -7
  88. package/src/recall/engine.ts +84 -1
  89. package/src/shared/llm-call.ts +97 -9
  90. package/src/sharing/types.ts +1 -1
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/storage/ensure-binding.ts +52 -0
  93. package/src/storage/sqlite.ts +310 -233
  94. package/src/telemetry.ts +172 -41
  95. package/src/tools/memory-search.ts +2 -1
  96. package/src/types.ts +1 -2
  97. package/src/viewer/html.ts +2991 -1041
  98. package/src/viewer/server.ts +984 -190
package/index.ts CHANGED
@@ -9,8 +9,10 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
9
9
  import { Type } from "@sinclair/typebox";
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
+ import { fileURLToPath } from "url";
12
13
  import { buildContext } from "./src/config";
13
14
  import type { HostModelsConfig } from "./src/openclaw-api";
15
+ import { ensureSqliteBinding } from "./src/storage/ensure-binding";
14
16
  import { SqliteStore } from "./src/storage/sqlite";
15
17
  import { Embedder } from "./src/embedding";
16
18
  import { IngestWorker } from "./src/ingest/worker";
@@ -21,7 +23,7 @@ import { ViewerServer } from "./src/viewer/server";
21
23
  import { HubServer } from "./src/hub/server";
22
24
  import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
23
25
  import { getHubStatus, connectToHub } from "./src/client/connector";
24
- import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
26
+ import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub, unpublishSkillBundleFromHub } from "./src/client/skill-sync";
25
27
  import { SkillEvolver } from "./src/skill/evolver";
26
28
  import { SkillInstaller } from "./src/skill/installer";
27
29
  import { Summarizer } from "./src/ingest/providers";
@@ -81,13 +83,20 @@ const memosLocalPlugin = {
81
83
 
82
84
  register(api: OpenClawPluginApi) {
83
85
  // ─── Ensure better-sqlite3 native module is available ───
84
- const pluginDir = path.dirname(new URL(import.meta.url).pathname);
86
+ const pluginDir = path.dirname(fileURLToPath(import.meta.url));
87
+
88
+ function normalizeFsPath(p: string): string {
89
+ return path.resolve(p).replace(/\\/g, "/").toLowerCase();
90
+ }
91
+
85
92
  let sqliteReady = false;
86
93
 
87
94
  function trySqliteLoad(): boolean {
88
95
  try {
89
96
  const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
90
- if (!resolved.startsWith(pluginDir)) {
97
+ const resolvedNorm = normalizeFsPath(resolved);
98
+ const pluginNorm = normalizeFsPath(pluginDir);
99
+ if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
91
100
  api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
92
101
  return false;
93
102
  }
@@ -196,6 +205,8 @@ const memosLocalPlugin = {
196
205
  error: (msg: string) => api.logger.warn(`[error] ${msg}`),
197
206
  }, hostModels);
198
207
 
208
+ ensureSqliteBinding(ctx.log);
209
+
199
210
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
200
211
  const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
201
212
  const worker = new IngestWorker(store, embedder, ctx);
@@ -205,6 +216,7 @@ const memosLocalPlugin = {
205
216
  const workspaceDir = api.resolvePath("~/.openclaw/workspace");
206
217
  const skillCtx = { ...ctx, workspaceDir };
207
218
  const skillEvolver = new SkillEvolver(store, engine, skillCtx);
219
+ skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
208
220
  const skillInstaller = new SkillInstaller(store, skillCtx);
209
221
 
210
222
  let pluginVersion = "0.0.0";
@@ -269,6 +281,18 @@ const memosLocalPlugin = {
269
281
  // Falls back to "main" when no hook has fired yet (single-agent setups).
270
282
  let currentAgentId = "main";
271
283
 
284
+ // ─── Check allowPromptInjection policy ───
285
+ // When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
286
+ // will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
287
+ const pluginEntry = (api.config as any)?.plugins?.entries?.[api.id];
288
+ const allowPromptInjection = pluginEntry?.hooks?.allowPromptInjection !== false;
289
+ if (!allowPromptInjection) {
290
+ api.logger.info("memos-local: allowPromptInjection=false, auto-recall disabled");
291
+ }
292
+ else {
293
+ api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
294
+ }
295
+
272
296
  const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
273
297
  async (...args: any[]) => {
274
298
  const t0 = performance.now();
@@ -280,6 +304,7 @@ const memosLocalPlugin = {
280
304
  return result;
281
305
  } catch (e) {
282
306
  ok = false;
307
+ telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
283
308
  throw e;
284
309
  } finally {
285
310
  const dur = performance.now() - t0;
@@ -301,6 +326,89 @@ const memosLocalPlugin = {
301
326
  }
302
327
  };
303
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
+
304
412
  // ─── Tool: memory_search ───
305
413
 
306
414
  api.registerTool(
@@ -309,24 +417,43 @@ const memosLocalPlugin = {
309
417
  label: "Memory Search",
310
418
  description:
311
419
  "Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
312
- "Relevant memories are automatically injected at the start of each turn, but call this tool when you need " +
313
- "to search with a different query or the auto-recalled context is insufficient. " +
314
- "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.",
315
422
  parameters: Type.Object({
316
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." })),
317
430
  }),
318
431
  execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
319
- const { query } = params as { query: string };
320
- const role = undefined;
321
- const minScore = undefined;
322
- const searchScope = "local";
323
- const searchLimit = 10;
324
- const hubAddress: string | undefined = undefined;
325
- const userToken: string | undefined = undefined;
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;
326
453
 
327
454
  const agentId = currentAgentId;
328
- const ownerFilter = [`agent:${agentId}`, "public"];
329
- const effectiveMaxResults = 10;
455
+ const ownerFilter = [getCurrentOwner(), "public"];
456
+ const effectiveMaxResults = searchLimit;
330
457
  ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
331
458
  const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
332
459
  ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
@@ -339,7 +466,7 @@ const memosLocalPlugin = {
339
466
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
340
467
  }));
341
468
 
342
- if (result.hits.length === 0) {
469
+ if (result.hits.length === 0 && searchScope === "local") {
343
470
  return {
344
471
  content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
345
472
  details: { candidates: [], meta: result.meta },
@@ -363,11 +490,13 @@ const memosLocalPlugin = {
363
490
  const indexSet = new Set(filterResult.relevant);
364
491
  filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
365
492
  ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
366
- } else {
493
+ } else if (searchScope === "local") {
367
494
  return {
368
495
  content: [{ type: "text", text: "No relevant memories found for this query." }],
369
496
  details: { candidates: rawCandidates, filtered: [], meta: result.meta },
370
497
  };
498
+ } else {
499
+ filteredHits = [];
371
500
  }
372
501
  }
373
502
 
@@ -843,7 +972,9 @@ ${detail.content}`,
843
972
  {
844
973
  name: "network_team_info",
845
974
  label: "Network Team Info",
846
- description: "Show current Hub connection status, signed-in user, role, and group memberships.",
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.",
847
978
  parameters: Type.Object({}),
848
979
  execute: trackTool("network_team_info", async () => {
849
980
  const status = await getHubStatus(store, ctx.config);
@@ -988,6 +1119,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
988
1119
  parameters: Type.Object({}),
989
1120
  execute: trackTool("memory_viewer", async () => {
990
1121
  ctx.log.debug(`memory_viewer called`);
1122
+ telemetry.trackViewerOpened();
991
1123
  const url = `http://127.0.0.1:${viewerPort}`;
992
1124
  return {
993
1125
  content: [
@@ -1018,12 +1150,13 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1018
1150
  api.registerTool(
1019
1151
  {
1020
1152
  name: "memory_write_public",
1021
- label: "Write Public Memory",
1153
+ label: "Write Local Shared Memory",
1022
1154
  description:
1023
- "Write a piece of information to public memory. Public memories are visible to all agents during memory_search. " +
1024
- "Use this for shared knowledge, team decisions, or cross-agent coordination information.",
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.",
1025
1158
  parameters: Type.Object({
1026
- content: Type.String({ description: "The content to write to public memory" }),
1159
+ content: Type.String({ description: "The content to write to local shared memory" }),
1027
1160
  summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
1028
1161
  }),
1029
1162
  execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
@@ -1068,7 +1201,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1068
1201
  }
1069
1202
 
1070
1203
  return {
1071
- content: [{ type: "text", text: `Public memory written successfully (id: ${chunkId}).` }],
1204
+ content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
1072
1205
  details: { chunkId, owner: "public" },
1073
1206
  };
1074
1207
  }),
@@ -1076,6 +1209,164 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1076
1209
  { name: "memory_write_public" },
1077
1210
  );
1078
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
+
1079
1370
  // ─── Tool: skill_search ───
1080
1371
 
1081
1372
  api.registerTool(
@@ -1083,16 +1374,16 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1083
1374
  name: "skill_search",
1084
1375
  label: "Skill Search",
1085
1376
  description:
1086
- "Search available skills by natural language. Searches local skills by default, or local + Hub skills when scope=group/all. " +
1087
- "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.",
1088
1379
  parameters: Type.Object({
1089
1380
  query: Type.String({ description: "Natural language description of the needed skill" }),
1090
- scope: Type.Optional(Type.String({ description: "Search scope: 'mix'/'self'/'public' for local search, or 'group'/'all' for local + Hub search" })),
1381
+ scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
1091
1382
  }),
1092
1383
  execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
1093
1384
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1094
1385
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1095
- const currentOwner = `agent:${currentAgentId}`;
1386
+ const currentOwner = getCurrentOwner();
1096
1387
 
1097
1388
  if (rawScope === "group" || rawScope === "all") {
1098
1389
  const [localHits, hub] = await Promise.all([
@@ -1108,7 +1399,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1108
1399
  }
1109
1400
 
1110
1401
  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")
1402
+ ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
1112
1403
  : "(none)";
1113
1404
  const hubText = hub.hits.length > 0
1114
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")
@@ -1130,7 +1421,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1130
1421
  }
1131
1422
 
1132
1423
  const text = hits.map((h, i) =>
1133
- `${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (public)" : ""}`,
1424
+ `${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
1134
1425
  ).join("\n");
1135
1426
 
1136
1427
  return {
@@ -1148,31 +1439,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1148
1439
  {
1149
1440
  name: "skill_publish",
1150
1441
  label: "Publish Skill",
1151
- description: "Make a skill public so other agents can discover and install it via skill_search.",
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.",
1152
1446
  parameters: Type.Object({
1153
1447
  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" })),
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." })),
1155
1451
  groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
1156
1452
  hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1157
1453
  userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1158
1454
  }),
1159
1455
  execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
1160
- const { skillId: pubSkillId, scope, groupId, hubAddress, userToken } = params as { skillId: string; scope?: string; groupId?: string; hubAddress?: string; userToken?: string };
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
+ };
1161
1473
  const skill = store.getSkill(pubSkillId);
1162
1474
  if (!skill) {
1163
1475
  return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
1164
1476
  }
1165
- if (scope === "public" || scope === "group") {
1166
- const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility: scope, groupId, hubAddress, userToken });
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 });
1167
1481
  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 },
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 },
1170
1484
  };
1171
1485
  }
1172
1486
  store.setSkillVisibility(pubSkillId, "public");
1173
1487
  return {
1174
- content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
1175
- details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
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 },
1176
1490
  };
1177
1491
  }),
1178
1492
  },
@@ -1185,20 +1499,46 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1185
1499
  {
1186
1500
  name: "skill_unpublish",
1187
1501
  label: "Unpublish Skill",
1188
- description: "Make a skill private. Other agents will no longer be able to discover it.",
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'.",
1189
1505
  parameters: Type.Object({
1190
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" })),
1191
1510
  }),
1192
1511
  execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
1193
- const { skillId: unpubSkillId } = params as { skillId: string };
1512
+ const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
1194
1513
  const skill = store.getSkill(unpubSkillId);
1195
1514
  if (!skill) {
1196
1515
  return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
1197
1516
  }
1198
- store.setSkillVisibility(unpubSkillId, "private");
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
+ }
1199
1539
  return {
1200
- content: [{ type: "text", text: `Skill "${skill.name}" is now private.` }],
1201
- details: { skillId: unpubSkillId, name: skill.name, visibility: "private" },
1540
+ content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
1541
+ details,
1202
1542
  };
1203
1543
  }),
1204
1544
  },
@@ -1231,6 +1571,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1231
1571
  // ─── Auto-recall: inject relevant memories before agent starts ───
1232
1572
 
1233
1573
  api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
1574
+ if (!allowPromptInjection) return {};
1234
1575
  if (!event.prompt || event.prompt.length < 3) return;
1235
1576
 
1236
1577
  const recallAgentId = hookCtx?.agentId ?? "main";
@@ -3,7 +3,7 @@
3
3
  "name": "MemOS Local Memory",
4
4
  "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary, memory_timeline, memory_viewer for layered retrieval.",
5
5
  "kind": "memory",
6
- "version": "0.1.11",
6
+ "version": "0.1.12",
7
7
  "skills": [
8
8
  "skill/memos-memory-guide"
9
9
  ],