@memtensor/memos-local-openclaw-plugin 1.0.4-beta.9 → 1.0.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 (100) hide show
  1. package/.env.example +7 -0
  2. package/README.md +94 -27
  3. package/dist/capture/index.js +3 -1
  4. package/dist/capture/index.js.map +1 -1
  5. package/dist/client/connector.d.ts +5 -0
  6. package/dist/client/connector.d.ts.map +1 -1
  7. package/dist/client/connector.js +89 -8
  8. package/dist/client/connector.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/hub/server.d.ts +2 -0
  13. package/dist/hub/server.d.ts.map +1 -1
  14. package/dist/hub/server.js +240 -35
  15. package/dist/hub/server.js.map +1 -1
  16. package/dist/hub/user-manager.d.ts +9 -0
  17. package/dist/hub/user-manager.d.ts.map +1 -1
  18. package/dist/hub/user-manager.js +26 -2
  19. package/dist/hub/user-manager.js.map +1 -1
  20. package/dist/ingest/chunker.d.ts +2 -1
  21. package/dist/ingest/chunker.d.ts.map +1 -1
  22. package/dist/ingest/chunker.js +14 -10
  23. package/dist/ingest/chunker.js.map +1 -1
  24. package/dist/ingest/providers/index.js +2 -2
  25. package/dist/ingest/providers/index.js.map +1 -1
  26. package/dist/recall/engine.d.ts.map +1 -1
  27. package/dist/recall/engine.js +22 -4
  28. package/dist/recall/engine.js.map +1 -1
  29. package/dist/shared/llm-call.d.ts.map +1 -1
  30. package/dist/shared/llm-call.js +2 -1
  31. package/dist/shared/llm-call.js.map +1 -1
  32. package/dist/sharing/types.d.ts +1 -1
  33. package/dist/sharing/types.d.ts.map +1 -1
  34. package/dist/skill/evolver.d.ts +2 -0
  35. package/dist/skill/evolver.d.ts.map +1 -1
  36. package/dist/skill/evolver.js +56 -5
  37. package/dist/skill/evolver.js.map +1 -1
  38. package/dist/skill/generator.d.ts +2 -0
  39. package/dist/skill/generator.d.ts.map +1 -1
  40. package/dist/skill/generator.js +45 -3
  41. package/dist/skill/generator.js.map +1 -1
  42. package/dist/skill/installer.d.ts +26 -0
  43. package/dist/skill/installer.d.ts.map +1 -1
  44. package/dist/skill/installer.js +80 -4
  45. package/dist/skill/installer.js.map +1 -1
  46. package/dist/skill/upgrader.d.ts +2 -0
  47. package/dist/skill/upgrader.d.ts.map +1 -1
  48. package/dist/skill/upgrader.js +139 -1
  49. package/dist/skill/upgrader.js.map +1 -1
  50. package/dist/skill/validator.d.ts +3 -0
  51. package/dist/skill/validator.d.ts.map +1 -1
  52. package/dist/skill/validator.js +75 -0
  53. package/dist/skill/validator.js.map +1 -1
  54. package/dist/storage/sqlite.d.ts +57 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +290 -35
  57. package/dist/storage/sqlite.js.map +1 -1
  58. package/dist/telemetry.d.ts +4 -1
  59. package/dist/telemetry.d.ts.map +1 -1
  60. package/dist/telemetry.js +39 -12
  61. package/dist/telemetry.js.map +1 -1
  62. package/dist/types.d.ts +10 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js +4 -0
  65. package/dist/types.js.map +1 -1
  66. package/dist/viewer/html.d.ts.map +1 -1
  67. package/dist/viewer/html.js +564 -225
  68. package/dist/viewer/html.js.map +1 -1
  69. package/dist/viewer/server.d.ts +9 -0
  70. package/dist/viewer/server.d.ts.map +1 -1
  71. package/dist/viewer/server.js +357 -108
  72. package/dist/viewer/server.js.map +1 -1
  73. package/index.ts +412 -53
  74. package/openclaw.plugin.json +1 -1
  75. package/package.json +2 -1
  76. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  77. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  78. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  79. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  80. package/src/capture/index.ts +4 -1
  81. package/src/client/connector.ts +92 -8
  82. package/src/config.ts +2 -1
  83. package/src/hub/server.ts +235 -35
  84. package/src/hub/user-manager.ts +42 -6
  85. package/src/ingest/chunker.ts +19 -13
  86. package/src/ingest/providers/index.ts +2 -2
  87. package/src/recall/engine.ts +20 -4
  88. package/src/shared/llm-call.ts +2 -1
  89. package/src/sharing/types.ts +1 -1
  90. package/src/skill/evolver.ts +58 -6
  91. package/src/skill/generator.ts +44 -5
  92. package/src/skill/installer.ts +107 -4
  93. package/src/skill/upgrader.ts +139 -1
  94. package/src/skill/validator.ts +79 -0
  95. package/src/storage/sqlite.ts +318 -40
  96. package/src/telemetry.ts +39 -14
  97. package/src/types.ts +11 -0
  98. package/src/viewer/html.ts +564 -225
  99. package/src/viewer/server.ts +333 -105
  100. package/telemetry.credentials.json +5 -0
package/src/hub/server.ts CHANGED
@@ -14,6 +14,7 @@ type HubServerOptions = {
14
14
  config: MemosLocalConfig;
15
15
  dataDir: string;
16
16
  embedder?: Embedder;
17
+ defaultHubPort?: number;
17
18
  };
18
19
 
19
20
  type HubAuthState = {
@@ -79,18 +80,31 @@ export class HubServer {
79
80
  }
80
81
  });
81
82
 
83
+ const MAX_PORT_RETRIES = 3;
84
+ let hubPort = this.port;
82
85
  await new Promise<void>((resolve, reject) => {
83
- const onError = (err: Error) => {
84
- this.server?.off("listening", onListening);
85
- reject(err);
86
+ let retries = 0;
87
+ const onError = (err: NodeJS.ErrnoException) => {
88
+ if (err.code === "EADDRINUSE" && retries < MAX_PORT_RETRIES) {
89
+ retries++;
90
+ hubPort = this.port + retries;
91
+ this.opts.log.warn(`Hub port ${hubPort - 1} in use, trying ${hubPort}`);
92
+ this.server!.listen(hubPort, "0.0.0.0");
93
+ } else {
94
+ this.server?.off("listening", onListening);
95
+ reject(err);
96
+ }
86
97
  };
87
98
  const onListening = () => {
88
99
  this.server?.off("error", onError);
100
+ if (hubPort !== this.port) {
101
+ this.opts.log.info(`Hub started on fallback port ${hubPort} (configured: ${this.port})`);
102
+ }
89
103
  resolve();
90
104
  };
91
- this.server!.once("error", onError);
105
+ this.server!.on("error", onError);
92
106
  this.server!.once("listening", onListening);
93
- this.server!.listen(this.port, "0.0.0.0");
107
+ this.server!.listen(hubPort, "0.0.0.0");
94
108
  });
95
109
 
96
110
  const bootstrap = this.userManager.ensureBootstrapAdmin(
@@ -109,19 +123,37 @@ export class HubServer {
109
123
  this.initOnlineTracking();
110
124
  this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
111
125
 
112
- return `http://127.0.0.1:${this.port}`;
126
+ return `http://127.0.0.1:${hubPort}`;
113
127
  }
114
128
 
115
129
  async stop(): Promise<void> {
116
130
  if (this.offlineCheckTimer) { clearInterval(this.offlineCheckTimer); this.offlineCheckTimer = undefined; }
117
131
  if (!this.server) return;
132
+
133
+ try {
134
+ const activeUsers = this.opts.store.listHubUsers("active");
135
+ const ownerId = this.authState.bootstrapAdminUserId || "";
136
+ for (const u of activeUsers) {
137
+ if (u.id === ownerId) continue;
138
+ try {
139
+ this.opts.store.insertHubNotification({
140
+ id: randomUUID(), userId: u.id, type: "hub_shutdown",
141
+ resource: "system", title: `Team server "${this.teamName}" has been shut down by the admin.`,
142
+ });
143
+ } catch { /* best-effort */ }
144
+ }
145
+ } catch { /* best-effort */ }
146
+
118
147
  const server = this.server;
119
148
  this.server = undefined;
120
149
  await new Promise<void>((resolve) => server.close(() => resolve()));
121
150
  }
122
151
 
123
152
  private get port(): number {
124
- return this.opts.config.sharing?.hub?.port ?? 18800;
153
+ const configured = this.opts.config.sharing?.hub?.port;
154
+ const derived = this.opts.defaultHubPort;
155
+ if (derived && (!configured || configured === 18800)) return derived;
156
+ return configured ?? 18800;
125
157
  }
126
158
 
127
159
  private get teamName(): string {
@@ -169,6 +201,20 @@ export class HubServer {
169
201
  });
170
202
  }
171
203
 
204
+ private embedSkillAsync(skillId: string, name: string, description: string, sourceUserId: string, sourceSkillId: string): void {
205
+ const embedder = this.opts.embedder;
206
+ if (!embedder) return;
207
+ const text = `${name}: ${description}`;
208
+ embedder.embed([text]).then((vectors) => {
209
+ if (vectors[0]) {
210
+ this.opts.store.upsertHubSkillEmbedding(skillId, Array.from(vectors[0]), sourceUserId, sourceSkillId);
211
+ this.opts.log.info(`hub: embedded shared skill ${skillId}`);
212
+ }
213
+ }).catch((err) => {
214
+ this.opts.log.warn(`hub: embedding shared skill failed: ${err}`);
215
+ });
216
+ }
217
+
172
218
  private embedMemoryAsync(memoryId: string, summary: string, content: string): void {
173
219
  const embedder = this.opts.embedder;
174
220
  if (!embedder) return;
@@ -205,40 +251,70 @@ export class HubServer {
205
251
  || (req.headers["x-client-ip"] as string)?.trim()
206
252
  || (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
207
253
  || req.socket.remoteAddress || "";
208
- const existingUsers = this.opts.store.listHubUsers();
209
- const existingUser = existingUsers.find(u => u.username === username);
254
+ const identityKey = typeof body.identityKey === "string" ? body.identityKey.trim() : "";
255
+
256
+ let existingUser = identityKey
257
+ ? this.userManager.findByIdentityKey(identityKey)
258
+ : null;
259
+ if (!existingUser) {
260
+ const existingUsers = this.opts.store.listHubUsers();
261
+ existingUser = existingUsers.find(u => u.username === username && u.status !== "left" && u.status !== "removed") ?? null;
262
+ }
263
+
210
264
  if (existingUser) {
211
265
  try { this.opts.store.updateHubUserActivity(existingUser.id, joinIp); } catch { /* best-effort */ }
266
+
212
267
  if (existingUser.status === "active") {
213
268
  const token = issueUserToken(
214
269
  { userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" },
215
270
  this.authSecret,
216
271
  );
217
272
  this.userManager.approveUser(existingUser.id, token);
218
- return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
273
+ if (identityKey && !existingUser.identityKey) {
274
+ this.opts.store.upsertHubUser({ ...existingUser, identityKey });
275
+ }
276
+ return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token, identityKey: existingUser.identityKey || identityKey });
219
277
  }
220
278
  if (existingUser.status === "pending") {
221
279
  this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
222
- return this.json(res, 200, { status: "pending", userId: existingUser.id });
280
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
223
281
  }
224
282
  if (existingUser.status === "rejected") {
225
283
  if (body.reapply === true) {
226
284
  this.userManager.resetToPending(existingUser.id);
227
285
  this.notifyAdmins("user_join_request", "user", username, "");
228
286
  this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
229
- return this.json(res, 200, { status: "pending", userId: existingUser.id });
287
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
230
288
  }
231
289
  return this.json(res, 200, { status: "rejected", userId: existingUser.id });
232
290
  }
291
+ if (existingUser.status === "removed") {
292
+ this.userManager.rejoinUser(existingUser.id);
293
+ this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
294
+ this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
295
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
296
+ }
297
+ if (existingUser.status === "left") {
298
+ this.userManager.rejoinUser(existingUser.id);
299
+ this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
300
+ this.opts.log.info(`Hub: left user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
301
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
302
+ }
303
+ if (existingUser.status === "blocked") {
304
+ return this.json(res, 200, { status: "blocked", userId: existingUser.id });
305
+ }
233
306
  }
307
+
308
+ const generatedIdentityKey = identityKey || randomUUID();
234
309
  const user = this.userManager.createPendingUser({
235
310
  username,
236
311
  deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
312
+ identityKey: generatedIdentityKey,
237
313
  });
238
314
  try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
239
315
  this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
240
316
  this.notifyAdmins("user_join_request", "user", username, "");
241
- return this.json(res, 200, { status: "pending", userId: user.id });
317
+ return this.json(res, 200, { status: "pending", userId: user.id, identityKey: generatedIdentityKey });
242
318
  }
243
319
 
244
320
  if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
@@ -256,16 +332,42 @@ export class HubServer {
256
332
  if (user.status === "rejected") {
257
333
  return this.json(res, 200, { status: "rejected" });
258
334
  }
335
+ if (user.status === "blocked") {
336
+ return this.json(res, 200, { status: "blocked" });
337
+ }
338
+ if (user.status === "left") {
339
+ return this.json(res, 200, { status: "left" });
340
+ }
341
+ if (user.status === "removed") {
342
+ return this.json(res, 200, { status: "removed" });
343
+ }
259
344
  if (user.status === "active") {
260
345
  const token = issueUserToken(
261
346
  { userId: user.id, username: user.username, role: user.role, status: user.status },
262
347
  this.authSecret,
263
348
  );
349
+ this.userManager.approveUser(user.id, token);
264
350
  return this.json(res, 200, { status: "active", userToken: token });
265
351
  }
266
352
  return this.json(res, 200, { status: user.status });
267
353
  }
268
354
 
355
+ if (req.method === "POST" && routePath === "/api/v1/hub/withdraw-pending") {
356
+ const body = await this.readJson(req);
357
+ if (!body || body.teamToken !== this.teamToken) {
358
+ return this.json(res, 403, { error: "invalid_team_token" });
359
+ }
360
+ const userId = String(body.userId || "");
361
+ if (!userId) return this.json(res, 400, { error: "missing_user_id" });
362
+ const user = this.opts.store.getHubUser(userId);
363
+ if (!user) return this.json(res, 200, { ok: true });
364
+ if (user.status === "pending") {
365
+ this.userManager.markUserLeft(userId);
366
+ this.opts.log.info(`Hub: user "${user.username}" (${userId}) withdrew pending application`);
367
+ }
368
+ return this.json(res, 200, { ok: true });
369
+ }
370
+
269
371
  // All endpoints below require authentication + rate limiting
270
372
  const auth = this.authenticate(req);
271
373
  if (!auth) return this.json(res, 401, { error: "unauthorized" });
@@ -280,12 +382,10 @@ export class HubServer {
280
382
  }
281
383
 
282
384
  if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
283
- try {
284
- this.opts.store.updateHubUserActivity(auth.userId, "", 0);
285
- } catch { /* best-effort */ }
385
+ this.userManager.markUserLeft(auth.userId);
286
386
  this.knownOnlineUsers.delete(auth.userId);
287
- this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
288
- this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
387
+ this.notifyAdmins("user_left", "user", auth.username, auth.userId);
388
+ this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, status set to "left"`);
289
389
  return this.json(res, 200, { ok: true });
290
390
  }
291
391
 
@@ -314,6 +414,10 @@ export class HubServer {
314
414
  ttlMs,
315
415
  );
316
416
  this.userManager.approveUser(updated.id, newToken);
417
+ if (updated.id === this.authState.bootstrapAdminUserId) {
418
+ this.authState.bootstrapAdminToken = newToken;
419
+ this.saveAuthState();
420
+ }
317
421
  this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
318
422
  return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
319
423
  }
@@ -326,18 +430,33 @@ export class HubServer {
326
430
  if (req.method === "POST" && routePath === "/api/v1/hub/admin/approve-user") {
327
431
  if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
328
432
  const body = await this.readJson(req);
329
- const token = issueUserToken({ userId: String(body.userId), username: String(body.username || ""), role: "member", status: "active" }, this.authSecret);
330
- const approved = this.userManager.approveUser(String(body.userId), token);
433
+ const userId = String(body.userId);
434
+ const username = String(body.username || "");
435
+ const token = issueUserToken({ userId, username, role: "member", status: "active" }, this.authSecret);
436
+ const approved = this.userManager.approveUser(userId, token);
331
437
  if (!approved) return this.json(res, 404, { error: "not_found" });
332
- try { this.opts.store.updateHubUserActivity(String(body.userId), ""); } catch { /* best-effort */ }
438
+ try { this.opts.store.updateHubUserActivity(userId, ""); } catch { /* best-effort */ }
439
+ try {
440
+ this.opts.store.insertHubNotification({
441
+ id: randomUUID(), userId, type: "membership_approved",
442
+ resource: "user", title: `Your request to join team "${this.teamName}" has been approved. Welcome!`,
443
+ });
444
+ } catch { /* best-effort */ }
333
445
  return this.json(res, 200, { status: "active", token });
334
446
  }
335
447
 
336
448
  if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
337
449
  if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
338
450
  const body = await this.readJson(req);
339
- const rejected = this.userManager.rejectUser(String(body.userId));
451
+ const userId = String(body.userId);
452
+ const rejected = this.userManager.rejectUser(userId);
340
453
  if (!rejected) return this.json(res, 404, { error: "not_found" });
454
+ try {
455
+ this.opts.store.insertHubNotification({
456
+ id: randomUUID(), userId, type: "membership_rejected",
457
+ resource: "user", title: `Your request to join team "${this.teamName}" has been declined.`,
458
+ });
459
+ } catch { /* best-effort */ }
341
460
  return this.json(res, 200, { status: "rejected" });
342
461
  }
343
462
 
@@ -374,6 +493,13 @@ export class HubServer {
374
493
  const updatedUser = { ...user, role: newRole as "admin" | "member" };
375
494
  this.opts.store.upsertHubUser(updatedUser);
376
495
  this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
496
+ try {
497
+ const notifType = newRole === "admin" ? "role_promoted" : "role_demoted";
498
+ this.opts.store.insertHubNotification({
499
+ id: randomUUID(), userId, type: notifType,
500
+ resource: "user", title: `Your role in team "${this.teamName}" has been changed to ${newRole}.`,
501
+ });
502
+ } catch { /* best-effort */ }
377
503
  return this.json(res, 200, { ok: true, role: newRole });
378
504
  }
379
505
 
@@ -400,6 +526,10 @@ export class HubServer {
400
526
  const updated = this.opts.store.getHubUser(userId)!;
401
527
  const finalUser = { ...updated, username: newUsername };
402
528
  this.opts.store.upsertHubUser(finalUser);
529
+ if (userId === this.authState.bootstrapAdminUserId) {
530
+ this.authState.bootstrapAdminToken = newToken;
531
+ this.saveAuthState();
532
+ }
403
533
  this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
404
534
  return this.json(res, 200, { ok: true, username: newUsername });
405
535
  }
@@ -411,9 +541,16 @@ export class HubServer {
411
541
  if (!userId) return this.json(res, 400, { error: "missing_user_id" });
412
542
  if (userId === auth.userId) return this.json(res, 400, { error: "cannot_remove_self" });
413
543
  if (userId === this.authState.bootstrapAdminUserId) return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
544
+ try {
545
+ this.opts.store.insertHubNotification({
546
+ id: randomUUID(), userId, type: "membership_removed",
547
+ resource: "user", title: `You have been removed from team "${this.teamName}" by the admin.`,
548
+ });
549
+ } catch { /* best-effort */ }
414
550
  const cleanResources = body?.cleanResources === true;
415
551
  const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
416
552
  if (!deleted) return this.json(res, 404, { error: "not_found" });
553
+ this.knownOnlineUsers.delete(userId);
417
554
  this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
418
555
  return this.json(res, 200, { ok: true });
419
556
  }
@@ -603,19 +740,70 @@ export class HubServer {
603
740
  }
604
741
 
605
742
  if (req.method === "GET" && routePath === "/api/v1/hub/skills") {
606
- const hits = this.opts.store.searchHubSkills(String(url.searchParams.get("query") || ""), {
743
+ const skillQuery = String(url.searchParams.get("query") || "");
744
+ const skillMaxResults = Number(url.searchParams.get("maxResults") || 10);
745
+ const ftsSkillHits = this.opts.store.searchHubSkills(skillQuery, {
607
746
  userId: auth.userId,
608
- maxResults: Number(url.searchParams.get("maxResults") || 10),
609
- }).map(({ hit }) => ({
610
- skillId: hit.id,
611
- name: hit.name,
612
- description: hit.description,
613
- version: hit.version,
614
- visibility: hit.visibility,
615
- groupName: hit.group_name,
616
- ownerName: hit.owner_name || "unknown",
617
- qualityScore: hit.quality_score,
618
- }));
747
+ maxResults: skillMaxResults * 2,
748
+ });
749
+
750
+ let mergedSkillIds: string[];
751
+ if (this.opts.embedder && skillQuery) {
752
+ try {
753
+ const [queryVec] = await this.opts.embedder.embed([skillQuery]);
754
+ if (queryVec) {
755
+ const skillEmbs = this.opts.store.getVisibleHubSkillEmbeddings();
756
+ const cosineSim = (vec: Float32Array) => {
757
+ let dot = 0, nA = 0, nB = 0;
758
+ for (let i = 0; i < queryVec.length && i < vec.length; i++) {
759
+ dot += queryVec[i] * vec[i]; nA += queryVec[i] * queryVec[i]; nB += vec[i] * vec[i];
760
+ }
761
+ return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
762
+ };
763
+ const vecScored = skillEmbs
764
+ .map(e => ({ id: e.skillId, score: cosineSim(e.vector) }))
765
+ .filter(e => e.score > 0.3)
766
+ .sort((a, b) => b.score - a.score)
767
+ .slice(0, skillMaxResults * 2);
768
+
769
+ const K = 60;
770
+ const rrfScores = new Map<string, number>();
771
+ ftsSkillHits.forEach(({ hit }, idx) => {
772
+ rrfScores.set(hit.id, (rrfScores.get(hit.id) ?? 0) + 1 / (K + idx + 1));
773
+ });
774
+ vecScored.forEach(({ id }, idx) => {
775
+ rrfScores.set(id, (rrfScores.get(id) ?? 0) + 1 / (K + idx + 1));
776
+ });
777
+ mergedSkillIds = [...rrfScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, skillMaxResults).map(([id]) => id);
778
+ } else {
779
+ mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
780
+ }
781
+ } catch {
782
+ mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
783
+ }
784
+ } else {
785
+ mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
786
+ }
787
+
788
+ const ftsSkillMap = new Map(ftsSkillHits.map(({ hit }) => [hit.id, hit]));
789
+ const hits = mergedSkillIds.map(id => {
790
+ const hit = ftsSkillMap.get(id);
791
+ if (hit) {
792
+ return {
793
+ skillId: hit.id, name: hit.name, description: hit.description,
794
+ version: hit.version, visibility: hit.visibility, groupName: hit.group_name,
795
+ ownerName: hit.owner_name || "unknown", ownerStatus: hit.owner_status || "",
796
+ qualityScore: hit.quality_score,
797
+ };
798
+ }
799
+ const skill = this.opts.store.getHubSkillById(id);
800
+ if (!skill) return null;
801
+ return {
802
+ skillId: skill.id, name: skill.name, description: skill.description,
803
+ version: skill.version, visibility: skill.visibility, groupName: "",
804
+ ownerName: "unknown", ownerStatus: "", qualityScore: skill.qualityScore,
805
+ };
806
+ }).filter(Boolean);
619
807
  return this.json(res, 200, { hits });
620
808
  }
621
809
 
@@ -641,6 +829,7 @@ export class HubServer {
641
829
  createdAt: existing?.createdAt ?? Date.now(),
642
830
  updatedAt: Date.now(),
643
831
  });
832
+ this.embedSkillAsync(skillId, String(metadata.name || sourceSkillId), String(metadata.description || ""), auth.userId, sourceSkillId);
644
833
  if (!existing) {
645
834
  this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
646
835
  }
@@ -761,7 +950,18 @@ export class HubServer {
761
950
  const deleted = this.opts.store.deleteHubMemoryById(memoryId);
762
951
  if (!deleted) return this.json(res, 404, { error: "not_found" });
763
952
  if (memInfo) {
764
- this.opts.store.insertHubNotification({ id: randomUUID(), userId: memInfo.sourceUserId, type: "resource_removed", resource: "memory", title: memInfo.summary || memInfo.id });
953
+ const payload = JSON.stringify({
954
+ memoryId,
955
+ sourceChunkId: memInfo.sourceChunkId,
956
+ });
957
+ this.opts.store.insertHubNotification({
958
+ id: randomUUID(),
959
+ userId: memInfo.sourceUserId,
960
+ type: "resource_removed",
961
+ resource: "memory",
962
+ title: memInfo.summary || memInfo.id,
963
+ message: payload,
964
+ });
765
965
  }
766
966
  return this.json(res, 200, { ok: true });
767
967
  }
@@ -4,13 +4,24 @@ import type { Logger } from "../types";
4
4
  import type { UserInfo } from "../sharing/types";
5
5
  import type { SqliteStore } from "../storage/sqlite";
6
6
 
7
- type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null; lastIp: string; lastActiveAt: number | null };
7
+ type ManagedHubUser = UserInfo & {
8
+ tokenHash: string;
9
+ createdAt: number;
10
+ approvedAt: number | null;
11
+ lastIp: string;
12
+ lastActiveAt: number | null;
13
+ identityKey?: string;
14
+ leftAt?: number | null;
15
+ removedAt?: number | null;
16
+ rejectedAt?: number | null;
17
+ rejoinRequestedAt?: number | null;
18
+ };
8
19
 
9
20
  export class HubUserManager {
10
21
  constructor(private store: SqliteStore, private log: Logger) {}
11
22
 
12
- createPendingUser(input: { username: string; deviceName?: string }): ManagedHubUser {
13
- const user = {
23
+ createPendingUser(input: { username: string; deviceName?: string; identityKey?: string }): ManagedHubUser {
24
+ const user: ManagedHubUser = {
14
25
  id: randomUUID(),
15
26
  username: input.username,
16
27
  deviceName: input.deviceName,
@@ -22,11 +33,36 @@ export class HubUserManager {
22
33
  approvedAt: null,
23
34
  lastIp: "",
24
35
  lastActiveAt: null,
36
+ identityKey: input.identityKey || "",
25
37
  };
26
38
  this.store.upsertHubUser(user);
27
39
  return user;
28
40
  }
29
41
 
42
+ findByIdentityKey(identityKey: string): ManagedHubUser | null {
43
+ if (!identityKey) return null;
44
+ return this.store.findHubUserByIdentityKey(identityKey);
45
+ }
46
+
47
+ markUserLeft(userId: string): boolean {
48
+ this.log.info(`Hub: user "${userId}" marked as left`);
49
+ return this.store.markHubUserLeft(userId);
50
+ }
51
+
52
+ rejoinUser(userId: string): ManagedHubUser | null {
53
+ const user = this.store.getHubUser(userId);
54
+ if (!user) return null;
55
+ const updated: ManagedHubUser = {
56
+ ...user,
57
+ status: "pending" as const,
58
+ tokenHash: "",
59
+ rejoinRequestedAt: Date.now(),
60
+ };
61
+ this.store.upsertHubUser(updated);
62
+ this.log.info(`Hub: user "${userId}" (${user.username}) requested rejoin, previous status: ${user.status}`);
63
+ return updated;
64
+ }
65
+
30
66
  listPendingUsers(): ManagedHubUser[] {
31
67
  return this.store.listHubUsers("pending");
32
68
  }
@@ -105,7 +141,7 @@ export class HubUserManager {
105
141
 
106
142
  isUsernameTaken(username: string, excludeUserId?: string): boolean {
107
143
  const users = this.store.listHubUsers();
108
- return users.some(u => u.username === username && u.id !== excludeUserId);
144
+ return users.some(u => u.username === username && u.id !== excludeUserId && u.status !== "left" && u.status !== "removed");
109
145
  }
110
146
 
111
147
  updateUsername(userId: string, newUsername: string): ManagedHubUser | null {
@@ -119,10 +155,10 @@ export class HubUserManager {
119
155
  rejectUser(userId: string): ManagedHubUser | null {
120
156
  const user = this.store.getHubUser(userId);
121
157
  if (!user) return null;
122
- const updated = {
158
+ const updated: ManagedHubUser = {
123
159
  ...user,
124
160
  status: "rejected" as const,
125
- approvedAt: Date.now(),
161
+ rejectedAt: Date.now(),
126
162
  };
127
163
  this.store.upsertHubUser(updated);
128
164
  return updated;
@@ -1,6 +1,8 @@
1
+ export type ChunkKind = "paragraph" | "code_block" | "error_stack" | "list" | "command";
2
+
1
3
  export interface RawChunk {
2
4
  content: string;
3
- kind: "paragraph";
5
+ kind: ChunkKind;
4
6
  }
5
7
 
6
8
  const MAX_CHUNK_CHARS = 3000;
@@ -28,21 +30,25 @@ const COMMAND_LINE_RE = /^(?:\$|>|#)\s+.+$/gm;
28
30
  */
29
31
  export function chunkText(text: string): RawChunk[] {
30
32
  let remaining = text;
31
- const slots: Array<{ placeholder: string; content: string }> = [];
33
+ const slots: Array<{ placeholder: string; content: string; kind: ChunkKind }> = [];
32
34
  let counter = 0;
33
35
 
34
- function ph(content: string): string {
36
+ function ph(content: string, kind: ChunkKind = "paragraph"): string {
35
37
  const tag = `\x00SLOT_${counter++}\x00`;
36
- slots.push({ placeholder: tag, content: content.trim() });
38
+ slots.push({ placeholder: tag, content: content.trim(), kind });
37
39
  return tag;
38
40
  }
39
41
 
40
- remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m));
42
+ remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m, "code_block"));
41
43
  remaining = extractBraceBlocks(remaining, ph);
42
44
 
43
- const structural: RegExp[] = [ERROR_STACK_RE, LIST_BLOCK_RE, COMMAND_LINE_RE];
44
- for (const re of structural) {
45
- remaining = remaining.replace(re, (m) => ph(m));
45
+ const structuralKinds: Array<[RegExp, ChunkKind]> = [
46
+ [ERROR_STACK_RE, "error_stack"],
47
+ [LIST_BLOCK_RE, "list"],
48
+ [COMMAND_LINE_RE, "command"],
49
+ ];
50
+ for (const [re, kind] of structuralKinds) {
51
+ remaining = remaining.replace(re, (m) => ph(m, kind));
46
52
  }
47
53
 
48
54
  const raw: RawChunk[] = [];
@@ -57,7 +63,7 @@ export function chunkText(text: string): RawChunk[] {
57
63
  for (const part of parts) {
58
64
  const slot = slots.find((s) => s.placeholder === part);
59
65
  if (slot) {
60
- raw.push({ content: slot.content, kind: "paragraph" });
66
+ raw.push({ content: slot.content, kind: slot.kind });
61
67
  } else if (part.trim().length >= MIN_CHUNK_CHARS) {
62
68
  raw.push({ content: part.trim(), kind: "paragraph" });
63
69
  }
@@ -69,7 +75,7 @@ export function chunkText(text: string): RawChunk[] {
69
75
 
70
76
  for (const s of slots) {
71
77
  if (!raw.some((c) => c.content === s.content)) {
72
- raw.push({ content: s.content, kind: "paragraph" });
78
+ raw.push({ content: s.content, kind: s.kind });
73
79
  }
74
80
  }
75
81
 
@@ -85,7 +91,7 @@ export function chunkText(text: string): RawChunk[] {
85
91
  */
86
92
  function extractBraceBlocks(
87
93
  text: string,
88
- ph: (content: string) => string,
94
+ ph: (content: string, kind?: ChunkKind) => string,
89
95
  ): string {
90
96
  const lines = text.split("\n");
91
97
  const result: string[] = [];
@@ -119,7 +125,7 @@ function extractBraceBlocks(
119
125
  if (depth <= 0 || (BLOCK_CLOSE_RE.test(line) && depth <= 0)) {
120
126
  const block = blockLines.join("\n");
121
127
  if (block.trim().length >= MIN_CHUNK_CHARS) {
122
- result.push(ph(block));
128
+ result.push(ph(block, "code_block"));
123
129
  } else {
124
130
  result.push(block);
125
131
  }
@@ -135,7 +141,7 @@ function extractBraceBlocks(
135
141
  if (blockLines.length > 0) {
136
142
  const block = blockLines.join("\n");
137
143
  if (block.trim().length >= MIN_CHUNK_CHARS) {
138
- result.push(ph(block));
144
+ result.push(ph(block, "code_block"));
139
145
  } else {
140
146
  result.push(block);
141
147
  }
@@ -49,8 +49,8 @@ function normalizeEndpointForProvider(
49
49
  function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
50
50
  try {
51
51
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
52
- const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
53
- const cfgPath = path.join(ocHome, "openclaw.json");
52
+ const cfgPath = process.env.OPENCLAW_CONFIG_PATH
53
+ || path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
54
54
  if (!fs.existsSync(cfgPath)) return undefined;
55
55
 
56
56
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));