@memtensor/memos-local-openclaw-plugin 0.1.4 → 0.1.5

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 (104) hide show
  1. package/README.md +196 -84
  2. package/dist/ingest/dedup.d.ts +8 -0
  3. package/dist/ingest/dedup.d.ts.map +1 -1
  4. package/dist/ingest/dedup.js +21 -0
  5. package/dist/ingest/dedup.js.map +1 -1
  6. package/dist/ingest/providers/anthropic.d.ts +14 -0
  7. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  8. package/dist/ingest/providers/anthropic.js +104 -0
  9. package/dist/ingest/providers/anthropic.js.map +1 -1
  10. package/dist/ingest/providers/bedrock.d.ts +14 -0
  11. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  12. package/dist/ingest/providers/bedrock.js +100 -0
  13. package/dist/ingest/providers/bedrock.js.map +1 -1
  14. package/dist/ingest/providers/gemini.d.ts +14 -0
  15. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  16. package/dist/ingest/providers/gemini.js +96 -0
  17. package/dist/ingest/providers/gemini.js.map +1 -1
  18. package/dist/ingest/providers/index.d.ts +22 -0
  19. package/dist/ingest/providers/index.d.ts.map +1 -1
  20. package/dist/ingest/providers/index.js +68 -0
  21. package/dist/ingest/providers/index.js.map +1 -1
  22. package/dist/ingest/providers/openai.d.ts +22 -0
  23. package/dist/ingest/providers/openai.d.ts.map +1 -1
  24. package/dist/ingest/providers/openai.js +143 -0
  25. package/dist/ingest/providers/openai.js.map +1 -1
  26. package/dist/ingest/task-processor.d.ts +2 -0
  27. package/dist/ingest/task-processor.d.ts.map +1 -1
  28. package/dist/ingest/task-processor.js +15 -0
  29. package/dist/ingest/task-processor.js.map +1 -1
  30. package/dist/ingest/worker.d.ts +2 -0
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +115 -12
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/recall/engine.d.ts.map +1 -1
  35. package/dist/recall/engine.js +1 -0
  36. package/dist/recall/engine.js.map +1 -1
  37. package/dist/skill/bundled-memory-guide.d.ts +6 -0
  38. package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
  39. package/dist/skill/bundled-memory-guide.js +95 -0
  40. package/dist/skill/bundled-memory-guide.js.map +1 -0
  41. package/dist/skill/evaluator.d.ts +31 -0
  42. package/dist/skill/evaluator.d.ts.map +1 -0
  43. package/dist/skill/evaluator.js +194 -0
  44. package/dist/skill/evaluator.js.map +1 -0
  45. package/dist/skill/evolver.d.ts +22 -0
  46. package/dist/skill/evolver.d.ts.map +1 -0
  47. package/dist/skill/evolver.js +193 -0
  48. package/dist/skill/evolver.js.map +1 -0
  49. package/dist/skill/generator.d.ts +25 -0
  50. package/dist/skill/generator.d.ts.map +1 -0
  51. package/dist/skill/generator.js +477 -0
  52. package/dist/skill/generator.js.map +1 -0
  53. package/dist/skill/installer.d.ts +16 -0
  54. package/dist/skill/installer.d.ts.map +1 -0
  55. package/dist/skill/installer.js +89 -0
  56. package/dist/skill/installer.js.map +1 -0
  57. package/dist/skill/upgrader.d.ts +19 -0
  58. package/dist/skill/upgrader.d.ts.map +1 -0
  59. package/dist/skill/upgrader.js +263 -0
  60. package/dist/skill/upgrader.js.map +1 -0
  61. package/dist/skill/validator.d.ts +29 -0
  62. package/dist/skill/validator.d.ts.map +1 -0
  63. package/dist/skill/validator.js +227 -0
  64. package/dist/skill/validator.js.map +1 -0
  65. package/dist/storage/sqlite.d.ts +75 -1
  66. package/dist/storage/sqlite.d.ts.map +1 -1
  67. package/dist/storage/sqlite.js +417 -6
  68. package/dist/storage/sqlite.js.map +1 -1
  69. package/dist/types.d.ts +78 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js +6 -0
  72. package/dist/types.js.map +1 -1
  73. package/dist/viewer/html.d.ts +1 -1
  74. package/dist/viewer/html.d.ts.map +1 -1
  75. package/dist/viewer/html.js +1549 -113
  76. package/dist/viewer/html.js.map +1 -1
  77. package/dist/viewer/server.d.ts +13 -0
  78. package/dist/viewer/server.d.ts.map +1 -1
  79. package/dist/viewer/server.js +289 -4
  80. package/dist/viewer/server.js.map +1 -1
  81. package/index.ts +489 -181
  82. package/package.json +1 -1
  83. package/skill/memos-memory-guide/SKILL.md +86 -0
  84. package/src/ingest/dedup.ts +29 -0
  85. package/src/ingest/providers/anthropic.ts +130 -0
  86. package/src/ingest/providers/bedrock.ts +126 -0
  87. package/src/ingest/providers/gemini.ts +124 -0
  88. package/src/ingest/providers/index.ts +86 -4
  89. package/src/ingest/providers/openai.ts +174 -0
  90. package/src/ingest/task-processor.ts +16 -0
  91. package/src/ingest/worker.ts +126 -21
  92. package/src/recall/engine.ts +1 -0
  93. package/src/skill/bundled-memory-guide.ts +91 -0
  94. package/src/skill/evaluator.ts +220 -0
  95. package/src/skill/evolver.ts +169 -0
  96. package/src/skill/generator.ts +506 -0
  97. package/src/skill/installer.ts +59 -0
  98. package/src/skill/upgrader.ts +257 -0
  99. package/src/skill/validator.ts +227 -0
  100. package/src/storage/sqlite.ts +508 -6
  101. package/src/types.ts +77 -0
  102. package/src/viewer/html.ts +1549 -113
  103. package/src/viewer/server.ts +285 -4
  104. package/skill/SKILL.md +0 -59
@@ -0,0 +1,220 @@
1
+ import type { Chunk, Task, Skill, PluginContext, SummarizerConfig } from "../types";
2
+ import { DEFAULTS } from "../types";
3
+
4
+ export interface CreateEvalResult {
5
+ shouldGenerate: boolean;
6
+ reason: string;
7
+ suggestedName: string;
8
+ suggestedTags: string[];
9
+ confidence: number;
10
+ }
11
+
12
+ export interface UpgradeEvalResult {
13
+ shouldUpgrade: boolean;
14
+ upgradeType: "refine" | "extend" | "fix";
15
+ dimensions: string[];
16
+ reason: string;
17
+ mergeStrategy: string;
18
+ confidence: number;
19
+ }
20
+
21
+ const CREATE_EVAL_PROMPT = `You are an experience evaluation expert. Based on the completed task record below, decide whether this task contains reusable experience worth distilling into a "skill".
22
+
23
+ A skill is a reusable guide that helps an AI agent handle similar tasks better in the future.
24
+
25
+ Worth distilling (any ONE qualifies):
26
+ - Contains concrete steps, commands, code, or configuration
27
+ - Solves a recurring problem with a specific approach/workflow
28
+ - Went through trial-and-error (wrong approach then corrected)
29
+ - Involves non-obvious usage of specific tools, APIs, or frameworks
30
+ - Contains debugging/troubleshooting with diagnostic reasoning
31
+ - Demonstrates a multi-step workflow using external tools (browser, search, file system, etc.)
32
+ - Reveals user preferences or style requirements that should be remembered
33
+ - Shows how to combine multiple tools/services to accomplish a goal
34
+ - Contains a process that required specific parameter tuning or configuration
35
+
36
+ NOT worth distilling:
37
+ - Pure factual Q&A with no process ("what is TCP", "what's the capital of France")
38
+ - Single-turn simple answers with no workflow
39
+ - Conversation too fragmented or incoherent to extract a clear process
40
+
41
+ Task title: {TITLE}
42
+ Task summary:
43
+ {SUMMARY}
44
+
45
+ Reply in JSON only, no extra text:
46
+ {
47
+ "shouldGenerate": boolean,
48
+ "reason": "brief explanation",
49
+ "suggestedName": "kebab-case-name",
50
+ "suggestedTags": ["tag1", "tag2"],
51
+ "confidence": 0.0-1.0
52
+ }`;
53
+
54
+ const UPGRADE_EVAL_PROMPT = `You are a skill upgrade evaluation expert.
55
+
56
+ Existing skill (v{VERSION}):
57
+ Name: {SKILL_NAME}
58
+ Content:
59
+ {SKILL_CONTENT}
60
+
61
+ Newly completed task:
62
+ Title: {TITLE}
63
+ Summary:
64
+ {SUMMARY}
65
+
66
+ Does the new task bring substantive improvements to the existing skill?
67
+
68
+ Worth upgrading (any one qualifies):
69
+ 1. Faster — shorter path discovered
70
+ 2. More elegant — cleaner, follows best practices better
71
+ 3. More convenient — fewer dependencies or complexity
72
+ 4. Fewer tokens — less exploration/trial-and-error needed
73
+ 5. More accurate — corrects wrong parameters/steps in old skill
74
+ 6. More robust — adds edge cases, error handling
75
+ 7. New scenario — covers a variant the old skill didn't
76
+ 8. Fixes outdated info — old skill has stale information
77
+
78
+ NOT worth upgrading:
79
+ - New task is identical to existing skill
80
+ - New task's approach is worse than existing skill
81
+ - Differences are trivial
82
+
83
+ Reply in JSON only, no extra text:
84
+ {
85
+ "shouldUpgrade": boolean,
86
+ "upgradeType": "refine" | "extend" | "fix",
87
+ "dimensions": ["faster", "more_elegant", "more_convenient", "fewer_tokens", "more_accurate", "more_robust", "new_scenario", "fix_outdated"],
88
+ "reason": "what new value the task brings",
89
+ "mergeStrategy": "which specific parts need updating",
90
+ "confidence": 0.0-1.0
91
+ }`;
92
+
93
+ export class SkillEvaluator {
94
+ constructor(private ctx: PluginContext) {}
95
+
96
+ passesRuleFilter(chunks: Chunk[], task: Task): { pass: boolean; skipReason: string } {
97
+ const minChunks = this.ctx.config.skillEvolution?.minChunksForEval ?? DEFAULTS.skillMinChunksForEval;
98
+ if (chunks.length < minChunks) {
99
+ return { pass: false, skipReason: `chunks不足 (${chunks.length} < ${minChunks})` };
100
+ }
101
+
102
+ if (task.status === "skipped") {
103
+ return { pass: false, skipReason: "task状态为skipped" };
104
+ }
105
+
106
+ if (task.summary.length < 100) {
107
+ return { pass: false, skipReason: `summary过短 (${task.summary.length} < 100)` };
108
+ }
109
+
110
+ const userChunks = chunks.filter(c => c.role === "user");
111
+ if (userChunks.length === 0) {
112
+ return { pass: false, skipReason: "无用户消息" };
113
+ }
114
+
115
+ const assistantChunks = chunks.filter(c => c.role === "assistant");
116
+ if (assistantChunks.length === 0) {
117
+ return { pass: false, skipReason: "无助手回复" };
118
+ }
119
+
120
+ return { pass: true, skipReason: "" };
121
+ }
122
+
123
+ async evaluateCreate(task: Task): Promise<CreateEvalResult> {
124
+ const cfg = this.getProviderConfig();
125
+ if (!cfg) {
126
+ return { shouldGenerate: false, reason: "no LLM configured", suggestedName: "", suggestedTags: [], confidence: 0 };
127
+ }
128
+
129
+ const prompt = CREATE_EVAL_PROMPT
130
+ .replace("{TITLE}", task.title)
131
+ .replace("{SUMMARY}", task.summary.slice(0, 3000));
132
+
133
+ try {
134
+ const raw = await this.callLLM(cfg, prompt);
135
+ return this.parseJSON<CreateEvalResult>(raw, {
136
+ shouldGenerate: false, reason: "parse failed", suggestedName: "", suggestedTags: [], confidence: 0,
137
+ });
138
+ } catch (err) {
139
+ this.ctx.log.warn(`SkillEvaluator.evaluateCreate failed: ${err}`);
140
+ return { shouldGenerate: false, reason: `error: ${err}`, suggestedName: "", suggestedTags: [], confidence: 0 };
141
+ }
142
+ }
143
+
144
+ async evaluateUpgrade(task: Task, skill: Skill, skillContent: string): Promise<UpgradeEvalResult> {
145
+ const cfg = this.getProviderConfig();
146
+ if (!cfg) {
147
+ return { shouldUpgrade: false, upgradeType: "refine", dimensions: [], reason: "no LLM configured", mergeStrategy: "", confidence: 0 };
148
+ }
149
+
150
+ const prompt = UPGRADE_EVAL_PROMPT
151
+ .replace("{VERSION}", String(skill.version))
152
+ .replace("{SKILL_NAME}", skill.name)
153
+ .replace("{SKILL_CONTENT}", skillContent.slice(0, 4000))
154
+ .replace("{TITLE}", task.title)
155
+ .replace("{SUMMARY}", task.summary.slice(0, 3000));
156
+
157
+ try {
158
+ const raw = await this.callLLM(cfg, prompt);
159
+ return this.parseJSON<UpgradeEvalResult>(raw, {
160
+ shouldUpgrade: false, upgradeType: "refine", dimensions: [], reason: "parse failed", mergeStrategy: "", confidence: 0,
161
+ });
162
+ } catch (err) {
163
+ this.ctx.log.warn(`SkillEvaluator.evaluateUpgrade failed: ${err}`);
164
+ return { shouldUpgrade: false, upgradeType: "refine", dimensions: [], reason: `error: ${err}`, mergeStrategy: "", confidence: 0 };
165
+ }
166
+ }
167
+
168
+ private getProviderConfig(): SummarizerConfig | undefined {
169
+ return this.ctx.config.skillEvolution?.summarizer ?? this.ctx.config.summarizer;
170
+ }
171
+
172
+ private async callLLM(cfg: SummarizerConfig, userContent: string): Promise<string> {
173
+ const endpoint = this.normalizeEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
174
+ const model = cfg.model ?? "gpt-4o-mini";
175
+ const headers: Record<string, string> = {
176
+ "Content-Type": "application/json",
177
+ Authorization: `Bearer ${cfg.apiKey}`,
178
+ ...cfg.headers,
179
+ };
180
+
181
+ const resp = await fetch(endpoint, {
182
+ method: "POST",
183
+ headers,
184
+ body: JSON.stringify({
185
+ model,
186
+ temperature: cfg.temperature ?? 0.1,
187
+ max_tokens: 1024,
188
+ messages: [
189
+ { role: "user", content: userContent },
190
+ ],
191
+ }),
192
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
193
+ });
194
+
195
+ if (!resp.ok) {
196
+ const body = await resp.text();
197
+ throw new Error(`LLM call failed (${resp.status}): ${body}`);
198
+ }
199
+
200
+ const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
201
+ return json.choices[0]?.message?.content?.trim() ?? "";
202
+ }
203
+
204
+ private parseJSON<T>(raw: string, fallback: T): T {
205
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
206
+ if (!jsonMatch) return fallback;
207
+ try {
208
+ return JSON.parse(jsonMatch[0]) as T;
209
+ } catch {
210
+ return fallback;
211
+ }
212
+ }
213
+
214
+ private normalizeEndpoint(url: string): string {
215
+ const stripped = url.replace(/\/+$/, "");
216
+ if (stripped.endsWith("/chat/completions")) return stripped;
217
+ if (stripped.endsWith("/completions")) return stripped;
218
+ return `${stripped}/chat/completions`;
219
+ }
220
+ }
@@ -0,0 +1,169 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { SqliteStore } from "../storage/sqlite";
4
+ import type { RecallEngine } from "../recall/engine";
5
+ import type { Task, Skill, Chunk, PluginContext } from "../types";
6
+ import { DEFAULTS } from "../types";
7
+ import { SkillEvaluator } from "./evaluator";
8
+ import { SkillGenerator } from "./generator";
9
+ import { SkillUpgrader } from "./upgrader";
10
+ import { SkillInstaller } from "./installer";
11
+
12
+ export class SkillEvolver {
13
+ private evaluator: SkillEvaluator;
14
+ private generator: SkillGenerator;
15
+ private upgrader: SkillUpgrader;
16
+ private installer: SkillInstaller;
17
+ private processing = false;
18
+
19
+ constructor(
20
+ private store: SqliteStore,
21
+ private engine: RecallEngine,
22
+ private ctx: PluginContext,
23
+ ) {
24
+ this.evaluator = new SkillEvaluator(ctx);
25
+ this.generator = new SkillGenerator(store, engine, ctx);
26
+ this.upgrader = new SkillUpgrader(store, ctx);
27
+ this.installer = new SkillInstaller(store, ctx);
28
+ }
29
+
30
+ async onTaskCompleted(task: Task): Promise<void> {
31
+ const enabled = this.ctx.config.skillEvolution?.enabled ?? DEFAULTS.skillEvolutionEnabled;
32
+ const autoEval = this.ctx.config.skillEvolution?.autoEvaluate ?? DEFAULTS.skillAutoEvaluate;
33
+ if (!enabled || !autoEval) return;
34
+
35
+ if (this.processing) {
36
+ this.ctx.log.debug("SkillEvolver: already processing, skipping");
37
+ return;
38
+ }
39
+ this.processing = true;
40
+ try {
41
+ await this.process(task);
42
+ } catch (err) {
43
+ this.ctx.log.error(`SkillEvolver error: ${err}`);
44
+ } finally {
45
+ this.processing = false;
46
+ }
47
+ }
48
+
49
+ private async process(task: Task): Promise<void> {
50
+ const chunks = this.store.getChunksByTask(task.id);
51
+
52
+ const { pass, skipReason } = this.evaluator.passesRuleFilter(chunks, task);
53
+ if (!pass) {
54
+ this.ctx.log.debug(`SkillEvolver: task ${task.id} skipped by rule filter: ${skipReason} (chunks=${chunks.length})`);
55
+ this.store.setTaskSkillMeta(task.id, { skillStatus: "skipped", skillReason: skipReason });
56
+ return;
57
+ }
58
+
59
+ const relatedSkill = await this.findRelatedSkill(task);
60
+
61
+ if (relatedSkill) {
62
+ await this.handleExistingSkill(task, chunks, relatedSkill);
63
+ } else {
64
+ await this.handleNewSkill(task, chunks);
65
+ }
66
+ }
67
+
68
+ private async findRelatedSkill(task: Task): Promise<Skill | null> {
69
+ try {
70
+ const result = await this.engine.search({
71
+ query: task.summary.slice(0, 500),
72
+ maxResults: 10,
73
+ minScore: 0.5,
74
+ });
75
+
76
+ for (const hit of result.hits) {
77
+ if (hit.skillId) {
78
+ const skill = this.store.getSkill(hit.skillId);
79
+ if (skill && (skill.status === "active" || skill.status === "draft")) {
80
+ this.ctx.log.debug(`SkillEvolver: found related skill "${skill.name}" via memory search`);
81
+ return skill;
82
+ }
83
+ }
84
+ }
85
+ } catch (err) {
86
+ this.ctx.log.warn(`SkillEvolver: memory search for related skill failed: ${err}`);
87
+ }
88
+ return null;
89
+ }
90
+
91
+ private async handleExistingSkill(task: Task, chunks: Chunk[], skill: Skill): Promise<void> {
92
+ const skillContent = this.readSkillContent(skill);
93
+ if (!skillContent) {
94
+ this.ctx.log.warn(`SkillEvolver: cannot read skill "${skill.name}" content, treating as new`);
95
+ await this.handleNewSkill(task, chunks);
96
+ return;
97
+ }
98
+
99
+ const minConfidence = this.ctx.config.skillEvolution?.minConfidence ?? DEFAULTS.skillMinConfidence;
100
+ const evalResult = await this.evaluator.evaluateUpgrade(task, skill, skillContent);
101
+
102
+ if (evalResult.shouldUpgrade && evalResult.confidence >= minConfidence) {
103
+ this.ctx.log.info(`SkillEvolver: upgrading skill "${skill.name}" — ${evalResult.reason}`);
104
+ const { upgraded } = await this.upgrader.upgrade(task, skill, evalResult);
105
+
106
+ this.markChunksWithSkill(chunks, skill.id);
107
+
108
+ if (upgraded) {
109
+ this.store.linkTaskSkill(task.id, skill.id, "evolved_from", skill.version + 1);
110
+ this.installer.syncIfInstalled(skill.name);
111
+ } else {
112
+ this.store.linkTaskSkill(task.id, skill.id, "applied_to", skill.version);
113
+ }
114
+ } else if (evalResult.confidence < 0.3) {
115
+ // Low confidence means the matched skill is likely unrelated — try creating a new one
116
+ this.ctx.log.info(
117
+ `SkillEvolver: skill "${skill.name}" has low relevance (confidence=${evalResult.confidence}), ` +
118
+ `falling back to new skill evaluation for task "${task.title}"`,
119
+ );
120
+ await this.handleNewSkill(task, chunks);
121
+ } else {
122
+ this.ctx.log.debug(`SkillEvolver: skill "${skill.name}" not worth upgrading (confidence=${evalResult.confidence})`);
123
+ this.markChunksWithSkill(chunks, skill.id);
124
+ this.store.linkTaskSkill(task.id, skill.id, "applied_to", skill.version);
125
+ }
126
+ }
127
+
128
+ private async handleNewSkill(task: Task, chunks: Chunk[]): Promise<void> {
129
+ const minConfidence = this.ctx.config.skillEvolution?.minConfidence ?? DEFAULTS.skillMinConfidence;
130
+ const evalResult = await this.evaluator.evaluateCreate(task);
131
+
132
+ if (evalResult.shouldGenerate && evalResult.confidence >= minConfidence) {
133
+ this.ctx.log.info(`SkillEvolver: generating new skill "${evalResult.suggestedName}" — ${evalResult.reason}`);
134
+ this.store.setTaskSkillMeta(task.id, { skillStatus: "generating", skillReason: evalResult.reason });
135
+
136
+ const skill = await this.generator.generate(task, chunks, evalResult);
137
+ this.markChunksWithSkill(chunks, skill.id);
138
+ this.store.linkTaskSkill(task.id, skill.id, "generated_from", 1);
139
+ this.store.setTaskSkillMeta(task.id, { skillStatus: "generated", skillReason: evalResult.reason });
140
+
141
+ const autoInstall = this.ctx.config.skillEvolution?.autoInstall ?? DEFAULTS.skillAutoInstall;
142
+ if (autoInstall && skill.status === "active") {
143
+ this.installer.install(skill.id);
144
+ }
145
+ } else {
146
+ const reason = evalResult.reason || `confidence不足 (${evalResult.confidence} < ${minConfidence})`;
147
+ this.ctx.log.debug(`SkillEvolver: task "${task.title}" not worth generating skill — ${reason}`);
148
+ this.store.setTaskSkillMeta(task.id, { skillStatus: "not_generated", skillReason: reason });
149
+ }
150
+ }
151
+
152
+ private markChunksWithSkill(chunks: Chunk[], skillId: string): void {
153
+ for (const chunk of chunks) {
154
+ this.store.setChunkSkillId(chunk.id, skillId);
155
+ }
156
+ this.ctx.log.debug(`SkillEvolver: marked ${chunks.length} chunks with skill_id=${skillId}`);
157
+ }
158
+
159
+ private readSkillContent(skill: Skill): string | null {
160
+ const filePath = path.join(skill.dirPath, "SKILL.md");
161
+ try {
162
+ if (fs.existsSync(filePath)) {
163
+ return fs.readFileSync(filePath, "utf-8");
164
+ }
165
+ } catch { /* fall through */ }
166
+ const sv = this.store.getLatestSkillVersion(skill.id);
167
+ return sv?.content ?? null;
168
+ }
169
+ }