@memtensor/memos-local-openclaw-plugin 1.0.3 → 1.0.4-beta.1

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 (138) hide show
  1. package/README.md +38 -21
  2. package/dist/client/connector.d.ts +30 -0
  3. package/dist/client/connector.d.ts.map +1 -0
  4. package/dist/client/connector.js +219 -0
  5. package/dist/client/connector.js.map +1 -0
  6. package/dist/client/hub.d.ts +61 -0
  7. package/dist/client/hub.d.ts.map +1 -0
  8. package/dist/client/hub.js +148 -0
  9. package/dist/client/hub.js.map +1 -0
  10. package/dist/client/skill-sync.d.ts +29 -0
  11. package/dist/client/skill-sync.d.ts.map +1 -0
  12. package/dist/client/skill-sync.js +216 -0
  13. package/dist/client/skill-sync.js.map +1 -0
  14. package/dist/config.d.ts +2 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +70 -3
  17. package/dist/config.js.map +1 -1
  18. package/dist/embedding/index.d.ts +4 -2
  19. package/dist/embedding/index.d.ts.map +1 -1
  20. package/dist/embedding/index.js +17 -1
  21. package/dist/embedding/index.js.map +1 -1
  22. package/dist/hub/auth.d.ts +19 -0
  23. package/dist/hub/auth.d.ts.map +1 -0
  24. package/dist/hub/auth.js +70 -0
  25. package/dist/hub/auth.js.map +1 -0
  26. package/dist/hub/server.d.ts +41 -0
  27. package/dist/hub/server.d.ts.map +1 -0
  28. package/dist/hub/server.js +747 -0
  29. package/dist/hub/server.js.map +1 -0
  30. package/dist/hub/user-manager.d.ts +29 -0
  31. package/dist/hub/user-manager.d.ts.map +1 -0
  32. package/dist/hub/user-manager.js +125 -0
  33. package/dist/hub/user-manager.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/providers/index.d.ts +10 -2
  39. package/dist/ingest/providers/index.d.ts.map +1 -1
  40. package/dist/ingest/providers/index.js +203 -6
  41. package/dist/ingest/providers/index.js.map +1 -1
  42. package/dist/ingest/providers/openai.d.ts +1 -0
  43. package/dist/ingest/providers/openai.d.ts.map +1 -1
  44. package/dist/ingest/providers/openai.js +1 -0
  45. package/dist/ingest/providers/openai.js.map +1 -1
  46. package/dist/ingest/task-processor.js +1 -1
  47. package/dist/ingest/task-processor.js.map +1 -1
  48. package/dist/openclaw-api.d.ts +53 -0
  49. package/dist/openclaw-api.d.ts.map +1 -0
  50. package/dist/openclaw-api.js +189 -0
  51. package/dist/openclaw-api.js.map +1 -0
  52. package/dist/recall/engine.js +1 -1
  53. package/dist/recall/engine.js.map +1 -1
  54. package/dist/shared/llm-call.d.ts +4 -1
  55. package/dist/shared/llm-call.d.ts.map +1 -1
  56. package/dist/shared/llm-call.js +15 -0
  57. package/dist/shared/llm-call.js.map +1 -1
  58. package/dist/sharing/types.contract.d.ts +2 -0
  59. package/dist/sharing/types.contract.d.ts.map +1 -0
  60. package/dist/sharing/types.contract.js +3 -0
  61. package/dist/sharing/types.contract.js.map +1 -0
  62. package/dist/sharing/types.d.ts +80 -0
  63. package/dist/sharing/types.d.ts.map +1 -0
  64. package/dist/sharing/types.js +3 -0
  65. package/dist/sharing/types.js.map +1 -0
  66. package/dist/skill/evaluator.d.ts.map +1 -1
  67. package/dist/skill/evaluator.js +2 -2
  68. package/dist/skill/evaluator.js.map +1 -1
  69. package/dist/skill/generator.d.ts.map +1 -1
  70. package/dist/skill/generator.js +4 -4
  71. package/dist/skill/generator.js.map +1 -1
  72. package/dist/skill/upgrader.js +1 -1
  73. package/dist/skill/upgrader.js.map +1 -1
  74. package/dist/skill/validator.js +1 -1
  75. package/dist/skill/validator.js.map +1 -1
  76. package/dist/storage/sqlite.d.ts +294 -0
  77. package/dist/storage/sqlite.d.ts.map +1 -1
  78. package/dist/storage/sqlite.js +828 -8
  79. package/dist/storage/sqlite.js.map +1 -1
  80. package/dist/tools/index.d.ts +1 -0
  81. package/dist/tools/index.d.ts.map +1 -1
  82. package/dist/tools/index.js +3 -1
  83. package/dist/tools/index.js.map +1 -1
  84. package/dist/tools/memory-search.d.ts +3 -2
  85. package/dist/tools/memory-search.d.ts.map +1 -1
  86. package/dist/tools/memory-search.js +48 -7
  87. package/dist/tools/memory-search.js.map +1 -1
  88. package/dist/tools/network-memory-detail.d.ts +4 -0
  89. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  90. package/dist/tools/network-memory-detail.js +34 -0
  91. package/dist/tools/network-memory-detail.js.map +1 -0
  92. package/dist/types.d.ts +47 -2
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js.map +1 -1
  95. package/dist/viewer/html.d.ts.map +1 -1
  96. package/dist/viewer/html.js +2595 -345
  97. package/dist/viewer/html.js.map +1 -1
  98. package/dist/viewer/server.d.ts +45 -0
  99. package/dist/viewer/server.d.ts.map +1 -1
  100. package/dist/viewer/server.js +1153 -15
  101. package/dist/viewer/server.js.map +1 -1
  102. package/index.ts +428 -16
  103. package/openclaw.plugin.json +2 -1
  104. package/package.json +3 -3
  105. package/scripts/postinstall.cjs +282 -45
  106. package/skill/memos-memory-guide/SKILL.md +26 -2
  107. package/src/client/connector.ts +218 -0
  108. package/src/client/hub.ts +189 -0
  109. package/src/client/skill-sync.ts +202 -0
  110. package/src/config.ts +92 -3
  111. package/src/embedding/index.ts +21 -1
  112. package/src/hub/auth.ts +78 -0
  113. package/src/hub/server.ts +740 -0
  114. package/src/hub/user-manager.ts +139 -0
  115. package/src/index.ts +7 -4
  116. package/src/ingest/providers/index.ts +240 -6
  117. package/src/ingest/providers/openai.ts +1 -1
  118. package/src/ingest/task-processor.ts +1 -1
  119. package/src/openclaw-api.ts +287 -0
  120. package/src/recall/engine.ts +1 -1
  121. package/src/shared/llm-call.ts +19 -1
  122. package/src/sharing/types.contract.ts +40 -0
  123. package/src/sharing/types.ts +102 -0
  124. package/src/skill/evaluator.ts +3 -2
  125. package/src/skill/generator.ts +6 -4
  126. package/src/skill/upgrader.ts +1 -1
  127. package/src/skill/validator.ts +1 -1
  128. package/src/storage/sqlite.ts +1093 -7
  129. package/src/tools/index.ts +1 -0
  130. package/src/tools/memory-search.ts +57 -8
  131. package/src/tools/network-memory-detail.ts +34 -0
  132. package/src/types.ts +42 -2
  133. package/src/viewer/html.ts +2595 -345
  134. package/src/viewer/server.ts +1068 -18
  135. package/dist/ingest/extra-paths.d.ts +0 -13
  136. package/dist/ingest/extra-paths.d.ts.map +0 -1
  137. package/dist/ingest/extra-paths.js +0 -173
  138. package/dist/ingest/extra-paths.js.map +0 -1
package/index.ts CHANGED
@@ -10,6 +10,7 @@ import { Type } from "@sinclair/typebox";
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
12
  import { buildContext } from "./src/config";
13
+ import type { HostModelsConfig } from "./src/openclaw-api";
13
14
  import { SqliteStore } from "./src/storage/sqlite";
14
15
  import { Embedder } from "./src/embedding";
15
16
  import { IngestWorker } from "./src/ingest/worker";
@@ -17,6 +18,10 @@ import { RecallEngine } from "./src/recall/engine";
17
18
  import { captureMessages, stripInboundMetadata } from "./src/capture";
18
19
  import { DEFAULTS } from "./src/types";
19
20
  import { ViewerServer } from "./src/viewer/server";
21
+ import { HubServer } from "./src/hub/server";
22
+ import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
23
+ import { getHubStatus, connectToHub } from "./src/client/connector";
24
+ import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
20
25
  import { SkillEvolver } from "./src/skill/evolver";
21
26
  import { SkillInstaller } from "./src/skill/installer";
22
27
  import { Summarizer } from "./src/ingest/providers";
@@ -164,17 +169,35 @@ const memosLocalPlugin = {
164
169
  }
165
170
  }
166
171
 
167
- const pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
172
+ let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
168
173
  const stateDir = api.resolvePath("~/.openclaw");
174
+
175
+ // Fallback: read config from file if not provided by OpenClaw
176
+ const configPath = path.join(stateDir, "state", "memos-local", "config.json");
177
+ if (Object.keys(pluginCfg).length === 0 && fs.existsSync(configPath)) {
178
+ try {
179
+ const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
180
+ pluginCfg = fileConfig;
181
+ api.logger.info(`memos-local: loaded config from ${configPath}`);
182
+ } catch (e) {
183
+ api.logger.warn(`memos-local: failed to load config from ${configPath}: ${e}`);
184
+ }
185
+ }
186
+
187
+ // Extract host model providers so OpenClawAPIClient can proxy completion/embedding
188
+ const hostModels: HostModelsConfig | undefined = api.config?.models?.providers
189
+ ? { providers: api.config.models.providers as Record<string, import("./src/openclaw-api").HostModelProvider> }
190
+ : undefined;
191
+
169
192
  const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, {
170
193
  debug: (msg: string) => api.logger.info(`[debug] ${msg}`),
171
194
  info: (msg: string) => api.logger.info(msg),
172
195
  warn: (msg: string) => api.logger.warn(msg),
173
196
  error: (msg: string) => api.logger.warn(`[error] ${msg}`),
174
- });
197
+ }, hostModels);
175
198
 
176
199
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
177
- const embedder = new Embedder(ctx.config.embedding, ctx.log);
200
+ const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
178
201
  const worker = new IngestWorker(store, embedder, ctx);
179
202
  const engine = new RecallEngine(store, embedder, ctx);
180
203
  const evidenceTag = ctx.config.capture?.evidenceWrapperTag ?? DEFAULTS.evidenceWrapperTag;
@@ -238,7 +261,7 @@ const memosLocalPlugin = {
238
261
  });
239
262
  });
240
263
 
241
- const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
264
+ const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI);
242
265
 
243
266
  api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
244
267
 
@@ -296,6 +319,10 @@ const memosLocalPlugin = {
296
319
  const { query } = params as { query: string };
297
320
  const role = undefined;
298
321
  const minScore = undefined;
322
+ const searchScope = "local";
323
+ const searchLimit = 10;
324
+ const hubAddress: string | undefined = undefined;
325
+ const userToken: string | undefined = undefined;
299
326
 
300
327
  const agentId = currentAgentId;
301
328
  const ownerFilter = [`agent:${agentId}`, "public"];
@@ -319,7 +346,6 @@ const memosLocalPlugin = {
319
346
  };
320
347
  }
321
348
 
322
- // LLM relevance + sufficiency filtering
323
349
  let filteredHits = result.hits;
324
350
  let sufficient = false;
325
351
 
@@ -345,6 +371,51 @@ const memosLocalPlugin = {
345
371
  }
346
372
  }
347
373
 
374
+ const beforeDedup = filteredHits.length;
375
+ filteredHits = deduplicateHits(filteredHits);
376
+ ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
377
+
378
+ const localDetailsHits = filteredHits.map((h) => {
379
+ let effectiveTaskId = h.taskId;
380
+ if (effectiveTaskId) {
381
+ const t = store.getTask(effectiveTaskId);
382
+ if (t && t.status === "skipped") effectiveTaskId = null;
383
+ }
384
+ return {
385
+ ref: h.ref,
386
+ chunkId: h.ref.chunkId,
387
+ taskId: effectiveTaskId,
388
+ skillId: h.skillId,
389
+ role: h.source.role,
390
+ score: h.score,
391
+ summary: h.summary,
392
+ };
393
+ });
394
+
395
+ if (searchScope !== "local") {
396
+ const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
397
+ const localText = filteredHits.length > 0
398
+ ? filteredHits.map((h, i) => {
399
+ const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
400
+ return `${i + 1}. [${h.source.role}] ${excerpt}`;
401
+ }).join("\n")
402
+ : "(none)";
403
+ const hubText = hub.hits.length > 0
404
+ ? hub.hits.map((h, i) => `${i + 1}. [${h.ownerName}] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
405
+ : "(none)";
406
+
407
+ return {
408
+ content: [{
409
+ type: "text",
410
+ text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
411
+ }],
412
+ details: {
413
+ local: { hits: localDetailsHits, meta: result.meta },
414
+ hub,
415
+ },
416
+ };
417
+ }
418
+
348
419
  if (filteredHits.length === 0) {
349
420
  return {
350
421
  content: [{ type: "text", text: "No relevant memories found for this query." }],
@@ -352,10 +423,6 @@ const memosLocalPlugin = {
352
423
  };
353
424
  }
354
425
 
355
- const beforeDedup = filteredHits.length;
356
- filteredHits = deduplicateHits(filteredHits);
357
- ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
358
-
359
426
  const lines = filteredHits.map((h, i) => {
360
427
  const excerpt = h.original_excerpt;
361
428
  const parts = [`${i + 1}. [${h.source.role}]`];
@@ -448,7 +515,7 @@ const memosLocalPlugin = {
448
515
  if (!anchorChunk) {
449
516
  return {
450
517
  content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
451
- details: { error: "not_found" },
518
+ details: { error: "not_found", entries: [] },
452
519
  };
453
520
  }
454
521
 
@@ -497,7 +564,7 @@ const memosLocalPlugin = {
497
564
  Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
498
565
  ),
499
566
  }),
500
- execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
567
+ execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
501
568
  const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
502
569
  const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
503
570
 
@@ -604,6 +671,207 @@ const memosLocalPlugin = {
604
671
  { name: "task_summary" },
605
672
  );
606
673
 
674
+ // ─── Tool: task_share ───
675
+
676
+ api.registerTool(
677
+ {
678
+ name: "task_share",
679
+ label: "Task Share",
680
+ description:
681
+ "Share one existing local task and its chunks to the configured hub. " +
682
+ "Minimal MVP path for validating team task sharing.",
683
+ parameters: Type.Object({
684
+ taskId: Type.String({ description: "Local task ID to share" }),
685
+ visibility: Type.Optional(Type.String({ description: "Share visibility: 'public' (default) or 'group'" })),
686
+ groupId: Type.Optional(Type.String({ description: "Optional group ID when visibility='group'" })),
687
+ }),
688
+ execute: trackTool("task_share", async (_toolCallId: any, params: any) => {
689
+ const { taskId, visibility: rawVisibility, groupId } = params as {
690
+ taskId: string;
691
+ visibility?: string;
692
+ groupId?: string;
693
+ };
694
+
695
+ const task = store.getTask(taskId);
696
+ if (!task) {
697
+ return {
698
+ content: [{ type: "text", text: `Task not found: ${taskId}` }],
699
+ details: { error: "not_found", taskId },
700
+ };
701
+ }
702
+
703
+ const chunks = store.getChunksByTask(taskId);
704
+ if (chunks.length === 0) {
705
+ return {
706
+ content: [{ type: "text", text: `Task ${taskId} has no chunks to share.` }],
707
+ details: { error: "no_chunks", taskId },
708
+ };
709
+ }
710
+
711
+ const visibility = rawVisibility === "group" ? "group" : "public";
712
+ const hubClient = await resolveHubClient(store, ctx);
713
+ const { v4: uuidv4 } = require("uuid");
714
+ const hubTaskId = uuidv4();
715
+
716
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
717
+ method: "POST",
718
+ body: JSON.stringify({
719
+ task: {
720
+ id: hubTaskId,
721
+ sourceTaskId: task.id,
722
+ sourceUserId: hubClient.userId,
723
+ title: task.title,
724
+ summary: task.summary,
725
+ groupId: visibility === "group" ? (groupId ?? null) : null,
726
+ visibility,
727
+ createdAt: task.startedAt,
728
+ updatedAt: task.updatedAt,
729
+ },
730
+ chunks: chunks.map((chunk) => ({
731
+ id: uuidv4(),
732
+ hubTaskId,
733
+ sourceTaskId: task.id,
734
+ sourceChunkId: chunk.id,
735
+ sourceUserId: hubClient.userId,
736
+ role: chunk.role,
737
+ content: chunk.content,
738
+ summary: chunk.summary,
739
+ kind: chunk.kind,
740
+ createdAt: chunk.createdAt,
741
+ })),
742
+ }),
743
+ }) as any;
744
+
745
+ store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId);
746
+
747
+ return {
748
+ content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
749
+ details: {
750
+ shared: true,
751
+ taskId: task.id,
752
+ visibility,
753
+ chunkCount: chunks.length,
754
+ hubUrl: hubClient.hubUrl,
755
+ response,
756
+ },
757
+ };
758
+ }),
759
+ },
760
+ { name: "task_share" },
761
+ );
762
+
763
+ // ─── Tool: task_unshare ───
764
+
765
+ api.registerTool(
766
+ {
767
+ name: "task_unshare",
768
+ label: "Task Unshare",
769
+ description: "Remove one previously shared task from the configured hub.",
770
+ parameters: Type.Object({
771
+ taskId: Type.String({ description: "Local task ID to unshare" }),
772
+ }),
773
+ execute: trackTool("task_unshare", async (_toolCallId: any, params: any) => {
774
+ const { taskId } = params as { taskId: string };
775
+
776
+ const task = store.getTask(taskId);
777
+ if (!task) {
778
+ return {
779
+ content: [{ type: "text", text: `Task not found: ${taskId}` }],
780
+ details: { error: "not_found", taskId },
781
+ };
782
+ }
783
+
784
+ const hubClient = await resolveHubClient(store, ctx);
785
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
786
+ method: "POST",
787
+ body: JSON.stringify({
788
+ sourceUserId: hubClient.userId,
789
+ sourceTaskId: task.id,
790
+ }),
791
+ });
792
+
793
+ store.unmarkTaskShared(task.id);
794
+
795
+ return {
796
+ content: [{ type: "text", text: `Unshared task "${task.title}" from the hub.` }],
797
+ details: {
798
+ unshared: true,
799
+ taskId: task.id,
800
+ hubUrl: hubClient.hubUrl,
801
+ },
802
+ };
803
+ }),
804
+ },
805
+ { name: "task_unshare" },
806
+ );
807
+
808
+ api.registerTool(
809
+ {
810
+ name: "network_memory_detail",
811
+ label: "Network Memory Detail",
812
+ description: "Fetch the full detail for a Hub search hit returned by memory_search(scope=group|all).",
813
+ parameters: Type.Object({
814
+ remoteHitId: Type.String({ description: "The remoteHitId returned by a Hub search hit" }),
815
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
816
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
817
+ }),
818
+ execute: trackTool("network_memory_detail", async (_toolCallId: any, params: any) => {
819
+ const { remoteHitId, hubAddress, userToken } = params as {
820
+ remoteHitId: string;
821
+ hubAddress?: string;
822
+ userToken?: string;
823
+ };
824
+
825
+ const detail = await hubGetMemoryDetail(store, ctx, { remoteHitId, hubAddress, userToken });
826
+ return {
827
+ content: [{
828
+ type: "text",
829
+ text: `## Shared Memory Detail
830
+
831
+ ${detail.summary}
832
+
833
+ ${detail.content}`,
834
+ }],
835
+ details: detail,
836
+ };
837
+ }),
838
+ },
839
+ { name: "network_memory_detail" },
840
+ );
841
+
842
+ api.registerTool(
843
+ {
844
+ name: "network_team_info",
845
+ label: "Network Team Info",
846
+ description: "Show current Hub connection status, signed-in user, role, and group memberships.",
847
+ parameters: Type.Object({}),
848
+ execute: trackTool("network_team_info", async () => {
849
+ const status = await getHubStatus(store, ctx.config);
850
+ if (!status.connected || !status.user) {
851
+ return {
852
+ content: [{ type: "text", text: "Hub is not connected." }],
853
+ details: status,
854
+ };
855
+ }
856
+
857
+ const groupNames = status.user.groups.map((group) => group.name);
858
+ return {
859
+ content: [{
860
+ type: "text",
861
+ text: `## Team Connection
862
+
863
+ User: ${status.user.username}
864
+ Role: ${status.user.role}
865
+ Hub: ${status.hubUrl ?? "(unknown)"}
866
+ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
867
+ }],
868
+ details: status,
869
+ };
870
+ }),
871
+ },
872
+ { name: "network_team_info" },
873
+ );
874
+
607
875
  // ─── Tool: skill_get ───
608
876
 
609
877
  api.registerTool(
@@ -815,17 +1083,43 @@ const memosLocalPlugin = {
815
1083
  name: "skill_search",
816
1084
  label: "Skill Search",
817
1085
  description:
818
- "Search available skills by natural language. Searches your own skills, public skills, or both. " +
1086
+ "Search available skills by natural language. Searches local skills by default, or local + Hub skills when scope=group/all. " +
819
1087
  "Use when you need a capability or guide and don't have a matching skill at hand.",
820
1088
  parameters: Type.Object({
821
1089
  query: Type.String({ description: "Natural language description of the needed skill" }),
822
- scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default, self + public), 'self' (own only), 'public' (public only)" })),
1090
+ scope: Type.Optional(Type.String({ description: "Search scope: 'mix'/'self'/'public' for local search, or 'group'/'all' for local + Hub search" })),
823
1091
  }),
824
- execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
1092
+ execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
825
1093
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
826
1094
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
827
1095
  const currentOwner = `agent:${currentAgentId}`;
828
1096
 
1097
+ if (rawScope === "group" || rawScope === "all") {
1098
+ const [localHits, hub] = await Promise.all([
1099
+ engine.searchSkills(skillQuery, "mix" as any, currentOwner),
1100
+ hubSearchSkills(store, ctx, { query: skillQuery, maxResults: 10 }).catch(() => ({ hits: [] })),
1101
+ ]);
1102
+
1103
+ if (localHits.length === 0 && hub.hits.length === 0) {
1104
+ return {
1105
+ content: [{ type: "text", text: `No relevant skills found for: "${skillQuery}" (scope: ${rawScope})` }],
1106
+ details: { query: skillQuery, scope: rawScope, local: { hits: [] }, hub },
1107
+ };
1108
+ }
1109
+
1110
+ const localText = localHits.length > 0
1111
+ ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (public)" : ""}`).join("\n")
1112
+ : "(none)";
1113
+ const hubText = hub.hits.length > 0
1114
+ ? hub.hits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
1115
+ : "(none)";
1116
+
1117
+ return {
1118
+ content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
1119
+ details: { query: skillQuery, scope: rawScope, local: { hits: localHits }, hub },
1120
+ };
1121
+ }
1122
+
829
1123
  const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
830
1124
 
831
1125
  if (hits.length === 0) {
@@ -857,17 +1151,28 @@ const memosLocalPlugin = {
857
1151
  description: "Make a skill public so other agents can discover and install it via skill_search.",
858
1152
  parameters: Type.Object({
859
1153
  skillId: Type.String({ description: "The skill ID to publish" }),
1154
+ scope: Type.Optional(Type.String({ description: "Publish scope: omit for local public, or use 'public' / 'group' to publish to Hub" })),
1155
+ groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
1156
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1157
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
860
1158
  }),
861
1159
  execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
862
- const { skillId: pubSkillId } = params as { skillId: string };
1160
+ const { skillId: pubSkillId, scope, groupId, hubAddress, userToken } = params as { skillId: string; scope?: string; groupId?: string; hubAddress?: string; userToken?: string };
863
1161
  const skill = store.getSkill(pubSkillId);
864
1162
  if (!skill) {
865
1163
  return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
866
1164
  }
1165
+ if (scope === "public" || scope === "group") {
1166
+ const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility: scope, groupId, hubAddress, userToken });
1167
+ return {
1168
+ content: [{ type: "text", text: `Skill "${skill.name}" published to hub (${published.visibility}).` }],
1169
+ details: { skillId: pubSkillId, name: skill.name, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
1170
+ };
1171
+ }
867
1172
  store.setSkillVisibility(pubSkillId, "public");
868
1173
  return {
869
1174
  content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
870
- details: { skillId: pubSkillId, name: skill.name, visibility: "public" },
1175
+ details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
871
1176
  };
872
1177
  }),
873
1178
  },
@@ -900,6 +1205,29 @@ const memosLocalPlugin = {
900
1205
  { name: "skill_unpublish" },
901
1206
  );
902
1207
 
1208
+ api.registerTool(
1209
+ {
1210
+ name: "network_skill_pull",
1211
+ label: "Network Skill Pull",
1212
+ description: "Download a published Hub skill bundle and restore it into local managed skills.",
1213
+ parameters: Type.Object({
1214
+ skillId: Type.String({ description: "The Hub skill ID to pull" }),
1215
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1216
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1217
+ }),
1218
+ execute: trackTool("network_skill_pull", async (_toolCallId: any, params: any) => {
1219
+ const { skillId, hubAddress, userToken } = params as { skillId: string; hubAddress?: string; userToken?: string };
1220
+ const payload = await fetchHubSkillBundle(store, ctx, { skillId, hubAddress, userToken });
1221
+ const restored = restoreSkillBundleFromHub(store, ctx, payload);
1222
+ return {
1223
+ content: [{ type: "text", text: `Pulled Hub skill "${restored.localName}" into local storage.` }],
1224
+ details: { pulled: true, hubSkillId: skillId, localSkillId: restored.localSkillId, localName: restored.localName, dirPath: restored.dirPath },
1225
+ };
1226
+ }),
1227
+ },
1228
+ { name: "network_skill_pull" },
1229
+ );
1230
+
903
1231
  // ─── Auto-recall: inject relevant memories before agent starts ───
904
1232
 
905
1233
  api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
@@ -1204,11 +1532,74 @@ const memosLocalPlugin = {
1204
1532
  worker.enqueue(captured);
1205
1533
  telemetry.trackMemoryIngested(captured.length);
1206
1534
  }
1535
+
1536
+ // Incremental push: sync new chunks for already-shared tasks
1537
+ syncSharedTasksIncremental().catch((err) => {
1538
+ ctx.log.warn(`incremental sync failed: ${err}`);
1539
+ });
1207
1540
  } catch (err) {
1208
1541
  api.logger.warn(`memos-local: capture failed: ${String(err)}`);
1209
1542
  }
1210
1543
  });
1211
1544
 
1545
+ async function syncSharedTasksIncremental(): Promise<void> {
1546
+ if (!ctx.config.sharing?.enabled || ctx.config.sharing.role !== "client") return;
1547
+ const shared = store.listLocalSharedTasks();
1548
+ if (shared.length === 0) return;
1549
+
1550
+ let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
1551
+ try {
1552
+ hubClient = await resolveHubClient(store, ctx);
1553
+ } catch {
1554
+ return;
1555
+ }
1556
+ const { v4: uuidv4 } = require("uuid");
1557
+
1558
+ for (const entry of shared) {
1559
+ const task = store.getTask(entry.taskId);
1560
+ if (!task) continue;
1561
+ const chunks = store.getChunksByTask(entry.taskId);
1562
+ if (chunks.length <= entry.syncedChunks) continue;
1563
+
1564
+ const newChunks = chunks.slice(entry.syncedChunks);
1565
+ ctx.log.info(`incremental sync: task=${entry.taskId} pushing ${newChunks.length} new chunk(s)`);
1566
+
1567
+ try {
1568
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1569
+ method: "POST",
1570
+ body: JSON.stringify({
1571
+ task: {
1572
+ id: entry.hubTaskId,
1573
+ sourceTaskId: entry.taskId,
1574
+ sourceUserId: hubClient.userId,
1575
+ title: task.title,
1576
+ summary: task.summary,
1577
+ groupId: entry.visibility === "group" ? entry.groupId ?? null : null,
1578
+ visibility: entry.visibility,
1579
+ createdAt: task.startedAt ?? task.updatedAt ?? Date.now(),
1580
+ updatedAt: task.updatedAt ?? Date.now(),
1581
+ },
1582
+ chunks: newChunks.map((chunk) => ({
1583
+ id: uuidv4(),
1584
+ hubTaskId: entry.hubTaskId,
1585
+ sourceTaskId: entry.taskId,
1586
+ sourceChunkId: chunk.id,
1587
+ sourceUserId: hubClient.userId,
1588
+ role: chunk.role,
1589
+ content: chunk.content,
1590
+ summary: chunk.summary,
1591
+ kind: chunk.kind,
1592
+ createdAt: chunk.createdAt,
1593
+ })),
1594
+ }),
1595
+ });
1596
+ store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
1597
+ } catch (err) {
1598
+ ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
1599
+ }
1600
+ }
1601
+ }
1602
+
1212
1603
  // ─── Memory Viewer (web UI) ───
1213
1604
 
1214
1605
  const viewer = new ViewerServer({
@@ -1220,11 +1611,30 @@ const memosLocalPlugin = {
1220
1611
  ctx,
1221
1612
  });
1222
1613
 
1614
+ const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
1615
+ ? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
1616
+ : null;
1617
+
1223
1618
  // ─── Service lifecycle ───
1224
1619
 
1225
1620
  api.registerService({
1226
1621
  id: "memos-local-openclaw-plugin",
1227
1622
  start: async () => {
1623
+ if (hubServer) {
1624
+ const hubUrl = await hubServer.start();
1625
+ api.logger.info(`memos-local: hub started at ${hubUrl}`);
1626
+ }
1627
+
1628
+ // Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
1629
+ if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
1630
+ try {
1631
+ const session = await connectToHub(store, ctx.config, ctx.log);
1632
+ api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
1633
+ } catch (err) {
1634
+ api.logger.warn(`memos-local: Hub connection failed: ${err}`);
1635
+ }
1636
+ }
1637
+
1228
1638
  try {
1229
1639
  const viewerUrl = await viewer.start();
1230
1640
  api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
@@ -1250,7 +1660,9 @@ const memosLocalPlugin = {
1250
1660
  );
1251
1661
  },
1252
1662
  stop: async () => {
1663
+ await worker.flush();
1253
1664
  await telemetry.shutdown();
1665
+ await hubServer?.stop();
1254
1666
  viewer.stop();
1255
1667
  store.close();
1256
1668
  api.logger.info("memos-local: stopped");
@@ -32,5 +32,6 @@
32
32
  "Memory Viewer will be available at http://127.0.0.1:18799",
33
33
  "If better-sqlite3 fails to build, ensure you have C++ build tools: xcode-select --install (macOS) or build-essential (Linux)"
34
34
  ]
35
- }
35
+ },
36
+ "extensions": ["./index.ts"]
36
37
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.3",
4
- "description": "MemOS Local memory plugin for OpenClaw \u2014 full-write, hybrid-recall, progressive retrieval",
3
+ "version": "1.0.4-beta.1",
4
+ "description": "MemOS Local memory plugin for OpenClaw full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "types": "dist/index.d.ts",
@@ -64,4 +64,4 @@
64
64
  "typescript": "^5.7.0",
65
65
  "vitest": "^2.1.0"
66
66
  }
67
- }
67
+ }