@memtensor/memos-local-openclaw-plugin 1.0.4-beta.10 → 1.0.4-beta.11
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/dist/client/connector.d.ts +1 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +37 -8
- package/dist/client/connector.js.map +1 -1
- package/dist/hub/server.d.ts +1 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +122 -28
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +9 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +26 -2
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +2 -0
- package/dist/recall/engine.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 +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +56 -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/sqlite.d.ts +15 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +91 -9
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +10 -0
- 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 +44 -23
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +35 -15
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +316 -13
- package/package.json +1 -1
- package/src/client/connector.ts +41 -8
- package/src/hub/server.ts +123 -27
- package/src/hub/user-manager.ts +42 -6
- package/src/recall/engine.ts +2 -0
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +58 -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/sqlite.ts +105 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +44 -23
- package/src/viewer/server.ts +35 -15
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;
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -114,6 +114,8 @@ export class SqliteStore {
|
|
|
114
114
|
this.migrateHubTables();
|
|
115
115
|
this.migrateHubFtsToTrigram();
|
|
116
116
|
this.migrateLocalSharedTasksOwner();
|
|
117
|
+
this.migrateHubUserIdentityFields();
|
|
118
|
+
this.migrateClientHubConnectionIdentityFields();
|
|
117
119
|
this.log.debug("Database schema initialized");
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -131,6 +133,49 @@ export class SqliteStore {
|
|
|
131
133
|
} catch { /* table may not exist yet */ }
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
private migrateHubUserIdentityFields(): void {
|
|
137
|
+
try {
|
|
138
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_users)").all() as Array<{ name: string }>;
|
|
139
|
+
if (cols.length === 0) return;
|
|
140
|
+
if (!cols.some(c => c.name === "identity_key")) {
|
|
141
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
|
|
142
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_hub_users_identity_key ON hub_users(identity_key)");
|
|
143
|
+
this.log.info("Migrated: added identity_key to hub_users");
|
|
144
|
+
}
|
|
145
|
+
if (!cols.some(c => c.name === "left_at")) {
|
|
146
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN left_at INTEGER");
|
|
147
|
+
this.log.info("Migrated: added left_at to hub_users");
|
|
148
|
+
}
|
|
149
|
+
if (!cols.some(c => c.name === "removed_at")) {
|
|
150
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN removed_at INTEGER");
|
|
151
|
+
this.log.info("Migrated: added removed_at to hub_users");
|
|
152
|
+
}
|
|
153
|
+
if (!cols.some(c => c.name === "rejected_at")) {
|
|
154
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN rejected_at INTEGER");
|
|
155
|
+
this.log.info("Migrated: added rejected_at to hub_users");
|
|
156
|
+
}
|
|
157
|
+
if (!cols.some(c => c.name === "rejoin_requested_at")) {
|
|
158
|
+
this.db.exec("ALTER TABLE hub_users ADD COLUMN rejoin_requested_at INTEGER");
|
|
159
|
+
this.log.info("Migrated: added rejoin_requested_at to hub_users");
|
|
160
|
+
}
|
|
161
|
+
} catch { /* table may not exist yet */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private migrateClientHubConnectionIdentityFields(): void {
|
|
165
|
+
try {
|
|
166
|
+
const cols = this.db.prepare("PRAGMA table_info(client_hub_connection)").all() as Array<{ name: string }>;
|
|
167
|
+
if (cols.length === 0) return;
|
|
168
|
+
if (!cols.some(c => c.name === "identity_key")) {
|
|
169
|
+
this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN identity_key TEXT NOT NULL DEFAULT ''");
|
|
170
|
+
this.log.info("Migrated: added identity_key to client_hub_connection");
|
|
171
|
+
}
|
|
172
|
+
if (!cols.some(c => c.name === "last_known_status")) {
|
|
173
|
+
this.db.exec("ALTER TABLE client_hub_connection ADD COLUMN last_known_status TEXT NOT NULL DEFAULT ''");
|
|
174
|
+
this.log.info("Migrated: added last_known_status to client_hub_connection");
|
|
175
|
+
}
|
|
176
|
+
} catch { /* table may not exist yet */ }
|
|
177
|
+
}
|
|
178
|
+
|
|
134
179
|
private migrateOwnerFields(): void {
|
|
135
180
|
const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
|
|
136
181
|
if (!chunkCols.some((c) => c.name === "owner")) {
|
|
@@ -1731,16 +1776,18 @@ export class SqliteStore {
|
|
|
1731
1776
|
|
|
1732
1777
|
setClientHubConnection(conn: ClientHubConnection): void {
|
|
1733
1778
|
this.db.prepare(`
|
|
1734
|
-
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at)
|
|
1735
|
-
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
1779
|
+
INSERT INTO client_hub_connection (id, hub_url, user_id, username, user_token, role, connected_at, identity_key, last_known_status)
|
|
1780
|
+
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1736
1781
|
ON CONFLICT(id) DO UPDATE SET
|
|
1737
1782
|
hub_url = excluded.hub_url,
|
|
1738
1783
|
user_id = excluded.user_id,
|
|
1739
1784
|
username = excluded.username,
|
|
1740
1785
|
user_token = excluded.user_token,
|
|
1741
1786
|
role = excluded.role,
|
|
1742
|
-
connected_at = excluded.connected_at
|
|
1743
|
-
|
|
1787
|
+
connected_at = excluded.connected_at,
|
|
1788
|
+
identity_key = excluded.identity_key,
|
|
1789
|
+
last_known_status = excluded.last_known_status
|
|
1790
|
+
`).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt, conn.identityKey ?? "", conn.lastKnownStatus ?? "");
|
|
1744
1791
|
}
|
|
1745
1792
|
|
|
1746
1793
|
getClientHubConnection(): ClientHubConnection | null {
|
|
@@ -1847,8 +1894,8 @@ export class SqliteStore {
|
|
|
1847
1894
|
|
|
1848
1895
|
upsertHubUser(user: HubUserRecord): void {
|
|
1849
1896
|
this.db.prepare(`
|
|
1850
|
-
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at)
|
|
1851
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1897
|
+
INSERT INTO hub_users (id, username, device_name, role, status, token_hash, created_at, approved_at, identity_key, left_at, removed_at, rejected_at, rejoin_requested_at)
|
|
1898
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1852
1899
|
ON CONFLICT(id) DO UPDATE SET
|
|
1853
1900
|
username = excluded.username,
|
|
1854
1901
|
device_name = excluded.device_name,
|
|
@@ -1856,8 +1903,13 @@ export class SqliteStore {
|
|
|
1856
1903
|
status = excluded.status,
|
|
1857
1904
|
token_hash = excluded.token_hash,
|
|
1858
1905
|
created_at = excluded.created_at,
|
|
1859
|
-
approved_at = excluded.approved_at
|
|
1860
|
-
|
|
1906
|
+
approved_at = excluded.approved_at,
|
|
1907
|
+
identity_key = excluded.identity_key,
|
|
1908
|
+
left_at = excluded.left_at,
|
|
1909
|
+
removed_at = excluded.removed_at,
|
|
1910
|
+
rejected_at = excluded.rejected_at,
|
|
1911
|
+
rejoin_requested_at = excluded.rejoin_requested_at
|
|
1912
|
+
`).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt, user.identityKey ?? "", user.leftAt ?? null, user.removedAt ?? null, user.rejectedAt ?? null, user.rejoinRequestedAt ?? null);
|
|
1861
1913
|
}
|
|
1862
1914
|
|
|
1863
1915
|
getHubUser(userId: string): HubUserRecord | null {
|
|
@@ -1881,7 +1933,18 @@ export class SqliteStore {
|
|
|
1881
1933
|
const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
|
|
1882
1934
|
return result.changes > 0;
|
|
1883
1935
|
}
|
|
1884
|
-
const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '' WHERE id = ?").run(userId);
|
|
1936
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '', removed_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1937
|
+
return result.changes > 0;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
findHubUserByIdentityKey(identityKey: string): HubUserRecord | null {
|
|
1941
|
+
if (!identityKey) return null;
|
|
1942
|
+
const row = this.db.prepare('SELECT * FROM hub_users WHERE identity_key = ?').get(identityKey) as HubUserRow | undefined;
|
|
1943
|
+
return row ? rowToHubUser(row) : null;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
markHubUserLeft(userId: string): boolean {
|
|
1947
|
+
const result = this.db.prepare("UPDATE hub_users SET status = 'left', token_hash = '', left_at = ? WHERE id = ?").run(Date.now(), userId);
|
|
1885
1948
|
return result.changes > 0;
|
|
1886
1949
|
}
|
|
1887
1950
|
|
|
@@ -2001,6 +2064,18 @@ export class SqliteStore {
|
|
|
2001
2064
|
return out;
|
|
2002
2065
|
}
|
|
2003
2066
|
|
|
2067
|
+
getVisibleHubSkillEmbeddings(): Array<{ skillId: string; vector: Float32Array }> {
|
|
2068
|
+
const rows = this.db.prepare(`
|
|
2069
|
+
SELECT hse.skill_id, hse.vector, hse.dimensions
|
|
2070
|
+
FROM hub_skill_embeddings hse
|
|
2071
|
+
JOIN hub_skills hs ON hs.id = hse.skill_id
|
|
2072
|
+
`).all() as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
|
|
2073
|
+
return rows.map(r => ({
|
|
2074
|
+
skillId: r.skill_id,
|
|
2075
|
+
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
2076
|
+
}));
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2004
2079
|
searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
|
|
2005
2080
|
const limit = options?.maxResults ?? 10;
|
|
2006
2081
|
const userId = options?.userId ?? "";
|
|
@@ -2549,6 +2624,8 @@ interface ClientHubConnection {
|
|
|
2549
2624
|
userToken: string;
|
|
2550
2625
|
role: UserRole;
|
|
2551
2626
|
connectedAt: number;
|
|
2627
|
+
identityKey?: string;
|
|
2628
|
+
lastKnownStatus?: string;
|
|
2552
2629
|
}
|
|
2553
2630
|
|
|
2554
2631
|
interface ClientHubConnectionRow {
|
|
@@ -2558,6 +2635,8 @@ interface ClientHubConnectionRow {
|
|
|
2558
2635
|
user_token: string;
|
|
2559
2636
|
role: string;
|
|
2560
2637
|
connected_at: number;
|
|
2638
|
+
identity_key?: string;
|
|
2639
|
+
last_known_status?: string;
|
|
2561
2640
|
}
|
|
2562
2641
|
|
|
2563
2642
|
function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnection {
|
|
@@ -2568,6 +2647,8 @@ function rowToClientHubConnection(row: ClientHubConnectionRow): ClientHubConnect
|
|
|
2568
2647
|
userToken: row.user_token,
|
|
2569
2648
|
role: row.role as UserRole,
|
|
2570
2649
|
connectedAt: row.connected_at,
|
|
2650
|
+
identityKey: row.identity_key || "",
|
|
2651
|
+
lastKnownStatus: row.last_known_status || "",
|
|
2571
2652
|
};
|
|
2572
2653
|
}
|
|
2573
2654
|
|
|
@@ -2577,6 +2658,11 @@ interface HubUserRecord extends UserInfo {
|
|
|
2577
2658
|
approvedAt: number | null;
|
|
2578
2659
|
lastIp: string;
|
|
2579
2660
|
lastActiveAt: number | null;
|
|
2661
|
+
identityKey?: string;
|
|
2662
|
+
leftAt?: number | null;
|
|
2663
|
+
removedAt?: number | null;
|
|
2664
|
+
rejectedAt?: number | null;
|
|
2665
|
+
rejoinRequestedAt?: number | null;
|
|
2580
2666
|
}
|
|
2581
2667
|
|
|
2582
2668
|
interface HubUserRow {
|
|
@@ -2590,6 +2676,11 @@ interface HubUserRow {
|
|
|
2590
2676
|
approved_at: number | null;
|
|
2591
2677
|
last_ip: string;
|
|
2592
2678
|
last_active_at: number | null;
|
|
2679
|
+
identity_key?: string;
|
|
2680
|
+
left_at?: number | null;
|
|
2681
|
+
removed_at?: number | null;
|
|
2682
|
+
rejected_at?: number | null;
|
|
2683
|
+
rejoin_requested_at?: number | null;
|
|
2593
2684
|
}
|
|
2594
2685
|
|
|
2595
2686
|
function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
@@ -2605,6 +2696,11 @@ function rowToHubUser(row: HubUserRow): HubUserRecord {
|
|
|
2605
2696
|
approvedAt: row.approved_at,
|
|
2606
2697
|
lastIp: row.last_ip || "",
|
|
2607
2698
|
lastActiveAt: row.last_active_at ?? null,
|
|
2699
|
+
identityKey: row.identity_key || "",
|
|
2700
|
+
leftAt: row.left_at ?? null,
|
|
2701
|
+
removedAt: row.removed_at ?? null,
|
|
2702
|
+
rejectedAt: row.rejected_at ?? null,
|
|
2703
|
+
rejoinRequestedAt: row.rejoin_requested_at ?? null,
|
|
2608
2704
|
};
|
|
2609
2705
|
}
|
|
2610
2706
|
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface ChunkRef {
|
|
|
66
66
|
|
|
67
67
|
// ─── Search / Recall ───
|
|
68
68
|
|
|
69
|
+
export type SearchHitOrigin = "local" | "local-shared" | "hub-memory" | "hub-remote";
|
|
70
|
+
|
|
69
71
|
export interface SearchHit {
|
|
70
72
|
summary: string;
|
|
71
73
|
original_excerpt: string;
|
|
@@ -74,6 +76,7 @@ export interface SearchHit {
|
|
|
74
76
|
taskId: string | null;
|
|
75
77
|
skillId: string | null;
|
|
76
78
|
owner?: string;
|
|
79
|
+
origin?: SearchHitOrigin;
|
|
77
80
|
source: {
|
|
78
81
|
ts: number;
|
|
79
82
|
role: Role;
|
|
@@ -249,6 +252,10 @@ export interface SkillEvolutionConfig {
|
|
|
249
252
|
minConfidence?: number;
|
|
250
253
|
maxSkillLines?: number;
|
|
251
254
|
autoInstall?: boolean;
|
|
255
|
+
autoRecallSkills?: boolean;
|
|
256
|
+
autoRecallSkillLimit?: number;
|
|
257
|
+
preferUpgradeExisting?: boolean;
|
|
258
|
+
redactSensitiveInSkill?: boolean;
|
|
252
259
|
/** Optional independent LLM config for skill evaluation/validation. Falls back to main summarizer if not set. */
|
|
253
260
|
summarizer?: SummarizerConfig;
|
|
254
261
|
}
|
|
@@ -344,6 +351,10 @@ export const DEFAULTS = {
|
|
|
344
351
|
skillMinConfidence: 0.7,
|
|
345
352
|
skillMaxLines: 400,
|
|
346
353
|
skillAutoInstall: false,
|
|
354
|
+
skillAutoRecall: true,
|
|
355
|
+
skillAutoRecallLimit: 2,
|
|
356
|
+
skillPreferUpgrade: true,
|
|
357
|
+
skillRedactSensitive: true,
|
|
347
358
|
} as const;
|
|
348
359
|
|
|
349
360
|
// ─── Plugin Hooks (OpenClaw integration) ───
|