@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.
Files changed (143) hide show
  1. package/README.md +38 -21
  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 +29 -3
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +29 -0
  7. package/dist/client/connector.d.ts.map +1 -0
  8. package/dist/client/connector.js +231 -0
  9. package/dist/client/connector.js.map +1 -0
  10. package/dist/client/hub.d.ts +61 -0
  11. package/dist/client/hub.d.ts.map +1 -0
  12. package/dist/client/hub.js +170 -0
  13. package/dist/client/hub.js.map +1 -0
  14. package/dist/client/skill-sync.d.ts +36 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -0
  16. package/dist/client/skill-sync.js +226 -0
  17. package/dist/client/skill-sync.js.map +1 -0
  18. package/dist/config.d.ts +2 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +70 -3
  21. package/dist/config.js.map +1 -1
  22. package/dist/embedding/index.d.ts +4 -2
  23. package/dist/embedding/index.d.ts.map +1 -1
  24. package/dist/embedding/index.js +17 -1
  25. package/dist/embedding/index.js.map +1 -1
  26. package/dist/hub/auth.d.ts +19 -0
  27. package/dist/hub/auth.d.ts.map +1 -0
  28. package/dist/hub/auth.js +70 -0
  29. package/dist/hub/auth.js.map +1 -0
  30. package/dist/hub/server.d.ts +48 -0
  31. package/dist/hub/server.d.ts.map +1 -0
  32. package/dist/hub/server.js +922 -0
  33. package/dist/hub/server.js.map +1 -0
  34. package/dist/hub/user-manager.d.ts +31 -0
  35. package/dist/hub/user-manager.d.ts.map +1 -0
  36. package/dist/hub/user-manager.js +129 -0
  37. package/dist/hub/user-manager.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +8 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/ingest/providers/index.d.ts +10 -2
  43. package/dist/ingest/providers/index.d.ts.map +1 -1
  44. package/dist/ingest/providers/index.js +203 -6
  45. package/dist/ingest/providers/index.js.map +1 -1
  46. package/dist/ingest/providers/openai.d.ts +1 -0
  47. package/dist/ingest/providers/openai.d.ts.map +1 -1
  48. package/dist/ingest/providers/openai.js +1 -0
  49. package/dist/ingest/providers/openai.js.map +1 -1
  50. package/dist/ingest/task-processor.js +1 -1
  51. package/dist/ingest/task-processor.js.map +1 -1
  52. package/dist/openclaw-api.d.ts +53 -0
  53. package/dist/openclaw-api.d.ts.map +1 -0
  54. package/dist/openclaw-api.js +189 -0
  55. package/dist/openclaw-api.js.map +1 -0
  56. package/dist/recall/engine.js +1 -1
  57. package/dist/recall/engine.js.map +1 -1
  58. package/dist/shared/llm-call.d.ts +4 -1
  59. package/dist/shared/llm-call.d.ts.map +1 -1
  60. package/dist/shared/llm-call.js +14 -1
  61. package/dist/shared/llm-call.js.map +1 -1
  62. package/dist/sharing/types.contract.d.ts +2 -0
  63. package/dist/sharing/types.contract.d.ts.map +1 -0
  64. package/dist/sharing/types.contract.js +3 -0
  65. package/dist/sharing/types.contract.js.map +1 -0
  66. package/dist/sharing/types.d.ts +80 -0
  67. package/dist/sharing/types.d.ts.map +1 -0
  68. package/dist/sharing/types.js +3 -0
  69. package/dist/sharing/types.js.map +1 -0
  70. package/dist/skill/evaluator.d.ts.map +1 -1
  71. package/dist/skill/evaluator.js +2 -2
  72. package/dist/skill/evaluator.js.map +1 -1
  73. package/dist/skill/generator.d.ts.map +1 -1
  74. package/dist/skill/generator.js +4 -4
  75. package/dist/skill/generator.js.map +1 -1
  76. package/dist/skill/upgrader.js +1 -1
  77. package/dist/skill/upgrader.js.map +1 -1
  78. package/dist/skill/validator.js +1 -1
  79. package/dist/skill/validator.js.map +1 -1
  80. package/dist/storage/ensure-binding.d.ts.map +1 -1
  81. package/dist/storage/ensure-binding.js +3 -1
  82. package/dist/storage/ensure-binding.js.map +1 -1
  83. package/dist/storage/sqlite.d.ts +332 -1
  84. package/dist/storage/sqlite.d.ts.map +1 -1
  85. package/dist/storage/sqlite.js +913 -4
  86. package/dist/storage/sqlite.js.map +1 -1
  87. package/dist/tools/index.d.ts +1 -0
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +3 -1
  90. package/dist/tools/index.js.map +1 -1
  91. package/dist/tools/memory-search.d.ts +5 -2
  92. package/dist/tools/memory-search.d.ts.map +1 -1
  93. package/dist/tools/memory-search.js +50 -7
  94. package/dist/tools/memory-search.js.map +1 -1
  95. package/dist/tools/network-memory-detail.d.ts +4 -0
  96. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  97. package/dist/tools/network-memory-detail.js +34 -0
  98. package/dist/tools/network-memory-detail.js.map +1 -0
  99. package/dist/types.d.ts +48 -2
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/types.js.map +1 -1
  102. package/dist/viewer/html.d.ts.map +1 -1
  103. package/dist/viewer/html.js +4299 -511
  104. package/dist/viewer/html.js.map +1 -1
  105. package/dist/viewer/server.d.ts +65 -0
  106. package/dist/viewer/server.d.ts.map +1 -1
  107. package/dist/viewer/server.js +1844 -90
  108. package/dist/viewer/server.js.map +1 -1
  109. package/index.ts +767 -41
  110. package/openclaw.plugin.json +3 -2
  111. package/package.json +3 -3
  112. package/scripts/postinstall.cjs +282 -45
  113. package/skill/memos-memory-guide/SKILL.md +82 -20
  114. package/src/capture/index.ts +30 -2
  115. package/src/client/connector.ts +225 -0
  116. package/src/client/hub.ts +207 -0
  117. package/src/client/skill-sync.ts +216 -0
  118. package/src/config.ts +92 -3
  119. package/src/embedding/index.ts +21 -1
  120. package/src/hub/auth.ts +78 -0
  121. package/src/hub/server.ts +906 -0
  122. package/src/hub/user-manager.ts +143 -0
  123. package/src/index.ts +13 -5
  124. package/src/ingest/providers/index.ts +240 -6
  125. package/src/ingest/providers/openai.ts +1 -1
  126. package/src/ingest/task-processor.ts +1 -1
  127. package/src/openclaw-api.ts +287 -0
  128. package/src/recall/engine.ts +1 -1
  129. package/src/shared/llm-call.ts +18 -2
  130. package/src/sharing/types.contract.ts +40 -0
  131. package/src/sharing/types.ts +102 -0
  132. package/src/skill/evaluator.ts +3 -2
  133. package/src/skill/generator.ts +6 -4
  134. package/src/skill/upgrader.ts +1 -1
  135. package/src/skill/validator.ts +1 -1
  136. package/src/storage/ensure-binding.ts +3 -1
  137. package/src/storage/sqlite.ts +1164 -4
  138. package/src/tools/index.ts +1 -0
  139. package/src/tools/memory-search.ts +58 -8
  140. package/src/tools/network-memory-detail.ts +34 -0
  141. package/src/types.ts +43 -2
  142. package/src/viewer/html.ts +4299 -511
  143. 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
- const pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
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
- "Relevant memories are automatically injected at the start of each turn, but call this tool when you need " +
315
- "to search with a different query or the auto-recalled context is insufficient. " +
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 { query } = params as { query: string };
322
- const role = undefined;
323
- const minScore = 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;
324
453
 
325
454
  const agentId = currentAgentId;
326
- const ownerFilter = [`agent:${agentId}`, "public"];
327
- const effectiveMaxResults = 10;
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 Public Memory",
1153
+ label: "Write Local Shared Memory",
780
1154
  description:
781
- "Write a piece of information to public memory. Public memories are visible to all agents during memory_search. " +
782
- "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.",
783
1158
  parameters: Type.Object({
784
- content: Type.String({ description: "The content to write to public memory" }),
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: `Public memory written successfully (id: ${chunkId}).` }],
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. Searches your own skills, public skills, or both. " +
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 + public), 'self' (own only), 'public' (public only)" })),
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 = `agent:${currentAgentId}`;
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" ? " (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: "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.",
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 { skillId: pubSkillId } = params as { skillId: 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
+ };
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 public.` }],
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: "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'.",
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
- 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
+ }
920
1539
  return {
921
- content: [{ type: "text", text: `Skill "${skill.name}" is now private.` }],
922
- details: { skillId: unpubSkillId, name: skill.name, visibility: "private" },
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");