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

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 (99) hide show
  1. package/.env.example +7 -0
  2. package/README.md +94 -27
  3. package/dist/capture/index.js +3 -1
  4. package/dist/capture/index.js.map +1 -1
  5. package/dist/client/connector.d.ts +5 -0
  6. package/dist/client/connector.d.ts.map +1 -1
  7. package/dist/client/connector.js +89 -8
  8. package/dist/client/connector.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/hub/server.d.ts +2 -0
  13. package/dist/hub/server.d.ts.map +1 -1
  14. package/dist/hub/server.js +240 -35
  15. package/dist/hub/server.js.map +1 -1
  16. package/dist/hub/user-manager.d.ts +9 -0
  17. package/dist/hub/user-manager.d.ts.map +1 -1
  18. package/dist/hub/user-manager.js +26 -2
  19. package/dist/hub/user-manager.js.map +1 -1
  20. package/dist/ingest/chunker.d.ts +2 -1
  21. package/dist/ingest/chunker.d.ts.map +1 -1
  22. package/dist/ingest/chunker.js +14 -10
  23. package/dist/ingest/chunker.js.map +1 -1
  24. package/dist/ingest/providers/index.js +2 -2
  25. package/dist/ingest/providers/index.js.map +1 -1
  26. package/dist/recall/engine.d.ts.map +1 -1
  27. package/dist/recall/engine.js +22 -4
  28. package/dist/recall/engine.js.map +1 -1
  29. package/dist/shared/llm-call.d.ts.map +1 -1
  30. package/dist/shared/llm-call.js +2 -1
  31. package/dist/shared/llm-call.js.map +1 -1
  32. package/dist/sharing/types.d.ts +1 -1
  33. package/dist/sharing/types.d.ts.map +1 -1
  34. package/dist/skill/evolver.d.ts +2 -0
  35. package/dist/skill/evolver.d.ts.map +1 -1
  36. package/dist/skill/evolver.js +56 -5
  37. package/dist/skill/evolver.js.map +1 -1
  38. package/dist/skill/generator.d.ts +2 -0
  39. package/dist/skill/generator.d.ts.map +1 -1
  40. package/dist/skill/generator.js +45 -3
  41. package/dist/skill/generator.js.map +1 -1
  42. package/dist/skill/installer.d.ts +26 -0
  43. package/dist/skill/installer.d.ts.map +1 -1
  44. package/dist/skill/installer.js +80 -4
  45. package/dist/skill/installer.js.map +1 -1
  46. package/dist/skill/upgrader.d.ts +2 -0
  47. package/dist/skill/upgrader.d.ts.map +1 -1
  48. package/dist/skill/upgrader.js +139 -1
  49. package/dist/skill/upgrader.js.map +1 -1
  50. package/dist/skill/validator.d.ts +3 -0
  51. package/dist/skill/validator.d.ts.map +1 -1
  52. package/dist/skill/validator.js +75 -0
  53. package/dist/skill/validator.js.map +1 -1
  54. package/dist/storage/sqlite.d.ts +57 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +290 -35
  57. package/dist/storage/sqlite.js.map +1 -1
  58. package/dist/telemetry.d.ts.map +1 -1
  59. package/dist/telemetry.js +27 -8
  60. package/dist/telemetry.js.map +1 -1
  61. package/dist/types.d.ts +10 -0
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +4 -0
  64. package/dist/types.js.map +1 -1
  65. package/dist/viewer/html.d.ts.map +1 -1
  66. package/dist/viewer/html.js +564 -225
  67. package/dist/viewer/html.js.map +1 -1
  68. package/dist/viewer/server.d.ts +9 -0
  69. package/dist/viewer/server.d.ts.map +1 -1
  70. package/dist/viewer/server.js +357 -108
  71. package/dist/viewer/server.js.map +1 -1
  72. package/index.ts +411 -52
  73. package/openclaw.plugin.json +1 -1
  74. package/package.json +2 -1
  75. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  76. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  77. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  78. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  79. package/src/capture/index.ts +4 -1
  80. package/src/client/connector.ts +92 -8
  81. package/src/config.ts +2 -1
  82. package/src/hub/server.ts +235 -35
  83. package/src/hub/user-manager.ts +42 -6
  84. package/src/ingest/chunker.ts +19 -13
  85. package/src/ingest/providers/index.ts +2 -2
  86. package/src/recall/engine.ts +20 -4
  87. package/src/shared/llm-call.ts +2 -1
  88. package/src/sharing/types.ts +1 -1
  89. package/src/skill/evolver.ts +58 -6
  90. package/src/skill/generator.ts +44 -5
  91. package/src/skill/installer.ts +107 -4
  92. package/src/skill/upgrader.ts +139 -1
  93. package/src/skill/validator.ts +79 -0
  94. package/src/storage/sqlite.ts +318 -40
  95. package/src/telemetry.ts +27 -9
  96. package/src/types.ts +11 -0
  97. package/src/viewer/html.ts +564 -225
  98. package/src/viewer/server.ts +333 -105
  99. package/telemetry.credentials.json +5 -0
package/index.ts CHANGED
@@ -19,6 +19,7 @@ import { IngestWorker } from "./src/ingest/worker";
19
19
  import { RecallEngine } from "./src/recall/engine";
20
20
  import { captureMessages, stripInboundMetadata } from "./src/capture";
21
21
  import { DEFAULTS } from "./src/types";
22
+ import type { SearchHit } from "./src/types";
22
23
  import { ViewerServer } from "./src/viewer/server";
23
24
  import { HubServer } from "./src/hub/server";
24
25
  import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
@@ -179,7 +180,7 @@ const memosLocalPlugin = {
179
180
  }
180
181
 
181
182
  let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
182
- const stateDir = api.resolvePath("~/.openclaw");
183
+ const stateDir = process.env.OPENCLAW_STATE_DIR || api.resolvePath("~/.openclaw");
183
184
 
184
185
  // Fallback: read config from file if not provided by OpenClaw
185
186
  const configPath = path.join(stateDir, "state", "memos-local", "config.json");
@@ -318,6 +319,21 @@ const memosLocalPlugin = {
318
319
  candidates: det.candidates,
319
320
  filtered: det.hits ?? det.filtered ?? [],
320
321
  });
322
+ } else if (det && det.local && det.hub) {
323
+ const localHits = det.local?.hits ?? [];
324
+ const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
325
+ score: h.score ?? 0,
326
+ role: h.source?.role ?? h.role ?? "assistant",
327
+ summary: h.summary ?? "",
328
+ original_excerpt: h.excerpt ?? h.summary ?? "",
329
+ origin: "hub-remote",
330
+ ownerName: h.ownerName ?? "",
331
+ groupName: h.groupName ?? "",
332
+ }));
333
+ outputText = JSON.stringify({
334
+ candidates: [...localHits, ...hubHits],
335
+ filtered: [...localHits, ...hubHits],
336
+ });
321
337
  } else {
322
338
  outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
323
339
  }
@@ -370,27 +386,30 @@ const memosLocalPlugin = {
370
386
  }),
371
387
  }) as { memoryId?: string; visibility?: "public" | "group" };
372
388
 
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
- });
389
+ const memoryId = response?.memoryId ?? `${chunk.id}-hub`;
390
+
391
+ // Hub role: full hub_memories row for local recall/embeddings. Client: metadata only (team_shared_chunks) for UI.
392
+ if (ctx.config.sharing?.role === "hub") {
393
+ const now = Date.now();
394
+ const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
395
+ store.upsertHubMemory({
396
+ id: memoryId,
397
+ sourceChunkId: chunk.id,
398
+ sourceUserId: hubClient.userId,
399
+ role: chunk.role,
400
+ content: chunk.content,
401
+ summary: chunk.summary ?? "",
402
+ kind: chunk.kind,
403
+ groupId,
404
+ visibility,
405
+ createdAt: existing?.createdAt ?? now,
406
+ updatedAt: now,
407
+ });
408
+ } else if (ctx.config.sharing?.enabled && hubClient.userId) {
409
+ store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: memoryId, visibility, groupId });
410
+ }
388
411
 
389
- return {
390
- memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
391
- visibility,
392
- groupId,
393
- };
412
+ return { memoryId, visibility, groupId };
394
413
  };
395
414
 
396
415
  const unshareMemoryFromHub = async (
@@ -407,6 +426,7 @@ const memosLocalPlugin = {
407
426
  body: JSON.stringify({ sourceChunkId: chunk.id }),
408
427
  });
409
428
  store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
429
+ store.deleteTeamSharedChunk(chunk.id);
410
430
  };
411
431
 
412
432
  // ─── Tool: memory_search ───
@@ -448,7 +468,10 @@ const memosLocalPlugin = {
448
468
  };
449
469
  const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
450
470
  const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
451
- const searchScope = resolveMemorySearchScope(rawScope);
471
+ let searchScope = resolveMemorySearchScope(rawScope);
472
+ if (searchScope === "local" && ctx.config?.sharing?.enabled) {
473
+ searchScope = "all";
474
+ }
452
475
  const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
453
476
 
454
477
  const agentId = currentAgentId;
@@ -464,6 +487,7 @@ const memosLocalPlugin = {
464
487
  score: h.score,
465
488
  summary: h.summary,
466
489
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
490
+ origin: h.origin || "local",
467
491
  }));
468
492
 
469
493
  if (result.hits.length === 0 && searchScope === "local") {
@@ -518,29 +542,79 @@ const memosLocalPlugin = {
518
542
  role: h.source.role,
519
543
  score: h.score,
520
544
  summary: h.summary,
545
+ origin: h.origin || "local",
521
546
  };
522
547
  });
523
548
 
524
549
  if (searchScope !== "local") {
525
550
  const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
551
+
552
+ let filteredHubHits = hub.hits;
553
+ if (hub.hits.length > 0) {
554
+ const hubCandidates = hub.hits.map((h, i) => ({
555
+ index: filteredHits.length + i + 1,
556
+ role: (h.source?.role || "assistant") as string,
557
+ content: (h.summary || h.excerpt || "").slice(0, 300),
558
+ time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
559
+ }));
560
+ const localCandidatesForMerge = filteredHits.map((h, i) => ({
561
+ index: i + 1,
562
+ role: h.source.role,
563
+ content: (h.original_excerpt ?? "").slice(0, 300),
564
+ time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
565
+ }));
566
+ const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
567
+ const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
568
+ if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
569
+ const relevantSet = new Set(mergedFilter.relevant);
570
+ const hubStartIdx = filteredHits.length + 1;
571
+ filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
572
+ filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
573
+ ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
574
+ }
575
+ }
576
+
577
+ const originLabel = (h: SearchHit) => {
578
+ if (h.origin === "hub-memory") return " [团队缓存]";
579
+ if (h.origin === "local-shared") return " [本机共享]";
580
+ return "";
581
+ };
526
582
  const localText = filteredHits.length > 0
527
583
  ? filteredHits.map((h, i) => {
528
584
  const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
529
- return `${i + 1}. [${h.source.role}] ${excerpt}`;
585
+ return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
530
586
  }).join("\n")
531
587
  : "(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")
588
+ const hubText = filteredHubHits.length > 0
589
+ ? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
534
590
  : "(none)";
535
591
 
592
+ const localDetailsFiltered = filteredHits.map((h) => {
593
+ let effectiveTaskId = h.taskId;
594
+ if (effectiveTaskId) {
595
+ const t = store.getTask(effectiveTaskId);
596
+ if (t && t.status === "skipped") effectiveTaskId = null;
597
+ }
598
+ return {
599
+ ref: h.ref,
600
+ chunkId: h.ref.chunkId,
601
+ taskId: effectiveTaskId,
602
+ skillId: h.skillId,
603
+ role: h.source.role,
604
+ score: h.score,
605
+ summary: h.summary,
606
+ origin: h.origin,
607
+ };
608
+ });
609
+
536
610
  return {
537
611
  content: [{
538
612
  type: "text",
539
613
  text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
540
614
  }],
541
615
  details: {
542
- local: { hits: localDetailsHits, meta: result.meta },
543
- hub,
616
+ local: { hits: localDetailsFiltered, meta: result.meta },
617
+ hub: { ...hub, hits: filteredHubHits },
544
618
  },
545
619
  };
546
620
  }
@@ -552,9 +626,15 @@ const memosLocalPlugin = {
552
626
  };
553
627
  }
554
628
 
629
+ const originTag = (o?: string) => {
630
+ if (o === "local-shared") return " [本机共享]";
631
+ if (o === "hub-memory") return " [团队缓存]";
632
+ if (o === "hub-remote") return " [团队]";
633
+ return "";
634
+ };
555
635
  const lines = filteredHits.map((h, i) => {
556
636
  const excerpt = h.original_excerpt;
557
- const parts = [`${i + 1}. [${h.source.role}]`];
637
+ const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
558
638
  if (excerpt) parts.push(` ${excerpt}`);
559
639
  parts.push(` chunkId="${h.ref.chunkId}"`);
560
640
  if (h.taskId) {
@@ -609,6 +689,7 @@ const memosLocalPlugin = {
609
689
  score: h.score,
610
690
  summary: h.summary,
611
691
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
692
+ origin: h.origin || "local",
612
693
  };
613
694
  }),
614
695
  meta: result.meta,
@@ -1058,10 +1139,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1058
1139
  };
1059
1140
  }
1060
1141
 
1142
+ const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
1143
+ let footer = "\n\n---\n";
1144
+
1145
+ if (manifest && manifest.hasCompanionFiles) {
1146
+ const fileSummary = manifest.files
1147
+ .filter(f => f.type !== "eval")
1148
+ .map(f => `\`${f.relativePath}\``)
1149
+ .join(", ");
1150
+ footer += `**Companion files available:** ${fileSummary}\n`;
1151
+ footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
1152
+ footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
1153
+ if (manifest.installMode === "install_recommended") {
1154
+ footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
1155
+ }
1156
+ if (manifest.installed && manifest.installedPath) {
1157
+ footer += `> Already installed at: ${manifest.installedPath}/\n`;
1158
+ }
1159
+ } else {
1160
+ footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
1161
+ }
1162
+
1061
1163
  return {
1062
1164
  content: [{
1063
1165
  type: "text",
1064
- text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}\n\n---\nTo install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`,
1166
+ text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
1065
1167
  }],
1066
1168
  details: {
1067
1169
  skillId: skill.id,
@@ -1069,6 +1171,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1069
1171
  version: skill.version,
1070
1172
  status: skill.status,
1071
1173
  installed: skill.installed,
1174
+ companionFiles: manifest?.hasCompanionFiles ?? false,
1175
+ installMode: manifest?.installMode ?? "inline",
1072
1176
  },
1073
1177
  };
1074
1178
  }),
@@ -1104,9 +1208,116 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1104
1208
  { name: "skill_install" },
1105
1209
  );
1106
1210
 
1211
+ // ─── Tool: skill_files ───
1212
+
1213
+ api.registerTool(
1214
+ {
1215
+ name: "skill_files",
1216
+ label: "List Skill Companion Files",
1217
+ description:
1218
+ "List companion files (scripts, references, evals) for a skill. " +
1219
+ "Use this after skill_get to see what additional files are available. " +
1220
+ "Returns file names, sizes, and whether the skill recommends installation.",
1221
+ parameters: Type.Object({
1222
+ skillId: Type.String({ description: "The skill_id to inspect" }),
1223
+ }),
1224
+ execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
1225
+ const { skillId } = params as { skillId: string };
1226
+ ctx.log.debug(`skill_files called for skill=${skillId}`);
1227
+
1228
+ const manifest = skillInstaller.getCompanionManifest(skillId);
1229
+ if (!manifest) {
1230
+ return {
1231
+ content: [{ type: "text", text: `Skill not found: ${skillId}` }],
1232
+ details: { error: "not_found" },
1233
+ };
1234
+ }
1235
+
1236
+ if (!manifest.hasCompanionFiles) {
1237
+ return {
1238
+ content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
1239
+ details: manifest,
1240
+ };
1241
+ }
1242
+
1243
+ const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
1244
+ if (manifest.scriptsCount > 0) {
1245
+ lines.push(`### Scripts (${manifest.scriptsCount})`);
1246
+ for (const f of manifest.files.filter(f => f.type === "script")) {
1247
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
1248
+ }
1249
+ }
1250
+ if (manifest.referencesCount > 0) {
1251
+ lines.push(`\n### References (${manifest.referencesCount})`);
1252
+ for (const f of manifest.files.filter(f => f.type === "reference")) {
1253
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
1254
+ }
1255
+ }
1256
+ if (manifest.evalsCount > 0) {
1257
+ lines.push(`\n### Evals (${manifest.evalsCount})`);
1258
+ for (const f of manifest.files.filter(f => f.type === "eval")) {
1259
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
1260
+ }
1261
+ }
1262
+
1263
+ if (manifest.installMode === "install_recommended") {
1264
+ lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
1265
+ }
1266
+ if (manifest.installed && manifest.installedPath) {
1267
+ lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
1268
+ }
1269
+
1270
+ return {
1271
+ content: [{ type: "text", text: lines.join("\n") }],
1272
+ details: manifest,
1273
+ };
1274
+ }),
1275
+ },
1276
+ { name: "skill_files" },
1277
+ );
1278
+
1279
+ // ─── Tool: skill_file_get ───
1280
+
1281
+ api.registerTool(
1282
+ {
1283
+ name: "skill_file_get",
1284
+ label: "Get Skill Companion File",
1285
+ description:
1286
+ "Read the content of a specific companion file (script, reference) from a skill. " +
1287
+ "Use after skill_files to retrieve a script or reference document. " +
1288
+ "Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
1289
+ parameters: Type.Object({
1290
+ skillId: Type.String({ description: "The skill_id" }),
1291
+ path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
1292
+ }),
1293
+ execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
1294
+ const { skillId, path: filePath } = params as { skillId: string; path: string };
1295
+ ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
1296
+
1297
+ const result = skillInstaller.readCompanionFile(skillId, filePath);
1298
+ if ("error" in result) {
1299
+ return {
1300
+ content: [{ type: "text", text: `Error: ${result.error}` }],
1301
+ details: result,
1302
+ };
1303
+ }
1304
+
1305
+ const ext = filePath.split(".").pop() || "";
1306
+ const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
1307
+
1308
+ return {
1309
+ content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
1310
+ details: { path: filePath, size: result.size },
1311
+ };
1312
+ }),
1313
+ },
1314
+ { name: "skill_file_get" },
1315
+ );
1316
+
1107
1317
  // ─── Tool: memory_viewer ───
1108
1318
 
1109
- const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
1319
+ const gatewayPort = (api.config as any)?.gateway?.port ?? 18789;
1320
+ const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
1110
1321
 
1111
1322
  api.registerTool(
1112
1323
  {
@@ -1613,11 +1824,73 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1613
1824
  ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
1614
1825
 
1615
1826
  const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
1827
+
1828
+ // Hub fallback helper: search team shared memories when local search has no relevant results
1829
+ const hubFallback = async (): Promise<SearchHit[]> => {
1830
+ if (!ctx.config?.sharing?.enabled) return [];
1831
+ try {
1832
+ const hubResult = await hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" });
1833
+ if (hubResult.hits.length === 0) return [];
1834
+ ctx.log.debug(`auto-recall: hub fallback returned ${hubResult.hits.length} hit(s)`);
1835
+ return hubResult.hits.map((h) => ({
1836
+ summary: h.summary,
1837
+ original_excerpt: h.excerpt || h.summary,
1838
+ ref: { sessionKey: "", chunkId: h.remoteHitId, turnId: "", seq: 0 },
1839
+ score: 0.9,
1840
+ taskId: null,
1841
+ skillId: null,
1842
+ origin: "hub-remote" as const,
1843
+ source: { ts: h.source.ts, role: h.source.role, sessionKey: "" },
1844
+ }));
1845
+ } catch (err) {
1846
+ ctx.log.debug(`auto-recall: hub fallback failed (${err})`);
1847
+ return [];
1848
+ }
1849
+ };
1850
+
1851
+ if (result.hits.length === 0) {
1852
+ // Local found nothing — try hub before giving up
1853
+ const hubHits = await hubFallback();
1854
+ if (hubHits.length > 0) {
1855
+ result.hits.push(...hubHits);
1856
+ ctx.log.debug(`auto-recall: local empty, using ${hubHits.length} hub hit(s)`);
1857
+ }
1858
+ }
1616
1859
  if (result.hits.length === 0) {
1617
- ctx.log.debug("auto-recall: no candidates found");
1860
+ ctx.log.debug("auto-recall: no memory candidates found");
1618
1861
  const dur = performance.now() - recallT0;
1619
1862
  store.recordToolCall("memory_search", dur, true);
1620
1863
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
1864
+
1865
+ // Even without memory hits, try skill recall
1866
+ const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
1867
+ if (skillAutoRecallEarly) {
1868
+ try {
1869
+ const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
1870
+ const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1871
+ const topSkills = skillHits.slice(0, skillLimit);
1872
+ if (topSkills.length > 0) {
1873
+ const skillLines = topSkills.map((sc, i) => {
1874
+ const manifest = skillInstaller.getCompanionManifest(sc.skillId);
1875
+ let badge = "";
1876
+ if (manifest?.installed) badge = " [installed]";
1877
+ else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
1878
+ else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
1879
+ return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
1880
+ });
1881
+ const skillContext = "## Relevant skills from past experience\n\n" +
1882
+ "No direct memory matches were found, but these skills from past tasks may help:\n\n" +
1883
+ skillLines.join("\n\n") +
1884
+ "\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
1885
+ ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
1886
+ try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
1887
+ return { prependContext: skillContext };
1888
+ }
1889
+ } catch (err) {
1890
+ ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
1891
+ }
1892
+ }
1893
+
1621
1894
  if (query.length > 50) {
1622
1895
  const noRecallHint =
1623
1896
  "## Memory system — ACTION REQUIRED\n\n" +
@@ -1646,22 +1919,36 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1646
1919
  const indexSet = new Set(filterResult.relevant);
1647
1920
  filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
1648
1921
  } else {
1649
- ctx.log.debug("auto-recall: LLM filter returned no relevant hits");
1650
- const dur = performance.now() - recallT0;
1651
- store.recordToolCall("memory_search", dur, true);
1652
- store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1653
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
1654
- filtered: []
1655
- }), dur, true);
1656
- if (query.length > 50) {
1657
- const noRecallHint =
1658
- "## Memory system ACTION REQUIRED\n\n" +
1659
- "Auto-recall found no relevant results for a long query. " +
1660
- "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
1661
- "Do NOT skip this step. Do NOT answer without searching first.";
1662
- return { prependContext: noRecallHint };
1922
+ ctx.log.debug("auto-recall: LLM filter returned no relevant local hits, trying hub fallback");
1923
+ const hubHits = await hubFallback();
1924
+ if (hubHits.length > 0) {
1925
+ ctx.log.debug(`auto-recall: hub fallback provided ${hubHits.length} hit(s) after local filter yielded 0`);
1926
+ filteredHits = hubHits;
1927
+ } else {
1928
+ const dur = performance.now() - recallT0;
1929
+ store.recordToolCall("memory_search", dur, true);
1930
+ store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1931
+ candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
1932
+ filtered: []
1933
+ }), dur, true);
1934
+ if (query.length > 50) {
1935
+ const noRecallHint =
1936
+ "## Memory system — ACTION REQUIRED\n\n" +
1937
+ "Auto-recall found no relevant results for a long query. " +
1938
+ "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
1939
+ "Do NOT skip this step. Do NOT answer without searching first.";
1940
+ return { prependContext: noRecallHint };
1941
+ }
1942
+ return;
1663
1943
  }
1664
- return;
1944
+ }
1945
+ }
1946
+
1947
+ if (!sufficient && filteredHits.length > 0 && ctx.config?.sharing?.enabled) {
1948
+ const hubSupp = await hubFallback();
1949
+ if (hubSupp.length > 0) {
1950
+ ctx.log.debug(`auto-recall: local insufficient, supplementing with ${hubSupp.length} hub hit(s)`);
1951
+ filteredHits.push(...hubSupp);
1665
1952
  }
1666
1953
  }
1667
1954
 
@@ -1671,7 +1958,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1671
1958
 
1672
1959
  const lines = filteredHits.map((h, i) => {
1673
1960
  const excerpt = h.original_excerpt;
1674
- const parts: string[] = [`${i + 1}. [${h.source.role}]`];
1961
+ const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
1962
+ const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
1675
1963
  if (excerpt) parts.push(` ${excerpt}`);
1676
1964
  parts.push(` chunkId="${h.ref.chunkId}"`);
1677
1965
  if (h.taskId) {
@@ -1706,17 +1994,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1706
1994
  lines.join("\n\n"),
1707
1995
  ];
1708
1996
  if (tipsText) contextParts.push(tipsText);
1997
+
1998
+ // ─── Skill auto-recall ───
1999
+ const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
2000
+ const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
2001
+ let skillSection = "";
2002
+
2003
+ if (skillAutoRecall) {
2004
+ try {
2005
+ const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
2006
+
2007
+ // Source 1: direct skill search based on user query
2008
+ try {
2009
+ const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
2010
+ for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
2011
+ if (!skillCandidateMap.has(sh.skillId)) {
2012
+ skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
2013
+ }
2014
+ }
2015
+ } catch (err) {
2016
+ ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
2017
+ }
2018
+
2019
+ // Source 2: skills linked to tasks from memory hits
2020
+ const taskIds = new Set<string>();
2021
+ for (const h of filteredHits) {
2022
+ if (h.taskId) {
2023
+ const t = store.getTask(h.taskId);
2024
+ if (t && t.status !== "skipped") taskIds.add(h.taskId);
2025
+ }
2026
+ }
2027
+ for (const tid of taskIds) {
2028
+ const linked = store.getSkillsByTask(tid);
2029
+ for (const rs of linked) {
2030
+ if (!skillCandidateMap.has(rs.skill.id)) {
2031
+ skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
2032
+ }
2033
+ }
2034
+ }
2035
+
2036
+ const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
2037
+
2038
+ if (skillCandidates.length > 0) {
2039
+ const skillLines = skillCandidates.map((sc, i) => {
2040
+ const manifest = skillInstaller.getCompanionManifest(sc.skillId);
2041
+ let badge = "";
2042
+ if (manifest?.installed) badge = " [installed]";
2043
+ else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
2044
+ else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
2045
+ const action = `call \`skill_get(skillId="${sc.skillId}")\``;
2046
+ return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
2047
+ });
2048
+ skillSection = "\n\n## Relevant skills from past experience\n\n" +
2049
+ "The following skills were distilled from similar previous tasks. " +
2050
+ "You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
2051
+ skillLines.join("\n\n");
2052
+
2053
+ ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
2054
+ try {
2055
+ store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
2056
+ } catch { /* best-effort */ }
2057
+ } else {
2058
+ ctx.log.debug("auto-recall-skill: no matching skills found");
2059
+ }
2060
+ } catch (err) {
2061
+ ctx.log.debug(`auto-recall-skill: failed: ${err}`);
2062
+ }
2063
+ }
2064
+
2065
+ if (skillSection) contextParts.push(skillSection);
1709
2066
  const context = contextParts.join("\n");
1710
2067
 
1711
2068
  const recallDur = performance.now() - recallT0;
1712
2069
  store.recordToolCall("memory_search", recallDur, true);
1713
2070
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1714
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
1715
- filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
2071
+ candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
2072
+ filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
1716
2073
  }), recallDur, true);
1717
2074
  telemetry.trackAutoRecall(filteredHits.length, recallDur);
1718
2075
 
1719
- ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
2076
+ ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
1720
2077
 
1721
2078
  if (!sufficient) {
1722
2079
  const searchHint =
@@ -1943,6 +2300,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1943
2300
 
1944
2301
  // ─── Memory Viewer (web UI) ───
1945
2302
 
2303
+ const derivedHubPort = gatewayPort + 11;
2304
+
1946
2305
  const viewer = new ViewerServer({
1947
2306
  store,
1948
2307
  embedder,
@@ -1950,10 +2309,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1950
2309
  log: ctx.log,
1951
2310
  dataDir: stateDir,
1952
2311
  ctx,
2312
+ defaultHubPort: derivedHubPort,
1953
2313
  });
1954
-
1955
2314
  const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
1956
- ? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
2315
+ ? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder, defaultHubPort: derivedHubPort })
1957
2316
  : null;
1958
2317
 
1959
2318
  // ─── Service lifecycle ───
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "memos-local-openclaw-plugin",
3
3
  "name": "MemOS Local Memory",
4
- "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary, memory_timeline, memory_viewer for layered retrieval.",
4
+ "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
5
5
  "kind": "memory",
6
6
  "version": "0.1.12",
7
7
  "skills": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.9",
3
+ "version": "1.0.4",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -13,6 +13,7 @@
13
13
  "prebuilds",
14
14
  "scripts/postinstall.cjs",
15
15
  "openclaw.plugin.json",
16
+ "telemetry.credentials.json",
16
17
  "README.md",
17
18
  ".env.example"
18
19
  ],