@memtensor/memos-local-openclaw-plugin 1.0.4-beta.5 → 1.0.4-beta.7

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 (55) hide show
  1. package/README.md +23 -23
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +28 -2
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +1 -2
  7. package/dist/client/connector.d.ts.map +1 -1
  8. package/dist/client/connector.js +18 -19
  9. package/dist/client/connector.js.map +1 -1
  10. package/dist/client/hub.d.ts.map +1 -1
  11. package/dist/client/hub.js +22 -0
  12. package/dist/client/hub.js.map +1 -1
  13. package/dist/client/skill-sync.d.ts +7 -0
  14. package/dist/client/skill-sync.d.ts.map +1 -1
  15. package/dist/client/skill-sync.js +10 -0
  16. package/dist/client/skill-sync.js.map +1 -1
  17. package/dist/hub/server.d.ts.map +1 -1
  18. package/dist/hub/server.js +101 -81
  19. package/dist/hub/server.js.map +1 -1
  20. package/dist/hub/user-manager.d.ts +2 -0
  21. package/dist/hub/user-manager.d.ts.map +1 -1
  22. package/dist/hub/user-manager.js +5 -1
  23. package/dist/hub/user-manager.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/storage/sqlite.d.ts +54 -20
  28. package/dist/storage/sqlite.d.ts.map +1 -1
  29. package/dist/storage/sqlite.js +185 -101
  30. package/dist/storage/sqlite.js.map +1 -1
  31. package/dist/tools/memory-search.d.ts +3 -1
  32. package/dist/tools/memory-search.d.ts.map +1 -1
  33. package/dist/tools/memory-search.js +3 -1
  34. package/dist/tools/memory-search.js.map +1 -1
  35. package/dist/viewer/html.d.ts.map +1 -1
  36. package/dist/viewer/html.js +1619 -629
  37. package/dist/viewer/html.js.map +1 -1
  38. package/dist/viewer/server.d.ts +14 -8
  39. package/dist/viewer/server.d.ts.map +1 -1
  40. package/dist/viewer/server.js +545 -141
  41. package/dist/viewer/server.js.map +1 -1
  42. package/index.ts +355 -41
  43. package/package.json +1 -1
  44. package/skill/memos-memory-guide/SKILL.md +64 -26
  45. package/src/capture/index.ts +29 -1
  46. package/src/client/connector.ts +15 -21
  47. package/src/client/hub.ts +18 -0
  48. package/src/client/skill-sync.ts +14 -0
  49. package/src/hub/server.ts +88 -74
  50. package/src/hub/user-manager.ts +7 -3
  51. package/src/index.ts +7 -2
  52. package/src/storage/sqlite.ts +192 -122
  53. package/src/tools/memory-search.ts +2 -1
  54. package/src/viewer/html.ts +1619 -629
  55. package/src/viewer/server.ts +506 -128
package/index.ts CHANGED
@@ -22,7 +22,7 @@ import { ViewerServer } from "./src/viewer/server";
22
22
  import { HubServer } from "./src/hub/server";
23
23
  import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
24
24
  import { getHubStatus, connectToHub } from "./src/client/connector";
25
- import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
25
+ import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub, unpublishSkillBundleFromHub } from "./src/client/skill-sync";
26
26
  import { SkillEvolver } from "./src/skill/evolver";
27
27
  import { SkillInstaller } from "./src/skill/installer";
28
28
  import { Summarizer } from "./src/ingest/providers";
@@ -304,6 +304,89 @@ const memosLocalPlugin = {
304
304
  }
305
305
  };
306
306
 
307
+ const getCurrentOwner = () => `agent:${currentAgentId}`;
308
+ const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
309
+ scope === "group" || scope === "all" ? scope : "local";
310
+ const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
311
+ target === "hub" || target === "both" ? target : "agents";
312
+ const resolveMemoryUnshareTarget = (target?: string): "agents" | "hub" | "all" =>
313
+ target === "agents" || target === "hub" ? target : "all";
314
+ const resolveSkillPublishTarget = (target?: string, scope?: string): "agents" | "hub" => {
315
+ if (target === "hub") return "hub";
316
+ if (target === "agents") return "agents";
317
+ return scope === "public" || scope === "group" ? "hub" : "agents";
318
+ };
319
+ const resolveSkillHubVisibility = (visibility?: string, scope?: string): "public" | "group" =>
320
+ visibility === "group" || scope === "group" ? "group" : "public";
321
+ const resolveSkillUnpublishTarget = (target?: string): "agents" | "hub" | "all" =>
322
+ target === "hub" || target === "all" ? target : "agents";
323
+
324
+ const shareMemoryToHub = async (
325
+ chunkId: string,
326
+ input?: { visibility?: "public" | "group"; groupId?: string; hubAddress?: string; userToken?: string },
327
+ ): Promise<{ memoryId: string; visibility: "public" | "group"; groupId: string | null }> => {
328
+ const chunk = store.getChunk(chunkId);
329
+ if (!chunk) {
330
+ throw new Error(`Memory not found: ${chunkId}`);
331
+ }
332
+
333
+ const visibility = input?.visibility === "group" ? "group" : "public";
334
+ const groupId = visibility === "group" ? (input?.groupId ?? null) : null;
335
+ const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
336
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
337
+ method: "POST",
338
+ body: JSON.stringify({
339
+ memory: {
340
+ sourceChunkId: chunk.id,
341
+ role: chunk.role,
342
+ content: chunk.content,
343
+ summary: chunk.summary,
344
+ kind: chunk.kind,
345
+ groupId,
346
+ visibility,
347
+ },
348
+ }),
349
+ }) as { memoryId?: string; visibility?: "public" | "group" };
350
+
351
+ const now = Date.now();
352
+ const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
353
+ store.upsertHubMemory({
354
+ id: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
355
+ sourceChunkId: chunk.id,
356
+ sourceUserId: hubClient.userId,
357
+ role: chunk.role,
358
+ content: chunk.content,
359
+ summary: chunk.summary ?? "",
360
+ kind: chunk.kind,
361
+ groupId,
362
+ visibility,
363
+ createdAt: existing?.createdAt ?? now,
364
+ updatedAt: now,
365
+ });
366
+
367
+ return {
368
+ memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
369
+ visibility,
370
+ groupId,
371
+ };
372
+ };
373
+
374
+ const unshareMemoryFromHub = async (
375
+ chunkId: string,
376
+ input?: { hubAddress?: string; userToken?: string },
377
+ ): Promise<void> => {
378
+ const chunk = store.getChunk(chunkId);
379
+ if (!chunk) {
380
+ throw new Error(`Memory not found: ${chunkId}`);
381
+ }
382
+ const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
383
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
384
+ method: "POST",
385
+ body: JSON.stringify({ sourceChunkId: chunk.id }),
386
+ });
387
+ store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
388
+ };
389
+
307
390
  // ─── Tool: memory_search ───
308
391
 
309
392
  api.registerTool(
@@ -312,24 +395,43 @@ const memosLocalPlugin = {
312
395
  label: "Memory Search",
313
396
  description:
314
397
  "Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
315
- "Relevant memories are automatically injected at the start of each turn, but call this tool when you need " +
316
- "to search with a different query or the auto-recalled context is insufficient. " +
317
- "Pass only a short natural-language query (2-5 key words).",
398
+ "Use scope='local' for this agent plus local shared memories, or scope='group'/'all' to include Hub-shared memories. " +
399
+ "Supports optional maxResults, minScore, and role filtering when you need tighter control.",
318
400
  parameters: Type.Object({
319
401
  query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
402
+ scope: Type.Optional(Type.String({ description: "Search scope: 'local' (default), 'group', or 'all'. Use group/all to include Hub-shared memories." })),
403
+ maxResults: Type.Optional(Type.Number({ description: "Maximum results to return. Default 10, max 20." })),
404
+ minScore: Type.Optional(Type.Number({ description: "Minimum score threshold for local recall. Default 0.45, floor 0.35." })),
405
+ role: Type.Optional(Type.String({ description: "Optional local role filter: 'user', 'assistant', 'tool', or 'system'." })),
406
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
407
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
320
408
  }),
321
409
  execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
322
- const { query } = params as { query: string };
323
- const role = undefined;
324
- const minScore = undefined;
325
- const searchScope = "local";
326
- const searchLimit = 10;
327
- const hubAddress: string | undefined = undefined;
328
- const userToken: string | undefined = undefined;
410
+ const {
411
+ query,
412
+ scope: rawScope,
413
+ maxResults,
414
+ minScore: rawMinScore,
415
+ role: rawRole,
416
+ hubAddress,
417
+ userToken,
418
+ } = params as {
419
+ query: string;
420
+ scope?: string;
421
+ maxResults?: number;
422
+ minScore?: number;
423
+ role?: string;
424
+ hubAddress?: string;
425
+ userToken?: string;
426
+ };
427
+ const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
428
+ const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
429
+ const searchScope = resolveMemorySearchScope(rawScope);
430
+ const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
329
431
 
330
432
  const agentId = currentAgentId;
331
- const ownerFilter = [`agent:${agentId}`, "public"];
332
- const effectiveMaxResults = 10;
433
+ const ownerFilter = [getCurrentOwner(), "public"];
434
+ const effectiveMaxResults = searchLimit;
333
435
  ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
334
436
  const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
335
437
  ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
@@ -342,7 +444,7 @@ const memosLocalPlugin = {
342
444
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
343
445
  }));
344
446
 
345
- if (result.hits.length === 0) {
447
+ if (result.hits.length === 0 && searchScope === "local") {
346
448
  return {
347
449
  content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
348
450
  details: { candidates: [], meta: result.meta },
@@ -366,11 +468,13 @@ const memosLocalPlugin = {
366
468
  const indexSet = new Set(filterResult.relevant);
367
469
  filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
368
470
  ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
369
- } else {
471
+ } else if (searchScope === "local") {
370
472
  return {
371
473
  content: [{ type: "text", text: "No relevant memories found for this query." }],
372
474
  details: { candidates: rawCandidates, filtered: [], meta: result.meta },
373
475
  };
476
+ } else {
477
+ filteredHits = [];
374
478
  }
375
479
  }
376
480
 
@@ -846,7 +950,9 @@ ${detail.content}`,
846
950
  {
847
951
  name: "network_team_info",
848
952
  label: "Network Team Info",
849
- description: "Show current Hub connection status, signed-in user, role, and group memberships.",
953
+ description:
954
+ "Show current Hub connection status, signed-in user, role, and group memberships. " +
955
+ "Use this as a preflight check before any Hub share/unshare or Hub pull operation.",
850
956
  parameters: Type.Object({}),
851
957
  execute: trackTool("network_team_info", async () => {
852
958
  const status = await getHubStatus(store, ctx.config);
@@ -1021,12 +1127,13 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1021
1127
  api.registerTool(
1022
1128
  {
1023
1129
  name: "memory_write_public",
1024
- label: "Write Public Memory",
1130
+ label: "Write Local Shared Memory",
1025
1131
  description:
1026
- "Write a piece of information to public memory. Public memories are visible to all agents during memory_search. " +
1027
- "Use this for shared knowledge, team decisions, or cross-agent coordination information.",
1132
+ "Write a piece of information to local shared memory for all agents in this OpenClaw workspace. " +
1133
+ "Use this when you are creating a new shared note from scratch. This does not publish to Hub. " +
1134
+ "If you already have a memory chunk and want to expose it, use memory_share instead.",
1028
1135
  parameters: Type.Object({
1029
- content: Type.String({ description: "The content to write to public memory" }),
1136
+ content: Type.String({ description: "The content to write to local shared memory" }),
1030
1137
  summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
1031
1138
  }),
1032
1139
  execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
@@ -1071,7 +1178,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1071
1178
  }
1072
1179
 
1073
1180
  return {
1074
- content: [{ type: "text", text: `Public memory written successfully (id: ${chunkId}).` }],
1181
+ content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
1075
1182
  details: { chunkId, owner: "public" },
1076
1183
  };
1077
1184
  }),
@@ -1079,6 +1186,164 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1079
1186
  { name: "memory_write_public" },
1080
1187
  );
1081
1188
 
1189
+ api.registerTool(
1190
+ {
1191
+ name: "memory_share",
1192
+ label: "Share Memory",
1193
+ description:
1194
+ "Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
1195
+ "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. " +
1196
+ "If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
1197
+ parameters: Type.Object({
1198
+ chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
1199
+ target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
1200
+ visibility: Type.Optional(Type.String({ description: "Hub visibility when target includes hub: 'public' (default) or 'group'" })),
1201
+ groupId: Type.Optional(Type.String({ description: "Optional Hub group ID when visibility='group'" })),
1202
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
1203
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
1204
+ }),
1205
+ execute: trackTool("memory_share", async (_toolCallId: any, params: any) => {
1206
+ const {
1207
+ chunkId,
1208
+ target: rawTarget,
1209
+ visibility: rawVisibility,
1210
+ groupId,
1211
+ hubAddress,
1212
+ userToken,
1213
+ } = params as {
1214
+ chunkId: string;
1215
+ target?: string;
1216
+ visibility?: string;
1217
+ groupId?: string;
1218
+ hubAddress?: string;
1219
+ userToken?: string;
1220
+ };
1221
+
1222
+ const chunk = store.getChunk(chunkId);
1223
+ if (!chunk) {
1224
+ return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
1225
+ }
1226
+
1227
+ const target = resolveMemoryShareTarget(rawTarget);
1228
+ const visibility = rawVisibility === "group" ? "group" : "public";
1229
+ const details: Record<string, unknown> = { chunkId, target };
1230
+ const messages: string[] = [];
1231
+
1232
+ if (target === "agents" || target === "both") {
1233
+ const local = store.markMemorySharedLocally(chunkId);
1234
+ if (!local.ok) {
1235
+ return { content: [{ type: "text", text: `Failed to share memory ${chunkId} to local agents.` }], details: { error: local.reason ?? "local_share_failed", chunkId, target } };
1236
+ }
1237
+ details.local = {
1238
+ shared: true,
1239
+ owner: local.owner,
1240
+ originalOwner: local.originalOwner ?? null,
1241
+ };
1242
+ messages.push("shared to local agents");
1243
+ }
1244
+
1245
+ if (target === "hub" || target === "both") {
1246
+ const hub = await shareMemoryToHub(chunkId, { visibility, groupId, hubAddress, userToken });
1247
+ details.hub = {
1248
+ shared: true,
1249
+ memoryId: hub.memoryId,
1250
+ visibility: hub.visibility,
1251
+ groupId: hub.groupId,
1252
+ };
1253
+ messages.push(`shared to Hub (${hub.visibility})`);
1254
+ }
1255
+
1256
+ return {
1257
+ content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
1258
+ details,
1259
+ };
1260
+ }),
1261
+ },
1262
+ { name: "memory_share" },
1263
+ );
1264
+
1265
+ api.registerTool(
1266
+ {
1267
+ name: "memory_unshare",
1268
+ label: "Unshare Memory",
1269
+ description:
1270
+ "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. " +
1271
+ "privateOwner is only needed for older public memories that were never tracked with an original owner.",
1272
+ parameters: Type.Object({
1273
+ chunkId: Type.String({ description: "Existing local memory chunk ID to unshare" }),
1274
+ target: Type.Optional(Type.String({ description: "Unshare target: 'agents', 'hub', or 'all' (default)" })),
1275
+ privateOwner: Type.Optional(Type.String({ description: "Optional owner to restore when converting a public memory back to private and no original owner was tracked" })),
1276
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
1277
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
1278
+ }),
1279
+ execute: trackTool("memory_unshare", async (_toolCallId: any, params: any) => {
1280
+ const {
1281
+ chunkId,
1282
+ target: rawTarget,
1283
+ privateOwner,
1284
+ hubAddress,
1285
+ userToken,
1286
+ } = params as {
1287
+ chunkId: string;
1288
+ target?: string;
1289
+ privateOwner?: string;
1290
+ hubAddress?: string;
1291
+ userToken?: string;
1292
+ };
1293
+
1294
+ const chunk = store.getChunk(chunkId);
1295
+ if (!chunk) {
1296
+ return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
1297
+ }
1298
+
1299
+ const target = resolveMemoryUnshareTarget(rawTarget);
1300
+ const details: Record<string, unknown> = { chunkId, target };
1301
+ const messages: string[] = [];
1302
+
1303
+ if (target === "agents" || target === "all") {
1304
+ const local = store.unmarkMemorySharedLocally(chunkId, privateOwner);
1305
+ if (!local.ok) {
1306
+ return {
1307
+ content: [{
1308
+ type: "text",
1309
+ text: local.reason === "original_owner_missing"
1310
+ ? `Cannot restore memory "${chunk.summary || chunk.id}" to a private owner automatically. Pass privateOwner to unshare it locally.`
1311
+ : `Failed to stop local sharing for memory ${chunkId}.`,
1312
+ }],
1313
+ details: { error: local.reason ?? "local_unshare_failed", chunkId, target },
1314
+ };
1315
+ }
1316
+ details.local = {
1317
+ shared: false,
1318
+ owner: local.owner,
1319
+ };
1320
+ messages.push("removed from local agent sharing");
1321
+ }
1322
+
1323
+ if (target === "hub" || target === "all") {
1324
+ try {
1325
+ await unshareMemoryFromHub(chunkId, { hubAddress, userToken });
1326
+ details.hub = { shared: false };
1327
+ messages.push("removed from Hub");
1328
+ } catch (err) {
1329
+ const msg = err instanceof Error ? err.message : String(err);
1330
+ if (target === "all" && msg.includes("hub client connection is not configured")) {
1331
+ details.hub = { shared: false, skipped: true, reason: "hub_not_configured" };
1332
+ } else {
1333
+ throw err;
1334
+ }
1335
+ }
1336
+ }
1337
+
1338
+ return {
1339
+ content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
1340
+ details,
1341
+ };
1342
+ }),
1343
+ },
1344
+ { name: "memory_unshare" },
1345
+ );
1346
+
1082
1347
  // ─── Tool: skill_search ───
1083
1348
 
1084
1349
  api.registerTool(
@@ -1086,16 +1351,16 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1086
1351
  name: "skill_search",
1087
1352
  label: "Skill Search",
1088
1353
  description:
1089
- "Search available skills by natural language. Searches local skills by default, or local + Hub skills when scope=group/all. " +
1090
- "Use when you need a capability or guide and don't have a matching skill at hand.",
1354
+ "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. " +
1355
+ "Use this when you need a capability or guide and don't have a matching skill at hand.",
1091
1356
  parameters: Type.Object({
1092
1357
  query: Type.String({ description: "Natural language description of the needed skill" }),
1093
- scope: Type.Optional(Type.String({ description: "Search scope: 'mix'/'self'/'public' for local search, or 'group'/'all' for local + Hub search" })),
1358
+ scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
1094
1359
  }),
1095
1360
  execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
1096
1361
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1097
1362
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1098
- const currentOwner = `agent:${currentAgentId}`;
1363
+ const currentOwner = getCurrentOwner();
1099
1364
 
1100
1365
  if (rawScope === "group" || rawScope === "all") {
1101
1366
  const [localHits, hub] = await Promise.all([
@@ -1111,7 +1376,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1111
1376
  }
1112
1377
 
1113
1378
  const localText = localHits.length > 0
1114
- ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (public)" : ""}`).join("\n")
1379
+ ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
1115
1380
  : "(none)";
1116
1381
  const hubText = hub.hits.length > 0
1117
1382
  ? 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")
@@ -1133,7 +1398,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1133
1398
  }
1134
1399
 
1135
1400
  const text = hits.map((h, i) =>
1136
- `${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (public)" : ""}`,
1401
+ `${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
1137
1402
  ).join("\n");
1138
1403
 
1139
1404
  return {
@@ -1151,31 +1416,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1151
1416
  {
1152
1417
  name: "skill_publish",
1153
1418
  label: "Publish Skill",
1154
- description: "Make a skill public so other agents can discover and install it via skill_search.",
1419
+ description:
1420
+ "Share a skill with local agents or publish it to the Hub. " +
1421
+ "Use target='agents' for local sharing, or target='hub' with visibility='public'/'group' for Hub publishing. " +
1422
+ "The old scope parameter is still accepted for backward compatibility.",
1155
1423
  parameters: Type.Object({
1156
1424
  skillId: Type.String({ description: "The skill ID to publish" }),
1157
- scope: Type.Optional(Type.String({ description: "Publish scope: omit for local public, or use 'public' / 'group' to publish to Hub" })),
1425
+ target: Type.Optional(Type.String({ description: "Publish target: 'agents' (default) or 'hub'." })),
1426
+ visibility: Type.Optional(Type.String({ description: "Hub visibility when target='hub': 'public' (default) or 'group'." })),
1427
+ scope: Type.Optional(Type.String({ description: "Deprecated alias: omit for local agents, or use 'public' / 'group' to publish to Hub." })),
1158
1428
  groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
1159
1429
  hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1160
1430
  userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1161
1431
  }),
1162
1432
  execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
1163
- const { skillId: pubSkillId, scope, groupId, hubAddress, userToken } = params as { skillId: string; scope?: string; groupId?: string; hubAddress?: string; userToken?: string };
1433
+ const {
1434
+ skillId: pubSkillId,
1435
+ target: rawTarget,
1436
+ visibility: rawVisibility,
1437
+ scope,
1438
+ groupId,
1439
+ hubAddress,
1440
+ userToken,
1441
+ } = params as {
1442
+ skillId: string;
1443
+ target?: string;
1444
+ visibility?: string;
1445
+ scope?: string;
1446
+ groupId?: string;
1447
+ hubAddress?: string;
1448
+ userToken?: string;
1449
+ };
1164
1450
  const skill = store.getSkill(pubSkillId);
1165
1451
  if (!skill) {
1166
1452
  return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
1167
1453
  }
1168
- if (scope === "public" || scope === "group") {
1169
- const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility: scope, groupId, hubAddress, userToken });
1454
+ const target = resolveSkillPublishTarget(rawTarget, scope);
1455
+ const visibility = resolveSkillHubVisibility(rawVisibility, scope);
1456
+ if (target === "hub") {
1457
+ const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility, groupId, hubAddress, userToken });
1170
1458
  return {
1171
- content: [{ type: "text", text: `Skill "${skill.name}" published to hub (${published.visibility}).` }],
1172
- details: { skillId: pubSkillId, name: skill.name, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
1459
+ content: [{ type: "text", text: `Skill "${skill.name}" shared to Hub (${published.visibility}).` }],
1460
+ details: { skillId: pubSkillId, name: skill.name, target, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
1173
1461
  };
1174
1462
  }
1175
1463
  store.setSkillVisibility(pubSkillId, "public");
1176
1464
  return {
1177
- content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
1178
- details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
1465
+ content: [{ type: "text", text: `Skill "${skill.name}" is now shared with local agents.` }],
1466
+ details: { skillId: pubSkillId, name: skill.name, target, visibility: "public", publishedToHub: false },
1179
1467
  };
1180
1468
  }),
1181
1469
  },
@@ -1188,20 +1476,46 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1188
1476
  {
1189
1477
  name: "skill_unpublish",
1190
1478
  label: "Unpublish Skill",
1191
- description: "Make a skill private. Other agents will no longer be able to discover it.",
1479
+ description:
1480
+ "Stop sharing a skill with local agents, remove it from the Hub, or do both. " +
1481
+ "Use target='agents' (default), 'hub', or 'all'.",
1192
1482
  parameters: Type.Object({
1193
1483
  skillId: Type.String({ description: "The skill ID to unpublish" }),
1484
+ target: Type.Optional(Type.String({ description: "Unpublish target: 'agents' (default), 'hub', or 'all'." })),
1485
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1486
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1194
1487
  }),
1195
1488
  execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
1196
- const { skillId: unpubSkillId } = params as { skillId: string };
1489
+ const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
1197
1490
  const skill = store.getSkill(unpubSkillId);
1198
1491
  if (!skill) {
1199
1492
  return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
1200
1493
  }
1201
- store.setSkillVisibility(unpubSkillId, "private");
1494
+ const resolvedTarget = resolveSkillUnpublishTarget(target);
1495
+ const messages: string[] = [];
1496
+ const details: Record<string, unknown> = { skillId: unpubSkillId, name: skill.name, target: resolvedTarget };
1497
+ if (resolvedTarget === "hub" || resolvedTarget === "all") {
1498
+ try {
1499
+ await unpublishSkillBundleFromHub(store, ctx, { skillId: unpubSkillId, hubAddress, userToken });
1500
+ details.hub = { unpublished: true };
1501
+ messages.push("removed from Hub sharing");
1502
+ } catch (err) {
1503
+ const msg = err instanceof Error ? err.message : String(err);
1504
+ if (resolvedTarget === "all" && msg.includes("hub client connection is not configured")) {
1505
+ details.hub = { unpublished: false, skipped: true, reason: "hub_not_configured" };
1506
+ } else {
1507
+ throw err;
1508
+ }
1509
+ }
1510
+ }
1511
+ if (resolvedTarget === "agents" || resolvedTarget === "all") {
1512
+ store.setSkillVisibility(unpubSkillId, "private");
1513
+ details.local = { visibility: "private" };
1514
+ messages.push("limited to this agent");
1515
+ }
1202
1516
  return {
1203
- content: [{ type: "text", text: `Skill "${skill.name}" is now private.` }],
1204
- details: { skillId: unpubSkillId, name: skill.name, visibility: "private" },
1517
+ content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
1518
+ details,
1205
1519
  };
1206
1520
  }),
1207
1521
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.5",
3
+ "version": "1.0.4-beta.7",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",