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

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 (119) hide show
  1. package/.env.example +7 -0
  2. package/README.md +24 -24
  3. package/dist/capture/index.d.ts +1 -1
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +34 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +2 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +122 -26
  10. package/dist/client/connector.js.map +1 -1
  11. package/dist/client/hub.d.ts.map +1 -1
  12. package/dist/client/hub.js +22 -0
  13. package/dist/client/hub.js.map +1 -1
  14. package/dist/client/skill-sync.d.ts +7 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -1
  16. package/dist/client/skill-sync.js +10 -0
  17. package/dist/client/skill-sync.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +0 -2
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +8 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +390 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +11 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +31 -3
  28. package/dist/hub/user-manager.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +7 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest/providers/index.d.ts.map +1 -1
  33. package/dist/ingest/providers/index.js +37 -6
  34. package/dist/ingest/providers/index.js.map +1 -1
  35. package/dist/recall/engine.d.ts.map +1 -1
  36. package/dist/recall/engine.js +93 -1
  37. package/dist/recall/engine.js.map +1 -1
  38. package/dist/shared/llm-call.d.ts +1 -0
  39. package/dist/shared/llm-call.d.ts.map +1 -1
  40. package/dist/shared/llm-call.js +82 -8
  41. package/dist/shared/llm-call.js.map +1 -1
  42. package/dist/sharing/types.d.ts +1 -1
  43. package/dist/sharing/types.d.ts.map +1 -1
  44. package/dist/skill/evolver.d.ts +4 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +59 -5
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/skill/generator.d.ts +2 -0
  49. package/dist/skill/generator.d.ts.map +1 -1
  50. package/dist/skill/generator.js +45 -3
  51. package/dist/skill/generator.js.map +1 -1
  52. package/dist/skill/installer.d.ts +26 -0
  53. package/dist/skill/installer.d.ts.map +1 -1
  54. package/dist/skill/installer.js +80 -4
  55. package/dist/skill/installer.js.map +1 -1
  56. package/dist/skill/upgrader.d.ts +2 -0
  57. package/dist/skill/upgrader.d.ts.map +1 -1
  58. package/dist/skill/upgrader.js +139 -1
  59. package/dist/skill/upgrader.js.map +1 -1
  60. package/dist/skill/validator.d.ts +3 -0
  61. package/dist/skill/validator.d.ts.map +1 -1
  62. package/dist/skill/validator.js +75 -0
  63. package/dist/skill/validator.js.map +1 -1
  64. package/dist/storage/ensure-binding.d.ts +12 -0
  65. package/dist/storage/ensure-binding.d.ts.map +1 -0
  66. package/dist/storage/ensure-binding.js +53 -0
  67. package/dist/storage/ensure-binding.js.map +1 -0
  68. package/dist/storage/sqlite.d.ts +89 -20
  69. package/dist/storage/sqlite.d.ts.map +1 -1
  70. package/dist/storage/sqlite.js +374 -124
  71. package/dist/storage/sqlite.js.map +1 -1
  72. package/dist/telemetry.d.ts +12 -5
  73. package/dist/telemetry.d.ts.map +1 -1
  74. package/dist/telemetry.js +156 -40
  75. package/dist/telemetry.js.map +1 -1
  76. package/dist/tools/memory-search.d.ts +3 -1
  77. package/dist/tools/memory-search.d.ts.map +1 -1
  78. package/dist/tools/memory-search.js +3 -1
  79. package/dist/tools/memory-search.js.map +1 -1
  80. package/dist/types.d.ts +11 -2
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/types.js +4 -0
  83. package/dist/types.js.map +1 -1
  84. package/dist/viewer/html.d.ts.map +1 -1
  85. package/dist/viewer/html.js +2671 -879
  86. package/dist/viewer/html.js.map +1 -1
  87. package/dist/viewer/server.d.ts +30 -8
  88. package/dist/viewer/server.d.ts.map +1 -1
  89. package/dist/viewer/server.js +990 -198
  90. package/dist/viewer/server.js.map +1 -1
  91. package/index.ts +700 -56
  92. package/openclaw.plugin.json +1 -1
  93. package/package.json +3 -2
  94. package/scripts/postinstall.cjs +1 -1
  95. package/skill/memos-memory-guide/SKILL.md +64 -26
  96. package/src/capture/index.ts +37 -1
  97. package/src/client/connector.ts +124 -28
  98. package/src/client/hub.ts +18 -0
  99. package/src/client/skill-sync.ts +14 -0
  100. package/src/config.ts +0 -2
  101. package/src/hub/server.ts +374 -97
  102. package/src/hub/user-manager.ts +48 -8
  103. package/src/index.ts +10 -2
  104. package/src/ingest/providers/index.ts +41 -7
  105. package/src/recall/engine.ts +86 -1
  106. package/src/shared/llm-call.ts +97 -9
  107. package/src/sharing/types.ts +1 -1
  108. package/src/skill/evolver.ts +63 -6
  109. package/src/skill/generator.ts +44 -5
  110. package/src/skill/installer.ts +107 -4
  111. package/src/skill/upgrader.ts +139 -1
  112. package/src/skill/validator.ts +79 -0
  113. package/src/storage/ensure-binding.ts +52 -0
  114. package/src/storage/sqlite.ts +395 -148
  115. package/src/telemetry.ts +172 -41
  116. package/src/tools/memory-search.ts +2 -1
  117. package/src/types.ts +12 -2
  118. package/src/viewer/html.ts +2671 -879
  119. package/src/viewer/server.ts +913 -182
package/index.ts CHANGED
@@ -9,19 +9,22 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
9
9
  import { Type } from "@sinclair/typebox";
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
+ import { fileURLToPath } from "url";
12
13
  import { buildContext } from "./src/config";
13
14
  import type { HostModelsConfig } from "./src/openclaw-api";
15
+ import { ensureSqliteBinding } from "./src/storage/ensure-binding";
14
16
  import { SqliteStore } from "./src/storage/sqlite";
15
17
  import { Embedder } from "./src/embedding";
16
18
  import { IngestWorker } from "./src/ingest/worker";
17
19
  import { RecallEngine } from "./src/recall/engine";
18
20
  import { captureMessages, stripInboundMetadata } from "./src/capture";
19
21
  import { DEFAULTS } from "./src/types";
22
+ import type { SearchHit } from "./src/types";
20
23
  import { ViewerServer } from "./src/viewer/server";
21
24
  import { HubServer } from "./src/hub/server";
22
25
  import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
23
26
  import { getHubStatus, connectToHub } from "./src/client/connector";
24
- import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
27
+ import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub, unpublishSkillBundleFromHub } from "./src/client/skill-sync";
25
28
  import { SkillEvolver } from "./src/skill/evolver";
26
29
  import { SkillInstaller } from "./src/skill/installer";
27
30
  import { Summarizer } from "./src/ingest/providers";
@@ -81,13 +84,20 @@ const memosLocalPlugin = {
81
84
 
82
85
  register(api: OpenClawPluginApi) {
83
86
  // ─── Ensure better-sqlite3 native module is available ───
84
- const pluginDir = path.dirname(new URL(import.meta.url).pathname);
87
+ const pluginDir = path.dirname(fileURLToPath(import.meta.url));
88
+
89
+ function normalizeFsPath(p: string): string {
90
+ return path.resolve(p).replace(/\\/g, "/").toLowerCase();
91
+ }
92
+
85
93
  let sqliteReady = false;
86
94
 
87
95
  function trySqliteLoad(): boolean {
88
96
  try {
89
97
  const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
90
- if (!resolved.startsWith(pluginDir)) {
98
+ const resolvedNorm = normalizeFsPath(resolved);
99
+ const pluginNorm = normalizeFsPath(pluginDir);
100
+ if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
91
101
  api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
92
102
  return false;
93
103
  }
@@ -196,6 +206,8 @@ const memosLocalPlugin = {
196
206
  error: (msg: string) => api.logger.warn(`[error] ${msg}`),
197
207
  }, hostModels);
198
208
 
209
+ ensureSqliteBinding(ctx.log);
210
+
199
211
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
200
212
  const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
201
213
  const worker = new IngestWorker(store, embedder, ctx);
@@ -205,6 +217,7 @@ const memosLocalPlugin = {
205
217
  const workspaceDir = api.resolvePath("~/.openclaw/workspace");
206
218
  const skillCtx = { ...ctx, workspaceDir };
207
219
  const skillEvolver = new SkillEvolver(store, engine, skillCtx);
220
+ skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
208
221
  const skillInstaller = new SkillInstaller(store, skillCtx);
209
222
 
210
223
  let pluginVersion = "0.0.0";
@@ -269,6 +282,18 @@ const memosLocalPlugin = {
269
282
  // Falls back to "main" when no hook has fired yet (single-agent setups).
270
283
  let currentAgentId = "main";
271
284
 
285
+ // ─── Check allowPromptInjection policy ───
286
+ // When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
287
+ // will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
288
+ const pluginEntry = (api.config as any)?.plugins?.entries?.[api.id];
289
+ const allowPromptInjection = pluginEntry?.hooks?.allowPromptInjection !== false;
290
+ if (!allowPromptInjection) {
291
+ api.logger.info("memos-local: allowPromptInjection=false, auto-recall disabled");
292
+ }
293
+ else {
294
+ api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
295
+ }
296
+
272
297
  const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
273
298
  async (...args: any[]) => {
274
299
  const t0 = performance.now();
@@ -280,6 +305,7 @@ const memosLocalPlugin = {
280
305
  return result;
281
306
  } catch (e) {
282
307
  ok = false;
308
+ telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
283
309
  throw e;
284
310
  } finally {
285
311
  const dur = performance.now() - t0;
@@ -293,6 +319,21 @@ const memosLocalPlugin = {
293
319
  candidates: det.candidates,
294
320
  filtered: det.hits ?? det.filtered ?? [],
295
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
+ });
296
337
  } else {
297
338
  outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
298
339
  }
@@ -301,6 +342,89 @@ const memosLocalPlugin = {
301
342
  }
302
343
  };
303
344
 
345
+ const getCurrentOwner = () => `agent:${currentAgentId}`;
346
+ const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
347
+ scope === "group" || scope === "all" ? scope : "local";
348
+ const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
349
+ target === "hub" || target === "both" ? target : "agents";
350
+ const resolveMemoryUnshareTarget = (target?: string): "agents" | "hub" | "all" =>
351
+ target === "agents" || target === "hub" ? target : "all";
352
+ const resolveSkillPublishTarget = (target?: string, scope?: string): "agents" | "hub" => {
353
+ if (target === "hub") return "hub";
354
+ if (target === "agents") return "agents";
355
+ return scope === "public" || scope === "group" ? "hub" : "agents";
356
+ };
357
+ const resolveSkillHubVisibility = (visibility?: string, scope?: string): "public" | "group" =>
358
+ visibility === "group" || scope === "group" ? "group" : "public";
359
+ const resolveSkillUnpublishTarget = (target?: string): "agents" | "hub" | "all" =>
360
+ target === "hub" || target === "all" ? target : "agents";
361
+
362
+ const shareMemoryToHub = async (
363
+ chunkId: string,
364
+ input?: { visibility?: "public" | "group"; groupId?: string; hubAddress?: string; userToken?: string },
365
+ ): Promise<{ memoryId: string; visibility: "public" | "group"; groupId: string | null }> => {
366
+ const chunk = store.getChunk(chunkId);
367
+ if (!chunk) {
368
+ throw new Error(`Memory not found: ${chunkId}`);
369
+ }
370
+
371
+ const visibility = input?.visibility === "group" ? "group" : "public";
372
+ const groupId = visibility === "group" ? (input?.groupId ?? null) : null;
373
+ const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
374
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
375
+ method: "POST",
376
+ body: JSON.stringify({
377
+ memory: {
378
+ sourceChunkId: chunk.id,
379
+ role: chunk.role,
380
+ content: chunk.content,
381
+ summary: chunk.summary,
382
+ kind: chunk.kind,
383
+ groupId,
384
+ visibility,
385
+ },
386
+ }),
387
+ }) as { memoryId?: string; visibility?: "public" | "group" };
388
+
389
+ const now = Date.now();
390
+ const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
391
+ store.upsertHubMemory({
392
+ id: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
393
+ sourceChunkId: chunk.id,
394
+ sourceUserId: hubClient.userId,
395
+ role: chunk.role,
396
+ content: chunk.content,
397
+ summary: chunk.summary ?? "",
398
+ kind: chunk.kind,
399
+ groupId,
400
+ visibility,
401
+ createdAt: existing?.createdAt ?? now,
402
+ updatedAt: now,
403
+ });
404
+
405
+ return {
406
+ memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
407
+ visibility,
408
+ groupId,
409
+ };
410
+ };
411
+
412
+ const unshareMemoryFromHub = async (
413
+ chunkId: string,
414
+ input?: { hubAddress?: string; userToken?: string },
415
+ ): Promise<void> => {
416
+ const chunk = store.getChunk(chunkId);
417
+ if (!chunk) {
418
+ throw new Error(`Memory not found: ${chunkId}`);
419
+ }
420
+ const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
421
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
422
+ method: "POST",
423
+ body: JSON.stringify({ sourceChunkId: chunk.id }),
424
+ });
425
+ store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
426
+ };
427
+
304
428
  // ─── Tool: memory_search ───
305
429
 
306
430
  api.registerTool(
@@ -309,24 +433,43 @@ const memosLocalPlugin = {
309
433
  label: "Memory Search",
310
434
  description:
311
435
  "Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
312
- "Relevant memories are automatically injected at the start of each turn, but call this tool when you need " +
313
- "to search with a different query or the auto-recalled context is insufficient. " +
314
- "Pass only a short natural-language query (2-5 key words).",
436
+ "Use scope='local' for this agent plus local shared memories, or scope='group'/'all' to include Hub-shared memories. " +
437
+ "Supports optional maxResults, minScore, and role filtering when you need tighter control.",
315
438
  parameters: Type.Object({
316
439
  query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
440
+ scope: Type.Optional(Type.String({ description: "Search scope: 'local' (default), 'group', or 'all'. Use group/all to include Hub-shared memories." })),
441
+ maxResults: Type.Optional(Type.Number({ description: "Maximum results to return. Default 10, max 20." })),
442
+ minScore: Type.Optional(Type.Number({ description: "Minimum score threshold for local recall. Default 0.45, floor 0.35." })),
443
+ role: Type.Optional(Type.String({ description: "Optional local role filter: 'user', 'assistant', 'tool', or 'system'." })),
444
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
445
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
317
446
  }),
318
447
  execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
319
- const { query } = params as { query: string };
320
- const role = undefined;
321
- const minScore = undefined;
322
- const searchScope = "local";
323
- const searchLimit = 10;
324
- const hubAddress: string | undefined = undefined;
325
- const userToken: string | undefined = undefined;
448
+ const {
449
+ query,
450
+ scope: rawScope,
451
+ maxResults,
452
+ minScore: rawMinScore,
453
+ role: rawRole,
454
+ hubAddress,
455
+ userToken,
456
+ } = params as {
457
+ query: string;
458
+ scope?: string;
459
+ maxResults?: number;
460
+ minScore?: number;
461
+ role?: string;
462
+ hubAddress?: string;
463
+ userToken?: string;
464
+ };
465
+ const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
466
+ const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
467
+ const searchScope = resolveMemorySearchScope(rawScope);
468
+ const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
326
469
 
327
470
  const agentId = currentAgentId;
328
- const ownerFilter = [`agent:${agentId}`, "public"];
329
- const effectiveMaxResults = 10;
471
+ const ownerFilter = [getCurrentOwner(), "public"];
472
+ const effectiveMaxResults = searchLimit;
330
473
  ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
331
474
  const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
332
475
  ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
@@ -337,9 +480,10 @@ const memosLocalPlugin = {
337
480
  score: h.score,
338
481
  summary: h.summary,
339
482
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
483
+ origin: h.origin || "local",
340
484
  }));
341
485
 
342
- if (result.hits.length === 0) {
486
+ if (result.hits.length === 0 && searchScope === "local") {
343
487
  return {
344
488
  content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
345
489
  details: { candidates: [], meta: result.meta },
@@ -363,11 +507,13 @@ const memosLocalPlugin = {
363
507
  const indexSet = new Set(filterResult.relevant);
364
508
  filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
365
509
  ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
366
- } else {
510
+ } else if (searchScope === "local") {
367
511
  return {
368
512
  content: [{ type: "text", text: "No relevant memories found for this query." }],
369
513
  details: { candidates: rawCandidates, filtered: [], meta: result.meta },
370
514
  };
515
+ } else {
516
+ filteredHits = [];
371
517
  }
372
518
  }
373
519
 
@@ -389,29 +535,79 @@ const memosLocalPlugin = {
389
535
  role: h.source.role,
390
536
  score: h.score,
391
537
  summary: h.summary,
538
+ origin: h.origin || "local",
392
539
  };
393
540
  });
394
541
 
395
542
  if (searchScope !== "local") {
396
543
  const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
544
+
545
+ let filteredHubHits = hub.hits;
546
+ if (hub.hits.length > 0) {
547
+ const hubCandidates = hub.hits.map((h, i) => ({
548
+ index: filteredHits.length + i + 1,
549
+ role: (h.source?.role || "assistant") as string,
550
+ content: (h.summary || h.excerpt || "").slice(0, 300),
551
+ time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
552
+ }));
553
+ const localCandidatesForMerge = filteredHits.map((h, i) => ({
554
+ index: i + 1,
555
+ role: h.source.role,
556
+ content: (h.original_excerpt ?? "").slice(0, 300),
557
+ time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
558
+ }));
559
+ const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
560
+ const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
561
+ if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
562
+ const relevantSet = new Set(mergedFilter.relevant);
563
+ const hubStartIdx = filteredHits.length + 1;
564
+ filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
565
+ filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
566
+ ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
567
+ }
568
+ }
569
+
570
+ const originLabel = (h: SearchHit) => {
571
+ if (h.origin === "hub-memory") return " [团队缓存]";
572
+ if (h.origin === "local-shared") return " [本机共享]";
573
+ return "";
574
+ };
397
575
  const localText = filteredHits.length > 0
398
576
  ? filteredHits.map((h, i) => {
399
577
  const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
400
- return `${i + 1}. [${h.source.role}] ${excerpt}`;
578
+ return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
401
579
  }).join("\n")
402
580
  : "(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")
581
+ const hubText = filteredHubHits.length > 0
582
+ ? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
405
583
  : "(none)";
406
584
 
585
+ const localDetailsFiltered = filteredHits.map((h) => {
586
+ let effectiveTaskId = h.taskId;
587
+ if (effectiveTaskId) {
588
+ const t = store.getTask(effectiveTaskId);
589
+ if (t && t.status === "skipped") effectiveTaskId = null;
590
+ }
591
+ return {
592
+ ref: h.ref,
593
+ chunkId: h.ref.chunkId,
594
+ taskId: effectiveTaskId,
595
+ skillId: h.skillId,
596
+ role: h.source.role,
597
+ score: h.score,
598
+ summary: h.summary,
599
+ origin: h.origin,
600
+ };
601
+ });
602
+
407
603
  return {
408
604
  content: [{
409
605
  type: "text",
410
606
  text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
411
607
  }],
412
608
  details: {
413
- local: { hits: localDetailsHits, meta: result.meta },
414
- hub,
609
+ local: { hits: localDetailsFiltered, meta: result.meta },
610
+ hub: { ...hub, hits: filteredHubHits },
415
611
  },
416
612
  };
417
613
  }
@@ -423,9 +619,15 @@ const memosLocalPlugin = {
423
619
  };
424
620
  }
425
621
 
622
+ const originTag = (o?: string) => {
623
+ if (o === "local-shared") return " [本机共享]";
624
+ if (o === "hub-memory") return " [团队缓存]";
625
+ if (o === "hub-remote") return " [团队]";
626
+ return "";
627
+ };
426
628
  const lines = filteredHits.map((h, i) => {
427
629
  const excerpt = h.original_excerpt;
428
- const parts = [`${i + 1}. [${h.source.role}]`];
630
+ const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
429
631
  if (excerpt) parts.push(` ${excerpt}`);
430
632
  parts.push(` chunkId="${h.ref.chunkId}"`);
431
633
  if (h.taskId) {
@@ -480,6 +682,7 @@ const memosLocalPlugin = {
480
682
  score: h.score,
481
683
  summary: h.summary,
482
684
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
685
+ origin: h.origin || "local",
483
686
  };
484
687
  }),
485
688
  meta: result.meta,
@@ -843,7 +1046,9 @@ ${detail.content}`,
843
1046
  {
844
1047
  name: "network_team_info",
845
1048
  label: "Network Team Info",
846
- description: "Show current Hub connection status, signed-in user, role, and group memberships.",
1049
+ description:
1050
+ "Show current Hub connection status, signed-in user, role, and group memberships. " +
1051
+ "Use this as a preflight check before any Hub share/unshare or Hub pull operation.",
847
1052
  parameters: Type.Object({}),
848
1053
  execute: trackTool("network_team_info", async () => {
849
1054
  const status = await getHubStatus(store, ctx.config);
@@ -927,10 +1132,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
927
1132
  };
928
1133
  }
929
1134
 
1135
+ const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
1136
+ let footer = "\n\n---\n";
1137
+
1138
+ if (manifest && manifest.hasCompanionFiles) {
1139
+ const fileSummary = manifest.files
1140
+ .filter(f => f.type !== "eval")
1141
+ .map(f => `\`${f.relativePath}\``)
1142
+ .join(", ");
1143
+ footer += `**Companion files available:** ${fileSummary}\n`;
1144
+ footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
1145
+ footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
1146
+ if (manifest.installMode === "install_recommended") {
1147
+ footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
1148
+ }
1149
+ if (manifest.installed && manifest.installedPath) {
1150
+ footer += `> Already installed at: ${manifest.installedPath}/\n`;
1151
+ }
1152
+ } else {
1153
+ footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
1154
+ }
1155
+
930
1156
  return {
931
1157
  content: [{
932
1158
  type: "text",
933
- 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}")`,
1159
+ text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
934
1160
  }],
935
1161
  details: {
936
1162
  skillId: skill.id,
@@ -938,6 +1164,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
938
1164
  version: skill.version,
939
1165
  status: skill.status,
940
1166
  installed: skill.installed,
1167
+ companionFiles: manifest?.hasCompanionFiles ?? false,
1168
+ installMode: manifest?.installMode ?? "inline",
941
1169
  },
942
1170
  };
943
1171
  }),
@@ -973,6 +1201,112 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
973
1201
  { name: "skill_install" },
974
1202
  );
975
1203
 
1204
+ // ─── Tool: skill_files ───
1205
+
1206
+ api.registerTool(
1207
+ {
1208
+ name: "skill_files",
1209
+ label: "List Skill Companion Files",
1210
+ description:
1211
+ "List companion files (scripts, references, evals) for a skill. " +
1212
+ "Use this after skill_get to see what additional files are available. " +
1213
+ "Returns file names, sizes, and whether the skill recommends installation.",
1214
+ parameters: Type.Object({
1215
+ skillId: Type.String({ description: "The skill_id to inspect" }),
1216
+ }),
1217
+ execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
1218
+ const { skillId } = params as { skillId: string };
1219
+ ctx.log.debug(`skill_files called for skill=${skillId}`);
1220
+
1221
+ const manifest = skillInstaller.getCompanionManifest(skillId);
1222
+ if (!manifest) {
1223
+ return {
1224
+ content: [{ type: "text", text: `Skill not found: ${skillId}` }],
1225
+ details: { error: "not_found" },
1226
+ };
1227
+ }
1228
+
1229
+ if (!manifest.hasCompanionFiles) {
1230
+ return {
1231
+ content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
1232
+ details: manifest,
1233
+ };
1234
+ }
1235
+
1236
+ const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
1237
+ if (manifest.scriptsCount > 0) {
1238
+ lines.push(`### Scripts (${manifest.scriptsCount})`);
1239
+ for (const f of manifest.files.filter(f => f.type === "script")) {
1240
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
1241
+ }
1242
+ }
1243
+ if (manifest.referencesCount > 0) {
1244
+ lines.push(`\n### References (${manifest.referencesCount})`);
1245
+ for (const f of manifest.files.filter(f => f.type === "reference")) {
1246
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
1247
+ }
1248
+ }
1249
+ if (manifest.evalsCount > 0) {
1250
+ lines.push(`\n### Evals (${manifest.evalsCount})`);
1251
+ for (const f of manifest.files.filter(f => f.type === "eval")) {
1252
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
1253
+ }
1254
+ }
1255
+
1256
+ if (manifest.installMode === "install_recommended") {
1257
+ lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
1258
+ }
1259
+ if (manifest.installed && manifest.installedPath) {
1260
+ lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
1261
+ }
1262
+
1263
+ return {
1264
+ content: [{ type: "text", text: lines.join("\n") }],
1265
+ details: manifest,
1266
+ };
1267
+ }),
1268
+ },
1269
+ { name: "skill_files" },
1270
+ );
1271
+
1272
+ // ─── Tool: skill_file_get ───
1273
+
1274
+ api.registerTool(
1275
+ {
1276
+ name: "skill_file_get",
1277
+ label: "Get Skill Companion File",
1278
+ description:
1279
+ "Read the content of a specific companion file (script, reference) from a skill. " +
1280
+ "Use after skill_files to retrieve a script or reference document. " +
1281
+ "Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
1282
+ parameters: Type.Object({
1283
+ skillId: Type.String({ description: "The skill_id" }),
1284
+ path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
1285
+ }),
1286
+ execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
1287
+ const { skillId, path: filePath } = params as { skillId: string; path: string };
1288
+ ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
1289
+
1290
+ const result = skillInstaller.readCompanionFile(skillId, filePath);
1291
+ if ("error" in result) {
1292
+ return {
1293
+ content: [{ type: "text", text: `Error: ${result.error}` }],
1294
+ details: result,
1295
+ };
1296
+ }
1297
+
1298
+ const ext = filePath.split(".").pop() || "";
1299
+ const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
1300
+
1301
+ return {
1302
+ content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
1303
+ details: { path: filePath, size: result.size },
1304
+ };
1305
+ }),
1306
+ },
1307
+ { name: "skill_file_get" },
1308
+ );
1309
+
976
1310
  // ─── Tool: memory_viewer ───
977
1311
 
978
1312
  const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
@@ -988,6 +1322,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
988
1322
  parameters: Type.Object({}),
989
1323
  execute: trackTool("memory_viewer", async () => {
990
1324
  ctx.log.debug(`memory_viewer called`);
1325
+ telemetry.trackViewerOpened();
991
1326
  const url = `http://127.0.0.1:${viewerPort}`;
992
1327
  return {
993
1328
  content: [
@@ -1018,12 +1353,13 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1018
1353
  api.registerTool(
1019
1354
  {
1020
1355
  name: "memory_write_public",
1021
- label: "Write Public Memory",
1356
+ label: "Write Local Shared Memory",
1022
1357
  description:
1023
- "Write a piece of information to public memory. Public memories are visible to all agents during memory_search. " +
1024
- "Use this for shared knowledge, team decisions, or cross-agent coordination information.",
1358
+ "Write a piece of information to local shared memory for all agents in this OpenClaw workspace. " +
1359
+ "Use this when you are creating a new shared note from scratch. This does not publish to Hub. " +
1360
+ "If you already have a memory chunk and want to expose it, use memory_share instead.",
1025
1361
  parameters: Type.Object({
1026
- content: Type.String({ description: "The content to write to public memory" }),
1362
+ content: Type.String({ description: "The content to write to local shared memory" }),
1027
1363
  summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
1028
1364
  }),
1029
1365
  execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
@@ -1068,7 +1404,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1068
1404
  }
1069
1405
 
1070
1406
  return {
1071
- content: [{ type: "text", text: `Public memory written successfully (id: ${chunkId}).` }],
1407
+ content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
1072
1408
  details: { chunkId, owner: "public" },
1073
1409
  };
1074
1410
  }),
@@ -1076,6 +1412,164 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1076
1412
  { name: "memory_write_public" },
1077
1413
  );
1078
1414
 
1415
+ api.registerTool(
1416
+ {
1417
+ name: "memory_share",
1418
+ label: "Share Memory",
1419
+ description:
1420
+ "Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
1421
+ "Use this only for an existing chunkId. Use target='agents' for local multi-agent sharing, target='hub' for team sharing, or target='both' for both. " +
1422
+ "If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
1423
+ parameters: Type.Object({
1424
+ chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
1425
+ target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
1426
+ visibility: Type.Optional(Type.String({ description: "Hub visibility when target includes hub: 'public' (default) or 'group'" })),
1427
+ groupId: Type.Optional(Type.String({ description: "Optional Hub group ID when visibility='group'" })),
1428
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
1429
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
1430
+ }),
1431
+ execute: trackTool("memory_share", async (_toolCallId: any, params: any) => {
1432
+ const {
1433
+ chunkId,
1434
+ target: rawTarget,
1435
+ visibility: rawVisibility,
1436
+ groupId,
1437
+ hubAddress,
1438
+ userToken,
1439
+ } = params as {
1440
+ chunkId: string;
1441
+ target?: string;
1442
+ visibility?: string;
1443
+ groupId?: string;
1444
+ hubAddress?: string;
1445
+ userToken?: string;
1446
+ };
1447
+
1448
+ const chunk = store.getChunk(chunkId);
1449
+ if (!chunk) {
1450
+ return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
1451
+ }
1452
+
1453
+ const target = resolveMemoryShareTarget(rawTarget);
1454
+ const visibility = rawVisibility === "group" ? "group" : "public";
1455
+ const details: Record<string, unknown> = { chunkId, target };
1456
+ const messages: string[] = [];
1457
+
1458
+ if (target === "agents" || target === "both") {
1459
+ const local = store.markMemorySharedLocally(chunkId);
1460
+ if (!local.ok) {
1461
+ return { content: [{ type: "text", text: `Failed to share memory ${chunkId} to local agents.` }], details: { error: local.reason ?? "local_share_failed", chunkId, target } };
1462
+ }
1463
+ details.local = {
1464
+ shared: true,
1465
+ owner: local.owner,
1466
+ originalOwner: local.originalOwner ?? null,
1467
+ };
1468
+ messages.push("shared to local agents");
1469
+ }
1470
+
1471
+ if (target === "hub" || target === "both") {
1472
+ const hub = await shareMemoryToHub(chunkId, { visibility, groupId, hubAddress, userToken });
1473
+ details.hub = {
1474
+ shared: true,
1475
+ memoryId: hub.memoryId,
1476
+ visibility: hub.visibility,
1477
+ groupId: hub.groupId,
1478
+ };
1479
+ messages.push(`shared to Hub (${hub.visibility})`);
1480
+ }
1481
+
1482
+ return {
1483
+ content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
1484
+ details,
1485
+ };
1486
+ }),
1487
+ },
1488
+ { name: "memory_share" },
1489
+ );
1490
+
1491
+ api.registerTool(
1492
+ {
1493
+ name: "memory_unshare",
1494
+ label: "Unshare Memory",
1495
+ description:
1496
+ "Remove sharing from an existing memory. Use target='agents' to stop local multi-agent sharing, target='hub' to remove it from Hub, or target='all' (default) to remove both. " +
1497
+ "privateOwner is only needed for older public memories that were never tracked with an original owner.",
1498
+ parameters: Type.Object({
1499
+ chunkId: Type.String({ description: "Existing local memory chunk ID to unshare" }),
1500
+ target: Type.Optional(Type.String({ description: "Unshare target: 'agents', 'hub', or 'all' (default)" })),
1501
+ privateOwner: Type.Optional(Type.String({ description: "Optional owner to restore when converting a public memory back to private and no original owner was tracked" })),
1502
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
1503
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
1504
+ }),
1505
+ execute: trackTool("memory_unshare", async (_toolCallId: any, params: any) => {
1506
+ const {
1507
+ chunkId,
1508
+ target: rawTarget,
1509
+ privateOwner,
1510
+ hubAddress,
1511
+ userToken,
1512
+ } = params as {
1513
+ chunkId: string;
1514
+ target?: string;
1515
+ privateOwner?: string;
1516
+ hubAddress?: string;
1517
+ userToken?: string;
1518
+ };
1519
+
1520
+ const chunk = store.getChunk(chunkId);
1521
+ if (!chunk) {
1522
+ return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
1523
+ }
1524
+
1525
+ const target = resolveMemoryUnshareTarget(rawTarget);
1526
+ const details: Record<string, unknown> = { chunkId, target };
1527
+ const messages: string[] = [];
1528
+
1529
+ if (target === "agents" || target === "all") {
1530
+ const local = store.unmarkMemorySharedLocally(chunkId, privateOwner);
1531
+ if (!local.ok) {
1532
+ return {
1533
+ content: [{
1534
+ type: "text",
1535
+ text: local.reason === "original_owner_missing"
1536
+ ? `Cannot restore memory "${chunk.summary || chunk.id}" to a private owner automatically. Pass privateOwner to unshare it locally.`
1537
+ : `Failed to stop local sharing for memory ${chunkId}.`,
1538
+ }],
1539
+ details: { error: local.reason ?? "local_unshare_failed", chunkId, target },
1540
+ };
1541
+ }
1542
+ details.local = {
1543
+ shared: false,
1544
+ owner: local.owner,
1545
+ };
1546
+ messages.push("removed from local agent sharing");
1547
+ }
1548
+
1549
+ if (target === "hub" || target === "all") {
1550
+ try {
1551
+ await unshareMemoryFromHub(chunkId, { hubAddress, userToken });
1552
+ details.hub = { shared: false };
1553
+ messages.push("removed from Hub");
1554
+ } catch (err) {
1555
+ const msg = err instanceof Error ? err.message : String(err);
1556
+ if (target === "all" && msg.includes("hub client connection is not configured")) {
1557
+ details.hub = { shared: false, skipped: true, reason: "hub_not_configured" };
1558
+ } else {
1559
+ throw err;
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ return {
1565
+ content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
1566
+ details,
1567
+ };
1568
+ }),
1569
+ },
1570
+ { name: "memory_unshare" },
1571
+ );
1572
+
1079
1573
  // ─── Tool: skill_search ───
1080
1574
 
1081
1575
  api.registerTool(
@@ -1083,16 +1577,16 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1083
1577
  name: "skill_search",
1084
1578
  label: "Skill Search",
1085
1579
  description:
1086
- "Search available skills by natural language. Searches local skills by default, or local + Hub skills when scope=group/all. " +
1087
- "Use when you need a capability or guide and don't have a matching skill at hand.",
1580
+ "Search available skills by natural language. Use scope='mix' (default) for this agent plus local shared skills, 'self' for this agent only, 'public' for local shared skills only, or 'group'/'all' to include Hub skills as well. " +
1581
+ "Use this when you need a capability or guide and don't have a matching skill at hand.",
1088
1582
  parameters: Type.Object({
1089
1583
  query: Type.String({ description: "Natural language description of the needed skill" }),
1090
- scope: Type.Optional(Type.String({ description: "Search scope: 'mix'/'self'/'public' for local search, or 'group'/'all' for local + Hub search" })),
1584
+ scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
1091
1585
  }),
1092
1586
  execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
1093
1587
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1094
1588
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1095
- const currentOwner = `agent:${currentAgentId}`;
1589
+ const currentOwner = getCurrentOwner();
1096
1590
 
1097
1591
  if (rawScope === "group" || rawScope === "all") {
1098
1592
  const [localHits, hub] = await Promise.all([
@@ -1108,7 +1602,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1108
1602
  }
1109
1603
 
1110
1604
  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")
1605
+ ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
1112
1606
  : "(none)";
1113
1607
  const hubText = hub.hits.length > 0
1114
1608
  ? 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")
@@ -1130,7 +1624,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1130
1624
  }
1131
1625
 
1132
1626
  const text = hits.map((h, i) =>
1133
- `${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (public)" : ""}`,
1627
+ `${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
1134
1628
  ).join("\n");
1135
1629
 
1136
1630
  return {
@@ -1148,31 +1642,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1148
1642
  {
1149
1643
  name: "skill_publish",
1150
1644
  label: "Publish Skill",
1151
- description: "Make a skill public so other agents can discover and install it via skill_search.",
1645
+ description:
1646
+ "Share a skill with local agents or publish it to the Hub. " +
1647
+ "Use target='agents' for local sharing, or target='hub' with visibility='public'/'group' for Hub publishing. " +
1648
+ "The old scope parameter is still accepted for backward compatibility.",
1152
1649
  parameters: Type.Object({
1153
1650
  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" })),
1651
+ target: Type.Optional(Type.String({ description: "Publish target: 'agents' (default) or 'hub'." })),
1652
+ visibility: Type.Optional(Type.String({ description: "Hub visibility when target='hub': 'public' (default) or 'group'." })),
1653
+ scope: Type.Optional(Type.String({ description: "Deprecated alias: omit for local agents, or use 'public' / 'group' to publish to Hub." })),
1155
1654
  groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
1156
1655
  hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1157
1656
  userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1158
1657
  }),
1159
1658
  execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
1160
- const { skillId: pubSkillId, scope, groupId, hubAddress, userToken } = params as { skillId: string; scope?: string; groupId?: string; hubAddress?: string; userToken?: string };
1659
+ const {
1660
+ skillId: pubSkillId,
1661
+ target: rawTarget,
1662
+ visibility: rawVisibility,
1663
+ scope,
1664
+ groupId,
1665
+ hubAddress,
1666
+ userToken,
1667
+ } = params as {
1668
+ skillId: string;
1669
+ target?: string;
1670
+ visibility?: string;
1671
+ scope?: string;
1672
+ groupId?: string;
1673
+ hubAddress?: string;
1674
+ userToken?: string;
1675
+ };
1161
1676
  const skill = store.getSkill(pubSkillId);
1162
1677
  if (!skill) {
1163
1678
  return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
1164
1679
  }
1165
- if (scope === "public" || scope === "group") {
1166
- const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility: scope, groupId, hubAddress, userToken });
1680
+ const target = resolveSkillPublishTarget(rawTarget, scope);
1681
+ const visibility = resolveSkillHubVisibility(rawVisibility, scope);
1682
+ if (target === "hub") {
1683
+ const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility, groupId, hubAddress, userToken });
1167
1684
  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 },
1685
+ content: [{ type: "text", text: `Skill "${skill.name}" shared to Hub (${published.visibility}).` }],
1686
+ details: { skillId: pubSkillId, name: skill.name, target, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
1170
1687
  };
1171
1688
  }
1172
1689
  store.setSkillVisibility(pubSkillId, "public");
1173
1690
  return {
1174
- content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
1175
- details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
1691
+ content: [{ type: "text", text: `Skill "${skill.name}" is now shared with local agents.` }],
1692
+ details: { skillId: pubSkillId, name: skill.name, target, visibility: "public", publishedToHub: false },
1176
1693
  };
1177
1694
  }),
1178
1695
  },
@@ -1185,20 +1702,46 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1185
1702
  {
1186
1703
  name: "skill_unpublish",
1187
1704
  label: "Unpublish Skill",
1188
- description: "Make a skill private. Other agents will no longer be able to discover it.",
1705
+ description:
1706
+ "Stop sharing a skill with local agents, remove it from the Hub, or do both. " +
1707
+ "Use target='agents' (default), 'hub', or 'all'.",
1189
1708
  parameters: Type.Object({
1190
1709
  skillId: Type.String({ description: "The skill ID to unpublish" }),
1710
+ target: Type.Optional(Type.String({ description: "Unpublish target: 'agents' (default), 'hub', or 'all'." })),
1711
+ hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1712
+ userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1191
1713
  }),
1192
1714
  execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
1193
- const { skillId: unpubSkillId } = params as { skillId: string };
1715
+ const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
1194
1716
  const skill = store.getSkill(unpubSkillId);
1195
1717
  if (!skill) {
1196
1718
  return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
1197
1719
  }
1198
- store.setSkillVisibility(unpubSkillId, "private");
1720
+ const resolvedTarget = resolveSkillUnpublishTarget(target);
1721
+ const messages: string[] = [];
1722
+ const details: Record<string, unknown> = { skillId: unpubSkillId, name: skill.name, target: resolvedTarget };
1723
+ if (resolvedTarget === "hub" || resolvedTarget === "all") {
1724
+ try {
1725
+ await unpublishSkillBundleFromHub(store, ctx, { skillId: unpubSkillId, hubAddress, userToken });
1726
+ details.hub = { unpublished: true };
1727
+ messages.push("removed from Hub sharing");
1728
+ } catch (err) {
1729
+ const msg = err instanceof Error ? err.message : String(err);
1730
+ if (resolvedTarget === "all" && msg.includes("hub client connection is not configured")) {
1731
+ details.hub = { unpublished: false, skipped: true, reason: "hub_not_configured" };
1732
+ } else {
1733
+ throw err;
1734
+ }
1735
+ }
1736
+ }
1737
+ if (resolvedTarget === "agents" || resolvedTarget === "all") {
1738
+ store.setSkillVisibility(unpubSkillId, "private");
1739
+ details.local = { visibility: "private" };
1740
+ messages.push("limited to this agent");
1741
+ }
1199
1742
  return {
1200
- content: [{ type: "text", text: `Skill "${skill.name}" is now private.` }],
1201
- details: { skillId: unpubSkillId, name: skill.name, visibility: "private" },
1743
+ content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
1744
+ details,
1202
1745
  };
1203
1746
  }),
1204
1747
  },
@@ -1231,6 +1774,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1231
1774
  // ─── Auto-recall: inject relevant memories before agent starts ───
1232
1775
 
1233
1776
  api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
1777
+ if (!allowPromptInjection) return {};
1234
1778
  if (!event.prompt || event.prompt.length < 3) return;
1235
1779
 
1236
1780
  const recallAgentId = hookCtx?.agentId ?? "main";
@@ -1273,10 +1817,40 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1273
1817
 
1274
1818
  const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
1275
1819
  if (result.hits.length === 0) {
1276
- ctx.log.debug("auto-recall: no candidates found");
1820
+ ctx.log.debug("auto-recall: no memory candidates found");
1277
1821
  const dur = performance.now() - recallT0;
1278
1822
  store.recordToolCall("memory_search", dur, true);
1279
1823
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
1824
+
1825
+ // Even without memory hits, try skill recall
1826
+ const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
1827
+ if (skillAutoRecallEarly) {
1828
+ try {
1829
+ const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
1830
+ const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1831
+ const topSkills = skillHits.slice(0, skillLimit);
1832
+ if (topSkills.length > 0) {
1833
+ const skillLines = topSkills.map((sc, i) => {
1834
+ const manifest = skillInstaller.getCompanionManifest(sc.skillId);
1835
+ let badge = "";
1836
+ if (manifest?.installed) badge = " [installed]";
1837
+ else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
1838
+ else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
1839
+ return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
1840
+ });
1841
+ const skillContext = "## Relevant skills from past experience\n\n" +
1842
+ "No direct memory matches were found, but these skills from past tasks may help:\n\n" +
1843
+ skillLines.join("\n\n") +
1844
+ "\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
1845
+ ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
1846
+ try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
1847
+ return { prependContext: skillContext };
1848
+ }
1849
+ } catch (err) {
1850
+ ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
1851
+ }
1852
+ }
1853
+
1280
1854
  if (query.length > 50) {
1281
1855
  const noRecallHint =
1282
1856
  "## Memory system — ACTION REQUIRED\n\n" +
@@ -1309,7 +1883,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1309
1883
  const dur = performance.now() - recallT0;
1310
1884
  store.recordToolCall("memory_search", dur, true);
1311
1885
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1312
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
1886
+ candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
1313
1887
  filtered: []
1314
1888
  }), dur, true);
1315
1889
  if (query.length > 50) {
@@ -1330,7 +1904,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1330
1904
 
1331
1905
  const lines = filteredHits.map((h, i) => {
1332
1906
  const excerpt = h.original_excerpt;
1333
- const parts: string[] = [`${i + 1}. [${h.source.role}]`];
1907
+ const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
1908
+ const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
1334
1909
  if (excerpt) parts.push(` ${excerpt}`);
1335
1910
  parts.push(` chunkId="${h.ref.chunkId}"`);
1336
1911
  if (h.taskId) {
@@ -1365,17 +1940,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1365
1940
  lines.join("\n\n"),
1366
1941
  ];
1367
1942
  if (tipsText) contextParts.push(tipsText);
1943
+
1944
+ // ─── Skill auto-recall ───
1945
+ const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
1946
+ const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
1947
+ let skillSection = "";
1948
+
1949
+ if (skillAutoRecall) {
1950
+ try {
1951
+ const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
1952
+
1953
+ // Source 1: direct skill search based on user query
1954
+ try {
1955
+ const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1956
+ for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
1957
+ if (!skillCandidateMap.has(sh.skillId)) {
1958
+ skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
1959
+ }
1960
+ }
1961
+ } catch (err) {
1962
+ ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
1963
+ }
1964
+
1965
+ // Source 2: skills linked to tasks from memory hits
1966
+ const taskIds = new Set<string>();
1967
+ for (const h of filteredHits) {
1968
+ if (h.taskId) {
1969
+ const t = store.getTask(h.taskId);
1970
+ if (t && t.status !== "skipped") taskIds.add(h.taskId);
1971
+ }
1972
+ }
1973
+ for (const tid of taskIds) {
1974
+ const linked = store.getSkillsByTask(tid);
1975
+ for (const rs of linked) {
1976
+ if (!skillCandidateMap.has(rs.skill.id)) {
1977
+ skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
1978
+ }
1979
+ }
1980
+ }
1981
+
1982
+ const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
1983
+
1984
+ if (skillCandidates.length > 0) {
1985
+ const skillLines = skillCandidates.map((sc, i) => {
1986
+ const manifest = skillInstaller.getCompanionManifest(sc.skillId);
1987
+ let badge = "";
1988
+ if (manifest?.installed) badge = " [installed]";
1989
+ else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
1990
+ else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
1991
+ const action = `call \`skill_get(skillId="${sc.skillId}")\``;
1992
+ return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
1993
+ });
1994
+ skillSection = "\n\n## Relevant skills from past experience\n\n" +
1995
+ "The following skills were distilled from similar previous tasks. " +
1996
+ "You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
1997
+ skillLines.join("\n\n");
1998
+
1999
+ ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
2000
+ try {
2001
+ store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
2002
+ } catch { /* best-effort */ }
2003
+ } else {
2004
+ ctx.log.debug("auto-recall-skill: no matching skills found");
2005
+ }
2006
+ } catch (err) {
2007
+ ctx.log.debug(`auto-recall-skill: failed: ${err}`);
2008
+ }
2009
+ }
2010
+
2011
+ if (skillSection) contextParts.push(skillSection);
1368
2012
  const context = contextParts.join("\n");
1369
2013
 
1370
2014
  const recallDur = performance.now() - recallT0;
1371
2015
  store.recordToolCall("memory_search", recallDur, true);
1372
2016
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1373
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
1374
- filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
2017
+ candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
2018
+ filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
1375
2019
  }), recallDur, true);
1376
2020
  telemetry.trackAutoRecall(filteredHits.length, recallDur);
1377
2021
 
1378
- ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
2022
+ ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
1379
2023
 
1380
2024
  if (!sufficient) {
1381
2025
  const searchHint =