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

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 (143) hide show
  1. package/README.md +38 -21
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +29 -3
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +29 -0
  7. package/dist/client/connector.d.ts.map +1 -0
  8. package/dist/client/connector.js +231 -0
  9. package/dist/client/connector.js.map +1 -0
  10. package/dist/client/hub.d.ts +61 -0
  11. package/dist/client/hub.d.ts.map +1 -0
  12. package/dist/client/hub.js +170 -0
  13. package/dist/client/hub.js.map +1 -0
  14. package/dist/client/skill-sync.d.ts +36 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -0
  16. package/dist/client/skill-sync.js +226 -0
  17. package/dist/client/skill-sync.js.map +1 -0
  18. package/dist/config.d.ts +2 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +70 -3
  21. package/dist/config.js.map +1 -1
  22. package/dist/embedding/index.d.ts +4 -2
  23. package/dist/embedding/index.d.ts.map +1 -1
  24. package/dist/embedding/index.js +17 -1
  25. package/dist/embedding/index.js.map +1 -1
  26. package/dist/hub/auth.d.ts +19 -0
  27. package/dist/hub/auth.d.ts.map +1 -0
  28. package/dist/hub/auth.js +70 -0
  29. package/dist/hub/auth.js.map +1 -0
  30. package/dist/hub/server.d.ts +48 -0
  31. package/dist/hub/server.d.ts.map +1 -0
  32. package/dist/hub/server.js +922 -0
  33. package/dist/hub/server.js.map +1 -0
  34. package/dist/hub/user-manager.d.ts +31 -0
  35. package/dist/hub/user-manager.d.ts.map +1 -0
  36. package/dist/hub/user-manager.js +129 -0
  37. package/dist/hub/user-manager.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +8 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/ingest/providers/index.d.ts +10 -2
  43. package/dist/ingest/providers/index.d.ts.map +1 -1
  44. package/dist/ingest/providers/index.js +203 -6
  45. package/dist/ingest/providers/index.js.map +1 -1
  46. package/dist/ingest/providers/openai.d.ts +1 -0
  47. package/dist/ingest/providers/openai.d.ts.map +1 -1
  48. package/dist/ingest/providers/openai.js +1 -0
  49. package/dist/ingest/providers/openai.js.map +1 -1
  50. package/dist/ingest/task-processor.js +1 -1
  51. package/dist/ingest/task-processor.js.map +1 -1
  52. package/dist/openclaw-api.d.ts +53 -0
  53. package/dist/openclaw-api.d.ts.map +1 -0
  54. package/dist/openclaw-api.js +189 -0
  55. package/dist/openclaw-api.js.map +1 -0
  56. package/dist/recall/engine.js +1 -1
  57. package/dist/recall/engine.js.map +1 -1
  58. package/dist/shared/llm-call.d.ts +4 -1
  59. package/dist/shared/llm-call.d.ts.map +1 -1
  60. package/dist/shared/llm-call.js +14 -1
  61. package/dist/shared/llm-call.js.map +1 -1
  62. package/dist/sharing/types.contract.d.ts +2 -0
  63. package/dist/sharing/types.contract.d.ts.map +1 -0
  64. package/dist/sharing/types.contract.js +3 -0
  65. package/dist/sharing/types.contract.js.map +1 -0
  66. package/dist/sharing/types.d.ts +80 -0
  67. package/dist/sharing/types.d.ts.map +1 -0
  68. package/dist/sharing/types.js +3 -0
  69. package/dist/sharing/types.js.map +1 -0
  70. package/dist/skill/evaluator.d.ts.map +1 -1
  71. package/dist/skill/evaluator.js +2 -2
  72. package/dist/skill/evaluator.js.map +1 -1
  73. package/dist/skill/generator.d.ts.map +1 -1
  74. package/dist/skill/generator.js +4 -4
  75. package/dist/skill/generator.js.map +1 -1
  76. package/dist/skill/upgrader.js +1 -1
  77. package/dist/skill/upgrader.js.map +1 -1
  78. package/dist/skill/validator.js +1 -1
  79. package/dist/skill/validator.js.map +1 -1
  80. package/dist/storage/ensure-binding.d.ts.map +1 -1
  81. package/dist/storage/ensure-binding.js +3 -1
  82. package/dist/storage/ensure-binding.js.map +1 -1
  83. package/dist/storage/sqlite.d.ts +332 -1
  84. package/dist/storage/sqlite.d.ts.map +1 -1
  85. package/dist/storage/sqlite.js +913 -4
  86. package/dist/storage/sqlite.js.map +1 -1
  87. package/dist/tools/index.d.ts +1 -0
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +3 -1
  90. package/dist/tools/index.js.map +1 -1
  91. package/dist/tools/memory-search.d.ts +5 -2
  92. package/dist/tools/memory-search.d.ts.map +1 -1
  93. package/dist/tools/memory-search.js +50 -7
  94. package/dist/tools/memory-search.js.map +1 -1
  95. package/dist/tools/network-memory-detail.d.ts +4 -0
  96. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  97. package/dist/tools/network-memory-detail.js +34 -0
  98. package/dist/tools/network-memory-detail.js.map +1 -0
  99. package/dist/types.d.ts +48 -2
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/types.js.map +1 -1
  102. package/dist/viewer/html.d.ts.map +1 -1
  103. package/dist/viewer/html.js +4299 -511
  104. package/dist/viewer/html.js.map +1 -1
  105. package/dist/viewer/server.d.ts +65 -0
  106. package/dist/viewer/server.d.ts.map +1 -1
  107. package/dist/viewer/server.js +1844 -90
  108. package/dist/viewer/server.js.map +1 -1
  109. package/index.ts +767 -41
  110. package/openclaw.plugin.json +3 -2
  111. package/package.json +3 -3
  112. package/scripts/postinstall.cjs +282 -45
  113. package/skill/memos-memory-guide/SKILL.md +82 -20
  114. package/src/capture/index.ts +30 -2
  115. package/src/client/connector.ts +225 -0
  116. package/src/client/hub.ts +207 -0
  117. package/src/client/skill-sync.ts +216 -0
  118. package/src/config.ts +92 -3
  119. package/src/embedding/index.ts +21 -1
  120. package/src/hub/auth.ts +78 -0
  121. package/src/hub/server.ts +906 -0
  122. package/src/hub/user-manager.ts +143 -0
  123. package/src/index.ts +13 -5
  124. package/src/ingest/providers/index.ts +240 -6
  125. package/src/ingest/providers/openai.ts +1 -1
  126. package/src/ingest/task-processor.ts +1 -1
  127. package/src/openclaw-api.ts +287 -0
  128. package/src/recall/engine.ts +1 -1
  129. package/src/shared/llm-call.ts +18 -2
  130. package/src/sharing/types.contract.ts +40 -0
  131. package/src/sharing/types.ts +102 -0
  132. package/src/skill/evaluator.ts +3 -2
  133. package/src/skill/generator.ts +6 -4
  134. package/src/skill/upgrader.ts +1 -1
  135. package/src/skill/validator.ts +1 -1
  136. package/src/storage/ensure-binding.ts +3 -1
  137. package/src/storage/sqlite.ts +1164 -4
  138. package/src/tools/index.ts +1 -0
  139. package/src/tools/memory-search.ts +58 -8
  140. package/src/tools/network-memory-detail.ts +34 -0
  141. package/src/types.ts +43 -2
  142. package/src/viewer/html.ts +4299 -511
  143. package/src/viewer/server.ts +1688 -73
@@ -0,0 +1,922 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HubServer = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const http = __importStar(require("http"));
39
+ const path = __importStar(require("path"));
40
+ const crypto_1 = require("crypto");
41
+ const auth_1 = require("./auth");
42
+ const user_manager_1 = require("./user-manager");
43
+ class HubServer {
44
+ opts;
45
+ server;
46
+ remoteHitMap = new Map();
47
+ userManager;
48
+ authStatePath;
49
+ authState;
50
+ static RATE_WINDOW_MS = 60_000;
51
+ static RATE_LIMIT_DEFAULT = 60;
52
+ static RATE_LIMIT_SEARCH = 30;
53
+ rateBuckets = new Map();
54
+ static OFFLINE_THRESHOLD_MS = 2 * 60 * 1000;
55
+ static OFFLINE_CHECK_INTERVAL_MS = 30 * 1000;
56
+ offlineCheckTimer;
57
+ knownOnlineUsers = new Set();
58
+ constructor(opts) {
59
+ this.opts = opts;
60
+ this.userManager = new user_manager_1.HubUserManager(opts.store, opts.log);
61
+ this.authStatePath = path.join(opts.dataDir, "hub-auth.json");
62
+ this.authState = this.loadAuthState();
63
+ }
64
+ checkRateLimit(userId, endpoint) {
65
+ const key = `${userId}:${endpoint}`;
66
+ const now = Date.now();
67
+ const limit = endpoint === "search" ? HubServer.RATE_LIMIT_SEARCH : HubServer.RATE_LIMIT_DEFAULT;
68
+ const bucket = this.rateBuckets.get(key);
69
+ if (!bucket || now - bucket.windowStart > HubServer.RATE_WINDOW_MS) {
70
+ this.rateBuckets.set(key, { count: 1, windowStart: now });
71
+ return true;
72
+ }
73
+ bucket.count++;
74
+ return bucket.count <= limit;
75
+ }
76
+ async start() {
77
+ if (!this.teamToken) {
78
+ throw new Error("team token is required to start hub mode");
79
+ }
80
+ if (this.server?.listening) {
81
+ return `http://127.0.0.1:${this.port}`;
82
+ }
83
+ this.server = http.createServer(async (req, res) => {
84
+ try {
85
+ await this.handle(req, res);
86
+ }
87
+ catch (err) {
88
+ const code = err?.statusCode ?? 500;
89
+ const message = code === 413 ? "request_body_too_large" : "internal_error";
90
+ this.opts.log.warn(`hub server error: ${String(err)}`);
91
+ res.statusCode = code;
92
+ res.setHeader("content-type", "application/json");
93
+ res.end(JSON.stringify({ error: message }));
94
+ }
95
+ });
96
+ await new Promise((resolve, reject) => {
97
+ const onError = (err) => {
98
+ this.server?.off("listening", onListening);
99
+ reject(err);
100
+ };
101
+ const onListening = () => {
102
+ this.server?.off("error", onError);
103
+ resolve();
104
+ };
105
+ this.server.once("error", onError);
106
+ this.server.once("listening", onListening);
107
+ this.server.listen(this.port, "0.0.0.0");
108
+ });
109
+ const bootstrap = this.userManager.ensureBootstrapAdmin(this.authSecret, "admin", this.authState.bootstrapAdminUserId, this.authState.bootstrapAdminToken);
110
+ if (bootstrap.token) {
111
+ this.authState.bootstrapAdminUserId = bootstrap.user.id;
112
+ this.authState.bootstrapAdminToken = bootstrap.token;
113
+ this.saveAuthState();
114
+ this.opts.log.info(`memos-local: bootstrap admin token persisted to ${this.authStatePath}`);
115
+ }
116
+ this.initOnlineTracking();
117
+ this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
118
+ return `http://127.0.0.1:${this.port}`;
119
+ }
120
+ async stop() {
121
+ if (this.offlineCheckTimer) {
122
+ clearInterval(this.offlineCheckTimer);
123
+ this.offlineCheckTimer = undefined;
124
+ }
125
+ if (!this.server)
126
+ return;
127
+ const server = this.server;
128
+ this.server = undefined;
129
+ await new Promise((resolve) => server.close(() => resolve()));
130
+ }
131
+ get port() {
132
+ return this.opts.config.sharing?.hub?.port ?? 18800;
133
+ }
134
+ get teamName() {
135
+ return this.opts.config.sharing?.hub?.teamName ?? "";
136
+ }
137
+ get teamToken() {
138
+ return this.opts.config.sharing?.hub?.teamToken ?? "";
139
+ }
140
+ get authSecret() {
141
+ return this.authState.authSecret;
142
+ }
143
+ loadAuthState() {
144
+ try {
145
+ const raw = fs.readFileSync(this.authStatePath, "utf8");
146
+ const parsed = JSON.parse(raw);
147
+ if (parsed.authSecret)
148
+ return parsed;
149
+ }
150
+ catch { }
151
+ const initial = { authSecret: (0, crypto_1.randomBytes)(32).toString("hex") };
152
+ fs.mkdirSync(path.dirname(this.authStatePath), { recursive: true });
153
+ fs.writeFileSync(this.authStatePath, JSON.stringify(initial, null, 2), "utf8");
154
+ return initial;
155
+ }
156
+ saveAuthState() {
157
+ fs.mkdirSync(path.dirname(this.authStatePath), { recursive: true });
158
+ fs.writeFileSync(this.authStatePath, JSON.stringify(this.authState, null, 2), "utf8");
159
+ }
160
+ embedChunksAsync(chunkIds, chunks) {
161
+ const embedder = this.opts.embedder;
162
+ if (!embedder)
163
+ return;
164
+ const texts = chunks.map(c => c.summary || (c.content ? c.content.slice(0, 500) : ""));
165
+ embedder.embed(texts).then((vectors) => {
166
+ for (let i = 0; i < vectors.length; i++) {
167
+ if (vectors[i]) {
168
+ this.opts.store.upsertHubEmbedding(chunkIds[i], new Float32Array(vectors[i]));
169
+ }
170
+ }
171
+ this.opts.log.info(`hub: embedded ${vectors.filter(Boolean).length}/${chunkIds.length} shared chunks`);
172
+ }).catch((err) => {
173
+ this.opts.log.warn(`hub: embedding shared chunks failed: ${err}`);
174
+ });
175
+ }
176
+ embedMemoryAsync(memoryId, summary, content) {
177
+ const embedder = this.opts.embedder;
178
+ if (!embedder)
179
+ return;
180
+ const text = summary || content.slice(0, 500);
181
+ embedder.embed([text]).then((vectors) => {
182
+ if (vectors[0]) {
183
+ this.opts.store.upsertHubMemoryEmbedding(memoryId, new Float32Array(vectors[0]));
184
+ this.opts.log.info(`hub: embedded shared memory ${memoryId}`);
185
+ }
186
+ }).catch((err) => {
187
+ this.opts.log.warn(`hub: embedding shared memory failed: ${err}`);
188
+ });
189
+ }
190
+ async handle(req, res) {
191
+ const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
192
+ const routePath = url.pathname;
193
+ if (req.method === "GET" && routePath === "/api/v1/hub/info") {
194
+ return this.json(res, 200, {
195
+ teamName: this.teamName,
196
+ version: "0.0.0",
197
+ apiVersion: "v1",
198
+ });
199
+ }
200
+ if (req.method === "POST" && routePath === "/api/v1/hub/join") {
201
+ const body = await this.readJson(req);
202
+ if (!body || body.teamToken !== this.teamToken) {
203
+ return this.json(res, 403, { error: "invalid_team_token" });
204
+ }
205
+ const username = String(body.username || `user-${(0, crypto_1.randomUUID)().slice(0, 8)}`);
206
+ const joinIp = (typeof body.clientIp === "string" && body.clientIp)
207
+ || req.headers["x-client-ip"]?.trim()
208
+ || req.headers["x-forwarded-for"]?.split(",")[0]?.trim()
209
+ || req.socket.remoteAddress || "";
210
+ const existingUsers = this.opts.store.listHubUsers();
211
+ const existingUser = existingUsers.find(u => u.username === username);
212
+ if (existingUser) {
213
+ try {
214
+ this.opts.store.updateHubUserActivity(existingUser.id, joinIp);
215
+ }
216
+ catch { /* best-effort */ }
217
+ if (existingUser.status === "active") {
218
+ const token = (0, auth_1.issueUserToken)({ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" }, this.authSecret);
219
+ this.userManager.approveUser(existingUser.id, token);
220
+ return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
221
+ }
222
+ if (existingUser.status === "pending") {
223
+ return this.json(res, 200, { status: "pending", userId: existingUser.id });
224
+ }
225
+ 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 });
229
+ }
230
+ }
231
+ const user = this.userManager.createPendingUser({
232
+ username,
233
+ deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
234
+ });
235
+ try {
236
+ this.opts.store.updateHubUserActivity(user.id, joinIp);
237
+ }
238
+ catch { /* best-effort */ }
239
+ this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
240
+ this.notifyAdmins("user_join_request", "user", username, "");
241
+ return this.json(res, 200, { status: "pending", userId: user.id });
242
+ }
243
+ if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
244
+ const body = await this.readJson(req);
245
+ if (!body || body.teamToken !== this.teamToken) {
246
+ return this.json(res, 403, { error: "invalid_team_token" });
247
+ }
248
+ const userId = String(body.userId || "");
249
+ if (!userId)
250
+ return this.json(res, 400, { error: "missing_user_id" });
251
+ const user = this.opts.store.getHubUser(userId);
252
+ if (!user)
253
+ return this.json(res, 404, { error: "not_found" });
254
+ if (user.status === "pending") {
255
+ return this.json(res, 200, { status: "pending" });
256
+ }
257
+ if (user.status === "rejected") {
258
+ return this.json(res, 200, { status: "rejected" });
259
+ }
260
+ if (user.status === "active") {
261
+ const token = (0, auth_1.issueUserToken)({ userId: user.id, username: user.username, role: user.role, status: user.status }, this.authSecret);
262
+ return this.json(res, 200, { status: "active", userToken: token });
263
+ }
264
+ return this.json(res, 200, { status: user.status });
265
+ }
266
+ // All endpoints below require authentication + rate limiting
267
+ const auth = this.authenticate(req);
268
+ if (!auth)
269
+ return this.json(res, 401, { error: "unauthorized" });
270
+ const endpointKey = routePath.replace(/^\/api\/v1\/hub\//, "").replace(/\/[^/]+\/bundle$/, "/bundle");
271
+ if (!this.checkRateLimit(auth.userId, endpointKey)) {
272
+ return this.json(res, 429, { error: "rate_limit_exceeded", retryAfterMs: HubServer.RATE_WINDOW_MS });
273
+ }
274
+ if (req.method === "POST" && routePath === "/api/v1/hub/heartbeat") {
275
+ return this.json(res, 200, { ok: true });
276
+ }
277
+ if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
278
+ try {
279
+ this.opts.store.updateHubUserActivity(auth.userId, "", 0);
280
+ }
281
+ catch { /* best-effort */ }
282
+ 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`);
285
+ return this.json(res, 200, { ok: true });
286
+ }
287
+ if (req.method === "GET" && routePath === "/api/v1/hub/me") {
288
+ const user = this.opts.store.getHubUser(auth.userId);
289
+ if (!user)
290
+ return this.json(res, 401, { error: "unauthorized" });
291
+ return this.json(res, 200, user);
292
+ }
293
+ if (req.method === "POST" && routePath === "/api/v1/hub/me/update-profile") {
294
+ const body = await this.readJson(req);
295
+ if (!body)
296
+ return this.json(res, 400, { error: "invalid_body" });
297
+ const newUsername = String(body.username || "").trim();
298
+ if (!newUsername || newUsername.length < 2 || newUsername.length > 32) {
299
+ return this.json(res, 400, { error: "invalid_username", message: "Username must be 2-32 characters" });
300
+ }
301
+ if (this.userManager.isUsernameTaken(newUsername, auth.userId)) {
302
+ return this.json(res, 409, { error: "username_taken", message: "Username already in use" });
303
+ }
304
+ const updated = this.userManager.updateUsername(auth.userId, newUsername);
305
+ if (!updated)
306
+ return this.json(res, 404, { error: "not_found" });
307
+ const ttlMs = updated.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
308
+ const newToken = (0, auth_1.issueUserToken)({ userId: updated.id, username: newUsername, role: updated.role, status: updated.status }, this.authSecret, ttlMs);
309
+ this.userManager.approveUser(updated.id, newToken);
310
+ this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
311
+ return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
312
+ }
313
+ if (req.method === "GET" && routePath === "/api/v1/hub/admin/pending-users") {
314
+ if (auth.role !== "admin")
315
+ return this.json(res, 403, { error: "forbidden" });
316
+ return this.json(res, 200, { users: this.userManager.listPendingUsers() });
317
+ }
318
+ if (req.method === "POST" && routePath === "/api/v1/hub/admin/approve-user") {
319
+ if (auth.role !== "admin")
320
+ return this.json(res, 403, { error: "forbidden" });
321
+ 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);
324
+ if (!approved)
325
+ return this.json(res, 404, { error: "not_found" });
326
+ try {
327
+ this.opts.store.updateHubUserActivity(String(body.userId), "");
328
+ }
329
+ catch { /* best-effort */ }
330
+ return this.json(res, 200, { status: "active", token });
331
+ }
332
+ if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
333
+ if (auth.role !== "admin")
334
+ return this.json(res, 403, { error: "forbidden" });
335
+ const body = await this.readJson(req);
336
+ const rejected = this.userManager.rejectUser(String(body.userId));
337
+ if (!rejected)
338
+ return this.json(res, 404, { error: "not_found" });
339
+ return this.json(res, 200, { status: "rejected" });
340
+ }
341
+ if (req.method === "GET" && routePath === "/api/v1/hub/admin/users") {
342
+ if (auth.role !== "admin")
343
+ return this.json(res, 403, { error: "forbidden" });
344
+ const users = this.opts.store.listHubUsers().filter(u => u.status === "active");
345
+ const contribs = this.opts.store.getHubUserContributions();
346
+ const ownerId = this.authState.bootstrapAdminUserId || "";
347
+ const now = Date.now();
348
+ return this.json(res, 200, { users: users.map(u => {
349
+ const c = contribs[u.id] || { memoryCount: 0, taskCount: 0, skillCount: 0 };
350
+ const isOnline = u.id === ownerId || (!!u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS);
351
+ return {
352
+ id: u.id, username: u.username, role: u.role, status: u.status,
353
+ deviceName: u.deviceName, createdAt: u.createdAt, approvedAt: u.approvedAt,
354
+ lastIp: u.lastIp || "", lastActiveAt: u.lastActiveAt,
355
+ isOwner: u.id === ownerId, isOnline,
356
+ memoryCount: c.memoryCount, taskCount: c.taskCount, skillCount: c.skillCount,
357
+ };
358
+ }) });
359
+ }
360
+ if (req.method === "POST" && routePath === "/api/v1/hub/admin/change-role") {
361
+ if (auth.role !== "admin")
362
+ return this.json(res, 403, { error: "forbidden" });
363
+ const body = await this.readJson(req);
364
+ const userId = String(body?.userId || "");
365
+ const newRole = String(body?.role || "");
366
+ if (!userId || (newRole !== "admin" && newRole !== "member"))
367
+ return this.json(res, 400, { error: "invalid_params" });
368
+ if (newRole === "member" && userId === this.authState.bootstrapAdminUserId) {
369
+ return this.json(res, 403, { error: "cannot_demote_owner", message: "The hub owner cannot be demoted" });
370
+ }
371
+ const user = this.opts.store.getHubUser(userId);
372
+ if (!user || user.status !== "active")
373
+ return this.json(res, 404, { error: "not_found" });
374
+ const updatedUser = { ...user, role: newRole };
375
+ this.opts.store.upsertHubUser(updatedUser);
376
+ this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
377
+ return this.json(res, 200, { ok: true, role: newRole });
378
+ }
379
+ if (req.method === "POST" && routePath === "/api/v1/hub/admin/rename-user") {
380
+ if (auth.role !== "admin")
381
+ return this.json(res, 403, { error: "forbidden" });
382
+ const body = await this.readJson(req);
383
+ const userId = String(body?.userId || "");
384
+ const newUsername = String(body?.username || "").trim();
385
+ if (!userId || !newUsername || newUsername.length < 2 || newUsername.length > 32) {
386
+ return this.json(res, 400, { error: "invalid_params", message: "userId and username (2-32 chars) required" });
387
+ }
388
+ if (this.userManager.isUsernameTaken(newUsername, userId)) {
389
+ return this.json(res, 409, { error: "username_taken", message: "Username already in use" });
390
+ }
391
+ const user = this.opts.store.getHubUser(userId);
392
+ if (!user || user.status !== "active")
393
+ return this.json(res, 404, { error: "not_found" });
394
+ const ttlMs = user.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
395
+ const newToken = (0, auth_1.issueUserToken)({ userId: user.id, username: newUsername, role: user.role, status: user.status }, this.authSecret, ttlMs);
396
+ this.userManager.approveUser(user.id, newToken);
397
+ const updated = this.opts.store.getHubUser(userId);
398
+ const finalUser = { ...updated, username: newUsername };
399
+ this.opts.store.upsertHubUser(finalUser);
400
+ this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
401
+ return this.json(res, 200, { ok: true, username: newUsername });
402
+ }
403
+ if (req.method === "POST" && routePath === "/api/v1/hub/admin/remove-user") {
404
+ if (auth.role !== "admin")
405
+ return this.json(res, 403, { error: "forbidden" });
406
+ const body = await this.readJson(req);
407
+ const userId = String(body?.userId || "");
408
+ if (!userId)
409
+ return this.json(res, 400, { error: "missing_user_id" });
410
+ if (userId === auth.userId)
411
+ return this.json(res, 400, { error: "cannot_remove_self" });
412
+ if (userId === this.authState.bootstrapAdminUserId)
413
+ return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
414
+ const cleanResources = body?.cleanResources === true;
415
+ const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
416
+ if (!deleted)
417
+ return this.json(res, 404, { error: "not_found" });
418
+ this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
419
+ return this.json(res, 200, { ok: true });
420
+ }
421
+ if (req.method === "POST" && routePath === "/api/v1/hub/tasks/share") {
422
+ const body = await this.readJson(req);
423
+ if (!body?.task)
424
+ return this.json(res, 400, { error: "invalid_payload" });
425
+ const task = { ...body.task, sourceUserId: auth.userId };
426
+ const existingTask = task.sourceTaskId ? this.opts.store.getHubTaskBySource(auth.userId, task.sourceTaskId) : null;
427
+ this.opts.store.upsertHubTask(task);
428
+ const chunks = Array.isArray(body.chunks) ? body.chunks : [];
429
+ const chunkIds = [];
430
+ for (const chunk of chunks) {
431
+ this.opts.store.upsertHubChunk({ ...chunk, sourceUserId: auth.userId });
432
+ chunkIds.push(chunk.id);
433
+ }
434
+ if (this.opts.embedder && chunkIds.length > 0) {
435
+ this.embedChunksAsync(chunkIds, chunks);
436
+ }
437
+ if (!existingTask) {
438
+ this.notifyAdmins("resource_shared", "task", String(task.title || task.sourceTaskId || ""), auth.userId);
439
+ }
440
+ return this.json(res, 200, { ok: true, chunks: chunkIds.length });
441
+ }
442
+ if (req.method === "POST" && routePath === "/api/v1/hub/tasks/unshare") {
443
+ const body = await this.readJson(req);
444
+ const srcTaskId = String(body.sourceTaskId);
445
+ const existing = this.opts.store.getHubTaskBySource(auth.userId, srcTaskId);
446
+ this.opts.store.deleteHubTaskBySource(auth.userId, srcTaskId);
447
+ if (existing) {
448
+ this.notifyAdmins("resource_unshared", "task", existing.title || srcTaskId, auth.userId);
449
+ }
450
+ return this.json(res, 200, { ok: true });
451
+ }
452
+ if (req.method === "POST" && routePath === "/api/v1/hub/memories/share") {
453
+ const body = await this.readJson(req);
454
+ if (!body?.memory)
455
+ return this.json(res, 400, { error: "invalid_payload" });
456
+ const m = body.memory;
457
+ const sourceChunkId = String(m.sourceChunkId || "");
458
+ if (!sourceChunkId)
459
+ return this.json(res, 400, { error: "missing_source_chunk_id" });
460
+ const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
461
+ const memoryId = existing?.id ?? (0, crypto_1.randomUUID)();
462
+ const visibility = "public";
463
+ const resolvedGroupId = null;
464
+ const now = Date.now();
465
+ this.opts.store.upsertHubMemory({
466
+ id: memoryId,
467
+ sourceChunkId,
468
+ sourceUserId: auth.userId,
469
+ role: String(m.role || "assistant"),
470
+ content: String(m.content || ""),
471
+ summary: String(m.summary || ""),
472
+ kind: String(m.kind || "paragraph"),
473
+ groupId: resolvedGroupId,
474
+ visibility,
475
+ createdAt: existing?.createdAt ?? now,
476
+ updatedAt: now,
477
+ });
478
+ if (this.opts.embedder) {
479
+ this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
480
+ }
481
+ if (!existing) {
482
+ this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
483
+ }
484
+ return this.json(res, 200, { ok: true, memoryId, visibility });
485
+ }
486
+ if (req.method === "POST" && routePath === "/api/v1/hub/memories/unshare") {
487
+ const body = await this.readJson(req);
488
+ const sourceChunkId = String(body?.sourceChunkId || "");
489
+ if (!sourceChunkId)
490
+ return this.json(res, 400, { error: "missing_source_chunk_id" });
491
+ const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
492
+ this.opts.store.deleteHubMemoryBySource(auth.userId, sourceChunkId);
493
+ if (existing) {
494
+ this.notifyAdmins("resource_unshared", "memory", existing.summary || existing.content?.slice(0, 60) || sourceChunkId, auth.userId);
495
+ }
496
+ return this.json(res, 200, { ok: true });
497
+ }
498
+ if (req.method === "GET" && routePath === "/api/v1/hub/memories") {
499
+ const limit = Number(url.searchParams.get("limit") || 40);
500
+ const memories = this.opts.store.listVisibleHubMemories(auth.userId, limit);
501
+ return this.json(res, 200, { memories });
502
+ }
503
+ if (req.method === "GET" && routePath === "/api/v1/hub/tasks") {
504
+ const limit = Number(url.searchParams.get("limit") || 40);
505
+ const tasks = this.opts.store.listVisibleHubTasks(auth.userId, limit);
506
+ return this.json(res, 200, { tasks });
507
+ }
508
+ if (req.method === "GET" && routePath === "/api/v1/hub/skills/list") {
509
+ const limit = Number(url.searchParams.get("limit") || 40);
510
+ const skills = this.opts.store.listVisibleHubSkills(auth.userId, limit);
511
+ return this.json(res, 200, { skills });
512
+ }
513
+ if (req.method === "POST" && routePath === "/api/v1/hub/search") {
514
+ const body = await this.readJson(req);
515
+ const query = String(body.query || "");
516
+ const maxResults = Number(body.maxResults || 10);
517
+ const ftsHits = this.opts.store.searchHubChunks(query, { userId: auth.userId, maxResults: maxResults * 2 });
518
+ const memFtsHits = this.opts.store.searchHubMemories(query, { userId: auth.userId, maxResults: maxResults * 2 });
519
+ // Track which IDs are memories vs chunks
520
+ const memoryIdSet = new Set(memFtsHits.map(({ hit }) => hit.id));
521
+ // Attempt vector search and RRF merge if embedder is available
522
+ let mergedIds;
523
+ if (this.opts.embedder) {
524
+ try {
525
+ const [queryVec] = await this.opts.embedder.embed([query]);
526
+ if (queryVec) {
527
+ const allEmb = this.opts.store.getVisibleHubEmbeddings(auth.userId);
528
+ const memEmb = this.opts.store.getVisibleHubMemoryEmbeddings(auth.userId);
529
+ const scored = [];
530
+ const cosineSim = (vec) => {
531
+ let dot = 0, nA = 0, nB = 0;
532
+ for (let i = 0; i < queryVec.length && i < vec.length; i++) {
533
+ dot += queryVec[i] * vec[i];
534
+ nA += queryVec[i] * queryVec[i];
535
+ nB += vec[i] * vec[i];
536
+ }
537
+ return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
538
+ };
539
+ for (const e of allEmb)
540
+ scored.push({ id: e.chunkId, score: cosineSim(e.vector) });
541
+ for (const e of memEmb) {
542
+ scored.push({ id: e.memoryId, score: cosineSim(e.vector) });
543
+ memoryIdSet.add(e.memoryId);
544
+ }
545
+ scored.sort((a, b) => b.score - a.score);
546
+ const topScored = scored.slice(0, maxResults * 2);
547
+ const K = 60;
548
+ const rrfScores = new Map();
549
+ ftsHits.forEach(({ hit }, idx) => {
550
+ rrfScores.set(hit.id, (rrfScores.get(hit.id) ?? 0) + 1 / (K + idx + 1));
551
+ });
552
+ memFtsHits.forEach(({ hit }, idx) => {
553
+ rrfScores.set(hit.id, (rrfScores.get(hit.id) ?? 0) + 1 / (K + idx + 1));
554
+ });
555
+ topScored.forEach(({ id }, idx) => {
556
+ rrfScores.set(id, (rrfScores.get(id) ?? 0) + 1 / (K + idx + 1));
557
+ });
558
+ mergedIds = [...rrfScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, maxResults).map(([id]) => id);
559
+ }
560
+ else {
561
+ mergedIds = [...ftsHits.map(({ hit }) => hit.id), ...memFtsHits.map(({ hit }) => hit.id)].slice(0, maxResults);
562
+ }
563
+ }
564
+ catch {
565
+ mergedIds = [...ftsHits.map(({ hit }) => hit.id), ...memFtsHits.map(({ hit }) => hit.id)].slice(0, maxResults);
566
+ }
567
+ }
568
+ else {
569
+ mergedIds = [...ftsHits.map(({ hit }) => hit.id), ...memFtsHits.map(({ hit }) => hit.id)].slice(0, maxResults);
570
+ }
571
+ const ftsMap = new Map(ftsHits.map(({ hit }) => [hit.id, hit]));
572
+ const memFtsMap = new Map(memFtsHits.map(({ hit }) => [hit.id, hit]));
573
+ const hits = mergedIds.map((id, rank) => {
574
+ const isMemory = memoryIdSet.has(id);
575
+ if (isMemory) {
576
+ let mhit = memFtsMap.get(id);
577
+ if (!mhit) {
578
+ const visibleHit = this.opts.store.getVisibleHubSearchHitByMemoryId(id, auth.userId);
579
+ if (!visibleHit)
580
+ return null;
581
+ mhit = visibleHit;
582
+ }
583
+ const remoteHitId = (0, crypto_1.randomUUID)();
584
+ this.remoteHitMap.set(remoteHitId, { chunkId: id, type: "memory", expiresAt: Date.now() + 10 * 60 * 1000, requesterUserId: auth.userId });
585
+ return {
586
+ remoteHitId, summary: mhit.summary, excerpt: mhit.content.slice(0, 240), hubRank: rank + 1,
587
+ taskTitle: null, ownerName: mhit.owner_name || "unknown", groupName: mhit.group_name,
588
+ visibility: mhit.visibility, source: { ts: mhit.created_at, role: mhit.role },
589
+ };
590
+ }
591
+ let hit = ftsMap.get(id);
592
+ if (!hit) {
593
+ const visibleHit = this.opts.store.getVisibleHubSearchHitByChunkId(id, auth.userId);
594
+ if (!visibleHit)
595
+ return null;
596
+ hit = visibleHit;
597
+ }
598
+ const remoteHitId = (0, crypto_1.randomUUID)();
599
+ this.remoteHitMap.set(remoteHitId, { chunkId: id, type: "chunk", expiresAt: Date.now() + 10 * 60 * 1000, requesterUserId: auth.userId });
600
+ return {
601
+ remoteHitId, summary: hit.summary, excerpt: hit.content.slice(0, 240), hubRank: rank + 1,
602
+ taskTitle: hit.task_title, ownerName: hit.owner_name || "unknown", groupName: hit.group_name,
603
+ visibility: hit.visibility, source: { ts: hit.created_at, role: hit.role },
604
+ };
605
+ }).filter(Boolean);
606
+ return this.json(res, 200, { hits, meta: { totalCandidates: hits.length, searchedGroups: [], includedPublic: true } });
607
+ }
608
+ if (req.method === "GET" && routePath === "/api/v1/hub/skills") {
609
+ const hits = this.opts.store.searchHubSkills(String(url.searchParams.get("query") || ""), {
610
+ 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
+ }));
622
+ return this.json(res, 200, { hits });
623
+ }
624
+ if (req.method === "POST" && routePath === "/api/v1/hub/skills/publish") {
625
+ const body = await this.readJson(req);
626
+ const metadata = body?.metadata ?? {};
627
+ const sourceSkillId = String(metadata.id || "");
628
+ if (!sourceSkillId)
629
+ return this.json(res, 400, { error: "missing_skill_id" });
630
+ const existing = this.opts.store.getHubSkillBySource(auth.userId, sourceSkillId);
631
+ const skillId = existing?.id ?? (0, crypto_1.randomUUID)();
632
+ const visibility = "public";
633
+ this.opts.store.upsertHubSkill({
634
+ id: skillId,
635
+ sourceSkillId,
636
+ sourceUserId: auth.userId,
637
+ name: String(metadata.name || sourceSkillId),
638
+ description: String(metadata.description || ""),
639
+ version: Number(metadata.version || 1),
640
+ groupId: null,
641
+ visibility,
642
+ bundle: JSON.stringify(body?.bundle ?? {}),
643
+ qualityScore: metadata.qualityScore == null ? null : Number(metadata.qualityScore),
644
+ createdAt: existing?.createdAt ?? Date.now(),
645
+ updatedAt: Date.now(),
646
+ });
647
+ if (!existing) {
648
+ this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
649
+ }
650
+ return this.json(res, 200, { ok: true, skillId, visibility });
651
+ }
652
+ const skillBundleMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/skills\/([^/]+)\/bundle$/) : null;
653
+ if (skillBundleMatch) {
654
+ const skill = this.opts.store.getHubSkillById(decodeURIComponent(skillBundleMatch[1]));
655
+ if (!skill)
656
+ return this.json(res, 404, { error: "not_found" });
657
+ return this.json(res, 200, {
658
+ skillId: skill.id,
659
+ metadata: {
660
+ id: skill.sourceSkillId,
661
+ name: skill.name,
662
+ description: skill.description,
663
+ version: skill.version,
664
+ qualityScore: skill.qualityScore,
665
+ },
666
+ bundle: JSON.parse(skill.bundle),
667
+ });
668
+ }
669
+ if (req.method === "POST" && routePath === "/api/v1/hub/skills/unpublish") {
670
+ const body = await this.readJson(req);
671
+ const srcSkillId = String(body?.sourceSkillId || "");
672
+ const existing = this.opts.store.getHubSkillBySource(auth.userId, srcSkillId);
673
+ this.opts.store.deleteHubSkillBySource(auth.userId, srcSkillId);
674
+ if (existing) {
675
+ this.notifyAdmins("resource_unshared", "skill", existing.name || srcSkillId, auth.userId);
676
+ }
677
+ return this.json(res, 200, { ok: true });
678
+ }
679
+ // ── Admin: shared tasks & skills management ──
680
+ if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-tasks") {
681
+ if (auth.role !== "admin")
682
+ return this.json(res, 403, { error: "forbidden" });
683
+ const tasks = this.opts.store.listAllHubTasks();
684
+ return this.json(res, 200, { tasks });
685
+ }
686
+ const hubTaskDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-tasks\/([^/]+)\/detail$/) : null;
687
+ if (hubTaskDetailMatch) {
688
+ const taskId = decodeURIComponent(hubTaskDetailMatch[1]);
689
+ const task = this.opts.store.getHubTaskById(taskId);
690
+ if (!task)
691
+ return this.json(res, 404, { error: "not_found" });
692
+ const chunks = this.opts.store.listHubChunksByTaskId(taskId);
693
+ return this.json(res, 200, {
694
+ id: task.id, title: task.title, summary: task.summary,
695
+ startedAt: task.createdAt, endedAt: task.updatedAt,
696
+ chunks: chunks.map(c => ({ role: c.role, content: c.content, summary: c.summary, kind: c.kind, createdAt: c.createdAt })),
697
+ });
698
+ }
699
+ const hubSkillDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-skills\/([^/]+)\/detail$/) : null;
700
+ if (hubSkillDetailMatch) {
701
+ const skillId = decodeURIComponent(hubSkillDetailMatch[1]);
702
+ const skill = this.opts.store.getHubSkillById(skillId);
703
+ if (!skill)
704
+ return this.json(res, 404, { error: "not_found" });
705
+ let files = [];
706
+ try {
707
+ const bundle = JSON.parse(skill.bundle || "{}");
708
+ if (Array.isArray(bundle.files)) {
709
+ files = bundle.files.map((f) => ({ path: f.path ?? f.name ?? "unknown", type: f.type ?? "file", size: f.size ?? (f.content ? f.content.length : 0) }));
710
+ }
711
+ }
712
+ catch { /* ignore parse error */ }
713
+ return this.json(res, 200, {
714
+ skill: { id: skill.id, name: skill.name, description: skill.description, version: skill.version, qualityScore: skill.qualityScore, status: "published" },
715
+ files,
716
+ versions: [],
717
+ });
718
+ }
719
+ const adminTaskDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-tasks\/([^/]+)$/) : null;
720
+ if (adminTaskDeleteMatch) {
721
+ if (auth.role !== "admin")
722
+ return this.json(res, 403, { error: "forbidden" });
723
+ const taskId = decodeURIComponent(adminTaskDeleteMatch[1]);
724
+ const taskInfo = this.opts.store.getHubTaskById(taskId);
725
+ const deleted = this.opts.store.deleteHubTaskById(taskId);
726
+ if (!deleted)
727
+ return this.json(res, 404, { error: "not_found" });
728
+ if (taskInfo) {
729
+ this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: taskInfo.sourceUserId, type: "resource_removed", resource: "task", title: taskInfo.title });
730
+ }
731
+ return this.json(res, 200, { ok: true });
732
+ }
733
+ if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-skills") {
734
+ if (auth.role !== "admin")
735
+ return this.json(res, 403, { error: "forbidden" });
736
+ const skills = this.opts.store.listAllHubSkills();
737
+ return this.json(res, 200, { skills });
738
+ }
739
+ const adminSkillDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-skills\/([^/]+)$/) : null;
740
+ if (adminSkillDeleteMatch) {
741
+ if (auth.role !== "admin")
742
+ return this.json(res, 403, { error: "forbidden" });
743
+ const skillId = decodeURIComponent(adminSkillDeleteMatch[1]);
744
+ const skillInfo = this.opts.store.getHubSkillById(skillId);
745
+ const deleted = this.opts.store.deleteHubSkillById(skillId);
746
+ if (!deleted)
747
+ return this.json(res, 404, { error: "not_found" });
748
+ if (skillInfo) {
749
+ this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: skillInfo.sourceUserId, type: "resource_removed", resource: "skill", title: skillInfo.name });
750
+ }
751
+ return this.json(res, 200, { ok: true });
752
+ }
753
+ if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-memories") {
754
+ if (auth.role !== "admin")
755
+ return this.json(res, 403, { error: "forbidden" });
756
+ const memories = this.opts.store.listAllHubMemories();
757
+ return this.json(res, 200, { memories });
758
+ }
759
+ const adminMemoryDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-memories\/([^/]+)$/) : null;
760
+ if (adminMemoryDeleteMatch) {
761
+ if (auth.role !== "admin")
762
+ return this.json(res, 403, { error: "forbidden" });
763
+ const memoryId = decodeURIComponent(adminMemoryDeleteMatch[1]);
764
+ const memInfo = this.opts.store.getHubMemoryById(memoryId);
765
+ const deleted = this.opts.store.deleteHubMemoryById(memoryId);
766
+ if (!deleted)
767
+ return this.json(res, 404, { error: "not_found" });
768
+ 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 });
770
+ }
771
+ return this.json(res, 200, { ok: true });
772
+ }
773
+ if (req.method === "POST" && routePath === "/api/v1/hub/memory-detail") {
774
+ const body = await this.readJson(req);
775
+ const hit = this.remoteHitMap.get(String(body.remoteHitId));
776
+ if (!hit || hit.expiresAt < Date.now())
777
+ return this.json(res, 404, { error: "not_found" });
778
+ if (hit.requesterUserId !== auth.userId)
779
+ return this.json(res, 403, { error: "forbidden" });
780
+ if (hit.type === "memory") {
781
+ const mem = this.opts.store.getHubMemoryById(hit.chunkId);
782
+ if (!mem)
783
+ return this.json(res, 404, { error: "not_found" });
784
+ return this.json(res, 200, { content: mem.content, summary: mem.summary, source: { ts: mem.createdAt, role: mem.role } });
785
+ }
786
+ const chunk = this.opts.store.getHubChunkById(hit.chunkId);
787
+ if (!chunk)
788
+ return this.json(res, 404, { error: "not_found" });
789
+ return this.json(res, 200, {
790
+ content: chunk.content,
791
+ summary: chunk.summary,
792
+ source: { ts: chunk.createdAt, role: chunk.role },
793
+ });
794
+ }
795
+ if (req.method === "GET" && routePath === "/api/v1/hub/notifications") {
796
+ const unread = (new URL(req.url, `http://${req.headers.host}`)).searchParams.get("unread") === "1";
797
+ const list = this.opts.store.listHubNotifications(auth.userId, { unreadOnly: unread, limit: 50 });
798
+ const unreadCount = this.opts.store.countUnreadHubNotifications(auth.userId);
799
+ return this.json(res, 200, { notifications: list, unreadCount });
800
+ }
801
+ if (req.method === "POST" && routePath === "/api/v1/hub/notifications/read") {
802
+ const body = await this.readJson(req);
803
+ const ids = Array.isArray(body.ids) ? body.ids : undefined;
804
+ this.opts.store.markHubNotificationsRead(auth.userId, ids);
805
+ return this.json(res, 200, { ok: true });
806
+ }
807
+ if (req.method === "POST" && routePath === "/api/v1/hub/notifications/clear") {
808
+ this.opts.store.clearHubNotifications(auth.userId);
809
+ return this.json(res, 200, { ok: true });
810
+ }
811
+ return this.json(res, 404, { error: "not_found" });
812
+ }
813
+ notifyAdmins(type, resource, title, fromUserId) {
814
+ try {
815
+ const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
816
+ for (const admin of admins) {
817
+ this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: admin.id, type, resource, title });
818
+ }
819
+ }
820
+ catch { /* best-effort */ }
821
+ }
822
+ initOnlineTracking() {
823
+ try {
824
+ const ownerId = this.authState.bootstrapAdminUserId || "";
825
+ const users = this.opts.store.listHubUsers("active");
826
+ const now = Date.now();
827
+ for (const u of users) {
828
+ if (u.id === ownerId)
829
+ continue;
830
+ if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
831
+ this.knownOnlineUsers.add(u.id);
832
+ }
833
+ }
834
+ }
835
+ catch { /* best-effort */ }
836
+ }
837
+ checkOfflineUsers() {
838
+ try {
839
+ const ownerId = this.authState.bootstrapAdminUserId || "";
840
+ const users = this.opts.store.listHubUsers("active");
841
+ const now = Date.now();
842
+ const currentlyOnline = new Set();
843
+ for (const u of users) {
844
+ if (u.id === ownerId)
845
+ continue;
846
+ if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
847
+ currentlyOnline.add(u.id);
848
+ }
849
+ }
850
+ for (const uid of this.knownOnlineUsers) {
851
+ if (!currentlyOnline.has(uid)) {
852
+ const user = users.find(u => u.id === uid);
853
+ if (user) {
854
+ this.notifyAdmins("user_offline", "user", user.username, uid);
855
+ this.opts.log.info(`Hub: user "${user.username}" (${uid}) went offline`);
856
+ }
857
+ }
858
+ }
859
+ for (const uid of currentlyOnline) {
860
+ if (!this.knownOnlineUsers.has(uid)) {
861
+ const user = users.find(u => u.id === uid);
862
+ if (user) {
863
+ this.notifyAdmins("user_online", "user", user.username, uid);
864
+ }
865
+ }
866
+ }
867
+ this.knownOnlineUsers = currentlyOnline;
868
+ }
869
+ catch { /* best-effort */ }
870
+ }
871
+ authenticate(req) {
872
+ const header = req.headers.authorization;
873
+ if (!header || !header.startsWith("Bearer "))
874
+ return null;
875
+ const token = header.slice("Bearer ".length);
876
+ const payload = (0, auth_1.verifyUserToken)(token, this.authSecret);
877
+ if (!payload)
878
+ return null;
879
+ const user = this.opts.store.getHubUser(payload.userId);
880
+ if (!user || user.status !== "active")
881
+ return null;
882
+ const hash = (0, crypto_1.createHash)("sha256").update(token).digest("hex");
883
+ if (user.tokenHash !== hash)
884
+ return null;
885
+ const clientIp = req.headers["x-client-ip"]?.trim()
886
+ || req.headers["x-forwarded-for"]?.split(",")[0]?.trim()
887
+ || req.socket.remoteAddress || "";
888
+ try {
889
+ this.opts.store.updateHubUserActivity(user.id, clientIp);
890
+ }
891
+ catch { /* best-effort */ }
892
+ return {
893
+ userId: user.id,
894
+ username: user.username,
895
+ role: user.role,
896
+ status: user.status,
897
+ };
898
+ }
899
+ static MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
900
+ async readJson(req) {
901
+ const chunks = [];
902
+ let totalBytes = 0;
903
+ for await (const chunk of req) {
904
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
905
+ totalBytes += buf.length;
906
+ if (totalBytes > HubServer.MAX_BODY_BYTES) {
907
+ req.destroy();
908
+ throw Object.assign(new Error("request body too large"), { statusCode: 413 });
909
+ }
910
+ chunks.push(buf);
911
+ }
912
+ const raw = Buffer.concat(chunks).toString("utf8");
913
+ return raw ? JSON.parse(raw) : {};
914
+ }
915
+ json(res, statusCode, body) {
916
+ res.statusCode = statusCode;
917
+ res.setHeader("content-type", "application/json");
918
+ res.end(JSON.stringify(body));
919
+ }
920
+ }
921
+ exports.HubServer = HubServer;
922
+ //# sourceMappingURL=server.js.map