@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.
- package/.env.example +7 -0
- package/README.md +111 -44
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +36 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +6 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +160 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -3
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +9 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +500 -112
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +84 -9
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +115 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +458 -110
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2952 -910
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +39 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1198 -227
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +774 -74
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +40 -1
- package/src/client/connector.ts +161 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +2 -3
- package/src/hub/server.ts +481 -107
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +99 -10
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +498 -137
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2952 -910
- package/src/viewer/server.ts +1109 -212
package/src/skill/evolver.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { DEFAULTS } from "../types";
|
|
|
9
9
|
import { SkillEvaluator } from "./evaluator";
|
|
10
10
|
import { SkillGenerator } from "./generator";
|
|
11
11
|
import { SkillUpgrader } from "./upgrader";
|
|
12
|
-
import { SkillInstaller } from "./installer";
|
|
12
|
+
import { SkillInstaller, type SkillInstallMode } 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,
|
|
@@ -93,10 +96,19 @@ export class SkillEvolver {
|
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
const preferUpgrade = this.ctx.config.skillEvolution?.preferUpgradeExisting ?? DEFAULTS.skillPreferUpgrade;
|
|
96
100
|
const relatedSkill = await this.findRelatedSkill(task);
|
|
97
101
|
|
|
98
102
|
if (relatedSkill) {
|
|
99
103
|
await this.handleExistingSkill(task, chunks, relatedSkill);
|
|
104
|
+
} else if (preferUpgrade) {
|
|
105
|
+
const nameCandidate = await this.findSkillByNameSimilarity(task);
|
|
106
|
+
if (nameCandidate) {
|
|
107
|
+
this.ctx.log.info(`SkillEvolver: preferUpgrade found name-similar skill "${nameCandidate.name}" for task "${task.title}"`);
|
|
108
|
+
await this.handleExistingSkill(task, chunks, nameCandidate);
|
|
109
|
+
} else {
|
|
110
|
+
await this.handleNewSkill(task, chunks);
|
|
111
|
+
}
|
|
100
112
|
} else {
|
|
101
113
|
await this.handleNewSkill(task, chunks);
|
|
102
114
|
}
|
|
@@ -278,7 +290,12 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
278
290
|
|
|
279
291
|
if (upgraded) {
|
|
280
292
|
this.store.linkTaskSkill(task.id, freshSkill.id, "evolved_from", freshSkill.version + 1);
|
|
281
|
-
|
|
293
|
+
if (freshSkill.installed) {
|
|
294
|
+
this.installer.syncIfInstalled(freshSkill.name);
|
|
295
|
+
} else {
|
|
296
|
+
this.autoInstallIfNeeded(freshSkill);
|
|
297
|
+
}
|
|
298
|
+
this.onSkillEvolved?.(freshSkill.name, "upgraded");
|
|
282
299
|
} else {
|
|
283
300
|
this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
|
|
284
301
|
}
|
|
@@ -300,6 +317,13 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
300
317
|
const evalResult = await this.evaluator.evaluateCreate(task);
|
|
301
318
|
|
|
302
319
|
if (evalResult.shouldGenerate && evalResult.confidence >= minConfidence) {
|
|
320
|
+
const existingByName = this.store.getSkillByName(evalResult.suggestedName);
|
|
321
|
+
if (existingByName && (existingByName.status === "active" || existingByName.status === "draft")) {
|
|
322
|
+
this.ctx.log.info(`SkillEvolver: skill "${evalResult.suggestedName}" already exists, redirecting to upgrade instead of create`);
|
|
323
|
+
await this.handleExistingSkill(task, chunks, existingByName);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
303
327
|
this.ctx.log.info(`SkillEvolver: generating new skill "${evalResult.suggestedName}" — ${evalResult.reason}`);
|
|
304
328
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generating", skillReason: evalResult.reason });
|
|
305
329
|
|
|
@@ -307,11 +331,9 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
307
331
|
this.markChunksWithSkill(chunks, skill.id);
|
|
308
332
|
this.store.linkTaskSkill(task.id, skill.id, "generated_from", 1);
|
|
309
333
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generated", skillReason: evalResult.reason });
|
|
334
|
+
this.onSkillEvolved?.(skill.name, "created");
|
|
310
335
|
|
|
311
|
-
|
|
312
|
-
if (autoInstall && skill.status === "active") {
|
|
313
|
-
this.installer.install(skill.id);
|
|
314
|
-
}
|
|
336
|
+
this.autoInstallIfNeeded(skill);
|
|
315
337
|
} else {
|
|
316
338
|
const reason = evalResult.reason || `confidence不足 (${evalResult.confidence} < ${minConfidence})`;
|
|
317
339
|
this.ctx.log.debug(`SkillEvolver: task "${task.title}" not worth generating skill — ${reason}`);
|
|
@@ -326,6 +348,41 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
326
348
|
this.ctx.log.debug(`SkillEvolver: marked ${chunks.length} chunks with skill_id=${skillId}`);
|
|
327
349
|
}
|
|
328
350
|
|
|
351
|
+
private async findSkillByNameSimilarity(task: Task): Promise<Skill | null> {
|
|
352
|
+
const query = task.title.slice(0, 200);
|
|
353
|
+
const owner = task.owner ?? "agent:main";
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const ftsHits = this.store.skillFtsSearch(query, 5, "mix", owner);
|
|
357
|
+
for (const hit of ftsHits) {
|
|
358
|
+
if (hit.score < 0.5) continue;
|
|
359
|
+
const skill = this.store.getSkill(hit.skillId);
|
|
360
|
+
if (skill && (skill.status === "active" || skill.status === "draft")) {
|
|
361
|
+
return skill;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch { /* best-effort */ }
|
|
365
|
+
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private autoInstallIfNeeded(skill: Skill): void {
|
|
370
|
+
if (skill.status !== "active") return;
|
|
371
|
+
|
|
372
|
+
const explicitAutoInstall = this.ctx.config.skillEvolution?.autoInstall ?? DEFAULTS.skillAutoInstall;
|
|
373
|
+
if (explicitAutoInstall) {
|
|
374
|
+
this.installer.install(skill.id);
|
|
375
|
+
this.ctx.log.info(`SkillEvolver: auto-installed "${skill.name}" (explicit autoInstall=true)`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const manifest = SkillInstaller.buildManifest(skill.dirPath, !!skill.installed, skill.name);
|
|
380
|
+
if (manifest.installMode === "install_recommended") {
|
|
381
|
+
this.installer.install(skill.id);
|
|
382
|
+
this.ctx.log.info(`SkillEvolver: auto-installed "${skill.name}" (install_recommended: ${manifest.scriptsCount} scripts, ${Math.round(manifest.totalSize / 1024)}KB)`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
329
386
|
private readSkillContent(skill: Skill): string | null {
|
|
330
387
|
const filePath = path.join(skill.dirPath, "SKILL.md");
|
|
331
388
|
try {
|
package/src/skill/generator.ts
CHANGED
|
@@ -484,14 +484,55 @@ export class SkillGenerator {
|
|
|
484
484
|
|
|
485
485
|
private buildConversationText(chunks: Chunk[]): string {
|
|
486
486
|
const lines: string[] = [];
|
|
487
|
+
const redact = this.ctx.config.skillEvolution?.redactSensitiveInSkill ?? true;
|
|
488
|
+
|
|
487
489
|
for (const c of chunks) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
490
|
+
let roleLabel: string;
|
|
491
|
+
switch (c.role) {
|
|
492
|
+
case "user": roleLabel = "User"; break;
|
|
493
|
+
case "assistant": roleLabel = "Assistant"; break;
|
|
494
|
+
case "tool": roleLabel = "Tool"; break;
|
|
495
|
+
case "system": roleLabel = "System"; break;
|
|
496
|
+
default: continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let content = c.content;
|
|
500
|
+
if (c.role === "system") continue;
|
|
501
|
+
|
|
502
|
+
if (c.role === "tool") {
|
|
503
|
+
content = this.truncateToolOutput(content);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (redact) {
|
|
507
|
+
content = SkillGenerator.redactSensitive(content);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
lines.push(`[${roleLabel}]: ${content}`);
|
|
491
511
|
}
|
|
492
512
|
return lines.join("\n\n");
|
|
493
513
|
}
|
|
494
514
|
|
|
515
|
+
private truncateToolOutput(content: string): string {
|
|
516
|
+
const MAX_TOOL_OUTPUT = 1500;
|
|
517
|
+
if (content.length <= MAX_TOOL_OUTPUT) return content;
|
|
518
|
+
const head = content.slice(0, MAX_TOOL_OUTPUT * 0.6);
|
|
519
|
+
const tail = content.slice(-MAX_TOOL_OUTPUT * 0.3);
|
|
520
|
+
return `${head}\n... (truncated ${content.length - MAX_TOOL_OUTPUT} chars) ...\n${tail}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
static redactSensitive(text: string): string {
|
|
524
|
+
let result = text;
|
|
525
|
+
result = result.replace(/\bsk-[a-zA-Z0-9]{20,}\b/g, "sk-***REDACTED***");
|
|
526
|
+
result = result.replace(/\bBearer\s+[a-zA-Z0-9_\-.]{20,}\b/g, "Bearer ***REDACTED***");
|
|
527
|
+
result = result.replace(/\bAKIA[0-9A-Z]{16}\b/g, "AKIA***REDACTED***");
|
|
528
|
+
result = result.replace(/(api[_-]?key|secret|token|password|credential)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
529
|
+
(match, key) => `${key}="***REDACTED***"`);
|
|
530
|
+
result = result.replace(/\/Users\/[a-zA-Z0-9._-]+\//g, "/Users/****/");
|
|
531
|
+
result = result.replace(/\/home\/[a-zA-Z0-9._-]+\//g, "/home/****/");
|
|
532
|
+
result = result.replace(/C:\\Users\\[a-zA-Z0-9._-]+\\/g, "C:\\Users\\****\\");
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
|
|
495
536
|
private parseDescription(content: string): string {
|
|
496
537
|
const match = content.match(/description:\s*"([^"]+)"/);
|
|
497
538
|
if (match) return match[1];
|
|
@@ -499,6 +540,4 @@ export class SkillGenerator {
|
|
|
499
540
|
if (match2) return match2[1];
|
|
500
541
|
return "";
|
|
501
542
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
543
|
}
|
package/src/skill/installer.ts
CHANGED
|
@@ -3,6 +3,26 @@ import * as path from "path";
|
|
|
3
3
|
import type { SqliteStore } from "../storage/sqlite";
|
|
4
4
|
import type { PluginContext } from "../types";
|
|
5
5
|
|
|
6
|
+
export type SkillInstallMode = "inline" | "on_demand" | "install_recommended";
|
|
7
|
+
|
|
8
|
+
export interface CompanionFileInfo {
|
|
9
|
+
relativePath: string;
|
|
10
|
+
size: number;
|
|
11
|
+
type: "script" | "reference" | "eval" | "other";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SkillCompanionManifest {
|
|
15
|
+
hasCompanionFiles: boolean;
|
|
16
|
+
installMode: SkillInstallMode;
|
|
17
|
+
installed: boolean;
|
|
18
|
+
installedPath?: string;
|
|
19
|
+
files: CompanionFileInfo[];
|
|
20
|
+
totalSize: number;
|
|
21
|
+
scriptsCount: number;
|
|
22
|
+
referencesCount: number;
|
|
23
|
+
evalsCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
export class SkillInstaller {
|
|
7
27
|
private workspaceSkillsDir: string;
|
|
8
28
|
|
|
@@ -13,6 +33,82 @@ export class SkillInstaller {
|
|
|
13
33
|
this.workspaceSkillsDir = path.join(ctx.workspaceDir, "skills");
|
|
14
34
|
}
|
|
15
35
|
|
|
36
|
+
getCompanionManifest(skillId: string): SkillCompanionManifest | null {
|
|
37
|
+
const skill = this.store.getSkill(skillId);
|
|
38
|
+
if (!skill) return null;
|
|
39
|
+
return SkillInstaller.buildManifest(skill.dirPath, !!skill.installed, skill.name, this.workspaceSkillsDir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static buildManifest(dirPath: string, installed: boolean, skillName: string, workspaceSkillsDir?: string): SkillCompanionManifest {
|
|
43
|
+
const files: CompanionFileInfo[] = [];
|
|
44
|
+
|
|
45
|
+
const scanDir = (subDir: string, type: CompanionFileInfo["type"]) => {
|
|
46
|
+
const fullDir = path.join(dirPath, subDir);
|
|
47
|
+
if (!fs.existsSync(fullDir)) return;
|
|
48
|
+
try {
|
|
49
|
+
for (const f of fs.readdirSync(fullDir)) {
|
|
50
|
+
const fp = path.join(fullDir, f);
|
|
51
|
+
try {
|
|
52
|
+
const stat = fs.statSync(fp);
|
|
53
|
+
if (stat.isFile()) {
|
|
54
|
+
files.push({ relativePath: `${subDir}/${f}`, size: stat.size, type });
|
|
55
|
+
}
|
|
56
|
+
} catch { /* best-effort */ }
|
|
57
|
+
}
|
|
58
|
+
} catch { /* best-effort */ }
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
scanDir("scripts", "script");
|
|
62
|
+
scanDir("references", "reference");
|
|
63
|
+
scanDir("evals", "eval");
|
|
64
|
+
|
|
65
|
+
const scriptsCount = files.filter(f => f.type === "script").length;
|
|
66
|
+
const referencesCount = files.filter(f => f.type === "reference").length;
|
|
67
|
+
const evalsCount = files.filter(f => f.type === "eval").length;
|
|
68
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
69
|
+
const hasCompanionFiles = files.filter(f => f.type !== "eval").length > 0;
|
|
70
|
+
|
|
71
|
+
let installMode: SkillInstallMode = "inline";
|
|
72
|
+
if (hasCompanionFiles) {
|
|
73
|
+
const executableScripts = files.filter(f => f.type === "script");
|
|
74
|
+
const largeFiles = files.filter(f => f.size > 5000);
|
|
75
|
+
if (executableScripts.length >= 3 || largeFiles.length >= 2 || totalSize > 20000) {
|
|
76
|
+
installMode = "install_recommended";
|
|
77
|
+
} else {
|
|
78
|
+
installMode = "on_demand";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const installedPath = installed && workspaceSkillsDir
|
|
83
|
+
? path.join(workspaceSkillsDir, skillName)
|
|
84
|
+
: undefined;
|
|
85
|
+
|
|
86
|
+
return { hasCompanionFiles, installMode, installed, installedPath, files, totalSize, scriptsCount, referencesCount, evalsCount };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
readCompanionFile(skillId: string, relativePath: string): { content: string; size: number } | { error: string } {
|
|
90
|
+
const skill = this.store.getSkill(skillId);
|
|
91
|
+
if (!skill) return { error: "Skill not found" };
|
|
92
|
+
|
|
93
|
+
const normalized = relativePath.replace(/\.\./g, "");
|
|
94
|
+
const fullPath = path.join(skill.dirPath, normalized);
|
|
95
|
+
|
|
96
|
+
if (!fullPath.startsWith(skill.dirPath)) {
|
|
97
|
+
return { error: "Path traversal not allowed" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(fullPath)) {
|
|
101
|
+
return { error: `File not found: ${relativePath}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
106
|
+
return { content, size: content.length };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { error: `Cannot read file: ${err}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
16
112
|
install(skillId: string): { installed: boolean; path: string; message: string } {
|
|
17
113
|
const skill = this.store.getSkill(skillId);
|
|
18
114
|
if (!skill) return { installed: false, path: "", message: "Skill not found" };
|
|
@@ -22,8 +118,7 @@ export class SkillInstaller {
|
|
|
22
118
|
}
|
|
23
119
|
|
|
24
120
|
const dstDir = path.join(this.workspaceSkillsDir, skill.name);
|
|
25
|
-
|
|
26
|
-
fs.cpSync(skill.dirPath, dstDir, { recursive: true });
|
|
121
|
+
this.cleanSync(skill.dirPath, dstDir);
|
|
27
122
|
this.store.updateSkill(skillId, { installed: 1 });
|
|
28
123
|
|
|
29
124
|
this.ctx.log.info(`Skill installed: "${skill.name}" v${skill.version} → ${dstDir}`);
|
|
@@ -51,9 +146,17 @@ export class SkillInstaller {
|
|
|
51
146
|
if (!skill || !skill.installed) return;
|
|
52
147
|
|
|
53
148
|
const dstDir = path.join(this.workspaceSkillsDir, skill.name);
|
|
54
|
-
if (fs.existsSync(
|
|
55
|
-
|
|
149
|
+
if (fs.existsSync(skill.dirPath)) {
|
|
150
|
+
this.cleanSync(skill.dirPath, dstDir);
|
|
56
151
|
this.ctx.log.info(`Skill synced: "${skill.name}" v${skill.version} → workspace`);
|
|
57
152
|
}
|
|
58
153
|
}
|
|
154
|
+
|
|
155
|
+
private cleanSync(srcDir: string, dstDir: string): void {
|
|
156
|
+
if (fs.existsSync(dstDir)) {
|
|
157
|
+
fs.rmSync(dstDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
160
|
+
fs.cpSync(srcDir, dstDir, { recursive: true });
|
|
161
|
+
}
|
|
59
162
|
}
|
package/src/skill/upgrader.ts
CHANGED
|
@@ -91,18 +91,30 @@ export class SkillUpgrader {
|
|
|
91
91
|
return { upgraded: false, qualityScore: null };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const backupDir = skill.dirPath + ".backup-" + Date.now();
|
|
95
|
+
try { fs.cpSync(skill.dirPath, backupDir, { recursive: true }); } catch { /* best-effort */ }
|
|
96
|
+
|
|
94
97
|
fs.writeFileSync(path.join(skill.dirPath, "SKILL.md"), newContent, "utf-8");
|
|
95
98
|
|
|
99
|
+
await this.rebuildCompanionFiles(skill, newContent, task);
|
|
100
|
+
|
|
96
101
|
const validation = await this.validator.validate(skill.dirPath, {
|
|
97
102
|
previousContent: currentContent,
|
|
98
103
|
});
|
|
99
104
|
|
|
100
105
|
if (!validation.valid) {
|
|
101
106
|
this.ctx.log.warn(`SkillUpgrader: validation failed for "${skill.name}", reverting: ${validation.errors.join("; ")}`);
|
|
102
|
-
fs.
|
|
107
|
+
if (fs.existsSync(backupDir)) {
|
|
108
|
+
fs.rmSync(skill.dirPath, { recursive: true });
|
|
109
|
+
fs.renameSync(backupDir, skill.dirPath);
|
|
110
|
+
} else {
|
|
111
|
+
fs.writeFileSync(path.join(skill.dirPath, "SKILL.md"), currentContent, "utf-8");
|
|
112
|
+
}
|
|
103
113
|
return { upgraded: false, qualityScore: null };
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
try { if (fs.existsSync(backupDir)) fs.rmSync(backupDir, { recursive: true }); } catch { /* cleanup */ }
|
|
117
|
+
|
|
106
118
|
const newVersion = skill.version + 1;
|
|
107
119
|
const newDescription = this.parseDescription(newContent) || skill.description;
|
|
108
120
|
|
|
@@ -216,4 +228,130 @@ export class SkillUpgrader {
|
|
|
216
228
|
if (match2) return match2[1];
|
|
217
229
|
return "";
|
|
218
230
|
}
|
|
231
|
+
|
|
232
|
+
private async rebuildCompanionFiles(skill: Skill, newContent: string, task: Task): Promise<void> {
|
|
233
|
+
const chain = buildSkillConfigChain(this.ctx);
|
|
234
|
+
if (chain.length === 0) return;
|
|
235
|
+
|
|
236
|
+
const chunks = this.store.getChunksByTask(task.id);
|
|
237
|
+
const conversationText = chunks
|
|
238
|
+
.filter(c => c.role === "user" || c.role === "assistant" || c.role === "tool")
|
|
239
|
+
.map(c => `[${c.role === "user" ? "User" : c.role === "assistant" ? "Assistant" : "Tool"}]: ${c.content.slice(0, 500)}`)
|
|
240
|
+
.join("\n\n")
|
|
241
|
+
.slice(0, 6000);
|
|
242
|
+
|
|
243
|
+
const scriptsPrompt = `Based on the following upgraded SKILL.md and task record, extract reusable automation scripts.
|
|
244
|
+
Rules:
|
|
245
|
+
- Only extract if the task record contains concrete shell commands, Python scripts, or TypeScript code that form a complete, reusable automation.
|
|
246
|
+
- Each script must be self-contained and runnable.
|
|
247
|
+
- If there are no automatable scripts, return an empty array.
|
|
248
|
+
- Don't fabricate scripts — only extract what was actually used.
|
|
249
|
+
|
|
250
|
+
SKILL.md:
|
|
251
|
+
${newContent.slice(0, 4000)}
|
|
252
|
+
|
|
253
|
+
Task conversation highlights:
|
|
254
|
+
${conversationText}
|
|
255
|
+
|
|
256
|
+
Reply with a JSON array only:
|
|
257
|
+
[{"filename": "deploy.sh", "content": "#!/bin/bash\\n..."}]
|
|
258
|
+
If no scripts, reply with: []`;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const raw = await callLLMWithFallback(chain, scriptsPrompt, this.ctx.log, "SkillUpgrader.scripts", {
|
|
262
|
+
maxTokens: 3000, temperature: 0.1, timeoutMs: 60_000, openclawAPI: this.ctx.openclawAPI,
|
|
263
|
+
});
|
|
264
|
+
const scripts = this.parseJSONArray<{ filename: string; content: string }>(raw);
|
|
265
|
+
|
|
266
|
+
const scriptsDir = path.join(skill.dirPath, "scripts");
|
|
267
|
+
if (fs.existsSync(scriptsDir)) fs.rmSync(scriptsDir, { recursive: true });
|
|
268
|
+
if (scripts.length > 0) {
|
|
269
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
270
|
+
for (const s of scripts) {
|
|
271
|
+
fs.writeFileSync(path.join(scriptsDir, s.filename), s.content, "utf-8");
|
|
272
|
+
}
|
|
273
|
+
this.ctx.log.info(`SkillUpgrader: rebuilt ${scripts.length} scripts for "${skill.name}"`);
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
this.ctx.log.warn(`SkillUpgrader: companion scripts rebuild failed: ${err}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const evalsPrompt = `Based on the following skill, generate 3-4 realistic test prompts that should trigger this skill.
|
|
281
|
+
Requirements:
|
|
282
|
+
- Write test prompts that a real user would type, mix direct and indirect phrasings
|
|
283
|
+
- LANGUAGE RULE: Write in the SAME language as the skill content.
|
|
284
|
+
|
|
285
|
+
Skill:
|
|
286
|
+
${newContent.slice(0, 4000)}
|
|
287
|
+
|
|
288
|
+
Reply with a JSON array only:
|
|
289
|
+
[{"id": 1, "prompt": "A realistic user message", "expectations": ["Expected behavior 1"], "trigger_confidence": "high"}]`;
|
|
290
|
+
|
|
291
|
+
const raw = await callLLMWithFallback(chain, evalsPrompt, this.ctx.log, "SkillUpgrader.evals", {
|
|
292
|
+
maxTokens: 2000, temperature: 0.3, timeoutMs: 60_000, openclawAPI: this.ctx.openclawAPI,
|
|
293
|
+
});
|
|
294
|
+
const evals = this.parseJSONArray<{ id: number; prompt: string; expectations: string[] }>(raw);
|
|
295
|
+
|
|
296
|
+
const evalsDir = path.join(skill.dirPath, "evals");
|
|
297
|
+
if (fs.existsSync(evalsDir)) fs.rmSync(evalsDir, { recursive: true });
|
|
298
|
+
if (evals.length > 0) {
|
|
299
|
+
fs.mkdirSync(evalsDir, { recursive: true });
|
|
300
|
+
fs.writeFileSync(
|
|
301
|
+
path.join(evalsDir, "evals.json"),
|
|
302
|
+
JSON.stringify({ skill_name: skill.name, evals }, null, 2),
|
|
303
|
+
"utf-8",
|
|
304
|
+
);
|
|
305
|
+
this.ctx.log.info(`SkillUpgrader: rebuilt ${evals.length} evals for "${skill.name}"`);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
this.ctx.log.warn(`SkillUpgrader: companion evals rebuild failed: ${err}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const refsPrompt = `Based on the following upgraded SKILL.md and task record, extract reference documentation worth preserving.
|
|
313
|
+
Rules:
|
|
314
|
+
- Only extract real reference content that appeared in the task (API docs, config examples, architecture notes).
|
|
315
|
+
- Each reference should be a standalone document useful for understanding the skill's domain.
|
|
316
|
+
- If there are no meaningful references, return an empty array.
|
|
317
|
+
- Don't fabricate content — only extract what was actually discussed or used.
|
|
318
|
+
- LANGUAGE RULE: Write in the SAME language as the skill content.
|
|
319
|
+
|
|
320
|
+
SKILL.md:
|
|
321
|
+
${newContent.slice(0, 4000)}
|
|
322
|
+
|
|
323
|
+
Task conversation highlights:
|
|
324
|
+
${conversationText}
|
|
325
|
+
|
|
326
|
+
Reply with a JSON array only:
|
|
327
|
+
[{"filename": "api-notes.md", "content": "# API Reference\\n..."}]
|
|
328
|
+
If no references, reply with: []`;
|
|
329
|
+
|
|
330
|
+
const raw = await callLLMWithFallback(chain, refsPrompt, this.ctx.log, "SkillUpgrader.references", {
|
|
331
|
+
maxTokens: 3000, temperature: 0.1, timeoutMs: 60_000, openclawAPI: this.ctx.openclawAPI,
|
|
332
|
+
});
|
|
333
|
+
const refs = this.parseJSONArray<{ filename: string; content: string }>(raw);
|
|
334
|
+
|
|
335
|
+
const refsDir = path.join(skill.dirPath, "references");
|
|
336
|
+
if (fs.existsSync(refsDir)) fs.rmSync(refsDir, { recursive: true });
|
|
337
|
+
if (refs.length > 0) {
|
|
338
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
339
|
+
for (const r of refs) {
|
|
340
|
+
fs.writeFileSync(path.join(refsDir, r.filename), r.content, "utf-8");
|
|
341
|
+
}
|
|
342
|
+
this.ctx.log.info(`SkillUpgrader: rebuilt ${refs.length} references for "${skill.name}"`);
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
this.ctx.log.warn(`SkillUpgrader: companion references rebuild failed: ${err}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private parseJSONArray<T>(raw: string): T[] {
|
|
350
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
351
|
+
if (!match) return [];
|
|
352
|
+
try {
|
|
353
|
+
const arr = JSON.parse(match[0]);
|
|
354
|
+
return Array.isArray(arr) ? arr : [];
|
|
355
|
+
} catch { return []; }
|
|
356
|
+
}
|
|
219
357
|
}
|
package/src/skill/validator.ts
CHANGED
|
@@ -31,6 +31,9 @@ export class SkillValidator {
|
|
|
31
31
|
this.validateFormat(dirPath, result);
|
|
32
32
|
if (!result.valid) return result;
|
|
33
33
|
|
|
34
|
+
this.checkCompanionConsistency(dirPath, result);
|
|
35
|
+
this.scanSecrets(dirPath, result);
|
|
36
|
+
|
|
34
37
|
if (opts?.previousContent) {
|
|
35
38
|
this.regressionCheck(dirPath, opts.previousContent, result);
|
|
36
39
|
}
|
|
@@ -133,6 +136,82 @@ export class SkillValidator {
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
private checkCompanionConsistency(dirPath: string, result: ValidationResult): void {
|
|
140
|
+
const skillMdPath = path.join(dirPath, "SKILL.md");
|
|
141
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
142
|
+
|
|
143
|
+
const referencedScripts = [...content.matchAll(/`scripts\/([^`]+)`/g)].map(m => m[1]);
|
|
144
|
+
const referencedRefs = [...content.matchAll(/`references\/([^`]+)`/g)].map(m => m[1]);
|
|
145
|
+
|
|
146
|
+
const scriptsDir = path.join(dirPath, "scripts");
|
|
147
|
+
const refsDir = path.join(dirPath, "references");
|
|
148
|
+
|
|
149
|
+
for (const f of referencedScripts) {
|
|
150
|
+
if (!fs.existsSync(path.join(scriptsDir, f))) {
|
|
151
|
+
result.warnings.push(`SKILL.md references scripts/${f} but file does not exist`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const f of referencedRefs) {
|
|
155
|
+
if (!fs.existsSync(path.join(refsDir, f))) {
|
|
156
|
+
result.warnings.push(`SKILL.md references references/${f} but file does not exist`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (fs.existsSync(scriptsDir)) {
|
|
161
|
+
try {
|
|
162
|
+
const actualScripts = fs.readdirSync(scriptsDir);
|
|
163
|
+
for (const f of actualScripts) {
|
|
164
|
+
if (!referencedScripts.includes(f)) {
|
|
165
|
+
result.warnings.push(`scripts/${f} exists but is not referenced in SKILL.md`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch { /* best-effort */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const evalsPath = path.join(dirPath, "evals", "evals.json");
|
|
172
|
+
if (fs.existsSync(evalsPath)) {
|
|
173
|
+
try {
|
|
174
|
+
const evalsData = JSON.parse(fs.readFileSync(evalsPath, "utf-8"));
|
|
175
|
+
if (!Array.isArray(evalsData?.evals) && !Array.isArray(evalsData)) {
|
|
176
|
+
result.warnings.push("evals/evals.json exists but has unexpected structure");
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
result.warnings.push("evals/evals.json exists but is not valid JSON");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private static readonly SECRET_PATTERNS: Array<{ label: string; regex: RegExp }> = [
|
|
185
|
+
{ label: "API key (sk-...)", regex: /\bsk-[a-zA-Z0-9]{20,}\b/ },
|
|
186
|
+
{ label: "Bearer token", regex: /\bBearer\s+[a-zA-Z0-9_\-.]{20,}\b/ },
|
|
187
|
+
{ label: "AWS key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
188
|
+
{ label: "Generic secret assignment", regex: /(api[_-]?key|secret|token|password|credential)\s*[:=]\s*["'][^"']{8,}["']/i },
|
|
189
|
+
{ label: "Base64 encoded secret (long)", regex: /\b[A-Za-z0-9+/]{40,}={0,2}\b/ },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
private scanSecrets(dirPath: string, result: ValidationResult): void {
|
|
193
|
+
const filesToScan = ["SKILL.md"];
|
|
194
|
+
const scriptsDir = path.join(dirPath, "scripts");
|
|
195
|
+
if (fs.existsSync(scriptsDir)) {
|
|
196
|
+
try {
|
|
197
|
+
for (const f of fs.readdirSync(scriptsDir)) filesToScan.push(path.join("scripts", f));
|
|
198
|
+
} catch { /* best-effort */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const relPath of filesToScan) {
|
|
202
|
+
const fullPath = path.join(dirPath, relPath);
|
|
203
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
204
|
+
try {
|
|
205
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
206
|
+
for (const { label, regex } of SkillValidator.SECRET_PATTERNS) {
|
|
207
|
+
if (regex.test(content)) {
|
|
208
|
+
result.warnings.push(`Potential secret detected in ${relPath}: ${label}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch { /* best-effort */ }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
136
215
|
private async assessQuality(dirPath: string, result: ValidationResult): Promise<void> {
|
|
137
216
|
const chain = buildSkillConfigChain(this.ctx);
|
|
138
217
|
if (chain.length === 0) return;
|
|
@@ -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
|
+
}
|