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

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 (124) hide show
  1. package/.env.example +7 -0
  2. package/README.md +111 -44
  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 +36 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +6 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +160 -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 +2 -3
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +9 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +500 -112
  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/chunker.d.ts +2 -1
  33. package/dist/ingest/chunker.d.ts.map +1 -1
  34. package/dist/ingest/chunker.js +14 -10
  35. package/dist/ingest/chunker.js.map +1 -1
  36. package/dist/ingest/providers/index.d.ts.map +1 -1
  37. package/dist/ingest/providers/index.js +37 -6
  38. package/dist/ingest/providers/index.js.map +1 -1
  39. package/dist/recall/engine.d.ts.map +1 -1
  40. package/dist/recall/engine.js +96 -1
  41. package/dist/recall/engine.js.map +1 -1
  42. package/dist/shared/llm-call.d.ts +1 -0
  43. package/dist/shared/llm-call.d.ts.map +1 -1
  44. package/dist/shared/llm-call.js +84 -9
  45. package/dist/shared/llm-call.js.map +1 -1
  46. package/dist/sharing/types.d.ts +1 -1
  47. package/dist/sharing/types.d.ts.map +1 -1
  48. package/dist/skill/evolver.d.ts +4 -0
  49. package/dist/skill/evolver.d.ts.map +1 -1
  50. package/dist/skill/evolver.js +59 -5
  51. package/dist/skill/evolver.js.map +1 -1
  52. package/dist/skill/generator.d.ts +2 -0
  53. package/dist/skill/generator.d.ts.map +1 -1
  54. package/dist/skill/generator.js +45 -3
  55. package/dist/skill/generator.js.map +1 -1
  56. package/dist/skill/installer.d.ts +26 -0
  57. package/dist/skill/installer.d.ts.map +1 -1
  58. package/dist/skill/installer.js +80 -4
  59. package/dist/skill/installer.js.map +1 -1
  60. package/dist/skill/upgrader.d.ts +2 -0
  61. package/dist/skill/upgrader.d.ts.map +1 -1
  62. package/dist/skill/upgrader.js +139 -1
  63. package/dist/skill/upgrader.js.map +1 -1
  64. package/dist/skill/validator.d.ts +3 -0
  65. package/dist/skill/validator.d.ts.map +1 -1
  66. package/dist/skill/validator.js +75 -0
  67. package/dist/skill/validator.js.map +1 -1
  68. package/dist/storage/ensure-binding.d.ts +12 -0
  69. package/dist/storage/ensure-binding.d.ts.map +1 -0
  70. package/dist/storage/ensure-binding.js +53 -0
  71. package/dist/storage/ensure-binding.js.map +1 -0
  72. package/dist/storage/sqlite.d.ts +115 -20
  73. package/dist/storage/sqlite.d.ts.map +1 -1
  74. package/dist/storage/sqlite.js +458 -110
  75. package/dist/storage/sqlite.js.map +1 -1
  76. package/dist/telemetry.d.ts +12 -5
  77. package/dist/telemetry.d.ts.map +1 -1
  78. package/dist/telemetry.js +156 -40
  79. package/dist/telemetry.js.map +1 -1
  80. package/dist/tools/memory-search.d.ts +3 -1
  81. package/dist/tools/memory-search.d.ts.map +1 -1
  82. package/dist/tools/memory-search.js +3 -1
  83. package/dist/tools/memory-search.js.map +1 -1
  84. package/dist/types.d.ts +11 -2
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js +4 -0
  87. package/dist/types.js.map +1 -1
  88. package/dist/viewer/html.d.ts.map +1 -1
  89. package/dist/viewer/html.js +2952 -910
  90. package/dist/viewer/html.js.map +1 -1
  91. package/dist/viewer/server.d.ts +39 -8
  92. package/dist/viewer/server.d.ts.map +1 -1
  93. package/dist/viewer/server.js +1198 -227
  94. package/dist/viewer/server.js.map +1 -1
  95. package/index.ts +774 -74
  96. package/openclaw.plugin.json +2 -2
  97. package/package.json +3 -2
  98. package/scripts/postinstall.cjs +1 -1
  99. package/skill/memos-memory-guide/SKILL.md +64 -26
  100. package/src/capture/index.ts +40 -1
  101. package/src/client/connector.ts +161 -28
  102. package/src/client/hub.ts +18 -0
  103. package/src/client/skill-sync.ts +14 -0
  104. package/src/config.ts +2 -3
  105. package/src/hub/server.ts +481 -107
  106. package/src/hub/user-manager.ts +48 -8
  107. package/src/index.ts +10 -2
  108. package/src/ingest/chunker.ts +19 -13
  109. package/src/ingest/providers/index.ts +41 -7
  110. package/src/recall/engine.ts +89 -1
  111. package/src/shared/llm-call.ts +99 -10
  112. package/src/sharing/types.ts +1 -1
  113. package/src/skill/evolver.ts +63 -6
  114. package/src/skill/generator.ts +44 -5
  115. package/src/skill/installer.ts +107 -4
  116. package/src/skill/upgrader.ts +139 -1
  117. package/src/skill/validator.ts +79 -0
  118. package/src/storage/ensure-binding.ts +52 -0
  119. package/src/storage/sqlite.ts +498 -137
  120. package/src/telemetry.ts +172 -41
  121. package/src/tools/memory-search.ts +2 -1
  122. package/src/types.ts +12 -2
  123. package/src/viewer/html.ts +2952 -910
  124. package/src/viewer/server.ts +1109 -212
@@ -1,16 +1,27 @@
1
1
  import { randomUUID, createHash } from "crypto";
2
- import { issueUserToken } from "./auth";
2
+ import { issueUserToken, verifyUserToken } from "./auth";
3
3
  import type { Logger } from "../types";
4
4
  import type { UserInfo } from "../sharing/types";
5
5
  import type { SqliteStore } from "../storage/sqlite";
6
6
 
7
- type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null };
7
+ type ManagedHubUser = UserInfo & {
8
+ tokenHash: string;
9
+ createdAt: number;
10
+ approvedAt: number | null;
11
+ lastIp: string;
12
+ lastActiveAt: number | null;
13
+ identityKey?: string;
14
+ leftAt?: number | null;
15
+ removedAt?: number | null;
16
+ rejectedAt?: number | null;
17
+ rejoinRequestedAt?: number | null;
18
+ };
8
19
 
9
20
  export class HubUserManager {
10
21
  constructor(private store: SqliteStore, private log: Logger) {}
11
22
 
12
- createPendingUser(input: { username: string; deviceName?: string }): ManagedHubUser {
13
- const user = {
23
+ createPendingUser(input: { username: string; deviceName?: string; identityKey?: string }): ManagedHubUser {
24
+ const user: ManagedHubUser = {
14
25
  id: randomUUID(),
15
26
  username: input.username,
16
27
  deviceName: input.deviceName,
@@ -20,11 +31,38 @@ export class HubUserManager {
20
31
  tokenHash: "",
21
32
  createdAt: Date.now(),
22
33
  approvedAt: null,
34
+ lastIp: "",
35
+ lastActiveAt: null,
36
+ identityKey: input.identityKey || "",
23
37
  };
24
38
  this.store.upsertHubUser(user);
25
39
  return user;
26
40
  }
27
41
 
42
+ findByIdentityKey(identityKey: string): ManagedHubUser | null {
43
+ if (!identityKey) return null;
44
+ return this.store.findHubUserByIdentityKey(identityKey);
45
+ }
46
+
47
+ markUserLeft(userId: string): boolean {
48
+ this.log.info(`Hub: user "${userId}" marked as left`);
49
+ return this.store.markHubUserLeft(userId);
50
+ }
51
+
52
+ rejoinUser(userId: string): ManagedHubUser | null {
53
+ const user = this.store.getHubUser(userId);
54
+ if (!user) return null;
55
+ const updated: ManagedHubUser = {
56
+ ...user,
57
+ status: "pending" as const,
58
+ tokenHash: "",
59
+ rejoinRequestedAt: Date.now(),
60
+ };
61
+ this.store.upsertHubUser(updated);
62
+ this.log.info(`Hub: user "${userId}" (${user.username}) requested rejoin, previous status: ${user.status}`);
63
+ return updated;
64
+ }
65
+
28
66
  listPendingUsers(): ManagedHubUser[] {
29
67
  return this.store.listHubUsers("pending");
30
68
  }
@@ -46,7 +84,7 @@ export class HubUserManager {
46
84
  if (bootstrapUserId) {
47
85
  const bootstrapUser = this.store.getHubUser(bootstrapUserId);
48
86
  if (bootstrapUser && bootstrapUser.role === "admin" && bootstrapUser.status === "active") {
49
- if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex")) {
87
+ if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex") && verifyUserToken(bootstrapToken, secret)) {
50
88
  return { user: bootstrapUser, token: bootstrapToken };
51
89
  }
52
90
  const refreshedToken = issueUserToken(
@@ -88,6 +126,8 @@ export class HubUserManager {
88
126
  tokenHash: "",
89
127
  createdAt: Date.now(),
90
128
  approvedAt: Date.now(),
129
+ lastIp: "",
130
+ lastActiveAt: null,
91
131
  };
92
132
  const token = issueUserToken(
93
133
  { userId: user.id, username: user.username, role: user.role, status: user.status },
@@ -101,7 +141,7 @@ export class HubUserManager {
101
141
 
102
142
  isUsernameTaken(username: string, excludeUserId?: string): boolean {
103
143
  const users = this.store.listHubUsers();
104
- return users.some(u => u.username === username && u.id !== excludeUserId);
144
+ return users.some(u => u.username === username && u.id !== excludeUserId && u.status !== "left" && u.status !== "removed");
105
145
  }
106
146
 
107
147
  updateUsername(userId: string, newUsername: string): ManagedHubUser | null {
@@ -115,10 +155,10 @@ export class HubUserManager {
115
155
  rejectUser(userId: string): ManagedHubUser | null {
116
156
  const user = this.store.getHubUser(userId);
117
157
  if (!user) return null;
118
- const updated = {
158
+ const updated: ManagedHubUser = {
119
159
  ...user,
120
160
  status: "rejected" as const,
121
- approvedAt: Date.now(),
161
+ rejectedAt: Date.now(),
122
162
  };
123
163
  this.store.upsertHubUser(updated);
124
164
  return updated;
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
  import { buildContext } from "./config";
3
+ import { ensureSqliteBinding } from "./storage/ensure-binding";
3
4
  import { SqliteStore } from "./storage/sqlite";
4
5
  import { Embedder } from "./embedding";
5
6
  import { IngestWorker } from "./ingest/worker";
@@ -56,13 +57,17 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
56
57
 
57
58
  ctx.log.info("Initializing memos-local plugin...");
58
59
 
60
+ ensureSqliteBinding(ctx.log);
61
+
59
62
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
60
63
  const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
61
64
  const worker = new IngestWorker(store, embedder, ctx);
62
65
  const engine = new RecallEngine(store, embedder, ctx);
63
66
 
67
+ const sharedState = { lastSearchTime: 0 };
68
+
64
69
  const tools: ToolDefinition[] = [
65
- createMemorySearchTool(engine, store, ctx),
70
+ createMemorySearchTool(engine, store, ctx, sharedState),
66
71
  createMemoryTimelineTool(store),
67
72
  createMemoryGetTool(store),
68
73
  createNetworkMemoryDetailTool(store, ctx),
@@ -84,7 +89,10 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
84
89
  const turnId = uuid();
85
90
  const tag = ctx.config.capture?.evidenceWrapperTag ?? "STORED_MEMORY";
86
91
 
87
- const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner);
92
+ const userSearchTime = sharedState.lastSearchTime || 0;
93
+ sharedState.lastSearchTime = 0;
94
+
95
+ const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner, userSearchTime);
88
96
  if (captured.length > 0) {
89
97
  worker.enqueue(captured);
90
98
  }
@@ -1,6 +1,8 @@
1
+ export type ChunkKind = "paragraph" | "code_block" | "error_stack" | "list" | "command";
2
+
1
3
  export interface RawChunk {
2
4
  content: string;
3
- kind: "paragraph";
5
+ kind: ChunkKind;
4
6
  }
5
7
 
6
8
  const MAX_CHUNK_CHARS = 3000;
@@ -28,21 +30,25 @@ const COMMAND_LINE_RE = /^(?:\$|>|#)\s+.+$/gm;
28
30
  */
29
31
  export function chunkText(text: string): RawChunk[] {
30
32
  let remaining = text;
31
- const slots: Array<{ placeholder: string; content: string }> = [];
33
+ const slots: Array<{ placeholder: string; content: string; kind: ChunkKind }> = [];
32
34
  let counter = 0;
33
35
 
34
- function ph(content: string): string {
36
+ function ph(content: string, kind: ChunkKind = "paragraph"): string {
35
37
  const tag = `\x00SLOT_${counter++}\x00`;
36
- slots.push({ placeholder: tag, content: content.trim() });
38
+ slots.push({ placeholder: tag, content: content.trim(), kind });
37
39
  return tag;
38
40
  }
39
41
 
40
- remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m));
42
+ remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m, "code_block"));
41
43
  remaining = extractBraceBlocks(remaining, ph);
42
44
 
43
- const structural: RegExp[] = [ERROR_STACK_RE, LIST_BLOCK_RE, COMMAND_LINE_RE];
44
- for (const re of structural) {
45
- remaining = remaining.replace(re, (m) => ph(m));
45
+ const structuralKinds: Array<[RegExp, ChunkKind]> = [
46
+ [ERROR_STACK_RE, "error_stack"],
47
+ [LIST_BLOCK_RE, "list"],
48
+ [COMMAND_LINE_RE, "command"],
49
+ ];
50
+ for (const [re, kind] of structuralKinds) {
51
+ remaining = remaining.replace(re, (m) => ph(m, kind));
46
52
  }
47
53
 
48
54
  const raw: RawChunk[] = [];
@@ -57,7 +63,7 @@ export function chunkText(text: string): RawChunk[] {
57
63
  for (const part of parts) {
58
64
  const slot = slots.find((s) => s.placeholder === part);
59
65
  if (slot) {
60
- raw.push({ content: slot.content, kind: "paragraph" });
66
+ raw.push({ content: slot.content, kind: slot.kind });
61
67
  } else if (part.trim().length >= MIN_CHUNK_CHARS) {
62
68
  raw.push({ content: part.trim(), kind: "paragraph" });
63
69
  }
@@ -69,7 +75,7 @@ export function chunkText(text: string): RawChunk[] {
69
75
 
70
76
  for (const s of slots) {
71
77
  if (!raw.some((c) => c.content === s.content)) {
72
- raw.push({ content: s.content, kind: "paragraph" });
78
+ raw.push({ content: s.content, kind: s.kind });
73
79
  }
74
80
  }
75
81
 
@@ -85,7 +91,7 @@ export function chunkText(text: string): RawChunk[] {
85
91
  */
86
92
  function extractBraceBlocks(
87
93
  text: string,
88
- ph: (content: string) => string,
94
+ ph: (content: string, kind?: ChunkKind) => string,
89
95
  ): string {
90
96
  const lines = text.split("\n");
91
97
  const result: string[] = [];
@@ -119,7 +125,7 @@ function extractBraceBlocks(
119
125
  if (depth <= 0 || (BLOCK_CLOSE_RE.test(line) && depth <= 0)) {
120
126
  const block = blockLines.join("\n");
121
127
  if (block.trim().length >= MIN_CHUNK_CHARS) {
122
- result.push(ph(block));
128
+ result.push(ph(block, "code_block"));
123
129
  } else {
124
130
  result.push(block);
125
131
  }
@@ -135,7 +141,7 @@ function extractBraceBlocks(
135
141
  if (blockLines.length > 0) {
136
142
  const block = blockLines.join("\n");
137
143
  if (block.trim().length >= MIN_CHUNK_CHARS) {
138
- result.push(ph(block));
144
+ result.push(ph(block, "code_block"));
139
145
  } else {
140
146
  result.push(block);
141
147
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import type { SummarizerConfig, Logger, OpenClawAPI } from "../../types";
3
+ import type { SummarizerConfig, SummaryProvider, Logger, OpenClawAPI } from "../../types";
4
4
  import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai";
5
5
  import type { FilterResult, DedupResult } from "./openai";
6
6
  export type { FilterResult, DedupResult } from "./openai";
@@ -8,6 +8,40 @@ import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic,
8
8
  import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
9
9
  import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
10
10
 
11
+ /**
12
+ * Detect provider type from provider key name or base URL.
13
+ */
14
+ function detectProvider(
15
+ providerKey: string | undefined,
16
+ baseUrl: string,
17
+ ): SummaryProvider {
18
+ const key = providerKey?.toLowerCase() ?? "";
19
+ const url = baseUrl.toLowerCase();
20
+ if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
21
+ if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
22
+ return "gemini";
23
+ }
24
+ if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
25
+ return "openai_compatible";
26
+ }
27
+
28
+ /**
29
+ * Return the correct endpoint for a given provider and base URL.
30
+ */
31
+ function normalizeEndpointForProvider(
32
+ provider: SummaryProvider,
33
+ baseUrl: string,
34
+ ): string {
35
+ const stripped = baseUrl.replace(/\/+$/, "");
36
+ if (provider === "anthropic") {
37
+ if (stripped.endsWith("/v1/messages")) return stripped;
38
+ return `${stripped}/v1/messages`;
39
+ }
40
+ if (stripped.endsWith("/chat/completions")) return stripped;
41
+ if (stripped.endsWith("/completions")) return stripped;
42
+ return `${stripped}/chat/completions`;
43
+ }
44
+
11
45
  /**
12
46
  * Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
13
47
  * This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
@@ -15,7 +49,8 @@ import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judge
15
49
  function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
16
50
  try {
17
51
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
18
- const cfgPath = path.join(home, ".openclaw", "openclaw.json");
52
+ const cfgPath = process.env.OPENCLAW_CONFIG_PATH
53
+ || path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
19
54
  if (!fs.existsSync(cfgPath)) return undefined;
20
55
 
21
56
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
@@ -36,13 +71,12 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
36
71
  const apiKey: string | undefined = providerCfg.apiKey;
37
72
  if (!baseUrl || !apiKey) return undefined;
38
73
 
39
- const endpoint = baseUrl.endsWith("/chat/completions")
40
- ? baseUrl
41
- : baseUrl.replace(/\/+$/, "") + "/chat/completions";
74
+ const provider = detectProvider(providerKey, baseUrl);
75
+ const endpoint = normalizeEndpointForProvider(provider, baseUrl);
42
76
 
43
- log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
77
+ log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
44
78
  return {
45
- provider: "openai_compatible",
79
+ provider,
46
80
  endpoint,
47
81
  apiKey,
48
82
  model: modelId,
@@ -74,10 +74,63 @@ export class RecallEngine {
74
74
  score: 1 / (i + 1),
75
75
  }));
76
76
 
77
+ // Step 1c: Hub memories search — only in Hub mode where local DB owns the
78
+ // hub_memories data and embeddings were generated by the same Embedder.
79
+ // Client mode must use remote API (hubSearchMemories) to avoid cross-model
80
+ // embedding mismatch.
81
+ let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
82
+ let hubMemVecRanked: Array<{ id: string; score: number }> = [];
83
+ let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
84
+ if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
85
+ try {
86
+ const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
87
+ hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
88
+ id: `hubmem:${hit.id}`, score: 1 / (i + 1),
89
+ }));
90
+ } catch { /* hub_memories table may not exist */ }
91
+ if (shortTerms.length > 0) {
92
+ try {
93
+ const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
94
+ hubMemPatternRanked = hubPatternHits.map((h, i) => ({
95
+ id: `hubmem:${h.memoryId}`, score: 1 / (i + 1),
96
+ }));
97
+ } catch { /* best-effort */ }
98
+ }
99
+ try {
100
+ const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
101
+ if (hubMemEmbs.length > 0) {
102
+ const qv = await this.embedder.embedQuery(query).catch(() => null);
103
+ if (qv) {
104
+ const scored: Array<{ id: string; score: number }> = [];
105
+ for (const e of hubMemEmbs) {
106
+ let dot = 0, nA = 0, nB = 0;
107
+ for (let i = 0; i < qv.length && i < e.vector.length; i++) {
108
+ dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
109
+ }
110
+ const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
111
+ if (sim > 0.3) {
112
+ scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
113
+ }
114
+ }
115
+ scored.sort((a, b) => b.score - a.score);
116
+ hubMemVecRanked = scored.slice(0, candidatePool);
117
+ }
118
+ }
119
+ } catch { /* best-effort */ }
120
+ const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
121
+ if (hubTotal > 0) {
122
+ this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);
123
+ }
124
+ }
125
+
77
126
  // Step 2: RRF fusion
78
127
  const ftsRanked = ftsCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
79
128
  const vecRanked = vecCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
80
- const rrfScores = rrfFuse([ftsRanked, vecRanked, patternRanked], recallCfg.rrfK);
129
+ const allRankedLists = [ftsRanked, vecRanked, patternRanked];
130
+ if (hubMemFtsRanked.length > 0) allRankedLists.push(hubMemFtsRanked);
131
+ if (hubMemVecRanked.length > 0) allRankedLists.push(hubMemVecRanked);
132
+ if (hubMemPatternRanked.length > 0) allRankedLists.push(hubMemPatternRanked);
133
+ const rrfScores = rrfFuse(allRankedLists, recallCfg.rrfK);
81
134
 
82
135
  if (rrfScores.size === 0) {
83
136
  this.recordQuery(query, maxResults, minScore, 0);
@@ -101,6 +154,11 @@ export class RecallEngine {
101
154
 
102
155
  // Step 4: Time decay
103
156
  const withTs = mmrResults.map((r) => {
157
+ if (r.id.startsWith("hubmem:")) {
158
+ const memId = r.id.slice(7);
159
+ const mem = this.store.getHubMemoryById(memId);
160
+ return { ...r, createdAt: mem?.createdAt ?? 0 };
161
+ }
104
162
  const chunk = this.store.getChunk(r.id);
105
163
  return { ...r, createdAt: chunk?.createdAt ?? 0 };
106
164
  });
@@ -128,6 +186,35 @@ export class RecallEngine {
128
186
  const hits: SearchHit[] = [];
129
187
  for (const candidate of normalized) {
130
188
  if (hits.length >= maxResults) break;
189
+
190
+ if (candidate.id.startsWith("hubmem:")) {
191
+ const memId = candidate.id.slice(7);
192
+ const mem = this.store.getHubMemoryById(memId);
193
+ if (!mem) continue;
194
+ if (roleFilter && mem.role !== roleFilter) continue;
195
+ hits.push({
196
+ summary: mem.summary || mem.content.slice(0, 200),
197
+ original_excerpt: mem.content,
198
+ ref: {
199
+ sessionKey: `hub-shared:${mem.sourceUserId}`,
200
+ chunkId: mem.id,
201
+ turnId: "",
202
+ seq: 0,
203
+ },
204
+ score: Math.round(candidate.score * 1000) / 1000,
205
+ taskId: null,
206
+ skillId: null,
207
+ owner: `hub-user:${mem.sourceUserId}`,
208
+ origin: "hub-memory",
209
+ source: {
210
+ ts: mem.createdAt,
211
+ role: (mem.role || "assistant") as any,
212
+ sessionKey: `hub-shared:${mem.sourceUserId}`,
213
+ },
214
+ });
215
+ continue;
216
+ }
217
+
131
218
  const chunk = this.store.getChunk(candidate.id);
132
219
  if (!chunk) continue;
133
220
  if (roleFilter && chunk.role !== roleFilter) continue;
@@ -145,6 +232,7 @@ export class RecallEngine {
145
232
  score: Math.round(candidate.score * 1000) / 1000,
146
233
  taskId: chunk.taskId,
147
234
  skillId: chunk.skillId,
235
+ origin: chunk.owner === "public" ? "local-shared" : "local",
148
236
  source: {
149
237
  ts: chunk.createdAt,
150
238
  role: chunk.role,
@@ -1,6 +1,34 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import type { SummarizerConfig, Logger, PluginContext, OpenClawAPI } from "../types";
3
+ import type { SummarizerConfig, SummaryProvider, Logger, PluginContext, OpenClawAPI } from "../types";
4
+
5
+ /**
6
+ * Detect provider type from provider key name or base URL.
7
+ */
8
+ function detectProvider(providerKey: string | undefined, baseUrl: string): SummaryProvider {
9
+ const key = providerKey?.toLowerCase() ?? "";
10
+ const url = baseUrl.toLowerCase();
11
+ if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
12
+ if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
13
+ return "gemini";
14
+ }
15
+ if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
16
+ return "openai_compatible";
17
+ }
18
+
19
+ /**
20
+ * Return the correct default endpoint for a given provider.
21
+ */
22
+ function defaultEndpointForProvider(provider: SummaryProvider, baseUrl: string): string {
23
+ const stripped = baseUrl.replace(/\/+$/, "");
24
+ if (provider === "anthropic") {
25
+ if (stripped.endsWith("/v1/messages")) return stripped;
26
+ return `${stripped}/v1/messages`;
27
+ }
28
+ if (stripped.endsWith("/chat/completions")) return stripped;
29
+ if (stripped.endsWith("/completions")) return stripped;
30
+ return `${stripped}/chat/completions`;
31
+ }
4
32
 
5
33
  /**
6
34
  * Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
@@ -9,7 +37,8 @@ import type { SummarizerConfig, Logger, PluginContext, OpenClawAPI } from "../ty
9
37
  export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
10
38
  try {
11
39
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
12
- const cfgPath = path.join(home, ".openclaw", "openclaw.json");
40
+ const cfgPath = process.env.OPENCLAW_CONFIG_PATH
41
+ || path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
13
42
  if (!fs.existsSync(cfgPath)) return undefined;
14
43
 
15
44
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
@@ -30,13 +59,12 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
30
59
  const apiKey: string | undefined = providerCfg.apiKey;
31
60
  if (!baseUrl || !apiKey) return undefined;
32
61
 
33
- const endpoint = baseUrl.endsWith("/chat/completions")
34
- ? baseUrl
35
- : baseUrl.replace(/\/+$/, "") + "/chat/completions";
62
+ const provider = detectProvider(providerKey, baseUrl);
63
+ const endpoint = defaultEndpointForProvider(provider, baseUrl);
36
64
 
37
- log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
65
+ log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
38
66
  return {
39
- provider: "openai_compatible",
67
+ provider,
40
68
  endpoint,
41
69
  apiKey,
42
70
  model: modelId,
@@ -70,23 +98,34 @@ export interface LLMCallOptions {
70
98
  openclawAPI?: OpenClawAPI;
71
99
  }
72
100
 
73
- function normalizeEndpoint(url: string): string {
101
+ function normalizeOpenAIEndpoint(url: string): string {
74
102
  const stripped = url.replace(/\/+$/, "");
75
103
  if (stripped.endsWith("/chat/completions")) return stripped;
76
104
  if (stripped.endsWith("/completions")) return stripped;
77
105
  return `${stripped}/chat/completions`;
78
106
  }
79
107
 
108
+ function normalizeAnthropicEndpoint(url: string): string {
109
+ const stripped = url.replace(/\/+$/, "");
110
+ if (stripped.endsWith("/v1/messages")) return stripped;
111
+ if (stripped.endsWith("/messages")) return stripped;
112
+ return `${stripped}/v1/messages`;
113
+ }
114
+
115
+ function isAnthropicProvider(cfg: SummarizerConfig): boolean {
116
+ return cfg.provider === "anthropic";
117
+ }
118
+
80
119
  /**
81
120
  * Make a single LLM call with the given config. Throws on failure.
82
121
  * When cfg.provider === "openclaw", delegates to the OpenClaw host completion API.
122
+ * Dispatches to Anthropic or OpenAI-compatible format based on provider.
83
123
  */
84
124
  export async function callLLMOnce(
85
125
  cfg: SummarizerConfig,
86
126
  prompt: string,
87
127
  opts: LLMCallOptions = {},
88
128
  ): Promise<string> {
89
- // Handle openclaw provider via host completion API
90
129
  if (cfg.provider === "openclaw") {
91
130
  const api = opts.openclawAPI;
92
131
  if (!api) {
@@ -101,7 +140,57 @@ export async function callLLMOnce(
101
140
  return response.text.trim();
102
141
  }
103
142
 
104
- const endpoint = normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
143
+ if (isAnthropicProvider(cfg)) {
144
+ return callLLMOnceAnthropic(cfg, prompt, opts);
145
+ }
146
+ return callLLMOnceOpenAI(cfg, prompt, opts);
147
+ }
148
+
149
+ async function callLLMOnceAnthropic(
150
+ cfg: SummarizerConfig,
151
+ prompt: string,
152
+ opts: LLMCallOptions = {},
153
+ ): Promise<string> {
154
+ const endpoint = normalizeAnthropicEndpoint(
155
+ cfg.endpoint ?? "https://api.anthropic.com/v1/messages",
156
+ );
157
+ const model = cfg.model ?? "claude-3-haiku-20240307";
158
+ const headers: Record<string, string> = {
159
+ "Content-Type": "application/json",
160
+ "x-api-key": cfg.apiKey ?? "",
161
+ "anthropic-version": "2023-06-01",
162
+ ...cfg.headers,
163
+ };
164
+
165
+ const resp = await fetch(endpoint, {
166
+ method: "POST",
167
+ headers,
168
+ body: JSON.stringify({
169
+ model,
170
+ temperature: opts.temperature ?? 0.1,
171
+ max_tokens: opts.maxTokens ?? 1024,
172
+ messages: [{ role: "user", content: prompt }],
173
+ }),
174
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
175
+ });
176
+
177
+ if (!resp.ok) {
178
+ const body = await resp.text();
179
+ throw new Error(`LLM call failed (${resp.status}): ${body}`);
180
+ }
181
+
182
+ const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
183
+ return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
184
+ }
185
+
186
+ async function callLLMOnceOpenAI(
187
+ cfg: SummarizerConfig,
188
+ prompt: string,
189
+ opts: LLMCallOptions = {},
190
+ ): Promise<string> {
191
+ const endpoint = normalizeOpenAIEndpoint(
192
+ cfg.endpoint ?? "https://api.openai.com/v1/chat/completions",
193
+ );
105
194
  const model = cfg.model ?? "gpt-4o-mini";
106
195
  const headers: Record<string, string> = {
107
196
  "Content-Type": "application/json",
@@ -12,7 +12,7 @@ import type {
12
12
  export type HubScope = "local" | "group" | "all";
13
13
  export type SharedVisibility = "group" | "public";
14
14
  export type UserRole = "admin" | "member";
15
- export type UserStatus = "pending" | "active" | "blocked" | "rejected";
15
+ export type UserStatus = "pending" | "active" | "blocked" | "rejected" | "removed" | "left";
16
16
 
17
17
  export type { ClientModeConfig, HubModeConfig, SharingCapabilities, SharingConfig, SharingRole };
18
18