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

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 (152) hide show
  1. package/README.md +22 -39
  2. package/dist/capture/index.d.ts.map +1 -1
  3. package/dist/capture/index.js +6 -0
  4. package/dist/capture/index.js.map +1 -1
  5. package/dist/config.d.ts +1 -2
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +3 -72
  8. package/dist/config.js.map +1 -1
  9. package/dist/embedding/index.d.ts +2 -4
  10. package/dist/embedding/index.d.ts.map +1 -1
  11. package/dist/embedding/index.js +1 -17
  12. package/dist/embedding/index.js.map +1 -1
  13. package/dist/index.d.ts +0 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/ingest/providers/index.d.ts +2 -10
  18. package/dist/ingest/providers/index.d.ts.map +1 -1
  19. package/dist/ingest/providers/index.js +43 -209
  20. package/dist/ingest/providers/index.js.map +1 -1
  21. package/dist/ingest/providers/openai.d.ts +0 -1
  22. package/dist/ingest/providers/openai.d.ts.map +1 -1
  23. package/dist/ingest/providers/openai.js +0 -1
  24. package/dist/ingest/providers/openai.js.map +1 -1
  25. package/dist/ingest/task-processor.js +1 -1
  26. package/dist/ingest/task-processor.js.map +1 -1
  27. package/dist/recall/engine.js +1 -1
  28. package/dist/recall/engine.js.map +1 -1
  29. package/dist/shared/llm-call.d.ts +2 -4
  30. package/dist/shared/llm-call.d.ts.map +1 -1
  31. package/dist/shared/llm-call.js +81 -20
  32. package/dist/shared/llm-call.js.map +1 -1
  33. package/dist/skill/evaluator.d.ts.map +1 -1
  34. package/dist/skill/evaluator.js +2 -2
  35. package/dist/skill/evaluator.js.map +1 -1
  36. package/dist/skill/evolver.d.ts +2 -0
  37. package/dist/skill/evolver.d.ts.map +1 -1
  38. package/dist/skill/evolver.js +3 -0
  39. package/dist/skill/evolver.js.map +1 -1
  40. package/dist/skill/generator.d.ts.map +1 -1
  41. package/dist/skill/generator.js +4 -4
  42. package/dist/skill/generator.js.map +1 -1
  43. package/dist/skill/upgrader.js +1 -1
  44. package/dist/skill/upgrader.js.map +1 -1
  45. package/dist/skill/validator.js +1 -1
  46. package/dist/skill/validator.js.map +1 -1
  47. package/dist/storage/ensure-binding.d.ts.map +1 -1
  48. package/dist/storage/ensure-binding.js +1 -3
  49. package/dist/storage/ensure-binding.js.map +1 -1
  50. package/dist/storage/sqlite.d.ts +0 -294
  51. package/dist/storage/sqlite.d.ts.map +1 -1
  52. package/dist/storage/sqlite.js +0 -821
  53. package/dist/storage/sqlite.js.map +1 -1
  54. package/dist/telemetry.d.ts +12 -5
  55. package/dist/telemetry.d.ts.map +1 -1
  56. package/dist/telemetry.js +135 -38
  57. package/dist/telemetry.js.map +1 -1
  58. package/dist/tools/index.d.ts +0 -1
  59. package/dist/tools/index.d.ts.map +1 -1
  60. package/dist/tools/index.js +1 -3
  61. package/dist/tools/index.js.map +1 -1
  62. package/dist/tools/memory-search.d.ts +2 -3
  63. package/dist/tools/memory-search.d.ts.map +1 -1
  64. package/dist/tools/memory-search.js +7 -48
  65. package/dist/tools/memory-search.js.map +1 -1
  66. package/dist/types.d.ts +2 -49
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js.map +1 -1
  69. package/dist/viewer/html.d.ts.map +1 -1
  70. package/dist/viewer/html.js +471 -2974
  71. package/dist/viewer/html.js.map +1 -1
  72. package/dist/viewer/server.d.ts +0 -45
  73. package/dist/viewer/server.d.ts.map +1 -1
  74. package/dist/viewer/server.js +18 -1155
  75. package/dist/viewer/server.js.map +1 -1
  76. package/index.ts +42 -430
  77. package/openclaw.plugin.json +1 -2
  78. package/package.json +3 -4
  79. package/scripts/postinstall.cjs +46 -283
  80. package/skill/memos-memory-guide/SKILL.md +2 -26
  81. package/src/capture/index.ts +8 -0
  82. package/src/config.ts +3 -94
  83. package/src/embedding/index.ts +1 -21
  84. package/src/index.ts +4 -7
  85. package/src/ingest/providers/index.ts +46 -246
  86. package/src/ingest/providers/openai.ts +1 -1
  87. package/src/ingest/task-processor.ts +1 -1
  88. package/src/recall/engine.ts +1 -1
  89. package/src/shared/llm-call.ts +95 -23
  90. package/src/skill/evaluator.ts +2 -3
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/skill/generator.ts +4 -6
  93. package/src/skill/upgrader.ts +1 -1
  94. package/src/skill/validator.ts +1 -1
  95. package/src/storage/ensure-binding.ts +1 -3
  96. package/src/storage/sqlite.ts +0 -1085
  97. package/src/telemetry.ts +152 -39
  98. package/src/tools/index.ts +0 -1
  99. package/src/tools/memory-search.ts +8 -57
  100. package/src/types.ts +2 -44
  101. package/src/viewer/html.ts +471 -2974
  102. package/src/viewer/server.ts +21 -1070
  103. package/dist/client/connector.d.ts +0 -30
  104. package/dist/client/connector.d.ts.map +0 -1
  105. package/dist/client/connector.js +0 -219
  106. package/dist/client/connector.js.map +0 -1
  107. package/dist/client/hub.d.ts +0 -61
  108. package/dist/client/hub.d.ts.map +0 -1
  109. package/dist/client/hub.js +0 -148
  110. package/dist/client/hub.js.map +0 -1
  111. package/dist/client/skill-sync.d.ts +0 -29
  112. package/dist/client/skill-sync.d.ts.map +0 -1
  113. package/dist/client/skill-sync.js +0 -216
  114. package/dist/client/skill-sync.js.map +0 -1
  115. package/dist/hub/auth.d.ts +0 -19
  116. package/dist/hub/auth.d.ts.map +0 -1
  117. package/dist/hub/auth.js +0 -70
  118. package/dist/hub/auth.js.map +0 -1
  119. package/dist/hub/server.d.ts +0 -41
  120. package/dist/hub/server.d.ts.map +0 -1
  121. package/dist/hub/server.js +0 -747
  122. package/dist/hub/server.js.map +0 -1
  123. package/dist/hub/user-manager.d.ts +0 -29
  124. package/dist/hub/user-manager.d.ts.map +0 -1
  125. package/dist/hub/user-manager.js +0 -125
  126. package/dist/hub/user-manager.js.map +0 -1
  127. package/dist/openclaw-api.d.ts +0 -53
  128. package/dist/openclaw-api.d.ts.map +0 -1
  129. package/dist/openclaw-api.js +0 -189
  130. package/dist/openclaw-api.js.map +0 -1
  131. package/dist/sharing/types.contract.d.ts +0 -2
  132. package/dist/sharing/types.contract.d.ts.map +0 -1
  133. package/dist/sharing/types.contract.js +0 -3
  134. package/dist/sharing/types.contract.js.map +0 -1
  135. package/dist/sharing/types.d.ts +0 -80
  136. package/dist/sharing/types.d.ts.map +0 -1
  137. package/dist/sharing/types.js +0 -3
  138. package/dist/sharing/types.js.map +0 -1
  139. package/dist/tools/network-memory-detail.d.ts +0 -4
  140. package/dist/tools/network-memory-detail.d.ts.map +0 -1
  141. package/dist/tools/network-memory-detail.js +0 -34
  142. package/dist/tools/network-memory-detail.js.map +0 -1
  143. package/src/client/connector.ts +0 -218
  144. package/src/client/hub.ts +0 -189
  145. package/src/client/skill-sync.ts +0 -202
  146. package/src/hub/auth.ts +0 -78
  147. package/src/hub/server.ts +0 -740
  148. package/src/hub/user-manager.ts +0 -139
  149. package/src/openclaw-api.ts +0 -287
  150. package/src/sharing/types.contract.ts +0 -40
  151. package/src/sharing/types.ts +0 -102
  152. package/src/tools/network-memory-detail.ts +0 -34
package/index.ts CHANGED
@@ -9,8 +9,8 @@ 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
- import type { HostModelsConfig } from "./src/openclaw-api";
14
14
  import { ensureSqliteBinding } from "./src/storage/ensure-binding";
15
15
  import { SqliteStore } from "./src/storage/sqlite";
16
16
  import { Embedder } from "./src/embedding";
@@ -19,10 +19,6 @@ import { RecallEngine } from "./src/recall/engine";
19
19
  import { captureMessages, stripInboundMetadata } from "./src/capture";
20
20
  import { DEFAULTS } from "./src/types";
21
21
  import { ViewerServer } from "./src/viewer/server";
22
- import { HubServer } from "./src/hub/server";
23
- import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
24
- import { getHubStatus, connectToHub } from "./src/client/connector";
25
- import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
26
22
  import { SkillEvolver } from "./src/skill/evolver";
27
23
  import { SkillInstaller } from "./src/skill/installer";
28
24
  import { Summarizer } from "./src/ingest/providers";
@@ -82,13 +78,20 @@ const memosLocalPlugin = {
82
78
 
83
79
  register(api: OpenClawPluginApi) {
84
80
  // ─── Ensure better-sqlite3 native module is available ───
85
- const pluginDir = path.dirname(new URL(import.meta.url).pathname);
81
+ const pluginDir = path.dirname(fileURLToPath(import.meta.url));
82
+
83
+ function normalizeFsPath(p: string): string {
84
+ return path.resolve(p).replace(/\\/g, "/").toLowerCase();
85
+ }
86
+
86
87
  let sqliteReady = false;
87
88
 
88
89
  function trySqliteLoad(): boolean {
89
90
  try {
90
91
  const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
91
- if (!resolved.startsWith(pluginDir)) {
92
+ const resolvedNorm = normalizeFsPath(resolved);
93
+ const pluginNorm = normalizeFsPath(pluginDir);
94
+ if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
92
95
  api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
93
96
  return false;
94
97
  }
@@ -170,37 +173,19 @@ const memosLocalPlugin = {
170
173
  }
171
174
  }
172
175
 
173
- let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
176
+ const pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
174
177
  const stateDir = api.resolvePath("~/.openclaw");
175
-
176
- // Fallback: read config from file if not provided by OpenClaw
177
- const configPath = path.join(stateDir, "state", "memos-local", "config.json");
178
- if (Object.keys(pluginCfg).length === 0 && fs.existsSync(configPath)) {
179
- try {
180
- const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
181
- pluginCfg = fileConfig;
182
- api.logger.info(`memos-local: loaded config from ${configPath}`);
183
- } catch (e) {
184
- api.logger.warn(`memos-local: failed to load config from ${configPath}: ${e}`);
185
- }
186
- }
187
-
188
- // Extract host model providers so OpenClawAPIClient can proxy completion/embedding
189
- const hostModels: HostModelsConfig | undefined = api.config?.models?.providers
190
- ? { providers: api.config.models.providers as Record<string, import("./src/openclaw-api").HostModelProvider> }
191
- : undefined;
192
-
193
178
  const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, {
194
179
  debug: (msg: string) => api.logger.info(`[debug] ${msg}`),
195
180
  info: (msg: string) => api.logger.info(msg),
196
181
  warn: (msg: string) => api.logger.warn(msg),
197
182
  error: (msg: string) => api.logger.warn(`[error] ${msg}`),
198
- }, hostModels);
183
+ });
199
184
 
200
185
  ensureSqliteBinding(ctx.log);
201
186
 
202
187
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
203
- const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
188
+ const embedder = new Embedder(ctx.config.embedding, ctx.log);
204
189
  const worker = new IngestWorker(store, embedder, ctx);
205
190
  const engine = new RecallEngine(store, embedder, ctx);
206
191
  const evidenceTag = ctx.config.capture?.evidenceWrapperTag ?? DEFAULTS.evidenceWrapperTag;
@@ -208,6 +193,7 @@ const memosLocalPlugin = {
208
193
  const workspaceDir = api.resolvePath("~/.openclaw/workspace");
209
194
  const skillCtx = { ...ctx, workspaceDir };
210
195
  const skillEvolver = new SkillEvolver(store, engine, skillCtx);
196
+ skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
211
197
  const skillInstaller = new SkillInstaller(store, skillCtx);
212
198
 
213
199
  let pluginVersion = "0.0.0";
@@ -264,7 +250,7 @@ const memosLocalPlugin = {
264
250
  });
265
251
  });
266
252
 
267
- const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI);
253
+ const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
268
254
 
269
255
  api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
270
256
 
@@ -272,6 +258,18 @@ const memosLocalPlugin = {
272
258
  // Falls back to "main" when no hook has fired yet (single-agent setups).
273
259
  let currentAgentId = "main";
274
260
 
261
+ // ─── Check allowPromptInjection policy ───
262
+ // When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
263
+ // will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
264
+ const pluginEntry = (api.config as any)?.plugins?.entries?.[api.id];
265
+ const allowPromptInjection = pluginEntry?.hooks?.allowPromptInjection !== false;
266
+ if (!allowPromptInjection) {
267
+ api.logger.info("memos-local: allowPromptInjection=false, auto-recall disabled");
268
+ }
269
+ else {
270
+ api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
271
+ }
272
+
275
273
  const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
276
274
  async (...args: any[]) => {
277
275
  const t0 = performance.now();
@@ -283,6 +281,7 @@ const memosLocalPlugin = {
283
281
  return result;
284
282
  } catch (e) {
285
283
  ok = false;
284
+ telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
286
285
  throw e;
287
286
  } finally {
288
287
  const dur = performance.now() - t0;
@@ -322,10 +321,6 @@ const memosLocalPlugin = {
322
321
  const { query } = params as { query: string };
323
322
  const role = undefined;
324
323
  const minScore = undefined;
325
- const searchScope = "local";
326
- const searchLimit = 10;
327
- const hubAddress: string | undefined = undefined;
328
- const userToken: string | undefined = undefined;
329
324
 
330
325
  const agentId = currentAgentId;
331
326
  const ownerFilter = [`agent:${agentId}`, "public"];
@@ -349,6 +344,7 @@ const memosLocalPlugin = {
349
344
  };
350
345
  }
351
346
 
347
+ // LLM relevance + sufficiency filtering
352
348
  let filteredHits = result.hits;
353
349
  let sufficient = false;
354
350
 
@@ -374,51 +370,6 @@ const memosLocalPlugin = {
374
370
  }
375
371
  }
376
372
 
377
- const beforeDedup = filteredHits.length;
378
- filteredHits = deduplicateHits(filteredHits);
379
- ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
380
-
381
- const localDetailsHits = filteredHits.map((h) => {
382
- let effectiveTaskId = h.taskId;
383
- if (effectiveTaskId) {
384
- const t = store.getTask(effectiveTaskId);
385
- if (t && t.status === "skipped") effectiveTaskId = null;
386
- }
387
- return {
388
- ref: h.ref,
389
- chunkId: h.ref.chunkId,
390
- taskId: effectiveTaskId,
391
- skillId: h.skillId,
392
- role: h.source.role,
393
- score: h.score,
394
- summary: h.summary,
395
- };
396
- });
397
-
398
- if (searchScope !== "local") {
399
- const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
400
- const localText = filteredHits.length > 0
401
- ? filteredHits.map((h, i) => {
402
- const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
403
- return `${i + 1}. [${h.source.role}] ${excerpt}`;
404
- }).join("\n")
405
- : "(none)";
406
- const hubText = hub.hits.length > 0
407
- ? hub.hits.map((h, i) => `${i + 1}. [${h.ownerName}] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
408
- : "(none)";
409
-
410
- return {
411
- content: [{
412
- type: "text",
413
- text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
414
- }],
415
- details: {
416
- local: { hits: localDetailsHits, meta: result.meta },
417
- hub,
418
- },
419
- };
420
- }
421
-
422
373
  if (filteredHits.length === 0) {
423
374
  return {
424
375
  content: [{ type: "text", text: "No relevant memories found for this query." }],
@@ -426,6 +377,10 @@ const memosLocalPlugin = {
426
377
  };
427
378
  }
428
379
 
380
+ const beforeDedup = filteredHits.length;
381
+ filteredHits = deduplicateHits(filteredHits);
382
+ ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
383
+
429
384
  const lines = filteredHits.map((h, i) => {
430
385
  const excerpt = h.original_excerpt;
431
386
  const parts = [`${i + 1}. [${h.source.role}]`];
@@ -518,7 +473,7 @@ const memosLocalPlugin = {
518
473
  if (!anchorChunk) {
519
474
  return {
520
475
  content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
521
- details: { error: "not_found", entries: [] },
476
+ details: { error: "not_found" },
522
477
  };
523
478
  }
524
479
 
@@ -567,7 +522,7 @@ const memosLocalPlugin = {
567
522
  Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
568
523
  ),
569
524
  }),
570
- execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
525
+ execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
571
526
  const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
572
527
  const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
573
528
 
@@ -674,207 +629,6 @@ const memosLocalPlugin = {
674
629
  { name: "task_summary" },
675
630
  );
676
631
 
677
- // ─── Tool: task_share ───
678
-
679
- api.registerTool(
680
- {
681
- name: "task_share",
682
- label: "Task Share",
683
- description:
684
- "Share one existing local task and its chunks to the configured hub. " +
685
- "Minimal MVP path for validating team task sharing.",
686
- parameters: Type.Object({
687
- taskId: Type.String({ description: "Local task ID to share" }),
688
- visibility: Type.Optional(Type.String({ description: "Share visibility: 'public' (default) or 'group'" })),
689
- groupId: Type.Optional(Type.String({ description: "Optional group ID when visibility='group'" })),
690
- }),
691
- execute: trackTool("task_share", async (_toolCallId: any, params: any) => {
692
- const { taskId, visibility: rawVisibility, groupId } = params as {
693
- taskId: string;
694
- visibility?: string;
695
- groupId?: string;
696
- };
697
-
698
- const task = store.getTask(taskId);
699
- if (!task) {
700
- return {
701
- content: [{ type: "text", text: `Task not found: ${taskId}` }],
702
- details: { error: "not_found", taskId },
703
- };
704
- }
705
-
706
- const chunks = store.getChunksByTask(taskId);
707
- if (chunks.length === 0) {
708
- return {
709
- content: [{ type: "text", text: `Task ${taskId} has no chunks to share.` }],
710
- details: { error: "no_chunks", taskId },
711
- };
712
- }
713
-
714
- const visibility = rawVisibility === "group" ? "group" : "public";
715
- const hubClient = await resolveHubClient(store, ctx);
716
- const { v4: uuidv4 } = require("uuid");
717
- const hubTaskId = uuidv4();
718
-
719
- const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
720
- method: "POST",
721
- body: JSON.stringify({
722
- task: {
723
- id: hubTaskId,
724
- sourceTaskId: task.id,
725
- sourceUserId: hubClient.userId,
726
- title: task.title,
727
- summary: task.summary,
728
- groupId: visibility === "group" ? (groupId ?? null) : null,
729
- visibility,
730
- createdAt: task.startedAt,
731
- updatedAt: task.updatedAt,
732
- },
733
- chunks: chunks.map((chunk) => ({
734
- id: uuidv4(),
735
- hubTaskId,
736
- sourceTaskId: task.id,
737
- sourceChunkId: chunk.id,
738
- sourceUserId: hubClient.userId,
739
- role: chunk.role,
740
- content: chunk.content,
741
- summary: chunk.summary,
742
- kind: chunk.kind,
743
- createdAt: chunk.createdAt,
744
- })),
745
- }),
746
- }) as any;
747
-
748
- store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId);
749
-
750
- return {
751
- content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
752
- details: {
753
- shared: true,
754
- taskId: task.id,
755
- visibility,
756
- chunkCount: chunks.length,
757
- hubUrl: hubClient.hubUrl,
758
- response,
759
- },
760
- };
761
- }),
762
- },
763
- { name: "task_share" },
764
- );
765
-
766
- // ─── Tool: task_unshare ───
767
-
768
- api.registerTool(
769
- {
770
- name: "task_unshare",
771
- label: "Task Unshare",
772
- description: "Remove one previously shared task from the configured hub.",
773
- parameters: Type.Object({
774
- taskId: Type.String({ description: "Local task ID to unshare" }),
775
- }),
776
- execute: trackTool("task_unshare", async (_toolCallId: any, params: any) => {
777
- const { taskId } = params as { taskId: string };
778
-
779
- const task = store.getTask(taskId);
780
- if (!task) {
781
- return {
782
- content: [{ type: "text", text: `Task not found: ${taskId}` }],
783
- details: { error: "not_found", taskId },
784
- };
785
- }
786
-
787
- const hubClient = await resolveHubClient(store, ctx);
788
- await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
789
- method: "POST",
790
- body: JSON.stringify({
791
- sourceUserId: hubClient.userId,
792
- sourceTaskId: task.id,
793
- }),
794
- });
795
-
796
- store.unmarkTaskShared(task.id);
797
-
798
- return {
799
- content: [{ type: "text", text: `Unshared task "${task.title}" from the hub.` }],
800
- details: {
801
- unshared: true,
802
- taskId: task.id,
803
- hubUrl: hubClient.hubUrl,
804
- },
805
- };
806
- }),
807
- },
808
- { name: "task_unshare" },
809
- );
810
-
811
- api.registerTool(
812
- {
813
- name: "network_memory_detail",
814
- label: "Network Memory Detail",
815
- description: "Fetch the full detail for a Hub search hit returned by memory_search(scope=group|all).",
816
- parameters: Type.Object({
817
- remoteHitId: Type.String({ description: "The remoteHitId returned by a Hub search hit" }),
818
- hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
819
- userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
820
- }),
821
- execute: trackTool("network_memory_detail", async (_toolCallId: any, params: any) => {
822
- const { remoteHitId, hubAddress, userToken } = params as {
823
- remoteHitId: string;
824
- hubAddress?: string;
825
- userToken?: string;
826
- };
827
-
828
- const detail = await hubGetMemoryDetail(store, ctx, { remoteHitId, hubAddress, userToken });
829
- return {
830
- content: [{
831
- type: "text",
832
- text: `## Shared Memory Detail
833
-
834
- ${detail.summary}
835
-
836
- ${detail.content}`,
837
- }],
838
- details: detail,
839
- };
840
- }),
841
- },
842
- { name: "network_memory_detail" },
843
- );
844
-
845
- api.registerTool(
846
- {
847
- name: "network_team_info",
848
- label: "Network Team Info",
849
- description: "Show current Hub connection status, signed-in user, role, and group memberships.",
850
- parameters: Type.Object({}),
851
- execute: trackTool("network_team_info", async () => {
852
- const status = await getHubStatus(store, ctx.config);
853
- if (!status.connected || !status.user) {
854
- return {
855
- content: [{ type: "text", text: "Hub is not connected." }],
856
- details: status,
857
- };
858
- }
859
-
860
- const groupNames = status.user.groups.map((group) => group.name);
861
- return {
862
- content: [{
863
- type: "text",
864
- text: `## Team Connection
865
-
866
- User: ${status.user.username}
867
- Role: ${status.user.role}
868
- Hub: ${status.hubUrl ?? "(unknown)"}
869
- Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
870
- }],
871
- details: status,
872
- };
873
- }),
874
- },
875
- { name: "network_team_info" },
876
- );
877
-
878
632
  // ─── Tool: skill_get ───
879
633
 
880
634
  api.registerTool(
@@ -991,6 +745,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
991
745
  parameters: Type.Object({}),
992
746
  execute: trackTool("memory_viewer", async () => {
993
747
  ctx.log.debug(`memory_viewer called`);
748
+ telemetry.trackViewerOpened();
994
749
  const url = `http://127.0.0.1:${viewerPort}`;
995
750
  return {
996
751
  content: [
@@ -1086,43 +841,17 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1086
841
  name: "skill_search",
1087
842
  label: "Skill Search",
1088
843
  description:
1089
- "Search available skills by natural language. Searches local skills by default, or local + Hub skills when scope=group/all. " +
844
+ "Search available skills by natural language. Searches your own skills, public skills, or both. " +
1090
845
  "Use when you need a capability or guide and don't have a matching skill at hand.",
1091
846
  parameters: Type.Object({
1092
847
  query: Type.String({ description: "Natural language description of the needed skill" }),
1093
- scope: Type.Optional(Type.String({ description: "Search scope: 'mix'/'self'/'public' for local search, or 'group'/'all' for local + Hub search" })),
848
+ scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default, self + public), 'self' (own only), 'public' (public only)" })),
1094
849
  }),
1095
- execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
850
+ execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
1096
851
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1097
852
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1098
853
  const currentOwner = `agent:${currentAgentId}`;
1099
854
 
1100
- if (rawScope === "group" || rawScope === "all") {
1101
- const [localHits, hub] = await Promise.all([
1102
- engine.searchSkills(skillQuery, "mix" as any, currentOwner),
1103
- hubSearchSkills(store, ctx, { query: skillQuery, maxResults: 10 }).catch(() => ({ hits: [] })),
1104
- ]);
1105
-
1106
- if (localHits.length === 0 && hub.hits.length === 0) {
1107
- return {
1108
- content: [{ type: "text", text: `No relevant skills found for: "${skillQuery}" (scope: ${rawScope})` }],
1109
- details: { query: skillQuery, scope: rawScope, local: { hits: [] }, hub },
1110
- };
1111
- }
1112
-
1113
- const localText = localHits.length > 0
1114
- ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (public)" : ""}`).join("\n")
1115
- : "(none)";
1116
- const hubText = hub.hits.length > 0
1117
- ? 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")
1118
- : "(none)";
1119
-
1120
- return {
1121
- content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
1122
- details: { query: skillQuery, scope: rawScope, local: { hits: localHits }, hub },
1123
- };
1124
- }
1125
-
1126
855
  const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
1127
856
 
1128
857
  if (hits.length === 0) {
@@ -1154,28 +883,17 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1154
883
  description: "Make a skill public so other agents can discover and install it via skill_search.",
1155
884
  parameters: Type.Object({
1156
885
  skillId: Type.String({ description: "The skill ID to publish" }),
1157
- scope: Type.Optional(Type.String({ description: "Publish scope: omit for local public, or use 'public' / 'group' to publish to Hub" })),
1158
- groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
1159
- hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1160
- userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1161
886
  }),
1162
887
  execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
1163
- const { skillId: pubSkillId, scope, groupId, hubAddress, userToken } = params as { skillId: string; scope?: string; groupId?: string; hubAddress?: string; userToken?: string };
888
+ const { skillId: pubSkillId } = params as { skillId: string };
1164
889
  const skill = store.getSkill(pubSkillId);
1165
890
  if (!skill) {
1166
891
  return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
1167
892
  }
1168
- if (scope === "public" || scope === "group") {
1169
- const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility: scope, groupId, hubAddress, userToken });
1170
- return {
1171
- content: [{ type: "text", text: `Skill "${skill.name}" published to hub (${published.visibility}).` }],
1172
- details: { skillId: pubSkillId, name: skill.name, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
1173
- };
1174
- }
1175
893
  store.setSkillVisibility(pubSkillId, "public");
1176
894
  return {
1177
895
  content: [{ type: "text", text: `Skill "${skill.name}" is now public.` }],
1178
- details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
896
+ details: { skillId: pubSkillId, name: skill.name, visibility: "public" },
1179
897
  };
1180
898
  }),
1181
899
  },
@@ -1208,32 +926,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1208
926
  { name: "skill_unpublish" },
1209
927
  );
1210
928
 
1211
- api.registerTool(
1212
- {
1213
- name: "network_skill_pull",
1214
- label: "Network Skill Pull",
1215
- description: "Download a published Hub skill bundle and restore it into local managed skills.",
1216
- parameters: Type.Object({
1217
- skillId: Type.String({ description: "The Hub skill ID to pull" }),
1218
- hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
1219
- userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
1220
- }),
1221
- execute: trackTool("network_skill_pull", async (_toolCallId: any, params: any) => {
1222
- const { skillId, hubAddress, userToken } = params as { skillId: string; hubAddress?: string; userToken?: string };
1223
- const payload = await fetchHubSkillBundle(store, ctx, { skillId, hubAddress, userToken });
1224
- const restored = restoreSkillBundleFromHub(store, ctx, payload);
1225
- return {
1226
- content: [{ type: "text", text: `Pulled Hub skill "${restored.localName}" into local storage.` }],
1227
- details: { pulled: true, hubSkillId: skillId, localSkillId: restored.localSkillId, localName: restored.localName, dirPath: restored.dirPath },
1228
- };
1229
- }),
1230
- },
1231
- { name: "network_skill_pull" },
1232
- );
1233
-
1234
929
  // ─── Auto-recall: inject relevant memories before agent starts ───
1235
930
 
1236
931
  api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
932
+ if (!allowPromptInjection) return {};
1237
933
  if (!event.prompt || event.prompt.length < 3) return;
1238
934
 
1239
935
  const recallAgentId = hookCtx?.agentId ?? "main";
@@ -1535,74 +1231,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1535
1231
  worker.enqueue(captured);
1536
1232
  telemetry.trackMemoryIngested(captured.length);
1537
1233
  }
1538
-
1539
- // Incremental push: sync new chunks for already-shared tasks
1540
- syncSharedTasksIncremental().catch((err) => {
1541
- ctx.log.warn(`incremental sync failed: ${err}`);
1542
- });
1543
1234
  } catch (err) {
1544
1235
  api.logger.warn(`memos-local: capture failed: ${String(err)}`);
1545
1236
  }
1546
1237
  });
1547
1238
 
1548
- async function syncSharedTasksIncremental(): Promise<void> {
1549
- if (!ctx.config.sharing?.enabled || ctx.config.sharing.role !== "client") return;
1550
- const shared = store.listLocalSharedTasks();
1551
- if (shared.length === 0) return;
1552
-
1553
- let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
1554
- try {
1555
- hubClient = await resolveHubClient(store, ctx);
1556
- } catch {
1557
- return;
1558
- }
1559
- const { v4: uuidv4 } = require("uuid");
1560
-
1561
- for (const entry of shared) {
1562
- const task = store.getTask(entry.taskId);
1563
- if (!task) continue;
1564
- const chunks = store.getChunksByTask(entry.taskId);
1565
- if (chunks.length <= entry.syncedChunks) continue;
1566
-
1567
- const newChunks = chunks.slice(entry.syncedChunks);
1568
- ctx.log.info(`incremental sync: task=${entry.taskId} pushing ${newChunks.length} new chunk(s)`);
1569
-
1570
- try {
1571
- await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1572
- method: "POST",
1573
- body: JSON.stringify({
1574
- task: {
1575
- id: entry.hubTaskId,
1576
- sourceTaskId: entry.taskId,
1577
- sourceUserId: hubClient.userId,
1578
- title: task.title,
1579
- summary: task.summary,
1580
- groupId: entry.visibility === "group" ? entry.groupId ?? null : null,
1581
- visibility: entry.visibility,
1582
- createdAt: task.startedAt ?? task.updatedAt ?? Date.now(),
1583
- updatedAt: task.updatedAt ?? Date.now(),
1584
- },
1585
- chunks: newChunks.map((chunk) => ({
1586
- id: uuidv4(),
1587
- hubTaskId: entry.hubTaskId,
1588
- sourceTaskId: entry.taskId,
1589
- sourceChunkId: chunk.id,
1590
- sourceUserId: hubClient.userId,
1591
- role: chunk.role,
1592
- content: chunk.content,
1593
- summary: chunk.summary,
1594
- kind: chunk.kind,
1595
- createdAt: chunk.createdAt,
1596
- })),
1597
- }),
1598
- });
1599
- store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
1600
- } catch (err) {
1601
- ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
1602
- }
1603
- }
1604
- }
1605
-
1606
1239
  // ─── Memory Viewer (web UI) ───
1607
1240
 
1608
1241
  const viewer = new ViewerServer({
@@ -1614,30 +1247,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1614
1247
  ctx,
1615
1248
  });
1616
1249
 
1617
- const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
1618
- ? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
1619
- : null;
1620
-
1621
1250
  // ─── Service lifecycle ───
1622
1251
 
1623
1252
  api.registerService({
1624
1253
  id: "memos-local-openclaw-plugin",
1625
1254
  start: async () => {
1626
- if (hubServer) {
1627
- const hubUrl = await hubServer.start();
1628
- api.logger.info(`memos-local: hub started at ${hubUrl}`);
1629
- }
1630
-
1631
- // Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
1632
- if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
1633
- try {
1634
- const session = await connectToHub(store, ctx.config, ctx.log);
1635
- api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
1636
- } catch (err) {
1637
- api.logger.warn(`memos-local: Hub connection failed: ${err}`);
1638
- }
1639
- }
1640
-
1641
1255
  try {
1642
1256
  const viewerUrl = await viewer.start();
1643
1257
  api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
@@ -1663,9 +1277,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1663
1277
  );
1664
1278
  },
1665
1279
  stop: async () => {
1666
- await worker.flush();
1667
1280
  await telemetry.shutdown();
1668
- await hubServer?.stop();
1669
1281
  viewer.stop();
1670
1282
  store.close();
1671
1283
  api.logger.info("memos-local: stopped");
@@ -32,6 +32,5 @@
32
32
  "Memory Viewer will be available at http://127.0.0.1:18799",
33
33
  "If better-sqlite3 fails to build, ensure you have C++ build tools: xcode-select --install (macOS) or build-essential (Linux)"
34
34
  ]
35
- },
36
- "extensions": ["./index.ts"]
35
+ }
37
36
  }