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

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 (98) 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 +5 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +173 -14
  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 +9 -11
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +7 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +301 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +3 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +18 -1
  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 +91 -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 +2 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +3 -0
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/storage/ensure-binding.d.ts +12 -0
  49. package/dist/storage/ensure-binding.d.ts.map +1 -0
  50. package/dist/storage/ensure-binding.js +53 -0
  51. package/dist/storage/ensure-binding.js.map +1 -0
  52. package/dist/storage/sqlite.d.ts +74 -20
  53. package/dist/storage/sqlite.d.ts.map +1 -1
  54. package/dist/storage/sqlite.js +301 -207
  55. package/dist/storage/sqlite.js.map +1 -1
  56. package/dist/telemetry.d.ts +12 -5
  57. package/dist/telemetry.d.ts.map +1 -1
  58. package/dist/telemetry.js +156 -40
  59. package/dist/telemetry.js.map +1 -1
  60. package/dist/tools/memory-search.d.ts +3 -1
  61. package/dist/tools/memory-search.d.ts.map +1 -1
  62. package/dist/tools/memory-search.js +3 -1
  63. package/dist/tools/memory-search.js.map +1 -1
  64. package/dist/types.d.ts +1 -2
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/viewer/html.d.ts.map +1 -1
  68. package/dist/viewer/html.js +2991 -1041
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +32 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +1122 -261
  73. package/dist/viewer/server.js.map +1 -1
  74. package/index.ts +384 -43
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +3 -2
  77. package/scripts/postinstall.cjs +1 -1
  78. package/skill/memos-memory-guide/SKILL.md +64 -26
  79. package/src/capture/index.ts +37 -1
  80. package/src/client/connector.ts +173 -16
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +9 -11
  84. package/src/hub/server.ts +285 -98
  85. package/src/hub/user-manager.ts +20 -3
  86. package/src/index.ts +10 -2
  87. package/src/ingest/providers/index.ts +41 -7
  88. package/src/recall/engine.ts +84 -1
  89. package/src/shared/llm-call.ts +97 -9
  90. package/src/sharing/types.ts +1 -1
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/storage/ensure-binding.ts +52 -0
  93. package/src/storage/sqlite.ts +310 -233
  94. package/src/telemetry.ts +172 -41
  95. package/src/tools/memory-search.ts +2 -1
  96. package/src/types.ts +1 -2
  97. package/src/viewer/html.ts +2991 -1041
  98. package/src/viewer/server.ts +984 -190
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,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 ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
53
+ const cfgPath = path.join(ocHome, "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,60 @@ export class RecallEngine {
74
74
  score: 1 / (i + 1),
75
75
  }));
76
76
 
77
+ // Step 1c: Hub memories search (when sharing is enabled and hub_memories exist)
78
+ let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
79
+ let hubMemVecRanked: Array<{ id: string; score: number }> = [];
80
+ let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
81
+ if (query && this.ctx.config.sharing?.enabled) {
82
+ try {
83
+ const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
84
+ hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
85
+ id: `hubmem:${hit.id}`, score: 1 / (i + 1),
86
+ }));
87
+ } catch { /* hub_memories table may not exist */ }
88
+ if (shortTerms.length > 0) {
89
+ try {
90
+ const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
91
+ hubMemPatternRanked = hubPatternHits.map((h, i) => ({
92
+ id: `hubmem:${h.memoryId}`, score: 1 / (i + 1),
93
+ }));
94
+ } catch { /* best-effort */ }
95
+ }
96
+ try {
97
+ const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
98
+ if (hubMemEmbs.length > 0) {
99
+ const qv = await this.embedder.embedQuery(query).catch(() => null);
100
+ if (qv) {
101
+ const scored: Array<{ id: string; score: number }> = [];
102
+ for (const e of hubMemEmbs) {
103
+ let dot = 0, nA = 0, nB = 0;
104
+ for (let i = 0; i < qv.length && i < e.vector.length; i++) {
105
+ dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
106
+ }
107
+ const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
108
+ if (sim > 0.3) {
109
+ scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
110
+ }
111
+ }
112
+ scored.sort((a, b) => b.score - a.score);
113
+ hubMemVecRanked = scored.slice(0, candidatePool);
114
+ }
115
+ }
116
+ } catch { /* best-effort */ }
117
+ const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
118
+ if (hubTotal > 0) {
119
+ this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);
120
+ }
121
+ }
122
+
77
123
  // Step 2: RRF fusion
78
124
  const ftsRanked = ftsCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
79
125
  const vecRanked = vecCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
80
- const rrfScores = rrfFuse([ftsRanked, vecRanked, patternRanked], recallCfg.rrfK);
126
+ const allRankedLists = [ftsRanked, vecRanked, patternRanked];
127
+ if (hubMemFtsRanked.length > 0) allRankedLists.push(hubMemFtsRanked);
128
+ if (hubMemVecRanked.length > 0) allRankedLists.push(hubMemVecRanked);
129
+ if (hubMemPatternRanked.length > 0) allRankedLists.push(hubMemPatternRanked);
130
+ const rrfScores = rrfFuse(allRankedLists, recallCfg.rrfK);
81
131
 
82
132
  if (rrfScores.size === 0) {
83
133
  this.recordQuery(query, maxResults, minScore, 0);
@@ -101,6 +151,11 @@ export class RecallEngine {
101
151
 
102
152
  // Step 4: Time decay
103
153
  const withTs = mmrResults.map((r) => {
154
+ if (r.id.startsWith("hubmem:")) {
155
+ const memId = r.id.slice(7);
156
+ const mem = this.store.getHubMemoryById(memId);
157
+ return { ...r, createdAt: mem?.createdAt ?? 0 };
158
+ }
104
159
  const chunk = this.store.getChunk(r.id);
105
160
  return { ...r, createdAt: chunk?.createdAt ?? 0 };
106
161
  });
@@ -128,6 +183,34 @@ export class RecallEngine {
128
183
  const hits: SearchHit[] = [];
129
184
  for (const candidate of normalized) {
130
185
  if (hits.length >= maxResults) break;
186
+
187
+ if (candidate.id.startsWith("hubmem:")) {
188
+ const memId = candidate.id.slice(7);
189
+ const mem = this.store.getHubMemoryById(memId);
190
+ if (!mem) continue;
191
+ if (roleFilter && mem.role !== roleFilter) continue;
192
+ hits.push({
193
+ summary: mem.summary || mem.content.slice(0, 200),
194
+ original_excerpt: mem.content,
195
+ ref: {
196
+ sessionKey: `hub-shared:${mem.sourceUserId}`,
197
+ chunkId: mem.id,
198
+ turnId: "",
199
+ seq: 0,
200
+ },
201
+ score: Math.round(candidate.score * 1000) / 1000,
202
+ taskId: null,
203
+ skillId: null,
204
+ owner: `hub-user:${mem.sourceUserId}`,
205
+ source: {
206
+ ts: mem.createdAt,
207
+ role: (mem.role || "assistant") as any,
208
+ sessionKey: `hub-shared:${mem.sourceUserId}`,
209
+ },
210
+ });
211
+ continue;
212
+ }
213
+
131
214
  const chunk = this.store.getChunk(candidate.id);
132
215
  if (!chunk) continue;
133
216
  if (roleFilter && chunk.role !== roleFilter) continue;
@@ -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).
@@ -30,13 +58,12 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
30
58
  const apiKey: string | undefined = providerCfg.apiKey;
31
59
  if (!baseUrl || !apiKey) return undefined;
32
60
 
33
- const endpoint = baseUrl.endsWith("/chat/completions")
34
- ? baseUrl
35
- : baseUrl.replace(/\/+$/, "") + "/chat/completions";
61
+ const provider = detectProvider(providerKey, baseUrl);
62
+ const endpoint = defaultEndpointForProvider(provider, baseUrl);
36
63
 
37
- log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
64
+ log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
38
65
  return {
39
- provider: "openai_compatible",
66
+ provider,
40
67
  endpoint,
41
68
  apiKey,
42
69
  model: modelId,
@@ -70,23 +97,34 @@ export interface LLMCallOptions {
70
97
  openclawAPI?: OpenClawAPI;
71
98
  }
72
99
 
73
- function normalizeEndpoint(url: string): string {
100
+ function normalizeOpenAIEndpoint(url: string): string {
74
101
  const stripped = url.replace(/\/+$/, "");
75
102
  if (stripped.endsWith("/chat/completions")) return stripped;
76
103
  if (stripped.endsWith("/completions")) return stripped;
77
104
  return `${stripped}/chat/completions`;
78
105
  }
79
106
 
107
+ function normalizeAnthropicEndpoint(url: string): string {
108
+ const stripped = url.replace(/\/+$/, "");
109
+ if (stripped.endsWith("/v1/messages")) return stripped;
110
+ if (stripped.endsWith("/messages")) return stripped;
111
+ return `${stripped}/v1/messages`;
112
+ }
113
+
114
+ function isAnthropicProvider(cfg: SummarizerConfig): boolean {
115
+ return cfg.provider === "anthropic";
116
+ }
117
+
80
118
  /**
81
119
  * Make a single LLM call with the given config. Throws on failure.
82
120
  * When cfg.provider === "openclaw", delegates to the OpenClaw host completion API.
121
+ * Dispatches to Anthropic or OpenAI-compatible format based on provider.
83
122
  */
84
123
  export async function callLLMOnce(
85
124
  cfg: SummarizerConfig,
86
125
  prompt: string,
87
126
  opts: LLMCallOptions = {},
88
127
  ): Promise<string> {
89
- // Handle openclaw provider via host completion API
90
128
  if (cfg.provider === "openclaw") {
91
129
  const api = opts.openclawAPI;
92
130
  if (!api) {
@@ -101,7 +139,57 @@ export async function callLLMOnce(
101
139
  return response.text.trim();
102
140
  }
103
141
 
104
- const endpoint = normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
142
+ if (isAnthropicProvider(cfg)) {
143
+ return callLLMOnceAnthropic(cfg, prompt, opts);
144
+ }
145
+ return callLLMOnceOpenAI(cfg, prompt, opts);
146
+ }
147
+
148
+ async function callLLMOnceAnthropic(
149
+ cfg: SummarizerConfig,
150
+ prompt: string,
151
+ opts: LLMCallOptions = {},
152
+ ): Promise<string> {
153
+ const endpoint = normalizeAnthropicEndpoint(
154
+ cfg.endpoint ?? "https://api.anthropic.com/v1/messages",
155
+ );
156
+ const model = cfg.model ?? "claude-3-haiku-20240307";
157
+ const headers: Record<string, string> = {
158
+ "Content-Type": "application/json",
159
+ "x-api-key": cfg.apiKey ?? "",
160
+ "anthropic-version": "2023-06-01",
161
+ ...cfg.headers,
162
+ };
163
+
164
+ const resp = await fetch(endpoint, {
165
+ method: "POST",
166
+ headers,
167
+ body: JSON.stringify({
168
+ model,
169
+ temperature: opts.temperature ?? 0.1,
170
+ max_tokens: opts.maxTokens ?? 1024,
171
+ messages: [{ role: "user", content: prompt }],
172
+ }),
173
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
174
+ });
175
+
176
+ if (!resp.ok) {
177
+ const body = await resp.text();
178
+ throw new Error(`LLM call failed (${resp.status}): ${body}`);
179
+ }
180
+
181
+ const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
182
+ return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
183
+ }
184
+
185
+ async function callLLMOnceOpenAI(
186
+ cfg: SummarizerConfig,
187
+ prompt: string,
188
+ opts: LLMCallOptions = {},
189
+ ): Promise<string> {
190
+ const endpoint = normalizeOpenAIEndpoint(
191
+ cfg.endpoint ?? "https://api.openai.com/v1/chat/completions",
192
+ );
105
193
  const model = cfg.model ?? "gpt-4o-mini";
106
194
  const headers: Record<string, string> = {
107
195
  "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";
16
16
 
17
17
  export type { ClientModeConfig, HubModeConfig, SharingCapabilities, SharingConfig, SharingRole };
18
18
 
@@ -12,6 +12,8 @@ import { SkillUpgrader } from "./upgrader";
12
12
  import { SkillInstaller } from "./installer";
13
13
  import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
14
14
 
15
+ export type SkillEvolvedCallback = (skillName: string, upgradeType: "created" | "upgraded") => void;
16
+
15
17
  export class SkillEvolver {
16
18
  private evaluator: SkillEvaluator;
17
19
  private generator: SkillGenerator;
@@ -19,6 +21,7 @@ export class SkillEvolver {
19
21
  private installer: SkillInstaller;
20
22
  private processing = false;
21
23
  private queue: Task[] = [];
24
+ onSkillEvolved: SkillEvolvedCallback | null = null;
22
25
 
23
26
  constructor(
24
27
  private store: SqliteStore,
@@ -279,6 +282,7 @@ Use selectedIndex 0 when none is highly relevant.`;
279
282
  if (upgraded) {
280
283
  this.store.linkTaskSkill(task.id, freshSkill.id, "evolved_from", freshSkill.version + 1);
281
284
  this.installer.syncIfInstalled(freshSkill.name);
285
+ this.onSkillEvolved?.(freshSkill.name, "upgraded");
282
286
  } else {
283
287
  this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
284
288
  }
@@ -307,6 +311,7 @@ Use selectedIndex 0 when none is highly relevant.`;
307
311
  this.markChunksWithSkill(chunks, skill.id);
308
312
  this.store.linkTaskSkill(task.id, skill.id, "generated_from", 1);
309
313
  this.store.setTaskSkillMeta(task.id, { skillStatus: "generated", skillReason: evalResult.reason });
314
+ this.onSkillEvolved?.(skill.name, "created");
310
315
 
311
316
  const autoInstall = this.ctx.config.skillEvolution?.autoInstall ?? DEFAULTS.skillAutoInstall;
312
317
  if (autoInstall && skill.status === "active") {
@@ -0,0 +1,52 @@
1
+ import { existsSync, mkdirSync, copyFileSync } from "fs";
2
+ import { execSync } from "child_process";
3
+ import path from "path";
4
+ import { createRequire } from "module";
5
+
6
+ /**
7
+ * Ensure the better-sqlite3 native binary is available.
8
+ *
9
+ * OpenClaw installs plugins with `--ignore-scripts`, which skips
10
+ * the native compilation step. This function checks for the binary
11
+ * and restores it from bundled prebuilds if missing.
12
+ */
13
+ export function ensureSqliteBinding(log?: { info: (msg: string) => void; warn: (msg: string) => void }): void {
14
+ const _req = typeof require !== "undefined" ? require : createRequire(__filename);
15
+ const bsqlPkg = _req.resolve("better-sqlite3/package.json");
16
+ const bsqlDir = path.dirname(bsqlPkg);
17
+ const bindingPath = path.join(bsqlDir, "build", "Release", "better_sqlite3.node");
18
+
19
+ if (existsSync(bindingPath)) return;
20
+
21
+ const platform = `${process.platform}-${process.arch}`;
22
+ const pluginRoot = path.resolve(__dirname, "..", "..");
23
+ const prebuildSrc = path.join(pluginRoot, "prebuilds", platform, "better_sqlite3.node");
24
+
25
+ if (existsSync(prebuildSrc)) {
26
+ log?.info(`[ensure-binding] Copying prebuild for ${platform}...`);
27
+ mkdirSync(path.dirname(bindingPath), { recursive: true });
28
+ copyFileSync(prebuildSrc, bindingPath);
29
+ log?.info(`[ensure-binding] Prebuild installed successfully.`);
30
+ return;
31
+ }
32
+
33
+ log?.warn(`[ensure-binding] No prebuild for ${platform}, attempting npm rebuild...`);
34
+ try {
35
+ const installDir = path.resolve(bsqlDir, "..", "..");
36
+ execSync("npm rebuild better-sqlite3", {
37
+ cwd: installDir,
38
+ stdio: "pipe",
39
+ timeout: 180_000,
40
+ });
41
+ if (existsSync(bindingPath)) {
42
+ log?.info(`[ensure-binding] Rebuilt better-sqlite3 successfully.`);
43
+ return;
44
+ }
45
+ } catch { /* fall through */ }
46
+
47
+ throw new Error(
48
+ `better-sqlite3 native binary not found for ${platform}.\n` +
49
+ `Prebuild not bundled and npm rebuild failed.\n` +
50
+ `Fix: cd ${path.resolve(bsqlDir, "..", "..")} && npm rebuild better-sqlite3`,
51
+ );
52
+ }