@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
@@ -1,13 +1,47 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import type { SummarizerConfig, Logger, OpenClawAPI } from "../../types";
4
- import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai";
3
+ import type { SummarizerConfig, SummaryProvider, Logger } from "../../types";
4
+ import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
5
5
  import type { FilterResult, DedupResult } from "./openai";
6
6
  export type { FilterResult, DedupResult } from "./openai";
7
7
  import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
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,
@@ -120,7 +154,6 @@ export class Summarizer {
120
154
  constructor(
121
155
  private cfg: SummarizerConfig | undefined,
122
156
  private log: Logger,
123
- private openclawAPI?: OpenClawAPI,
124
157
  strongCfg?: SummarizerConfig,
125
158
  ) {
126
159
  this.strongCfg = strongCfg;
@@ -130,20 +163,11 @@ export class Summarizer {
130
163
  /**
131
164
  * Ordered config chain: strongCfg → cfg → fallbackCfg (OpenClaw native model).
132
165
  * Returns configs that are defined, in priority order.
133
- * Openclaw configs without hostCompletion capability or without openclawAPI are excluded.
134
166
  */
135
167
  private getConfigChain(): SummarizerConfig[] {
136
168
  const chain: SummarizerConfig[] = [];
137
169
  if (this.strongCfg) chain.push(this.strongCfg);
138
- if (this.cfg) {
139
- if (this.cfg.provider === "openclaw") {
140
- if (this.cfg.capabilities?.hostCompletion === true && this.openclawAPI) {
141
- chain.push(this.cfg);
142
- }
143
- } else {
144
- chain.push(this.cfg);
145
- }
146
- }
170
+ if (this.cfg) chain.push(this.cfg);
147
171
  if (this.fallbackCfg) chain.push(this.fallbackCfg);
148
172
  return chain;
149
173
  }
@@ -227,9 +251,7 @@ export class Summarizer {
227
251
  return taskFallback(text);
228
252
  }
229
253
 
230
- const result = await this.tryChain("summarizeTask", (cfg) =>
231
- cfg.provider === "openclaw" ? this.summarizeTaskOpenClaw(text) : callSummarizeTask(cfg, text, this.log),
232
- );
254
+ const result = await this.tryChain("summarizeTask", (cfg) => callSummarizeTask(cfg, text, this.log));
233
255
  return result ?? taskFallback(text);
234
256
  }
235
257
 
@@ -268,11 +290,7 @@ export class Summarizer {
268
290
  if (!this.cfg && !this.fallbackCfg) return null;
269
291
  if (candidates.length === 0) return { relevant: [], sufficient: true };
270
292
 
271
- const result = await this.tryChain("filterRelevant", (cfg) =>
272
- cfg.provider === "openclaw"
273
- ? this.filterRelevantOpenClaw(query, candidates)
274
- : callFilterRelevant(cfg, query, candidates, this.log),
275
- );
293
+ const result = await this.tryChain("filterRelevant", (cfg) => callFilterRelevant(cfg, query, candidates, this.log));
276
294
  return result ?? null;
277
295
  }
278
296
 
@@ -283,144 +301,13 @@ export class Summarizer {
283
301
  if (!this.cfg && !this.fallbackCfg) return null;
284
302
  if (candidates.length === 0) return null;
285
303
 
286
- const result = await this.tryChain("judgeDedup", (cfg) =>
287
- cfg.provider === "openclaw"
288
- ? this.judgeDedupOpenClaw(newSummary, candidates)
289
- : callJudgeDedup(cfg, newSummary, candidates, this.log),
290
- );
304
+ const result = await this.tryChain("judgeDedup", (cfg) => callJudgeDedup(cfg, newSummary, candidates, this.log));
291
305
  return result ?? { action: "NEW", reason: "all_models_failed" };
292
306
  }
293
307
 
294
308
  getStrongConfig(): SummarizerConfig | undefined {
295
309
  return this.strongCfg;
296
310
  }
297
-
298
- // ─── OpenClaw API Implementation ───
299
-
300
- private requireOpenClawAPI(): void {
301
- if (!this.openclawAPI) {
302
- throw new Error(
303
- "OpenClaw API not available. Ensure sharing.capabilities.hostCompletion is enabled in config."
304
- );
305
- }
306
- }
307
-
308
- private async summarizeOpenClaw(text: string): Promise<string> {
309
- this.requireOpenClawAPI();
310
- const prompt = [
311
- `Summarize the text in ONE concise sentence (max 120 characters). IMPORTANT: Use the SAME language as the input text — if the input is Chinese, write Chinese; if English, write English. Preserve exact names, commands, error codes. No bullet points, no preamble — output only the sentence.`,
312
- ``,
313
- text.slice(0, 2000),
314
- ].join("\n");
315
-
316
- const response = await this.openclawAPI!.complete({
317
- prompt,
318
- maxTokens: 100,
319
- temperature: 0,
320
- model: this.cfg?.model,
321
- });
322
-
323
- return response.text.trim().slice(0, 200);
324
- }
325
-
326
- private async summarizeTaskOpenClaw(text: string): Promise<string> {
327
- this.requireOpenClawAPI();
328
- const prompt = [
329
- OPENCLAW_TASK_SUMMARY_PROMPT,
330
- ``,
331
- text,
332
- ].join("\n");
333
-
334
- const response = await this.openclawAPI!.complete({
335
- prompt,
336
- maxTokens: 4096,
337
- temperature: 0.1,
338
- model: this.cfg?.model,
339
- });
340
-
341
- return response.text.trim();
342
- }
343
-
344
- private async judgeNewTopicOpenClaw(currentContext: string, newMessage: string): Promise<boolean> {
345
- this.requireOpenClawAPI();
346
- const prompt = [
347
- OPENCLAW_TOPIC_JUDGE_PROMPT,
348
- ``,
349
- `CURRENT CONVERSATION SUMMARY:`,
350
- currentContext,
351
- ``,
352
- `NEW USER MESSAGE:`,
353
- newMessage,
354
- ].join("\n");
355
-
356
- const response = await this.openclawAPI!.complete({
357
- prompt,
358
- maxTokens: 10,
359
- temperature: 0,
360
- model: this.cfg?.model,
361
- });
362
-
363
- const answer = response.text.trim().toUpperCase();
364
- this.log.debug(`Topic judge result: "${answer}"`);
365
- return answer.startsWith("NEW");
366
- }
367
-
368
- private async filterRelevantOpenClaw(
369
- query: string,
370
- candidates: Array<{ index: number; role: string; content: string; time?: string }>,
371
- ): Promise<FilterResult> {
372
- this.requireOpenClawAPI();
373
- const candidateText = candidates
374
- .map((c) => `${c.index}. [${c.role}] ${c.content}`)
375
- .join("\n");
376
-
377
- const prompt = [
378
- OPENCLAW_FILTER_RELEVANT_PROMPT,
379
- ``,
380
- `QUERY: ${query}`,
381
- ``,
382
- `CANDIDATES:`,
383
- candidateText,
384
- ].join("\n");
385
-
386
- const response = await this.openclawAPI!.complete({
387
- prompt,
388
- maxTokens: 200,
389
- temperature: 0,
390
- model: this.cfg?.model,
391
- });
392
-
393
- return parseFilterResult(response.text.trim(), this.log);
394
- }
395
-
396
- private async judgeDedupOpenClaw(
397
- newSummary: string,
398
- candidates: Array<{ index: number; summary: string; chunkId: string }>,
399
- ): Promise<DedupResult> {
400
- this.requireOpenClawAPI();
401
- const candidateText = candidates
402
- .map((c) => `${c.index}. ${c.summary}`)
403
- .join("\n");
404
-
405
- const prompt = [
406
- OPENCLAW_DEDUP_JUDGE_PROMPT,
407
- ``,
408
- `NEW MEMORY:`,
409
- newSummary,
410
- ``,
411
- `EXISTING MEMORIES:`,
412
- candidateText,
413
- ].join("\n");
414
-
415
- const response = await this.openclawAPI!.complete({
416
- prompt,
417
- maxTokens: 300,
418
- temperature: 0,
419
- model: this.cfg?.model,
420
- });
421
-
422
- return parseDedupResult(response.text.trim(), this.log);
423
- }
424
311
  }
425
312
 
426
313
  // ─── Dispatch helpers ───
@@ -596,90 +483,3 @@ function wordCount(text: string): number {
596
483
  return count;
597
484
  }
598
485
 
599
- // ─── OpenClaw Prompt Templates ───
600
-
601
- const OPENCLAW_TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
602
-
603
- CRITICAL LANGUAGE RULE: You MUST write in the SAME language as the user's messages. Chinese input → Chinese output. English input → English output. NEVER mix languages.
604
-
605
- Output EXACTLY this structure:
606
-
607
- 📌 Title
608
- A short, descriptive title (10-30 characters). Like a chat group name.
609
-
610
- 🎯 Goal
611
- One sentence: what the user wanted to accomplish.
612
-
613
- 📋 Key Steps
614
- - Describe each meaningful step in detail
615
- - Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
616
- - For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
617
- - For configs: include the actual config values and structure
618
- - For lists/instructions: include the actual items, not just "provided a list"
619
- - Merge only truly trivial back-and-forth (like "ok" / "sure")
620
- - Do NOT over-summarize: "provided a function" is BAD; show the actual function
621
-
622
- ✅ Result
623
- What was the final outcome? Include the final version of any code/config/content produced.
624
-
625
- 💡 Key Details
626
- - Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
627
- - Specific values: numbers, versions, thresholds, URLs, file paths, model names
628
- - Omit this section only if there truly are no noteworthy details
629
-
630
- RULES:
631
- - This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough.
632
- - PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts
633
- - DISCARD only: greetings, filler, the assistant explaining what it will do before doing it
634
- - Replace secrets (API keys, tokens, passwords) with [REDACTED]
635
- - Target length: 30-50% of the original conversation length. Longer conversations need longer summaries.
636
- - Output summary only, no preamble.`;
637
-
638
- const OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given a summary of the CURRENT conversation and a NEW user message, determine if the new message starts a DIFFERENT topic/task.
639
-
640
- Answer ONLY "NEW" or "SAME".
641
-
642
- Rules:
643
- - "NEW" = the new message is about a completely different subject, project, or task
644
- - "SAME" = the new message continues, follows up on, or is closely related to the current topic
645
- - Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
646
- - Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
647
- - A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
648
-
649
- Output exactly one word: NEW or SAME`;
650
-
651
- const OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge. Given a user's QUERY and a list of CANDIDATE memory summaries, do two things:
652
-
653
- 1. Select ALL candidates that could be useful for answering the query. When in doubt, INCLUDE the candidate.
654
- - For questions about lists, history, or "what/where/who" across multiple items, include ALL matching items.
655
- - For factual lookups, a single direct answer is enough.
656
- 2. Judge whether the selected memories are SUFFICIENT to fully answer the query WITHOUT fetching additional context.
657
-
658
- IMPORTANT for "sufficient" judgment:
659
- - sufficient=true ONLY when the memories contain a concrete ANSWER, fact, decision, or actionable information that directly addresses the query.
660
- - sufficient=false when the memories only repeat the question, show related topics but lack the specific detail, or contain partial information.
661
-
662
- Output a JSON object with exactly two fields:
663
- {"relevant":[1,3,5],"sufficient":true}
664
-
665
- - "relevant": array of candidate numbers that are useful. Empty array [] if none are relevant.
666
- - "sufficient": true ONLY if the memories contain a direct answer; false otherwise.
667
-
668
- Output ONLY the JSON object, nothing else.`;
669
-
670
- const OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system. Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
671
-
672
- For each EXISTING memory, the NEW memory is either:
673
- - "DUPLICATE": NEW is fully covered by an EXISTING memory — no new information at all
674
- - "UPDATE": NEW contains information that supplements or updates an EXISTING memory (new data, status change, additional detail)
675
- - "NEW": NEW is a different topic/event despite surface similarity
676
-
677
- Pick the BEST match among all candidates. If none match well, choose "NEW".
678
-
679
- Output a single JSON object:
680
- - If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"..."}
681
- - If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"...","mergedSummary":"a combined summary preserving all info from both old and new, same language as input"}
682
- - If NEW: {"action":"NEW","reason":"..."}
683
-
684
- CRITICAL: mergedSummary must use the SAME language as the input. Output ONLY the JSON object.`;
685
-
@@ -316,7 +316,7 @@ export async function filterRelevantOpenAI(
316
316
  return parseFilterResult(raw, log);
317
317
  }
318
318
 
319
- export function parseFilterResult(raw: string, log: Logger): FilterResult {
319
+ function parseFilterResult(raw: string, log: Logger): FilterResult {
320
320
  try {
321
321
  const match = raw.match(/\{[\s\S]*\}/);
322
322
  if (match) {
@@ -39,7 +39,7 @@ export class TaskProcessor {
39
39
  private ctx: PluginContext,
40
40
  ) {
41
41
  const strongCfg = ctx.config.skillEvolution?.summarizer;
42
- this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI, strongCfg);
42
+ this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, strongCfg);
43
43
  }
44
44
 
45
45
  onTaskCompleted(cb: (task: Task) => void): void {
@@ -246,7 +246,7 @@ export class RecallEngine {
246
246
  if (candidateSkills.length === 0) return [];
247
247
 
248
248
  // LLM relevance judgment
249
- const summarizer = new Summarizer(this.ctx.config.summarizer, this.ctx.log, this.ctx.openclawAPI);
249
+ const summarizer = new Summarizer(this.ctx.config.summarizer, this.ctx.log);
250
250
  const relevantIndices = await this.judgeSkillRelevance(summarizer, query, candidateSkills);
251
251
 
252
252
  return relevantIndices.map((idx) => {
@@ -1,6 +1,35 @@
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 } 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
+ // OpenAI-compatible providers
29
+ if (stripped.endsWith("/chat/completions")) return stripped;
30
+ if (stripped.endsWith("/completions")) return stripped;
31
+ return `${stripped}/chat/completions`;
32
+ }
4
33
 
5
34
  /**
6
35
  * Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
@@ -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,
@@ -66,42 +94,86 @@ export interface LLMCallOptions {
66
94
  maxTokens?: number;
67
95
  temperature?: number;
68
96
  timeoutMs?: number;
69
- /** Pass ctx.openclawAPI so callLLMOnce can handle provider === "openclaw" */
70
- openclawAPI?: OpenClawAPI;
71
97
  }
72
98
 
73
- function normalizeEndpoint(url: string): string {
99
+ function normalizeOpenAIEndpoint(url: string): string {
74
100
  const stripped = url.replace(/\/+$/, "");
75
101
  if (stripped.endsWith("/chat/completions")) return stripped;
76
102
  if (stripped.endsWith("/completions")) return stripped;
77
103
  return `${stripped}/chat/completions`;
78
104
  }
79
105
 
106
+ function normalizeAnthropicEndpoint(url: string): string {
107
+ const stripped = url.replace(/\/+$/, "");
108
+ if (stripped.endsWith("/v1/messages")) return stripped;
109
+ if (stripped.endsWith("/messages")) return stripped;
110
+ return `${stripped}/v1/messages`;
111
+ }
112
+
113
+ function isAnthropicProvider(cfg: SummarizerConfig): boolean {
114
+ return cfg.provider === "anthropic";
115
+ }
116
+
80
117
  /**
81
118
  * Make a single LLM call with the given config. Throws on failure.
82
- * When cfg.provider === "openclaw", delegates to the OpenClaw host completion API.
119
+ * Dispatches to Anthropic or OpenAI-compatible format based on provider.
83
120
  */
84
121
  export async function callLLMOnce(
85
122
  cfg: SummarizerConfig,
86
123
  prompt: string,
87
124
  opts: LLMCallOptions = {},
88
125
  ): Promise<string> {
89
- // Handle openclaw provider via host completion API
90
- if (cfg.provider === "openclaw") {
91
- const api = opts.openclawAPI;
92
- if (!api) {
93
- throw new Error("OpenClaw API not available. Ensure sharing.capabilities.hostCompletion is enabled.");
94
- }
95
- const response = await api.complete({
96
- prompt,
97
- maxTokens: opts.maxTokens ?? 1024,
126
+ if (isAnthropicProvider(cfg)) {
127
+ return callLLMOnceAnthropic(cfg, prompt, opts);
128
+ }
129
+ return callLLMOnceOpenAI(cfg, prompt, opts);
130
+ }
131
+
132
+ async function callLLMOnceAnthropic(
133
+ cfg: SummarizerConfig,
134
+ prompt: string,
135
+ opts: LLMCallOptions = {},
136
+ ): Promise<string> {
137
+ const endpoint = normalizeAnthropicEndpoint(
138
+ cfg.endpoint ?? "https://api.anthropic.com/v1/messages",
139
+ );
140
+ const model = cfg.model ?? "claude-3-haiku-20240307";
141
+ const headers: Record<string, string> = {
142
+ "Content-Type": "application/json",
143
+ "x-api-key": cfg.apiKey ?? "",
144
+ "anthropic-version": "2023-06-01",
145
+ ...cfg.headers,
146
+ };
147
+
148
+ const resp = await fetch(endpoint, {
149
+ method: "POST",
150
+ headers,
151
+ body: JSON.stringify({
152
+ model,
98
153
  temperature: opts.temperature ?? 0.1,
99
- model: cfg.model,
100
- });
101
- return response.text.trim();
154
+ max_tokens: opts.maxTokens ?? 1024,
155
+ messages: [{ role: "user", content: prompt }],
156
+ }),
157
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
158
+ });
159
+
160
+ if (!resp.ok) {
161
+ const body = await resp.text();
162
+ throw new Error(`LLM call failed (${resp.status}): ${body}`);
102
163
  }
103
164
 
104
- const endpoint = normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
165
+ const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
166
+ return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
167
+ }
168
+
169
+ async function callLLMOnceOpenAI(
170
+ cfg: SummarizerConfig,
171
+ prompt: string,
172
+ opts: LLMCallOptions = {},
173
+ ): Promise<string> {
174
+ const endpoint = normalizeOpenAIEndpoint(
175
+ cfg.endpoint ?? "https://api.openai.com/v1/chat/completions",
176
+ );
105
177
  const model = cfg.model ?? "gpt-4o-mini";
106
178
  const headers: Record<string, string> = {
107
179
  "Content-Type": "application/json",
@@ -145,7 +145,7 @@ export class SkillEvaluator {
145
145
  .replace("{SUMMARY}", task.summary.slice(0, 3000));
146
146
 
147
147
  try {
148
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvaluator.create", { openclawAPI: this.ctx.openclawAPI });
148
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvaluator.create");
149
149
  return this.parseJSON<CreateEvalResult>(raw, {
150
150
  shouldGenerate: false, reason: "parse failed", suggestedName: "", suggestedTags: [], confidence: 0,
151
151
  });
@@ -169,7 +169,7 @@ export class SkillEvaluator {
169
169
  .replace("{SUMMARY}", task.summary.slice(0, 3000));
170
170
 
171
171
  try {
172
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvaluator.upgrade", { openclawAPI: this.ctx.openclawAPI });
172
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillEvaluator.upgrade");
173
173
  return this.parseJSON<UpgradeEvalResult>(raw, {
174
174
  shouldUpgrade: false, upgradeType: "refine", dimensions: [], reason: "parse failed", mergeStrategy: "", confidence: 0,
175
175
  });
@@ -179,7 +179,6 @@ export class SkillEvaluator {
179
179
  }
180
180
  }
181
181
 
182
-
183
182
  private parseJSON<T>(raw: string, fallback: T): T {
184
183
  const jsonMatch = raw.match(/\{[\s\S]*\}/);
185
184
  if (!jsonMatch) return fallback;
@@ -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") {
@@ -356,7 +356,7 @@ export class SkillGenerator {
356
356
  .replace("{CONVERSATION}", conversationText.slice(0, 12000))
357
357
  + langInstruction;
358
358
 
359
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.step1", { maxTokens: 6000, temperature: 0.2, timeoutMs: 120_000, openclawAPI: this.ctx.openclawAPI });
359
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.step1", { maxTokens: 6000, temperature: 0.2, timeoutMs: 120_000 });
360
360
 
361
361
  const trimmed = raw.trim();
362
362
  if (trimmed.startsWith("---")) return trimmed;
@@ -379,7 +379,7 @@ export class SkillGenerator {
379
379
  .replace("{CONVERSATION}", conversationText.slice(0, 6000));
380
380
 
381
381
  try {
382
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.scripts", { maxTokens: 3000, temperature: 0.1, timeoutMs: 120_000, openclawAPI: this.ctx.openclawAPI });
382
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.scripts", { maxTokens: 3000, temperature: 0.1, timeoutMs: 120_000 });
383
383
  return this.parseJSONArray<{ filename: string; content: string }>(raw);
384
384
  } catch (err) {
385
385
  this.ctx.log.warn(`SkillGenerator: script extraction failed: ${err}`);
@@ -401,7 +401,7 @@ export class SkillGenerator {
401
401
  .replace("{CONVERSATION}", conversationText.slice(0, 6000));
402
402
 
403
403
  try {
404
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.refs", { maxTokens: 3000, temperature: 0.1, timeoutMs: 120_000, openclawAPI: this.ctx.openclawAPI });
404
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.refs", { maxTokens: 3000, temperature: 0.1, timeoutMs: 120_000 });
405
405
  return this.parseJSONArray<{ filename: string; content: string }>(raw);
406
406
  } catch (err) {
407
407
  this.ctx.log.warn(`SkillGenerator: reference extraction failed: ${err}`);
@@ -423,7 +423,7 @@ export class SkillGenerator {
423
423
  + `\n\n⚠️ LANGUAGE: Write test prompts and expectations in ${lang}, matching the skill's language.\n`;
424
424
 
425
425
  try {
426
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.evals", { maxTokens: 2000, temperature: 0.3, timeoutMs: 120_000, openclawAPI: this.ctx.openclawAPI });
426
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillGenerator.evals", { maxTokens: 2000, temperature: 0.3, timeoutMs: 120_000 });
427
427
  return this.parseJSONArray(raw);
428
428
  } catch (err) {
429
429
  this.ctx.log.warn(`SkillGenerator: eval generation failed: ${err}`);
@@ -467,7 +467,6 @@ export class SkillGenerator {
467
467
  return { hitCount, results };
468
468
  }
469
469
 
470
-
471
470
  // ─── Helpers ───
472
471
 
473
472
  private parseJSONArray<T>(raw: string): T[] {
@@ -500,5 +499,4 @@ export class SkillGenerator {
500
499
  return "";
501
500
  }
502
501
 
503
-
504
502
  }
@@ -190,7 +190,7 @@ export class SkillUpgrader {
190
190
  .replace("{TASK_ID}", task.id)
191
191
  + langInstruction;
192
192
 
193
- const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillUpgrader.upgrade", { maxTokens: 6000, temperature: 0.2, timeoutMs: 90_000, openclawAPI: this.ctx.openclawAPI });
193
+ const raw = await callLLMWithFallback(chain, prompt, this.ctx.log, "SkillUpgrader.upgrade", { maxTokens: 6000, temperature: 0.2, timeoutMs: 90_000 });
194
194
 
195
195
  const changelogSep = raw.indexOf("---CHANGELOG---");
196
196
  if (changelogSep !== -1) {