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

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 +26 -0
  3. package/dist/client/connector.d.ts.map +1 -0
  4. package/dist/client/connector.js +127 -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 +21 -4
  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 +742 -0
  29. package/dist/hub/server.js.map +1 -0
  30. package/dist/hub/user-manager.d.ts +28 -0
  31. package/dist/hub/user-manager.d.ts.map +1 -0
  32. package/dist/hub/user-manager.js +112 -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 +242 -8
  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 +2 -2
  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 +902 -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/update-check.d.ts.map +1 -1
  96. package/dist/update-check.js +0 -1
  97. package/dist/update-check.js.map +1 -1
  98. package/dist/viewer/html.d.ts.map +1 -1
  99. package/dist/viewer/html.js +2396 -289
  100. package/dist/viewer/html.js.map +1 -1
  101. package/dist/viewer/server.d.ts +43 -0
  102. package/dist/viewer/server.d.ts.map +1 -1
  103. package/dist/viewer/server.js +1180 -33
  104. package/dist/viewer/server.js.map +1 -1
  105. package/index.ts +445 -25
  106. package/openclaw.plugin.json +2 -1
  107. package/package.json +2 -1
  108. package/scripts/postinstall.cjs +282 -45
  109. package/skill/memos-memory-guide/SKILL.md +26 -2
  110. package/src/client/connector.ts +124 -0
  111. package/src/client/hub.ts +189 -0
  112. package/src/client/skill-sync.ts +202 -0
  113. package/src/config.ts +92 -3
  114. package/src/embedding/index.ts +25 -3
  115. package/src/hub/auth.ts +78 -0
  116. package/src/hub/server.ts +734 -0
  117. package/src/hub/user-manager.ts +126 -0
  118. package/src/index.ts +7 -4
  119. package/src/ingest/providers/index.ts +279 -8
  120. package/src/ingest/providers/openai.ts +1 -1
  121. package/src/ingest/task-processor.ts +1 -1
  122. package/src/openclaw-api.ts +287 -0
  123. package/src/recall/engine.ts +2 -2
  124. package/src/shared/llm-call.ts +19 -1
  125. package/src/sharing/types.contract.ts +40 -0
  126. package/src/sharing/types.ts +102 -0
  127. package/src/skill/evaluator.ts +3 -2
  128. package/src/skill/generator.ts +6 -4
  129. package/src/skill/upgrader.ts +1 -1
  130. package/src/skill/validator.ts +1 -1
  131. package/src/storage/sqlite.ts +1167 -7
  132. package/src/tools/index.ts +1 -0
  133. package/src/tools/memory-search.ts +57 -8
  134. package/src/tools/network-memory-detail.ts +34 -0
  135. package/src/types.ts +48 -2
  136. package/src/update-check.ts +0 -1
  137. package/src/viewer/html.ts +2396 -289
  138. package/src/viewer/server.ts +1087 -34
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";
@@ -130,13 +135,16 @@ const memosLocalPlugin = {
130
135
  }
131
136
 
132
137
  if (!sqliteReady) {
133
- const msg = [
138
+ const nodeVer = process.version;
139
+ const nodeMajor = parseInt(process.versions?.node?.split(".")[0] ?? "0", 10);
140
+ const isNode25Plus = nodeMajor >= 25;
141
+ const lines = [
134
142
  "",
135
143
  "╔══════════════════════════════════════════════════════════════╗",
136
144
  "║ MemOS Local Memory — better-sqlite3 native module missing ║",
137
145
  "╠══════════════════════════════════════════════════════════════╣",
138
146
  "║ ║",
139
- "║ Auto-rebuild failed. Run these commands manually: ║",
147
+ "║ Auto-rebuild failed (Node " + nodeVer + "). Run manually: ║",
140
148
  "║ ║",
141
149
  `║ cd ${pluginDir}`,
142
150
  "║ npm rebuild better-sqlite3 ║",
@@ -145,28 +153,51 @@ const memosLocalPlugin = {
145
153
  "║ If rebuild fails, install build tools first: ║",
146
154
  "║ macOS: xcode-select --install ║",
147
155
  "║ Linux: sudo apt install build-essential python3 ║",
148
- "║ ║",
149
- "╚══════════════════════════════════════════════════════════════╝",
150
- "",
151
- ].join("\n");
152
- api.logger.warn(msg);
156
+ ];
157
+ if (isNode25Plus) {
158
+ lines.push("║ ║");
159
+ lines.push("║ Node 25+ has no prebuild: build tools required, or use ║");
160
+ lines.push("║ Node LTS (20/22): nvm install 22 && nvm use 22 ║");
161
+ }
162
+ lines.push("║ ║");
163
+ lines.push("╚══════════════════════════════════════════════════════════════╝");
164
+ lines.push("");
165
+ api.logger.warn(lines.join("\n"));
153
166
  throw new Error(
154
- `better-sqlite3 native module not found. Auto-rebuild failed. Fix: cd ${pluginDir} && npm rebuild better-sqlite3`
167
+ `better-sqlite3 native module not found (Node ${nodeVer}). Auto-rebuild failed. Fix: install build tools, then cd ${pluginDir} && npm rebuild better-sqlite3. Or use Node LTS (20/22).`
155
168
  );
156
169
  }
157
170
  }
158
171
 
159
- const pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
172
+ let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
160
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
+
161
192
  const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, {
162
193
  debug: (msg: string) => api.logger.info(`[debug] ${msg}`),
163
194
  info: (msg: string) => api.logger.info(msg),
164
195
  warn: (msg: string) => api.logger.warn(msg),
165
196
  error: (msg: string) => api.logger.warn(`[error] ${msg}`),
166
- });
197
+ }, hostModels);
167
198
 
168
199
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
169
- const embedder = new Embedder(ctx.config.embedding, ctx.log);
200
+ const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
170
201
  const worker = new IngestWorker(store, embedder, ctx);
171
202
  const engine = new RecallEngine(store, embedder, ctx);
172
203
  const evidenceTag = ctx.config.capture?.evidenceWrapperTag ?? DEFAULTS.evidenceWrapperTag;
@@ -230,7 +261,7 @@ const memosLocalPlugin = {
230
261
  });
231
262
  });
232
263
 
233
- const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
264
+ const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI);
234
265
 
235
266
  api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
236
267
 
@@ -288,6 +319,10 @@ const memosLocalPlugin = {
288
319
  const { query } = params as { query: string };
289
320
  const role = undefined;
290
321
  const minScore = undefined;
322
+ const searchScope = "local";
323
+ const searchLimit = 10;
324
+ const hubAddress: string | undefined = undefined;
325
+ const userToken: string | undefined = undefined;
291
326
 
292
327
  const agentId = currentAgentId;
293
328
  const ownerFilter = [`agent:${agentId}`, "public"];
@@ -311,7 +346,6 @@ const memosLocalPlugin = {
311
346
  };
312
347
  }
313
348
 
314
- // LLM relevance + sufficiency filtering
315
349
  let filteredHits = result.hits;
316
350
  let sufficient = false;
317
351
 
@@ -337,6 +371,51 @@ const memosLocalPlugin = {
337
371
  }
338
372
  }
339
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
+
340
419
  if (filteredHits.length === 0) {
341
420
  return {
342
421
  content: [{ type: "text", text: "No relevant memories found for this query." }],
@@ -344,10 +423,6 @@ const memosLocalPlugin = {
344
423
  };
345
424
  }
346
425
 
347
- const beforeDedup = filteredHits.length;
348
- filteredHits = deduplicateHits(filteredHits);
349
- ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
350
-
351
426
  const lines = filteredHits.map((h, i) => {
352
427
  const excerpt = h.original_excerpt;
353
428
  const parts = [`${i + 1}. [${h.source.role}]`];
@@ -440,7 +515,7 @@ const memosLocalPlugin = {
440
515
  if (!anchorChunk) {
441
516
  return {
442
517
  content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
443
- details: { error: "not_found" },
518
+ details: { error: "not_found", entries: [] },
444
519
  };
445
520
  }
446
521
 
@@ -489,7 +564,7 @@ const memosLocalPlugin = {
489
564
  Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
490
565
  ),
491
566
  }),
492
- execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
567
+ execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
493
568
  const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
494
569
  const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
495
570
 
@@ -596,6 +671,207 @@ const memosLocalPlugin = {
596
671
  { name: "task_summary" },
597
672
  );
598
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
+
599
875
  // ─── Tool: skill_get ───
600
876
 
601
877
  api.registerTool(
@@ -807,17 +1083,43 @@ const memosLocalPlugin = {
807
1083
  name: "skill_search",
808
1084
  label: "Skill Search",
809
1085
  description:
810
- "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. " +
811
1087
  "Use when you need a capability or guide and don't have a matching skill at hand.",
812
1088
  parameters: Type.Object({
813
1089
  query: Type.String({ description: "Natural language description of the needed skill" }),
814
- 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" })),
815
1091
  }),
816
- execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
1092
+ execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
817
1093
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
818
1094
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
819
1095
  const currentOwner = `agent:${currentAgentId}`;
820
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
+
821
1123
  const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
822
1124
 
823
1125
  if (hits.length === 0) {
@@ -849,17 +1151,28 @@ const memosLocalPlugin = {
849
1151
  description: "Make a skill public so other agents can discover and install it via skill_search.",
850
1152
  parameters: Type.Object({
851
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" })),
852
1158
  }),
853
1159
  execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
854
- 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 };
855
1161
  const skill = store.getSkill(pubSkillId);
856
1162
  if (!skill) {
857
1163
  return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
858
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
+ }
859
1172
  store.setSkillVisibility(pubSkillId, "public");
860
1173
  return {
861
1174
  content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
862
- details: { skillId: pubSkillId, name: skill.name, visibility: "public" },
1175
+ details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
863
1176
  };
864
1177
  }),
865
1178
  },
@@ -892,6 +1205,29 @@ const memosLocalPlugin = {
892
1205
  { name: "skill_unpublish" },
893
1206
  );
894
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
+
895
1231
  // ─── Auto-recall: inject relevant memories before agent starts ───
896
1232
 
897
1233
  api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
@@ -1194,13 +1530,76 @@ const memosLocalPlugin = {
1194
1530
 
1195
1531
  if (captured.length > 0) {
1196
1532
  worker.enqueue(captured);
1197
- telemetry.trackMemoryIngested(filteredCaptured.length);
1533
+ telemetry.trackMemoryIngested(captured.length);
1198
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
+ });
1199
1540
  } catch (err) {
1200
1541
  api.logger.warn(`memos-local: capture failed: ${String(err)}`);
1201
1542
  }
1202
1543
  });
1203
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
+
1204
1603
  // ─── Memory Viewer (web UI) ───
1205
1604
 
1206
1605
  const viewer = new ViewerServer({
@@ -1212,11 +1611,30 @@ const memosLocalPlugin = {
1212
1611
  ctx,
1213
1612
  });
1214
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
+
1215
1618
  // ─── Service lifecycle ───
1216
1619
 
1217
1620
  api.registerService({
1218
1621
  id: "memos-local-openclaw-plugin",
1219
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
+
1220
1638
  try {
1221
1639
  const viewerUrl = await viewer.start();
1222
1640
  api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
@@ -1242,7 +1660,9 @@ const memosLocalPlugin = {
1242
1660
  );
1243
1661
  },
1244
1662
  stop: async () => {
1663
+ await worker.flush();
1245
1664
  await telemetry.shutdown();
1665
+ await hubServer?.stop();
1246
1666
  viewer.stop();
1247
1667
  store.close();
1248
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.2",
3
+ "version": "1.0.4-beta.0",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -58,6 +58,7 @@
58
58
  "devDependencies": {
59
59
  "@types/better-sqlite3": "^7.6.12",
60
60
  "@types/node": "^22.10.0",
61
+ "@types/semver": "^7.7.1",
61
62
  "@types/uuid": "^10.0.0",
62
63
  "tsx": "^4.21.0",
63
64
  "typescript": "^5.7.0",