@memtensor/memos-local-openclaw-plugin 1.0.4-beta.5 → 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 (55) hide show
  1. package/README.md +23 -23
  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 +28 -2
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +1 -2
  7. package/dist/client/connector.d.ts.map +1 -1
  8. package/dist/client/connector.js +18 -19
  9. package/dist/client/connector.js.map +1 -1
  10. package/dist/client/hub.d.ts.map +1 -1
  11. package/dist/client/hub.js +22 -0
  12. package/dist/client/hub.js.map +1 -1
  13. package/dist/client/skill-sync.d.ts +7 -0
  14. package/dist/client/skill-sync.d.ts.map +1 -1
  15. package/dist/client/skill-sync.js +10 -0
  16. package/dist/client/skill-sync.js.map +1 -1
  17. package/dist/hub/server.d.ts.map +1 -1
  18. package/dist/hub/server.js +101 -81
  19. package/dist/hub/server.js.map +1 -1
  20. package/dist/hub/user-manager.d.ts +2 -0
  21. package/dist/hub/user-manager.d.ts.map +1 -1
  22. package/dist/hub/user-manager.js +5 -1
  23. package/dist/hub/user-manager.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/storage/sqlite.d.ts +54 -20
  28. package/dist/storage/sqlite.d.ts.map +1 -1
  29. package/dist/storage/sqlite.js +185 -101
  30. package/dist/storage/sqlite.js.map +1 -1
  31. package/dist/tools/memory-search.d.ts +3 -1
  32. package/dist/tools/memory-search.d.ts.map +1 -1
  33. package/dist/tools/memory-search.js +3 -1
  34. package/dist/tools/memory-search.js.map +1 -1
  35. package/dist/viewer/html.d.ts.map +1 -1
  36. package/dist/viewer/html.js +1619 -629
  37. package/dist/viewer/html.js.map +1 -1
  38. package/dist/viewer/server.d.ts +14 -8
  39. package/dist/viewer/server.d.ts.map +1 -1
  40. package/dist/viewer/server.js +545 -141
  41. package/dist/viewer/server.js.map +1 -1
  42. package/index.ts +355 -41
  43. package/package.json +1 -1
  44. package/skill/memos-memory-guide/SKILL.md +64 -26
  45. package/src/capture/index.ts +29 -1
  46. package/src/client/connector.ts +15 -21
  47. package/src/client/hub.ts +18 -0
  48. package/src/client/skill-sync.ts +14 -0
  49. package/src/hub/server.ts +88 -74
  50. package/src/hub/user-manager.ts +7 -3
  51. package/src/index.ts +7 -2
  52. package/src/storage/sqlite.ts +192 -122
  53. package/src/tools/memory-search.ts +2 -1
  54. package/src/viewer/html.ts +1619 -629
  55. package/src/viewer/server.ts +506 -128
@@ -224,6 +224,11 @@ export class ViewerServer {
224
224
  }
225
225
 
226
226
  if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
227
+ else if (p === "/api/memories/share-local" && req.method === "POST") this.handleMemoryLocalShare(req, res);
228
+ else if (p === "/api/memories/unshare-local" && req.method === "POST") this.handleMemoryLocalUnshare(req, res);
229
+ else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT") this.handleMemoryScope(req, res, p);
230
+ else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT") this.handleTaskScope(req, res, p);
231
+ else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT") this.handleSkillScope(req, res, p);
227
232
  else if (p === "/api/stats") this.serveStats(res, url);
228
233
  else if (p === "/api/metrics") this.serveMetrics(res, url);
229
234
  else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
@@ -251,6 +256,8 @@ export class ViewerServer {
251
256
  else if (p === "/api/sharing/pending-users" && req.method === "GET") this.serveSharingPendingUsers(res);
252
257
  else if (p === "/api/sharing/approve-user" && req.method === "POST") this.handleSharingApproveUser(req, res);
253
258
  else if (p === "/api/sharing/reject-user" && req.method === "POST") this.handleSharingRejectUser(req, res);
259
+ else if (p === "/api/sharing/remove-user" && req.method === "POST") this.handleSharingRemoveUser(req, res);
260
+ else if (p === "/api/sharing/change-role" && req.method === "POST") this.handleSharingChangeRole(req, res);
254
261
  else if (p === "/api/sharing/retry-join" && req.method === "POST") this.handleRetryJoin(req, res);
255
262
  else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
256
263
  else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
@@ -261,20 +268,17 @@ export class ViewerServer {
261
268
  else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
262
269
  else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
263
270
  else if (p === "/api/sharing/update-username" && req.method === "POST") this.handleUpdateUsername(req, res);
271
+ else if (p === "/api/sharing/rename-user" && req.method === "POST") this.handleAdminRenameUser(req, res);
264
272
  else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
265
273
  else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
266
274
  else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
267
275
  else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
268
276
  else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
269
277
  else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
270
- else if (p === "/api/sharing/groups" && req.method === "GET") this.serveSharingGroups(res);
271
- else if (p === "/api/sharing/groups" && req.method === "POST") this.handleSharingGroupCreate(req, res);
272
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT") this.handleSharingGroupUpdate(req, res, p);
273
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE") this.handleSharingGroupDelete(res, p);
274
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET") this.serveSharingGroupMembers(res, p);
275
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST") this.handleSharingGroupAddMember(req, res, p);
276
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE") this.handleSharingGroupRemoveMember(req, res, p);
277
278
  else if (p === "/api/sharing/users" && req.method === "GET") this.serveSharingUsers(res);
279
+ else if (p === "/api/sharing/notifications" && req.method === "GET") this.serveSharingNotifications(res, url);
280
+ else if (p === "/api/sharing/notifications/read" && req.method === "POST") this.handleSharingNotificationsRead(req, res);
281
+ else if (p === "/api/sharing/notifications/clear" && req.method === "POST") this.handleSharingNotificationsClear(req, res);
278
282
  else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
279
283
  else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
280
284
  else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
@@ -443,11 +447,14 @@ export class ViewerServer {
443
447
 
444
448
  const chunkIds = rawMemories.map((m: any) => m.id);
445
449
  const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
450
+ const localShareMap = new Map<string, { original_owner: string; shared_at: number }>();
446
451
  if (chunkIds.length > 0) {
447
452
  try {
448
453
  const placeholders = chunkIds.map(() => "?").join(",");
449
454
  const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
450
455
  for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
456
+ const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; original_owner: string; shared_at: number }>;
457
+ for (const r of localRows) localShareMap.set(r.chunk_id, r);
451
458
  } catch {
452
459
  }
453
460
  }
@@ -458,8 +465,12 @@ export class ViewerServer {
458
465
  out.merge_sources = sources;
459
466
  }
460
467
  const shared = sharingMap.get(m.id);
468
+ const localShared = localShareMap.get(m.id);
461
469
  out.sharingVisibility = shared?.visibility ?? null;
462
470
  out.sharingGroupId = shared?.group_id ?? null;
471
+ out.localSharing = out.owner === "public";
472
+ out.localSharingManaged = !!localShared;
473
+ out.localOriginalOwner = localShared?.original_owner ?? null;
463
474
  return out;
464
475
  });
465
476
 
@@ -477,7 +488,21 @@ export class ViewerServer {
477
488
  }
478
489
 
479
490
  private serveToolMetrics(res: http.ServerResponse, url: URL): void {
480
- const minutes = Math.min(1440, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
491
+ const fromParam = url.searchParams.get("from");
492
+ const toParam = url.searchParams.get("to");
493
+ if (fromParam) {
494
+ const fromMs = new Date(fromParam).getTime();
495
+ const toMs = toParam ? new Date(toParam).getTime() : Date.now();
496
+ if (isNaN(fromMs) || isNaN(toMs)) {
497
+ this.jsonResponse(res, { error: "Invalid date" }, 400);
498
+ return;
499
+ }
500
+ const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
501
+ const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
502
+ this.jsonResponse(res, data);
503
+ return;
504
+ }
505
+ const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
481
506
  const data = this.store.getToolMetrics(minutes);
482
507
  this.jsonResponse(res, data);
483
508
  }
@@ -491,7 +516,8 @@ export class ViewerServer {
491
516
 
492
517
  const db = (this.store as any).db;
493
518
  const items = tasks.map((t) => {
494
- const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null } | undefined;
519
+ const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
520
+ const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id) as { visibility: string } | undefined;
495
521
  return {
496
522
  id: t.id,
497
523
  sessionKey: t.sessionKey,
@@ -502,6 +528,8 @@ export class ViewerServer {
502
528
  endedAt: t.endedAt,
503
529
  chunkCount: this.store.countChunksByTask(t.id),
504
530
  skillStatus: meta?.skill_status ?? null,
531
+ owner: meta?.owner ?? "agent:main",
532
+ sharingVisibility: sharedTask?.visibility ?? null,
505
533
  };
506
534
  });
507
535
 
@@ -544,6 +572,7 @@ export class ViewerServer {
544
572
  title: task.title,
545
573
  summary: task.summary,
546
574
  status: task.status,
575
+ owner: task.owner ?? "agent:main",
547
576
  startedAt: task.startedAt,
548
577
  endedAt: task.endedAt,
549
578
  chunks: chunkItems,
@@ -552,6 +581,7 @@ export class ViewerServer {
552
581
  skillLinks,
553
582
  sharingVisibility: sharedTask?.visibility ?? null,
554
583
  sharingGroupId: sharedTask?.group_id ?? null,
584
+ hubTaskId: sharedTask ? true : false,
555
585
  });
556
586
  }
557
587
 
@@ -727,7 +757,12 @@ export class ViewerServer {
727
757
  if (visibility) {
728
758
  skills = skills.filter(s => s.visibility === visibility);
729
759
  }
730
- this.jsonResponse(res, { skills });
760
+ const db = (this.store as any).db;
761
+ const enriched = skills.map(s => {
762
+ const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id) as { visibility: string } | undefined;
763
+ return { ...s, sharingVisibility: hub?.visibility ?? null };
764
+ });
765
+ this.jsonResponse(res, { skills: enriched });
731
766
  }
732
767
 
733
768
  private serveSkillDetail(res: http.ServerResponse, urlPath: string): void {
@@ -1029,7 +1064,15 @@ export class ViewerServer {
1029
1064
  const cleaned = chunk.role === "user" && chunk.content
1030
1065
  ? { ...chunk, content: stripInboundMetadata(chunk.content) }
1031
1066
  : chunk;
1032
- this.jsonResponse(res, { memory: cleaned });
1067
+ const localShared = this.store.getLocalSharedMemory(chunkId);
1068
+ this.jsonResponse(res, {
1069
+ memory: {
1070
+ ...cleaned,
1071
+ localSharing: cleaned.owner === "public",
1072
+ localSharingManaged: !!localShared,
1073
+ localOriginalOwner: localShared?.originalOwner ?? null,
1074
+ },
1075
+ });
1033
1076
  }
1034
1077
 
1035
1078
  private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
@@ -1058,6 +1101,340 @@ export class ViewerServer {
1058
1101
  else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
1059
1102
  }
1060
1103
 
1104
+ private handleMemoryLocalShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1105
+ this.readBody(req, (body) => {
1106
+ try {
1107
+ const parsed = JSON.parse(body || "{}");
1108
+ const chunkId = String(parsed.chunkId || "");
1109
+ if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
1110
+ const result = this.store.markMemorySharedLocally(chunkId);
1111
+ if (!result.ok) {
1112
+ return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
1113
+ }
1114
+ this.jsonResponse(res, {
1115
+ ok: true,
1116
+ chunkId,
1117
+ owner: result.owner,
1118
+ localSharing: true,
1119
+ localSharingManaged: true,
1120
+ localOriginalOwner: result.originalOwner ?? null,
1121
+ });
1122
+ } catch (err) {
1123
+ this.jsonResponse(res, { ok: false, error: String(err) }, 400);
1124
+ }
1125
+ });
1126
+ }
1127
+
1128
+ private handleMemoryLocalUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1129
+ this.readBody(req, (body) => {
1130
+ try {
1131
+ const parsed = JSON.parse(body || "{}");
1132
+ const chunkId = String(parsed.chunkId || "");
1133
+ const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
1134
+ if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
1135
+ const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
1136
+ if (!result.ok) {
1137
+ return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
1138
+ }
1139
+ this.jsonResponse(res, {
1140
+ ok: true,
1141
+ chunkId,
1142
+ owner: result.owner,
1143
+ localSharing: false,
1144
+ localOriginalOwner: result.originalOwner ?? null,
1145
+ });
1146
+ } catch (err) {
1147
+ this.jsonResponse(res, { ok: false, error: String(err) }, 400);
1148
+ }
1149
+ });
1150
+ }
1151
+
1152
+ // ─── Unified scope API ───
1153
+
1154
+ private handleMemoryScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
1155
+ const chunkId = urlPath.split("/")[3];
1156
+ this.readBody(req, async (body) => {
1157
+ try {
1158
+ const parsed = JSON.parse(body || "{}");
1159
+ const scope = parsed.scope as string;
1160
+ if (!["private", "local", "team"].includes(scope)) {
1161
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1162
+ }
1163
+ const db = (this.store as any).db;
1164
+ const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
1165
+ if (!chunk) return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
1166
+
1167
+ if (chunk.dedup_status && chunk.dedup_status !== "active") {
1168
+ return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
1169
+ }
1170
+
1171
+ const isLocalShared = chunk.owner === "public";
1172
+ const hubMemory = this.getHubMemoryForChunk(chunkId);
1173
+ const isTeamShared = !!hubMemory;
1174
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1175
+
1176
+ if (scope === currentScope) {
1177
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1178
+ }
1179
+
1180
+ let hubSynced = false;
1181
+
1182
+ if (scope === "team") {
1183
+ if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
1184
+ if (!isTeamShared) {
1185
+ const hubClient = await this.resolveHubClientAware();
1186
+ const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
1187
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
1188
+ method: "POST",
1189
+ body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
1190
+ });
1191
+ if (hubClient.userId) {
1192
+ const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
1193
+ this.store.upsertHubMemory({
1194
+ id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
1195
+ sourceChunkId: chunkId, sourceUserId: hubClient.userId,
1196
+ role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
1197
+ kind: refreshedChunk.kind, groupId: null, visibility: "public",
1198
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1199
+ });
1200
+ }
1201
+ hubSynced = true;
1202
+ }
1203
+ } else if (scope === "local") {
1204
+ if (isTeamShared) {
1205
+ try {
1206
+ const hubClient = await this.resolveHubClientAware();
1207
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1208
+ method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1209
+ });
1210
+ if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1211
+ hubSynced = true;
1212
+ } catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
1213
+ }
1214
+ if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
1215
+ } else {
1216
+ if (isTeamShared) {
1217
+ try {
1218
+ const hubClient = await this.resolveHubClientAware();
1219
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1220
+ method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1221
+ });
1222
+ if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1223
+ hubSynced = true;
1224
+ } catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
1225
+ }
1226
+ if (isLocalShared) this.store.unmarkMemorySharedLocally(chunkId);
1227
+ }
1228
+
1229
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1230
+ } catch (err) {
1231
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1232
+ }
1233
+ });
1234
+ }
1235
+
1236
+ private handleTaskScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
1237
+ const taskId = urlPath.split("/")[3];
1238
+ this.readBody(req, async (body) => {
1239
+ try {
1240
+ const parsed = JSON.parse(body || "{}");
1241
+ const scope = parsed.scope as string;
1242
+ if (!["private", "local", "team"].includes(scope)) {
1243
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1244
+ }
1245
+ const task = this.store.getTask(taskId);
1246
+ if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
1247
+
1248
+ if (scope !== "private" && task.status !== "completed") {
1249
+ return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
1250
+ }
1251
+
1252
+ const isLocalShared = task.owner === "public";
1253
+ const hubTask = this.getHubTaskForLocal(taskId);
1254
+ const isTeamShared = !!hubTask;
1255
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1256
+
1257
+ if (scope === currentScope) {
1258
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1259
+ }
1260
+
1261
+ let hubSynced = false;
1262
+
1263
+ if (scope === "local" || scope === "team") {
1264
+ if (!isLocalShared) {
1265
+ const originalOwner = task.owner;
1266
+ const db = (this.store as any).db;
1267
+ db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
1268
+ db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
1269
+ }
1270
+ }
1271
+
1272
+ if (scope === "team") {
1273
+ if (!isTeamShared) {
1274
+ const chunks = this.store.getChunksByTask(taskId);
1275
+ const hubClient = await this.resolveHubClientAware();
1276
+ const refreshedTask = this.store.getTask(taskId)!;
1277
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1278
+ method: "POST",
1279
+ body: JSON.stringify({
1280
+ task: { id: refreshedTask.id, sourceTaskId: refreshedTask.id, title: refreshedTask.title, summary: refreshedTask.summary, groupId: null, visibility: "public", createdAt: refreshedTask.startedAt ?? Date.now(), updatedAt: refreshedTask.updatedAt ?? Date.now() },
1281
+ chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })),
1282
+ }),
1283
+ });
1284
+ if (hubClient.userId) {
1285
+ const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
1286
+ this.store.upsertHubTask({
1287
+ id: (response as any)?.taskId ?? existing?.id ?? crypto.randomUUID(),
1288
+ sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
1289
+ summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
1290
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1291
+ });
1292
+ }
1293
+ hubSynced = true;
1294
+ }
1295
+ }
1296
+
1297
+ if (scope === "local" && isTeamShared) {
1298
+ try {
1299
+ const hubClient = await this.resolveHubClientAware();
1300
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1301
+ method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1302
+ });
1303
+ if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1304
+ hubSynced = true;
1305
+ } catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
1306
+ }
1307
+
1308
+ if (scope === "private") {
1309
+ if (isTeamShared) {
1310
+ try {
1311
+ const hubClient = await this.resolveHubClientAware();
1312
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1313
+ method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1314
+ });
1315
+ if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1316
+ hubSynced = true;
1317
+ } catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
1318
+ }
1319
+ if (isLocalShared) {
1320
+ const db = (this.store as any).db;
1321
+ const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId) as any;
1322
+ const restoreOwner = shared?.original_owner ?? task.owner;
1323
+ if (restoreOwner && restoreOwner !== "public") {
1324
+ db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
1325
+ }
1326
+ db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
1327
+ }
1328
+ }
1329
+
1330
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1331
+ } catch (err) {
1332
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1333
+ }
1334
+ });
1335
+ }
1336
+
1337
+ private handleSkillScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
1338
+ const skillId = urlPath.split("/")[3];
1339
+ this.readBody(req, async (body) => {
1340
+ try {
1341
+ const parsed = JSON.parse(body || "{}");
1342
+ const scope = parsed.scope as string;
1343
+ if (!["private", "local", "team"].includes(scope)) {
1344
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1345
+ }
1346
+ const skill = this.store.getSkill(skillId);
1347
+ if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
1348
+
1349
+ if (scope !== "private" && skill.status !== "active") {
1350
+ return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
1351
+ }
1352
+
1353
+ const isLocalShared = skill.visibility === "public";
1354
+ const hubSkill = this.getHubSkillForLocal(skillId);
1355
+ const isTeamShared = !!hubSkill;
1356
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1357
+
1358
+ if (scope === currentScope) {
1359
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1360
+ }
1361
+
1362
+ let hubSynced = false;
1363
+
1364
+ if (scope === "local" || scope === "team") {
1365
+ if (!isLocalShared) this.store.setSkillVisibility(skillId, "public");
1366
+ }
1367
+
1368
+ if (scope === "team") {
1369
+ if (!isTeamShared) {
1370
+ const bundle = buildSkillBundleForHub(this.store, skillId);
1371
+ const hubClient = await this.resolveHubClientAware();
1372
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
1373
+ method: "POST",
1374
+ body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
1375
+ });
1376
+ if (hubClient.userId) {
1377
+ const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
1378
+ this.store.upsertHubSkill({
1379
+ id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
1380
+ sourceSkillId: skillId, sourceUserId: hubClient.userId,
1381
+ name: skill.name, description: skill.description, version: skill.version,
1382
+ groupId: null, visibility: "public",
1383
+ bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
1384
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1385
+ });
1386
+ }
1387
+ hubSynced = true;
1388
+ }
1389
+ }
1390
+
1391
+ if (scope === "local" && isTeamShared) {
1392
+ try {
1393
+ const hubClient = await this.resolveHubClientAware();
1394
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1395
+ method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1396
+ });
1397
+ if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1398
+ hubSynced = true;
1399
+ } catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
1400
+ }
1401
+
1402
+ if (scope === "private") {
1403
+ if (isTeamShared) {
1404
+ try {
1405
+ const hubClient = await this.resolveHubClientAware();
1406
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1407
+ method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1408
+ });
1409
+ if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1410
+ hubSynced = true;
1411
+ } catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
1412
+ }
1413
+ if (isLocalShared) this.store.setSkillVisibility(skillId, "private");
1414
+ }
1415
+
1416
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1417
+ } catch (err) {
1418
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1419
+ }
1420
+ });
1421
+ }
1422
+
1423
+ private getHubMemoryForChunk(chunkId: string): any {
1424
+ const db = (this.store as any).db;
1425
+ return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1426
+ }
1427
+
1428
+ private getHubTaskForLocal(taskId: string): any {
1429
+ const db = (this.store as any).db;
1430
+ return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1431
+ }
1432
+
1433
+ private getHubSkillForLocal(skillId: string): any {
1434
+ const db = (this.store as any).db;
1435
+ return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1436
+ }
1437
+
1061
1438
  private handleDeleteSession(res: http.ServerResponse, url: URL): void {
1062
1439
  const key = url.searchParams.get("key");
1063
1440
  if (!key) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Missing key" })); return; }
@@ -1157,8 +1534,7 @@ export class ViewerServer {
1157
1534
  base.connection.connected = true;
1158
1535
  base.connection.hubUrl = resolvedHubUrl ?? undefined;
1159
1536
 
1160
- // 通过 hub API 获取 admin 用户的真实信息(含分组)
1161
- let adminUser: any = { username: "hub-admin", role: "admin", groups: [] };
1537
+ let adminUser: any = { username: "hub-admin", role: "admin" };
1162
1538
  try {
1163
1539
  const hub = this.resolveHubConnection();
1164
1540
  if (hub) {
@@ -1168,7 +1544,6 @@ export class ViewerServer {
1168
1544
  id: me.id,
1169
1545
  username: me.username ?? "hub-admin",
1170
1546
  role: me.role ?? "admin",
1171
- groups: Array.isArray(me.groups) ? me.groups : [],
1172
1547
  };
1173
1548
  }
1174
1549
  }
@@ -1269,6 +1644,60 @@ export class ViewerServer {
1269
1644
  });
1270
1645
  }
1271
1646
 
1647
+ private handleSharingChangeRole(req: http.IncomingMessage, res: http.ServerResponse): void {
1648
+ this.readBody(req, async (body) => {
1649
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1650
+ try {
1651
+ const parsed = JSON.parse(body || "{}");
1652
+ const hub = this.resolveHubConnection();
1653
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1654
+ const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
1655
+ method: "POST",
1656
+ body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
1657
+ });
1658
+ this.jsonResponse(res, { ok: true, result });
1659
+ } catch (err) {
1660
+ this.jsonResponse(res, { ok: false, error: String(err) });
1661
+ }
1662
+ });
1663
+ }
1664
+
1665
+ private handleSharingRemoveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
1666
+ this.readBody(req, async (body) => {
1667
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1668
+ try {
1669
+ const parsed = JSON.parse(body || "{}");
1670
+ const hub = this.resolveHubConnection();
1671
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1672
+ const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
1673
+ method: "POST",
1674
+ body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
1675
+ });
1676
+ this.jsonResponse(res, { ok: true, result });
1677
+ } catch (err) {
1678
+ this.jsonResponse(res, { ok: false, error: String(err) });
1679
+ }
1680
+ });
1681
+ }
1682
+
1683
+ private handleAdminRenameUser(req: http.IncomingMessage, res: http.ServerResponse): void {
1684
+ this.readBody(req, async (body) => {
1685
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1686
+ try {
1687
+ const parsed = JSON.parse(body || "{}");
1688
+ const hub = this.resolveHubConnection();
1689
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1690
+ const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
1691
+ method: "POST",
1692
+ body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
1693
+ });
1694
+ this.jsonResponse(res, { ok: true, result });
1695
+ } catch (err) {
1696
+ this.jsonResponse(res, { ok: false, error: String(err) });
1697
+ }
1698
+ });
1699
+ }
1700
+
1272
1701
  private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
1273
1702
  this.readBody(req, async (_body) => {
1274
1703
  if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
@@ -1720,117 +2149,6 @@ export class ViewerServer {
1720
2149
  return resolveHubClient(this.store, this.ctx);
1721
2150
  }
1722
2151
 
1723
- private extractGroupId(path: string): string {
1724
- const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
1725
- return m ? decodeURIComponent(m[1]) : "";
1726
- }
1727
-
1728
- private async serveSharingGroups(res: http.ServerResponse): Promise<void> {
1729
- const hub = this.resolveHubConnection();
1730
- if (!hub) return this.jsonResponse(res, { groups: [], error: "not_configured" });
1731
- try {
1732
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" }) as any;
1733
- this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
1734
- } catch (err) {
1735
- this.jsonResponse(res, { groups: [], error: String(err) });
1736
- }
1737
- }
1738
-
1739
- private handleSharingGroupCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
1740
- this.readBody(req, async (body) => {
1741
- const hub = this.resolveHubConnection();
1742
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1743
- try {
1744
- const parsed = JSON.parse(body || "{}");
1745
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
1746
- method: "POST",
1747
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1748
- }) as any;
1749
- this.jsonResponse(res, { ok: true, ...data });
1750
- } catch (err) {
1751
- this.jsonResponse(res, { ok: false, error: String(err) });
1752
- }
1753
- });
1754
- }
1755
-
1756
- private handleSharingGroupUpdate(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1757
- this.readBody(req, async (body) => {
1758
- const hub = this.resolveHubConnection();
1759
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1760
- const groupId = this.extractGroupId(p);
1761
- try {
1762
- const parsed = JSON.parse(body || "{}");
1763
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
1764
- method: "PUT",
1765
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1766
- });
1767
- this.jsonResponse(res, { ok: true });
1768
- } catch (err) {
1769
- this.jsonResponse(res, { ok: false, error: String(err) });
1770
- }
1771
- });
1772
- }
1773
-
1774
- private async handleSharingGroupDelete(res: http.ServerResponse, p: string): Promise<void> {
1775
- const hub = this.resolveHubConnection();
1776
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1777
- const groupId = this.extractGroupId(p);
1778
- try {
1779
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
1780
- this.jsonResponse(res, { ok: true });
1781
- } catch (err) {
1782
- this.jsonResponse(res, { ok: false, error: String(err) });
1783
- }
1784
- }
1785
-
1786
- private async serveSharingGroupMembers(res: http.ServerResponse, p: string): Promise<void> {
1787
- const hub = this.resolveHubConnection();
1788
- if (!hub) return this.jsonResponse(res, { members: [], error: "not_configured" });
1789
- const groupId = this.extractGroupId(p);
1790
- try {
1791
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" }) as any;
1792
- this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
1793
- } catch (err) {
1794
- this.jsonResponse(res, { members: [], error: String(err) });
1795
- }
1796
- }
1797
-
1798
- private handleSharingGroupAddMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1799
- this.readBody(req, async (body) => {
1800
- const hub = this.resolveHubConnection();
1801
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1802
- const groupId = this.extractGroupId(p);
1803
- try {
1804
- const parsed = JSON.parse(body || "{}");
1805
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1806
- method: "POST",
1807
- body: JSON.stringify({ userId: parsed.userId }),
1808
- });
1809
- this.jsonResponse(res, { ok: true });
1810
- } catch (err) {
1811
- this.jsonResponse(res, { ok: false, error: String(err) });
1812
- }
1813
- });
1814
- }
1815
-
1816
- private handleSharingGroupRemoveMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1817
- this.readBody(req, async (body) => {
1818
- const hub = this.resolveHubConnection();
1819
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1820
- const groupId = this.extractGroupId(p);
1821
- try {
1822
- const parsed = JSON.parse(body || "{}");
1823
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1824
- method: "DELETE",
1825
- body: JSON.stringify({ userId: parsed.userId }),
1826
- });
1827
- this.jsonResponse(res, { ok: true });
1828
- } catch (err) {
1829
- this.jsonResponse(res, { ok: false, error: String(err) });
1830
- }
1831
- });
1832
- }
1833
-
1834
2152
  private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
1835
2153
  const hub = this.resolveHubConnection();
1836
2154
  if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
@@ -1849,7 +2167,14 @@ export class ViewerServer {
1849
2167
  if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
1850
2168
  try {
1851
2169
  const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
1852
- this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
2170
+ const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
2171
+ for (const tk of tasks) {
2172
+ if (!tk.summary && tk.sourceTaskId) {
2173
+ const local = this.store.getTask(tk.sourceTaskId);
2174
+ if (local) { tk.summary = local.summary; tk.title = tk.title || local.title; }
2175
+ }
2176
+ }
2177
+ this.jsonResponse(res, { tasks });
1853
2178
  } catch (err) {
1854
2179
  this.jsonResponse(res, { tasks: [], error: String(err) });
1855
2180
  }
@@ -1872,7 +2197,14 @@ export class ViewerServer {
1872
2197
  if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
1873
2198
  try {
1874
2199
  const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
1875
- this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
2200
+ const skills = Array.isArray(data?.skills) ? data.skills : [];
2201
+ for (const sk of skills) {
2202
+ if (!sk.description && sk.sourceSkillId) {
2203
+ const local = this.store.getSkill(sk.sourceSkillId);
2204
+ if (local) { sk.description = sk.description || local.description; sk.name = sk.name || local.name; }
2205
+ }
2206
+ }
2207
+ this.jsonResponse(res, { skills });
1876
2208
  } catch (err) {
1877
2209
  this.jsonResponse(res, { skills: [], error: String(err) });
1878
2210
  }
@@ -1895,7 +2227,14 @@ export class ViewerServer {
1895
2227
  if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
1896
2228
  try {
1897
2229
  const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
1898
- this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
2230
+ const memories = Array.isArray(data?.memories) ? data.memories : [];
2231
+ for (const m of memories) {
2232
+ if (!m.content && m.sourceChunkId) {
2233
+ const local = this.store.getChunk(m.sourceChunkId);
2234
+ if (local) { m.content = local.content; if (!m.summary && local.summary) m.summary = local.summary; }
2235
+ }
2236
+ }
2237
+ this.jsonResponse(res, { memories });
1899
2238
  } catch (err) {
1900
2239
  this.jsonResponse(res, { memories: [], error: String(err) });
1901
2240
  }
@@ -1913,6 +2252,45 @@ export class ViewerServer {
1913
2252
  }
1914
2253
  }
1915
2254
 
2255
+ private async serveSharingNotifications(res: http.ServerResponse, url: URL): Promise<void> {
2256
+ const hub = this.resolveHubConnection();
2257
+ if (!hub) return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
2258
+ try {
2259
+ const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
2260
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`) as any;
2261
+ this.jsonResponse(res, data);
2262
+ } catch {
2263
+ this.jsonResponse(res, { notifications: [], unreadCount: 0 });
2264
+ }
2265
+ }
2266
+
2267
+ private handleSharingNotificationsRead(req: http.IncomingMessage, res: http.ServerResponse): void {
2268
+ const hub = this.resolveHubConnection();
2269
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
2270
+ this.readBody(req, async (raw) => {
2271
+ try {
2272
+ const body = JSON.parse(raw || "{}");
2273
+ await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
2274
+ this.jsonResponse(res, { ok: true });
2275
+ } catch (err) {
2276
+ this.jsonResponse(res, { ok: false, error: String(err) });
2277
+ }
2278
+ });
2279
+ }
2280
+
2281
+ private handleSharingNotificationsClear(req: http.IncomingMessage, res: http.ServerResponse): void {
2282
+ const hub = this.resolveHubConnection();
2283
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
2284
+ this.readBody(req, async () => {
2285
+ try {
2286
+ await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
2287
+ this.jsonResponse(res, { ok: true });
2288
+ } catch (err) {
2289
+ this.jsonResponse(res, { ok: false, error: String(err) });
2290
+ }
2291
+ });
2292
+ }
2293
+
1916
2294
  private getLocalIPs(): string[] {
1917
2295
  const nets = os.networkInterfaces();
1918
2296
  const ips: string[] = [];