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

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