@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.
Files changed (67) hide show
  1. package/dist/client/connector.d.ts +1 -0
  2. package/dist/client/connector.d.ts.map +1 -1
  3. package/dist/client/connector.js +37 -8
  4. package/dist/client/connector.js.map +1 -1
  5. package/dist/hub/server.d.ts +1 -0
  6. package/dist/hub/server.d.ts.map +1 -1
  7. package/dist/hub/server.js +122 -28
  8. package/dist/hub/server.js.map +1 -1
  9. package/dist/hub/user-manager.d.ts +9 -0
  10. package/dist/hub/user-manager.d.ts.map +1 -1
  11. package/dist/hub/user-manager.js +26 -2
  12. package/dist/hub/user-manager.js.map +1 -1
  13. package/dist/recall/engine.d.ts.map +1 -1
  14. package/dist/recall/engine.js +2 -0
  15. package/dist/recall/engine.js.map +1 -1
  16. package/dist/sharing/types.d.ts +1 -1
  17. package/dist/sharing/types.d.ts.map +1 -1
  18. package/dist/skill/evolver.d.ts +2 -0
  19. package/dist/skill/evolver.d.ts.map +1 -1
  20. package/dist/skill/evolver.js +56 -5
  21. package/dist/skill/evolver.js.map +1 -1
  22. package/dist/skill/generator.d.ts +2 -0
  23. package/dist/skill/generator.d.ts.map +1 -1
  24. package/dist/skill/generator.js +45 -3
  25. package/dist/skill/generator.js.map +1 -1
  26. package/dist/skill/installer.d.ts +26 -0
  27. package/dist/skill/installer.d.ts.map +1 -1
  28. package/dist/skill/installer.js +80 -4
  29. package/dist/skill/installer.js.map +1 -1
  30. package/dist/skill/upgrader.d.ts +2 -0
  31. package/dist/skill/upgrader.d.ts.map +1 -1
  32. package/dist/skill/upgrader.js +139 -1
  33. package/dist/skill/upgrader.js.map +1 -1
  34. package/dist/skill/validator.d.ts +3 -0
  35. package/dist/skill/validator.d.ts.map +1 -1
  36. package/dist/skill/validator.js +75 -0
  37. package/dist/skill/validator.js.map +1 -1
  38. package/dist/storage/sqlite.d.ts +15 -0
  39. package/dist/storage/sqlite.d.ts.map +1 -1
  40. package/dist/storage/sqlite.js +91 -9
  41. package/dist/storage/sqlite.js.map +1 -1
  42. package/dist/types.d.ts +10 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js +4 -0
  45. package/dist/types.js.map +1 -1
  46. package/dist/viewer/html.d.ts.map +1 -1
  47. package/dist/viewer/html.js +44 -23
  48. package/dist/viewer/html.js.map +1 -1
  49. package/dist/viewer/server.d.ts.map +1 -1
  50. package/dist/viewer/server.js +35 -15
  51. package/dist/viewer/server.js.map +1 -1
  52. package/index.ts +316 -13
  53. package/package.json +1 -1
  54. package/src/client/connector.ts +41 -8
  55. package/src/hub/server.ts +123 -27
  56. package/src/hub/user-manager.ts +42 -6
  57. package/src/recall/engine.ts +2 -0
  58. package/src/sharing/types.ts +1 -1
  59. package/src/skill/evolver.ts +58 -6
  60. package/src/skill/generator.ts +44 -5
  61. package/src/skill/installer.ts +107 -4
  62. package/src/skill/upgrader.ts +139 -1
  63. package/src/skill/validator.ts +79 -0
  64. package/src/storage/sqlite.ts +105 -9
  65. package/src/types.ts +11 -0
  66. package/src/viewer/html.ts +44 -23
  67. package/src/viewer/server.ts +35 -15
@@ -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.writeFileSync(path.join(skill.dirPath, "SKILL.md"), currentContent, "utf-8");
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
  }
@@ -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;
@@ -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
- `).run(conn.hubUrl, conn.userId, conn.username, conn.userToken, conn.role, conn.connectedAt);
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
- `).run(user.id, user.username, user.deviceName ?? "", user.role, user.status, user.tokenHash, user.createdAt, user.approvedAt);
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) ───