@memtensor/memos-local-openclaw-plugin 1.0.4-beta.8 → 1.0.4

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 (99) 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 +132 -10
  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 +251 -38
  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 +96 -1
  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 +58 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +295 -35
  57. package/dist/storage/sqlite.js.map +1 -1
  58. package/dist/telemetry.d.ts.map +1 -1
  59. package/dist/telemetry.js +27 -8
  60. package/dist/telemetry.js.map +1 -1
  61. package/dist/types.d.ts +10 -0
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +4 -0
  64. package/dist/types.js.map +1 -1
  65. package/dist/viewer/html.d.ts.map +1 -1
  66. package/dist/viewer/html.js +796 -289
  67. package/dist/viewer/html.js.map +1 -1
  68. package/dist/viewer/server.d.ts +11 -0
  69. package/dist/viewer/server.d.ts.map +1 -1
  70. package/dist/viewer/server.js +456 -92
  71. package/dist/viewer/server.js.map +1 -1
  72. package/index.ts +411 -52
  73. package/openclaw.plugin.json +1 -1
  74. package/package.json +2 -1
  75. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  76. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  77. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  78. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  79. package/src/capture/index.ts +4 -1
  80. package/src/client/connector.ts +136 -10
  81. package/src/config.ts +2 -1
  82. package/src/hub/server.ts +246 -38
  83. package/src/hub/user-manager.ts +42 -6
  84. package/src/ingest/chunker.ts +19 -13
  85. package/src/ingest/providers/index.ts +2 -2
  86. package/src/recall/engine.ts +89 -1
  87. package/src/shared/llm-call.ts +2 -1
  88. package/src/sharing/types.ts +1 -1
  89. package/src/skill/evolver.ts +58 -6
  90. package/src/skill/generator.ts +44 -5
  91. package/src/skill/installer.ts +107 -4
  92. package/src/skill/upgrader.ts +139 -1
  93. package/src/skill/validator.ts +79 -0
  94. package/src/storage/sqlite.ts +326 -40
  95. package/src/telemetry.ts +27 -9
  96. package/src/types.ts +11 -0
  97. package/src/viewer/html.ts +796 -289
  98. package/src/viewer/server.ts +430 -89
  99. package/telemetry.credentials.json +5 -0
@@ -93,18 +93,32 @@ class HubServer {
93
93
  res.end(JSON.stringify({ error: message }));
94
94
  }
95
95
  });
96
+ const MAX_PORT_RETRIES = 3;
97
+ let hubPort = this.port;
96
98
  await new Promise((resolve, reject) => {
99
+ let retries = 0;
97
100
  const onError = (err) => {
98
- this.server?.off("listening", onListening);
99
- reject(err);
101
+ if (err.code === "EADDRINUSE" && retries < MAX_PORT_RETRIES) {
102
+ retries++;
103
+ hubPort = this.port + retries;
104
+ this.opts.log.warn(`Hub port ${hubPort - 1} in use, trying ${hubPort}`);
105
+ this.server.listen(hubPort, "0.0.0.0");
106
+ }
107
+ else {
108
+ this.server?.off("listening", onListening);
109
+ reject(err);
110
+ }
100
111
  };
101
112
  const onListening = () => {
102
113
  this.server?.off("error", onError);
114
+ if (hubPort !== this.port) {
115
+ this.opts.log.info(`Hub started on fallback port ${hubPort} (configured: ${this.port})`);
116
+ }
103
117
  resolve();
104
118
  };
105
- this.server.once("error", onError);
119
+ this.server.on("error", onError);
106
120
  this.server.once("listening", onListening);
107
- this.server.listen(this.port, "0.0.0.0");
121
+ this.server.listen(hubPort, "0.0.0.0");
108
122
  });
109
123
  const bootstrap = this.userManager.ensureBootstrapAdmin(this.authSecret, "admin", this.authState.bootstrapAdminUserId, this.authState.bootstrapAdminToken);
110
124
  if (bootstrap.token) {
@@ -115,7 +129,7 @@ class HubServer {
115
129
  }
116
130
  this.initOnlineTracking();
117
131
  this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
118
- return `http://127.0.0.1:${this.port}`;
132
+ return `http://127.0.0.1:${hubPort}`;
119
133
  }
120
134
  async stop() {
121
135
  if (this.offlineCheckTimer) {
@@ -124,12 +138,32 @@ class HubServer {
124
138
  }
125
139
  if (!this.server)
126
140
  return;
141
+ try {
142
+ const activeUsers = this.opts.store.listHubUsers("active");
143
+ const ownerId = this.authState.bootstrapAdminUserId || "";
144
+ for (const u of activeUsers) {
145
+ if (u.id === ownerId)
146
+ continue;
147
+ try {
148
+ this.opts.store.insertHubNotification({
149
+ id: (0, crypto_1.randomUUID)(), userId: u.id, type: "hub_shutdown",
150
+ resource: "system", title: `Team server "${this.teamName}" has been shut down by the admin.`,
151
+ });
152
+ }
153
+ catch { /* best-effort */ }
154
+ }
155
+ }
156
+ catch { /* best-effort */ }
127
157
  const server = this.server;
128
158
  this.server = undefined;
129
159
  await new Promise((resolve) => server.close(() => resolve()));
130
160
  }
131
161
  get port() {
132
- return this.opts.config.sharing?.hub?.port ?? 18800;
162
+ const configured = this.opts.config.sharing?.hub?.port;
163
+ const derived = this.opts.defaultHubPort;
164
+ if (derived && (!configured || configured === 18800))
165
+ return derived;
166
+ return configured ?? 18800;
133
167
  }
134
168
  get teamName() {
135
169
  return this.opts.config.sharing?.hub?.teamName ?? "";
@@ -173,6 +207,20 @@ class HubServer {
173
207
  this.opts.log.warn(`hub: embedding shared chunks failed: ${err}`);
174
208
  });
175
209
  }
210
+ embedSkillAsync(skillId, name, description, sourceUserId, sourceSkillId) {
211
+ const embedder = this.opts.embedder;
212
+ if (!embedder)
213
+ return;
214
+ const text = `${name}: ${description}`;
215
+ embedder.embed([text]).then((vectors) => {
216
+ if (vectors[0]) {
217
+ this.opts.store.upsertHubSkillEmbedding(skillId, Array.from(vectors[0]), sourceUserId, sourceSkillId);
218
+ this.opts.log.info(`hub: embedded shared skill ${skillId}`);
219
+ }
220
+ }).catch((err) => {
221
+ this.opts.log.warn(`hub: embedding shared skill failed: ${err}`);
222
+ });
223
+ }
176
224
  embedMemoryAsync(memoryId, summary, content) {
177
225
  const embedder = this.opts.embedder;
178
226
  if (!embedder)
@@ -207,8 +255,14 @@ class HubServer {
207
255
  || req.headers["x-client-ip"]?.trim()
208
256
  || req.headers["x-forwarded-for"]?.split(",")[0]?.trim()
209
257
  || req.socket.remoteAddress || "";
210
- const existingUsers = this.opts.store.listHubUsers();
211
- const existingUser = existingUsers.find(u => u.username === username);
258
+ const identityKey = typeof body.identityKey === "string" ? body.identityKey.trim() : "";
259
+ let existingUser = identityKey
260
+ ? this.userManager.findByIdentityKey(identityKey)
261
+ : null;
262
+ if (!existingUser) {
263
+ const existingUsers = this.opts.store.listHubUsers();
264
+ existingUser = existingUsers.find(u => u.username === username && u.status !== "left" && u.status !== "removed") ?? null;
265
+ }
212
266
  if (existingUser) {
213
267
  try {
214
268
  this.opts.store.updateHubUserActivity(existingUser.id, joinIp);
@@ -217,20 +271,45 @@ class HubServer {
217
271
  if (existingUser.status === "active") {
218
272
  const token = (0, auth_1.issueUserToken)({ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" }, this.authSecret);
219
273
  this.userManager.approveUser(existingUser.id, token);
220
- return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
274
+ if (identityKey && !existingUser.identityKey) {
275
+ this.opts.store.upsertHubUser({ ...existingUser, identityKey });
276
+ }
277
+ return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token, identityKey: existingUser.identityKey || identityKey });
221
278
  }
222
279
  if (existingUser.status === "pending") {
223
- return this.json(res, 200, { status: "pending", userId: existingUser.id });
280
+ this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
281
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
224
282
  }
225
283
  if (existingUser.status === "rejected") {
226
- this.userManager.resetToPending(existingUser.id);
227
- this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
228
- return this.json(res, 200, { status: "pending", userId: existingUser.id });
284
+ if (body.reapply === true) {
285
+ this.userManager.resetToPending(existingUser.id);
286
+ this.notifyAdmins("user_join_request", "user", username, "");
287
+ this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
288
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
289
+ }
290
+ return this.json(res, 200, { status: "rejected", userId: existingUser.id });
291
+ }
292
+ if (existingUser.status === "removed") {
293
+ this.userManager.rejoinUser(existingUser.id);
294
+ this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
295
+ this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
296
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
297
+ }
298
+ if (existingUser.status === "left") {
299
+ this.userManager.rejoinUser(existingUser.id);
300
+ this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
301
+ this.opts.log.info(`Hub: left user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
302
+ return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
303
+ }
304
+ if (existingUser.status === "blocked") {
305
+ return this.json(res, 200, { status: "blocked", userId: existingUser.id });
229
306
  }
230
307
  }
308
+ const generatedIdentityKey = identityKey || (0, crypto_1.randomUUID)();
231
309
  const user = this.userManager.createPendingUser({
232
310
  username,
233
311
  deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
312
+ identityKey: generatedIdentityKey,
234
313
  });
235
314
  try {
236
315
  this.opts.store.updateHubUserActivity(user.id, joinIp);
@@ -238,7 +317,7 @@ class HubServer {
238
317
  catch { /* best-effort */ }
239
318
  this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
240
319
  this.notifyAdmins("user_join_request", "user", username, "");
241
- return this.json(res, 200, { status: "pending", userId: user.id });
320
+ return this.json(res, 200, { status: "pending", userId: user.id, identityKey: generatedIdentityKey });
242
321
  }
243
322
  if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
244
323
  const body = await this.readJson(req);
@@ -257,12 +336,39 @@ class HubServer {
257
336
  if (user.status === "rejected") {
258
337
  return this.json(res, 200, { status: "rejected" });
259
338
  }
339
+ if (user.status === "blocked") {
340
+ return this.json(res, 200, { status: "blocked" });
341
+ }
342
+ if (user.status === "left") {
343
+ return this.json(res, 200, { status: "left" });
344
+ }
345
+ if (user.status === "removed") {
346
+ return this.json(res, 200, { status: "removed" });
347
+ }
260
348
  if (user.status === "active") {
261
349
  const token = (0, auth_1.issueUserToken)({ userId: user.id, username: user.username, role: user.role, status: user.status }, this.authSecret);
350
+ this.userManager.approveUser(user.id, token);
262
351
  return this.json(res, 200, { status: "active", userToken: token });
263
352
  }
264
353
  return this.json(res, 200, { status: user.status });
265
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)
362
+ return this.json(res, 400, { error: "missing_user_id" });
363
+ const user = this.opts.store.getHubUser(userId);
364
+ if (!user)
365
+ return this.json(res, 200, { ok: true });
366
+ if (user.status === "pending") {
367
+ this.userManager.markUserLeft(userId);
368
+ this.opts.log.info(`Hub: user "${user.username}" (${userId}) withdrew pending application`);
369
+ }
370
+ return this.json(res, 200, { ok: true });
371
+ }
266
372
  // All endpoints below require authentication + rate limiting
267
373
  const auth = this.authenticate(req);
268
374
  if (!auth)
@@ -275,13 +381,10 @@ class HubServer {
275
381
  return this.json(res, 200, { ok: true });
276
382
  }
277
383
  if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
278
- try {
279
- this.opts.store.updateHubUserActivity(auth.userId, "", 0);
280
- }
281
- catch { /* best-effort */ }
384
+ this.userManager.markUserLeft(auth.userId);
282
385
  this.knownOnlineUsers.delete(auth.userId);
283
- this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
284
- this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
386
+ this.notifyAdmins("user_left", "user", auth.username, auth.userId);
387
+ this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, status set to "left"`);
285
388
  return this.json(res, 200, { ok: true });
286
389
  }
287
390
  if (req.method === "GET" && routePath === "/api/v1/hub/me") {
@@ -307,6 +410,10 @@ class HubServer {
307
410
  const ttlMs = updated.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
308
411
  const newToken = (0, auth_1.issueUserToken)({ userId: updated.id, username: newUsername, role: updated.role, status: updated.status }, this.authSecret, ttlMs);
309
412
  this.userManager.approveUser(updated.id, newToken);
413
+ if (updated.id === this.authState.bootstrapAdminUserId) {
414
+ this.authState.bootstrapAdminToken = newToken;
415
+ this.saveAuthState();
416
+ }
310
417
  this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
311
418
  return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
312
419
  }
@@ -319,12 +426,21 @@ class HubServer {
319
426
  if (auth.role !== "admin")
320
427
  return this.json(res, 403, { error: "forbidden" });
321
428
  const body = await this.readJson(req);
322
- const token = (0, auth_1.issueUserToken)({ userId: String(body.userId), username: String(body.username || ""), role: "member", status: "active" }, this.authSecret);
323
- const approved = this.userManager.approveUser(String(body.userId), token);
429
+ const userId = String(body.userId);
430
+ const username = String(body.username || "");
431
+ const token = (0, auth_1.issueUserToken)({ userId, username, role: "member", status: "active" }, this.authSecret);
432
+ const approved = this.userManager.approveUser(userId, token);
324
433
  if (!approved)
325
434
  return this.json(res, 404, { error: "not_found" });
326
435
  try {
327
- this.opts.store.updateHubUserActivity(String(body.userId), "");
436
+ this.opts.store.updateHubUserActivity(userId, "");
437
+ }
438
+ catch { /* best-effort */ }
439
+ try {
440
+ this.opts.store.insertHubNotification({
441
+ id: (0, crypto_1.randomUUID)(), userId, type: "membership_approved",
442
+ resource: "user", title: `Your request to join team "${this.teamName}" has been approved. Welcome!`,
443
+ });
328
444
  }
329
445
  catch { /* best-effort */ }
330
446
  return this.json(res, 200, { status: "active", token });
@@ -333,9 +449,17 @@ class HubServer {
333
449
  if (auth.role !== "admin")
334
450
  return this.json(res, 403, { error: "forbidden" });
335
451
  const body = await this.readJson(req);
336
- const rejected = this.userManager.rejectUser(String(body.userId));
452
+ const userId = String(body.userId);
453
+ const rejected = this.userManager.rejectUser(userId);
337
454
  if (!rejected)
338
455
  return this.json(res, 404, { error: "not_found" });
456
+ try {
457
+ this.opts.store.insertHubNotification({
458
+ id: (0, crypto_1.randomUUID)(), userId, type: "membership_rejected",
459
+ resource: "user", title: `Your request to join team "${this.teamName}" has been declined.`,
460
+ });
461
+ }
462
+ catch { /* best-effort */ }
339
463
  return this.json(res, 200, { status: "rejected" });
340
464
  }
341
465
  if (req.method === "GET" && routePath === "/api/v1/hub/admin/users") {
@@ -374,6 +498,14 @@ class HubServer {
374
498
  const updatedUser = { ...user, role: newRole };
375
499
  this.opts.store.upsertHubUser(updatedUser);
376
500
  this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
501
+ try {
502
+ const notifType = newRole === "admin" ? "role_promoted" : "role_demoted";
503
+ this.opts.store.insertHubNotification({
504
+ id: (0, crypto_1.randomUUID)(), userId, type: notifType,
505
+ resource: "user", title: `Your role in team "${this.teamName}" has been changed to ${newRole}.`,
506
+ });
507
+ }
508
+ catch { /* best-effort */ }
377
509
  return this.json(res, 200, { ok: true, role: newRole });
378
510
  }
379
511
  if (req.method === "POST" && routePath === "/api/v1/hub/admin/rename-user") {
@@ -397,6 +529,10 @@ class HubServer {
397
529
  const updated = this.opts.store.getHubUser(userId);
398
530
  const finalUser = { ...updated, username: newUsername };
399
531
  this.opts.store.upsertHubUser(finalUser);
532
+ if (userId === this.authState.bootstrapAdminUserId) {
533
+ this.authState.bootstrapAdminToken = newToken;
534
+ this.saveAuthState();
535
+ }
400
536
  this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
401
537
  return this.json(res, 200, { ok: true, username: newUsername });
402
538
  }
@@ -411,10 +547,18 @@ class HubServer {
411
547
  return this.json(res, 400, { error: "cannot_remove_self" });
412
548
  if (userId === this.authState.bootstrapAdminUserId)
413
549
  return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
550
+ try {
551
+ this.opts.store.insertHubNotification({
552
+ id: (0, crypto_1.randomUUID)(), userId, type: "membership_removed",
553
+ resource: "user", title: `You have been removed from team "${this.teamName}" by the admin.`,
554
+ });
555
+ }
556
+ catch { /* best-effort */ }
414
557
  const cleanResources = body?.cleanResources === true;
415
558
  const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
416
559
  if (!deleted)
417
560
  return this.json(res, 404, { error: "not_found" });
561
+ this.knownOnlineUsers.delete(userId);
418
562
  this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
419
563
  return this.json(res, 200, { ok: true });
420
564
  }
@@ -606,19 +750,73 @@ class HubServer {
606
750
  return this.json(res, 200, { hits, meta: { totalCandidates: hits.length, searchedGroups: [], includedPublic: true } });
607
751
  }
608
752
  if (req.method === "GET" && routePath === "/api/v1/hub/skills") {
609
- const hits = this.opts.store.searchHubSkills(String(url.searchParams.get("query") || ""), {
753
+ const skillQuery = String(url.searchParams.get("query") || "");
754
+ const skillMaxResults = Number(url.searchParams.get("maxResults") || 10);
755
+ const ftsSkillHits = this.opts.store.searchHubSkills(skillQuery, {
610
756
  userId: auth.userId,
611
- maxResults: Number(url.searchParams.get("maxResults") || 10),
612
- }).map(({ hit }) => ({
613
- skillId: hit.id,
614
- name: hit.name,
615
- description: hit.description,
616
- version: hit.version,
617
- visibility: hit.visibility,
618
- groupName: hit.group_name,
619
- ownerName: hit.owner_name || "unknown",
620
- qualityScore: hit.quality_score,
621
- }));
757
+ maxResults: skillMaxResults * 2,
758
+ });
759
+ let mergedSkillIds;
760
+ if (this.opts.embedder && skillQuery) {
761
+ try {
762
+ const [queryVec] = await this.opts.embedder.embed([skillQuery]);
763
+ if (queryVec) {
764
+ const skillEmbs = this.opts.store.getVisibleHubSkillEmbeddings();
765
+ const cosineSim = (vec) => {
766
+ let dot = 0, nA = 0, nB = 0;
767
+ for (let i = 0; i < queryVec.length && i < vec.length; i++) {
768
+ dot += queryVec[i] * vec[i];
769
+ nA += queryVec[i] * queryVec[i];
770
+ nB += vec[i] * vec[i];
771
+ }
772
+ return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
773
+ };
774
+ const vecScored = skillEmbs
775
+ .map(e => ({ id: e.skillId, score: cosineSim(e.vector) }))
776
+ .filter(e => e.score > 0.3)
777
+ .sort((a, b) => b.score - a.score)
778
+ .slice(0, skillMaxResults * 2);
779
+ const K = 60;
780
+ const rrfScores = new Map();
781
+ ftsSkillHits.forEach(({ hit }, idx) => {
782
+ rrfScores.set(hit.id, (rrfScores.get(hit.id) ?? 0) + 1 / (K + idx + 1));
783
+ });
784
+ vecScored.forEach(({ id }, idx) => {
785
+ rrfScores.set(id, (rrfScores.get(id) ?? 0) + 1 / (K + idx + 1));
786
+ });
787
+ mergedSkillIds = [...rrfScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, skillMaxResults).map(([id]) => id);
788
+ }
789
+ else {
790
+ mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
791
+ }
792
+ }
793
+ catch {
794
+ mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
795
+ }
796
+ }
797
+ else {
798
+ mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
799
+ }
800
+ const ftsSkillMap = new Map(ftsSkillHits.map(({ hit }) => [hit.id, hit]));
801
+ const hits = mergedSkillIds.map(id => {
802
+ const hit = ftsSkillMap.get(id);
803
+ if (hit) {
804
+ return {
805
+ skillId: hit.id, name: hit.name, description: hit.description,
806
+ version: hit.version, visibility: hit.visibility, groupName: hit.group_name,
807
+ ownerName: hit.owner_name || "unknown", ownerStatus: hit.owner_status || "",
808
+ qualityScore: hit.quality_score,
809
+ };
810
+ }
811
+ const skill = this.opts.store.getHubSkillById(id);
812
+ if (!skill)
813
+ return null;
814
+ return {
815
+ skillId: skill.id, name: skill.name, description: skill.description,
816
+ version: skill.version, visibility: skill.visibility, groupName: "",
817
+ ownerName: "unknown", ownerStatus: "", qualityScore: skill.qualityScore,
818
+ };
819
+ }).filter(Boolean);
622
820
  return this.json(res, 200, { hits });
623
821
  }
624
822
  if (req.method === "POST" && routePath === "/api/v1/hub/skills/publish") {
@@ -644,6 +842,7 @@ class HubServer {
644
842
  createdAt: existing?.createdAt ?? Date.now(),
645
843
  updatedAt: Date.now(),
646
844
  });
845
+ this.embedSkillAsync(skillId, String(metadata.name || sourceSkillId), String(metadata.description || ""), auth.userId, sourceSkillId);
647
846
  if (!existing) {
648
847
  this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
649
848
  }
@@ -766,7 +965,18 @@ class HubServer {
766
965
  if (!deleted)
767
966
  return this.json(res, 404, { error: "not_found" });
768
967
  if (memInfo) {
769
- this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: memInfo.sourceUserId, type: "resource_removed", resource: "memory", title: memInfo.summary || memInfo.id });
968
+ const payload = JSON.stringify({
969
+ memoryId,
970
+ sourceChunkId: memInfo.sourceChunkId,
971
+ });
972
+ this.opts.store.insertHubNotification({
973
+ id: (0, crypto_1.randomUUID)(),
974
+ userId: memInfo.sourceUserId,
975
+ type: "resource_removed",
976
+ resource: "memory",
977
+ title: memInfo.summary || memInfo.id,
978
+ message: payload,
979
+ });
770
980
  }
771
981
  return this.json(res, 200, { ok: true });
772
982
  }
@@ -810,10 +1020,13 @@ class HubServer {
810
1020
  }
811
1021
  return this.json(res, 404, { error: "not_found" });
812
1022
  }
813
- notifyAdmins(type, resource, title, fromUserId) {
1023
+ notifyAdmins(type, resource, title, fromUserId, opts) {
814
1024
  try {
815
1025
  const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
816
1026
  for (const admin of admins) {
1027
+ if (opts?.dedup && this.opts.store.hasRecentHubNotification(admin.id, type, resource, opts.deduoWindowMs ?? 300_000)) {
1028
+ continue;
1029
+ }
817
1030
  this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: admin.id, type, resource, title });
818
1031
  }
819
1032
  }