@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.
- package/.env.example +7 -0
- package/README.md +94 -27
- package/dist/capture/index.js +3 -1
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +132 -10
- package/dist/client/connector.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +251 -38
- 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/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.js +2 -2
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +2 -1
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +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 +58 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +295 -35
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +27 -8
- package/dist/telemetry.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 +796 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +11 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +456 -92
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +411 -52
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/src/capture/index.ts +4 -1
- package/src/client/connector.ts +136 -10
- package/src/config.ts +2 -1
- package/src/hub/server.ts +246 -38
- package/src/hub/user-manager.ts +42 -6
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +2 -2
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +2 -1
- 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 +326 -40
- package/src/telemetry.ts +27 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +796 -289
- package/src/viewer/server.ts +430 -89
- package/telemetry.credentials.json +5 -0
package/dist/hub/server.js
CHANGED
|
@@ -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
|
-
|
|
99
|
-
|
|
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.
|
|
119
|
+
this.server.on("error", onError);
|
|
106
120
|
this.server.once("listening", onListening);
|
|
107
|
-
this.server.listen(
|
|
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:${
|
|
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
|
-
|
|
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
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
323
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
}
|