@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
@@ -228,6 +228,16 @@ class ViewerServer {
228
228
  }
229
229
  if (p === "/api/memories" && req.method === "GET")
230
230
  this.serveMemories(res, url);
231
+ else if (p === "/api/memories/share-local" && req.method === "POST")
232
+ this.handleMemoryLocalShare(req, res);
233
+ else if (p === "/api/memories/unshare-local" && req.method === "POST")
234
+ this.handleMemoryLocalUnshare(req, res);
235
+ else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT")
236
+ this.handleMemoryScope(req, res, p);
237
+ else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT")
238
+ this.handleTaskScope(req, res, p);
239
+ else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT")
240
+ this.handleSkillScope(req, res, p);
231
241
  else if (p === "/api/stats")
232
242
  this.serveStats(res, url);
233
243
  else if (p === "/api/metrics")
@@ -282,6 +292,10 @@ class ViewerServer {
282
292
  this.handleSharingApproveUser(req, res);
283
293
  else if (p === "/api/sharing/reject-user" && req.method === "POST")
284
294
  this.handleSharingRejectUser(req, res);
295
+ else if (p === "/api/sharing/remove-user" && req.method === "POST")
296
+ this.handleSharingRemoveUser(req, res);
297
+ else if (p === "/api/sharing/change-role" && req.method === "POST")
298
+ this.handleSharingChangeRole(req, res);
285
299
  else if (p === "/api/sharing/retry-join" && req.method === "POST")
286
300
  this.handleRetryJoin(req, res);
287
301
  else if (p === "/api/sharing/search/memories" && req.method === "POST")
@@ -302,6 +316,8 @@ class ViewerServer {
302
316
  this.handleSharingTaskUnshare(req, res);
303
317
  else if (p === "/api/sharing/update-username" && req.method === "POST")
304
318
  this.handleUpdateUsername(req, res);
319
+ else if (p === "/api/sharing/rename-user" && req.method === "POST")
320
+ this.handleAdminRenameUser(req, res);
305
321
  else if (p === "/api/sharing/test-hub" && req.method === "POST")
306
322
  this.handleTestHubConnection(req, res);
307
323
  else if (p === "/api/sharing/memories/share" && req.method === "POST")
@@ -314,22 +330,14 @@ class ViewerServer {
314
330
  this.handleSharingSkillShare(req, res);
315
331
  else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
316
332
  this.handleSharingSkillUnshare(req, res);
317
- else if (p === "/api/sharing/groups" && req.method === "GET")
318
- this.serveSharingGroups(res);
319
- else if (p === "/api/sharing/groups" && req.method === "POST")
320
- this.handleSharingGroupCreate(req, res);
321
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT")
322
- this.handleSharingGroupUpdate(req, res, p);
323
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE")
324
- this.handleSharingGroupDelete(res, p);
325
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET")
326
- this.serveSharingGroupMembers(res, p);
327
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST")
328
- this.handleSharingGroupAddMember(req, res, p);
329
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE")
330
- this.handleSharingGroupRemoveMember(req, res, p);
331
333
  else if (p === "/api/sharing/users" && req.method === "GET")
332
334
  this.serveSharingUsers(res);
335
+ else if (p === "/api/sharing/notifications" && req.method === "GET")
336
+ this.serveSharingNotifications(res, url);
337
+ else if (p === "/api/sharing/notifications/read" && req.method === "POST")
338
+ this.handleSharingNotificationsRead(req, res);
339
+ else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
340
+ this.handleSharingNotificationsClear(req, res);
333
341
  else if (p === "/api/admin/shared-tasks" && req.method === "GET")
334
342
  this.serveAdminSharedTasks(res);
335
343
  else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
@@ -531,12 +539,16 @@ class ViewerServer {
531
539
  const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
532
540
  const chunkIds = rawMemories.map((m) => m.id);
533
541
  const sharingMap = new Map();
542
+ const localShareMap = new Map();
534
543
  if (chunkIds.length > 0) {
535
544
  try {
536
545
  const placeholders = chunkIds.map(() => "?").join(",");
537
546
  const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
538
547
  for (const r of sharedRows)
539
548
  sharingMap.set(r.source_chunk_id, r);
549
+ const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
550
+ for (const r of localRows)
551
+ localShareMap.set(r.chunk_id, r);
540
552
  }
541
553
  catch {
542
554
  }
@@ -548,8 +560,12 @@ class ViewerServer {
548
560
  out.merge_sources = sources;
549
561
  }
550
562
  const shared = sharingMap.get(m.id);
563
+ const localShared = localShareMap.get(m.id);
551
564
  out.sharingVisibility = shared?.visibility ?? null;
552
565
  out.sharingGroupId = shared?.group_id ?? null;
566
+ out.localSharing = out.owner === "public";
567
+ out.localSharingManaged = !!localShared;
568
+ out.localOriginalOwner = localShared?.original_owner ?? null;
553
569
  return out;
554
570
  });
555
571
  this.store.recordViewerEvent("list");
@@ -564,7 +580,21 @@ class ViewerServer {
564
580
  this.jsonResponse(res, data);
565
581
  }
566
582
  serveToolMetrics(res, url) {
567
- const minutes = Math.min(1440, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
583
+ const fromParam = url.searchParams.get("from");
584
+ const toParam = url.searchParams.get("to");
585
+ if (fromParam) {
586
+ const fromMs = new Date(fromParam).getTime();
587
+ const toMs = toParam ? new Date(toParam).getTime() : Date.now();
588
+ if (isNaN(fromMs) || isNaN(toMs)) {
589
+ this.jsonResponse(res, { error: "Invalid date" }, 400);
590
+ return;
591
+ }
592
+ const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
593
+ const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
594
+ this.jsonResponse(res, data);
595
+ return;
596
+ }
597
+ const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
568
598
  const data = this.store.getToolMetrics(minutes);
569
599
  this.jsonResponse(res, data);
570
600
  }
@@ -576,7 +606,8 @@ class ViewerServer {
576
606
  const { tasks, total } = this.store.listTasks({ status, limit, offset });
577
607
  const db = this.store.db;
578
608
  const items = tasks.map((t) => {
579
- const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id);
609
+ const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
610
+ const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id);
580
611
  return {
581
612
  id: t.id,
582
613
  sessionKey: t.sessionKey,
@@ -587,6 +618,8 @@ class ViewerServer {
587
618
  endedAt: t.endedAt,
588
619
  chunkCount: this.store.countChunksByTask(t.id),
589
620
  skillStatus: meta?.skill_status ?? null,
621
+ owner: meta?.owner ?? "agent:main",
622
+ sharingVisibility: sharedTask?.visibility ?? null,
590
623
  };
591
624
  });
592
625
  this.jsonResponse(res, { tasks: items, total, limit, offset });
@@ -622,6 +655,7 @@ class ViewerServer {
622
655
  title: task.title,
623
656
  summary: task.summary,
624
657
  status: task.status,
658
+ owner: task.owner ?? "agent:main",
625
659
  startedAt: task.startedAt,
626
660
  endedAt: task.endedAt,
627
661
  chunks: chunkItems,
@@ -630,6 +664,7 @@ class ViewerServer {
630
664
  skillLinks,
631
665
  sharingVisibility: sharedTask?.visibility ?? null,
632
666
  sharingGroupId: sharedTask?.group_id ?? null,
667
+ hubTaskId: sharedTask ? true : false,
633
668
  });
634
669
  }
635
670
  serveStats(res, url) {
@@ -817,7 +852,12 @@ class ViewerServer {
817
852
  if (visibility) {
818
853
  skills = skills.filter(s => s.visibility === visibility);
819
854
  }
820
- this.jsonResponse(res, { skills });
855
+ const db = this.store.db;
856
+ const enriched = skills.map(s => {
857
+ const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id);
858
+ return { ...s, sharingVisibility: hub?.visibility ?? null };
859
+ });
860
+ this.jsonResponse(res, { skills: enriched });
821
861
  }
822
862
  serveSkillDetail(res, urlPath) {
823
863
  const skillId = urlPath.replace("/api/skill/", "");
@@ -1135,7 +1175,15 @@ class ViewerServer {
1135
1175
  const cleaned = chunk.role === "user" && chunk.content
1136
1176
  ? { ...chunk, content: (0, capture_1.stripInboundMetadata)(chunk.content) }
1137
1177
  : chunk;
1138
- this.jsonResponse(res, { memory: cleaned });
1178
+ const localShared = this.store.getLocalSharedMemory(chunkId);
1179
+ this.jsonResponse(res, {
1180
+ memory: {
1181
+ ...cleaned,
1182
+ localSharing: cleaned.owner === "public",
1183
+ localSharingManaged: !!localShared,
1184
+ localOriginalOwner: localShared?.originalOwner ?? null,
1185
+ },
1186
+ });
1139
1187
  }
1140
1188
  handleUpdate(req, res, urlPath) {
1141
1189
  const chunkId = urlPath.replace("/api/memory/", "");
@@ -1170,6 +1218,348 @@ class ViewerServer {
1170
1218
  res.end(JSON.stringify({ error: "Not found" }));
1171
1219
  }
1172
1220
  }
1221
+ handleMemoryLocalShare(req, res) {
1222
+ this.readBody(req, (body) => {
1223
+ try {
1224
+ const parsed = JSON.parse(body || "{}");
1225
+ const chunkId = String(parsed.chunkId || "");
1226
+ if (!chunkId)
1227
+ return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
1228
+ const result = this.store.markMemorySharedLocally(chunkId);
1229
+ if (!result.ok) {
1230
+ return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
1231
+ }
1232
+ this.jsonResponse(res, {
1233
+ ok: true,
1234
+ chunkId,
1235
+ owner: result.owner,
1236
+ localSharing: true,
1237
+ localSharingManaged: true,
1238
+ localOriginalOwner: result.originalOwner ?? null,
1239
+ });
1240
+ }
1241
+ catch (err) {
1242
+ this.jsonResponse(res, { ok: false, error: String(err) }, 400);
1243
+ }
1244
+ });
1245
+ }
1246
+ handleMemoryLocalUnshare(req, res) {
1247
+ this.readBody(req, (body) => {
1248
+ try {
1249
+ const parsed = JSON.parse(body || "{}");
1250
+ const chunkId = String(parsed.chunkId || "");
1251
+ const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
1252
+ if (!chunkId)
1253
+ return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
1254
+ const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
1255
+ if (!result.ok) {
1256
+ return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
1257
+ }
1258
+ this.jsonResponse(res, {
1259
+ ok: true,
1260
+ chunkId,
1261
+ owner: result.owner,
1262
+ localSharing: false,
1263
+ localOriginalOwner: result.originalOwner ?? null,
1264
+ });
1265
+ }
1266
+ catch (err) {
1267
+ this.jsonResponse(res, { ok: false, error: String(err) }, 400);
1268
+ }
1269
+ });
1270
+ }
1271
+ // ─── Unified scope API ───
1272
+ handleMemoryScope(req, res, urlPath) {
1273
+ const chunkId = urlPath.split("/")[3];
1274
+ this.readBody(req, async (body) => {
1275
+ try {
1276
+ const parsed = JSON.parse(body || "{}");
1277
+ const scope = parsed.scope;
1278
+ if (!["private", "local", "team"].includes(scope)) {
1279
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1280
+ }
1281
+ const db = this.store.db;
1282
+ const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
1283
+ if (!chunk)
1284
+ return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
1285
+ if (chunk.dedup_status && chunk.dedup_status !== "active") {
1286
+ return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
1287
+ }
1288
+ const isLocalShared = chunk.owner === "public";
1289
+ const hubMemory = this.getHubMemoryForChunk(chunkId);
1290
+ const isTeamShared = !!hubMemory;
1291
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1292
+ if (scope === currentScope) {
1293
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1294
+ }
1295
+ let hubSynced = false;
1296
+ if (scope === "team") {
1297
+ if (!isLocalShared)
1298
+ this.store.markMemorySharedLocally(chunkId);
1299
+ if (!isTeamShared) {
1300
+ const hubClient = await this.resolveHubClientAware();
1301
+ const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
1302
+ const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
1303
+ method: "POST",
1304
+ body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
1305
+ });
1306
+ if (hubClient.userId) {
1307
+ const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
1308
+ this.store.upsertHubMemory({
1309
+ id: response?.memoryId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1310
+ sourceChunkId: chunkId, sourceUserId: hubClient.userId,
1311
+ role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
1312
+ kind: refreshedChunk.kind, groupId: null, visibility: "public",
1313
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1314
+ });
1315
+ }
1316
+ hubSynced = true;
1317
+ }
1318
+ }
1319
+ else if (scope === "local") {
1320
+ if (isTeamShared) {
1321
+ try {
1322
+ const hubClient = await this.resolveHubClientAware();
1323
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1324
+ method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1325
+ });
1326
+ if (hubClient.userId)
1327
+ this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1328
+ hubSynced = true;
1329
+ }
1330
+ catch (err) {
1331
+ this.log.warn(`Failed to unshare memory from team: ${err}`);
1332
+ }
1333
+ }
1334
+ if (!isLocalShared)
1335
+ this.store.markMemorySharedLocally(chunkId);
1336
+ }
1337
+ else {
1338
+ if (isTeamShared) {
1339
+ try {
1340
+ const hubClient = await this.resolveHubClientAware();
1341
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1342
+ method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1343
+ });
1344
+ if (hubClient.userId)
1345
+ this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1346
+ hubSynced = true;
1347
+ }
1348
+ catch (err) {
1349
+ this.log.warn(`Failed to unshare memory from team: ${err}`);
1350
+ }
1351
+ }
1352
+ if (isLocalShared)
1353
+ this.store.unmarkMemorySharedLocally(chunkId);
1354
+ }
1355
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1356
+ }
1357
+ catch (err) {
1358
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1359
+ }
1360
+ });
1361
+ }
1362
+ handleTaskScope(req, res, urlPath) {
1363
+ const taskId = urlPath.split("/")[3];
1364
+ this.readBody(req, async (body) => {
1365
+ try {
1366
+ const parsed = JSON.parse(body || "{}");
1367
+ const scope = parsed.scope;
1368
+ if (!["private", "local", "team"].includes(scope)) {
1369
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1370
+ }
1371
+ const task = this.store.getTask(taskId);
1372
+ if (!task)
1373
+ return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
1374
+ if (scope !== "private" && task.status !== "completed") {
1375
+ return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
1376
+ }
1377
+ const isLocalShared = task.owner === "public";
1378
+ const hubTask = this.getHubTaskForLocal(taskId);
1379
+ const isTeamShared = !!hubTask;
1380
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1381
+ if (scope === currentScope) {
1382
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1383
+ }
1384
+ let hubSynced = false;
1385
+ if (scope === "local" || scope === "team") {
1386
+ if (!isLocalShared) {
1387
+ const originalOwner = task.owner;
1388
+ const db = this.store.db;
1389
+ 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());
1390
+ db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
1391
+ }
1392
+ }
1393
+ if (scope === "team") {
1394
+ if (!isTeamShared) {
1395
+ const chunks = this.store.getChunksByTask(taskId);
1396
+ const hubClient = await this.resolveHubClientAware();
1397
+ const refreshedTask = this.store.getTask(taskId);
1398
+ const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1399
+ method: "POST",
1400
+ body: JSON.stringify({
1401
+ 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() },
1402
+ 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() })),
1403
+ }),
1404
+ });
1405
+ if (hubClient.userId) {
1406
+ const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
1407
+ this.store.upsertHubTask({
1408
+ id: response?.taskId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1409
+ sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
1410
+ summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
1411
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1412
+ });
1413
+ }
1414
+ hubSynced = true;
1415
+ }
1416
+ }
1417
+ if (scope === "local" && isTeamShared) {
1418
+ try {
1419
+ const hubClient = await this.resolveHubClientAware();
1420
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1421
+ method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1422
+ });
1423
+ if (hubClient.userId)
1424
+ this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1425
+ hubSynced = true;
1426
+ }
1427
+ catch (err) {
1428
+ this.log.warn(`Failed to unshare task from team: ${err}`);
1429
+ }
1430
+ }
1431
+ if (scope === "private") {
1432
+ if (isTeamShared) {
1433
+ try {
1434
+ const hubClient = await this.resolveHubClientAware();
1435
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1436
+ method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1437
+ });
1438
+ if (hubClient.userId)
1439
+ this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1440
+ hubSynced = true;
1441
+ }
1442
+ catch (err) {
1443
+ this.log.warn(`Failed to unshare task from team: ${err}`);
1444
+ }
1445
+ }
1446
+ if (isLocalShared) {
1447
+ const db = this.store.db;
1448
+ const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId);
1449
+ const restoreOwner = shared?.original_owner ?? task.owner;
1450
+ if (restoreOwner && restoreOwner !== "public") {
1451
+ db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
1452
+ }
1453
+ db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
1454
+ }
1455
+ }
1456
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1457
+ }
1458
+ catch (err) {
1459
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1460
+ }
1461
+ });
1462
+ }
1463
+ handleSkillScope(req, res, urlPath) {
1464
+ const skillId = urlPath.split("/")[3];
1465
+ this.readBody(req, async (body) => {
1466
+ try {
1467
+ const parsed = JSON.parse(body || "{}");
1468
+ const scope = parsed.scope;
1469
+ if (!["private", "local", "team"].includes(scope)) {
1470
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1471
+ }
1472
+ const skill = this.store.getSkill(skillId);
1473
+ if (!skill)
1474
+ return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
1475
+ if (scope !== "private" && skill.status !== "active") {
1476
+ return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
1477
+ }
1478
+ const isLocalShared = skill.visibility === "public";
1479
+ const hubSkill = this.getHubSkillForLocal(skillId);
1480
+ const isTeamShared = !!hubSkill;
1481
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1482
+ if (scope === currentScope) {
1483
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1484
+ }
1485
+ let hubSynced = false;
1486
+ if (scope === "local" || scope === "team") {
1487
+ if (!isLocalShared)
1488
+ this.store.setSkillVisibility(skillId, "public");
1489
+ }
1490
+ if (scope === "team") {
1491
+ if (!isTeamShared) {
1492
+ const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
1493
+ const hubClient = await this.resolveHubClientAware();
1494
+ const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
1495
+ method: "POST",
1496
+ body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
1497
+ });
1498
+ if (hubClient.userId) {
1499
+ const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
1500
+ this.store.upsertHubSkill({
1501
+ id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1502
+ sourceSkillId: skillId, sourceUserId: hubClient.userId,
1503
+ name: skill.name, description: skill.description, version: skill.version,
1504
+ groupId: null, visibility: "public",
1505
+ bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
1506
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1507
+ });
1508
+ }
1509
+ hubSynced = true;
1510
+ }
1511
+ }
1512
+ if (scope === "local" && isTeamShared) {
1513
+ try {
1514
+ const hubClient = await this.resolveHubClientAware();
1515
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1516
+ method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1517
+ });
1518
+ if (hubClient.userId)
1519
+ this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1520
+ hubSynced = true;
1521
+ }
1522
+ catch (err) {
1523
+ this.log.warn(`Failed to unpublish skill from team: ${err}`);
1524
+ }
1525
+ }
1526
+ if (scope === "private") {
1527
+ if (isTeamShared) {
1528
+ try {
1529
+ const hubClient = await this.resolveHubClientAware();
1530
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1531
+ method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1532
+ });
1533
+ if (hubClient.userId)
1534
+ this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1535
+ hubSynced = true;
1536
+ }
1537
+ catch (err) {
1538
+ this.log.warn(`Failed to unpublish skill from team: ${err}`);
1539
+ }
1540
+ }
1541
+ if (isLocalShared)
1542
+ this.store.setSkillVisibility(skillId, "private");
1543
+ }
1544
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1545
+ }
1546
+ catch (err) {
1547
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1548
+ }
1549
+ });
1550
+ }
1551
+ getHubMemoryForChunk(chunkId) {
1552
+ const db = this.store.db;
1553
+ return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1554
+ }
1555
+ getHubTaskForLocal(taskId) {
1556
+ const db = this.store.db;
1557
+ return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1558
+ }
1559
+ getHubSkillForLocal(skillId) {
1560
+ const db = this.store.db;
1561
+ return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1562
+ }
1173
1563
  handleDeleteSession(res, url) {
1174
1564
  const key = url.searchParams.get("key");
1175
1565
  if (!key) {
@@ -1263,8 +1653,7 @@ class ViewerServer {
1263
1653
  base.admin.rejectSupported = true;
1264
1654
  base.connection.connected = true;
1265
1655
  base.connection.hubUrl = resolvedHubUrl ?? undefined;
1266
- // 通过 hub API 获取 admin 用户的真实信息(含分组)
1267
- let adminUser = { username: "hub-admin", role: "admin", groups: [] };
1656
+ let adminUser = { username: "hub-admin", role: "admin" };
1268
1657
  try {
1269
1658
  const hub = this.resolveHubConnection();
1270
1659
  if (hub) {
@@ -1274,7 +1663,6 @@ class ViewerServer {
1274
1663
  id: me.id,
1275
1664
  username: me.username ?? "hub-admin",
1276
1665
  role: me.role ?? "admin",
1277
- groups: Array.isArray(me.groups) ? me.groups : [],
1278
1666
  };
1279
1667
  }
1280
1668
  }
@@ -1383,6 +1771,66 @@ class ViewerServer {
1383
1771
  }
1384
1772
  });
1385
1773
  }
1774
+ handleSharingChangeRole(req, res) {
1775
+ this.readBody(req, async (body) => {
1776
+ if (!this.ctx)
1777
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1778
+ try {
1779
+ const parsed = JSON.parse(body || "{}");
1780
+ const hub = this.resolveHubConnection();
1781
+ if (!hub)
1782
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
1783
+ const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
1784
+ method: "POST",
1785
+ body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
1786
+ });
1787
+ this.jsonResponse(res, { ok: true, result });
1788
+ }
1789
+ catch (err) {
1790
+ this.jsonResponse(res, { ok: false, error: String(err) });
1791
+ }
1792
+ });
1793
+ }
1794
+ handleSharingRemoveUser(req, res) {
1795
+ this.readBody(req, async (body) => {
1796
+ if (!this.ctx)
1797
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1798
+ try {
1799
+ const parsed = JSON.parse(body || "{}");
1800
+ const hub = this.resolveHubConnection();
1801
+ if (!hub)
1802
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
1803
+ const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
1804
+ method: "POST",
1805
+ body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
1806
+ });
1807
+ this.jsonResponse(res, { ok: true, result });
1808
+ }
1809
+ catch (err) {
1810
+ this.jsonResponse(res, { ok: false, error: String(err) });
1811
+ }
1812
+ });
1813
+ }
1814
+ handleAdminRenameUser(req, res) {
1815
+ this.readBody(req, async (body) => {
1816
+ if (!this.ctx)
1817
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1818
+ try {
1819
+ const parsed = JSON.parse(body || "{}");
1820
+ const hub = this.resolveHubConnection();
1821
+ if (!hub)
1822
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
1823
+ const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
1824
+ method: "POST",
1825
+ body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
1826
+ });
1827
+ this.jsonResponse(res, { ok: true, result });
1828
+ }
1829
+ catch (err) {
1830
+ this.jsonResponse(res, { ok: false, error: String(err) });
1831
+ }
1832
+ });
1833
+ }
1386
1834
  handleRetryJoin(req, res) {
1387
1835
  this.readBody(req, async (_body) => {
1388
1836
  if (!this.ctx)
@@ -1860,123 +2308,6 @@ class ViewerServer {
1860
2308
  }
1861
2309
  return (0, hub_1.resolveHubClient)(this.store, this.ctx);
1862
2310
  }
1863
- extractGroupId(path) {
1864
- const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
1865
- return m ? decodeURIComponent(m[1]) : "";
1866
- }
1867
- async serveSharingGroups(res) {
1868
- const hub = this.resolveHubConnection();
1869
- if (!hub)
1870
- return this.jsonResponse(res, { groups: [], error: "not_configured" });
1871
- try {
1872
- const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" });
1873
- this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
1874
- }
1875
- catch (err) {
1876
- this.jsonResponse(res, { groups: [], error: String(err) });
1877
- }
1878
- }
1879
- handleSharingGroupCreate(req, res) {
1880
- this.readBody(req, async (body) => {
1881
- const hub = this.resolveHubConnection();
1882
- if (!hub)
1883
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1884
- try {
1885
- const parsed = JSON.parse(body || "{}");
1886
- const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
1887
- method: "POST",
1888
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1889
- });
1890
- this.jsonResponse(res, { ok: true, ...data });
1891
- }
1892
- catch (err) {
1893
- this.jsonResponse(res, { ok: false, error: String(err) });
1894
- }
1895
- });
1896
- }
1897
- handleSharingGroupUpdate(req, res, p) {
1898
- this.readBody(req, async (body) => {
1899
- const hub = this.resolveHubConnection();
1900
- if (!hub)
1901
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1902
- const groupId = this.extractGroupId(p);
1903
- try {
1904
- const parsed = JSON.parse(body || "{}");
1905
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
1906
- method: "PUT",
1907
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1908
- });
1909
- this.jsonResponse(res, { ok: true });
1910
- }
1911
- catch (err) {
1912
- this.jsonResponse(res, { ok: false, error: String(err) });
1913
- }
1914
- });
1915
- }
1916
- async handleSharingGroupDelete(res, p) {
1917
- const hub = this.resolveHubConnection();
1918
- if (!hub)
1919
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1920
- const groupId = this.extractGroupId(p);
1921
- try {
1922
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
1923
- this.jsonResponse(res, { ok: true });
1924
- }
1925
- catch (err) {
1926
- this.jsonResponse(res, { ok: false, error: String(err) });
1927
- }
1928
- }
1929
- async serveSharingGroupMembers(res, p) {
1930
- const hub = this.resolveHubConnection();
1931
- if (!hub)
1932
- return this.jsonResponse(res, { members: [], error: "not_configured" });
1933
- const groupId = this.extractGroupId(p);
1934
- try {
1935
- const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" });
1936
- this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
1937
- }
1938
- catch (err) {
1939
- this.jsonResponse(res, { members: [], error: String(err) });
1940
- }
1941
- }
1942
- handleSharingGroupAddMember(req, res, p) {
1943
- this.readBody(req, async (body) => {
1944
- const hub = this.resolveHubConnection();
1945
- if (!hub)
1946
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1947
- const groupId = this.extractGroupId(p);
1948
- try {
1949
- const parsed = JSON.parse(body || "{}");
1950
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1951
- method: "POST",
1952
- body: JSON.stringify({ userId: parsed.userId }),
1953
- });
1954
- this.jsonResponse(res, { ok: true });
1955
- }
1956
- catch (err) {
1957
- this.jsonResponse(res, { ok: false, error: String(err) });
1958
- }
1959
- });
1960
- }
1961
- handleSharingGroupRemoveMember(req, res, p) {
1962
- this.readBody(req, async (body) => {
1963
- const hub = this.resolveHubConnection();
1964
- if (!hub)
1965
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1966
- const groupId = this.extractGroupId(p);
1967
- try {
1968
- const parsed = JSON.parse(body || "{}");
1969
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1970
- method: "DELETE",
1971
- body: JSON.stringify({ userId: parsed.userId }),
1972
- });
1973
- this.jsonResponse(res, { ok: true });
1974
- }
1975
- catch (err) {
1976
- this.jsonResponse(res, { ok: false, error: String(err) });
1977
- }
1978
- });
1979
- }
1980
2311
  async serveSharingUsers(res) {
1981
2312
  const hub = this.resolveHubConnection();
1982
2313
  if (!hub)
@@ -1996,7 +2327,17 @@ class ViewerServer {
1996
2327
  return this.jsonResponse(res, { tasks: [], error: "not_configured" });
1997
2328
  try {
1998
2329
  const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
1999
- this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
2330
+ const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
2331
+ for (const tk of tasks) {
2332
+ if (!tk.summary && tk.sourceTaskId) {
2333
+ const local = this.store.getTask(tk.sourceTaskId);
2334
+ if (local) {
2335
+ tk.summary = local.summary;
2336
+ tk.title = tk.title || local.title;
2337
+ }
2338
+ }
2339
+ }
2340
+ this.jsonResponse(res, { tasks });
2000
2341
  }
2001
2342
  catch (err) {
2002
2343
  this.jsonResponse(res, { tasks: [], error: String(err) });
@@ -2021,7 +2362,17 @@ class ViewerServer {
2021
2362
  return this.jsonResponse(res, { skills: [], error: "not_configured" });
2022
2363
  try {
2023
2364
  const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
2024
- this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
2365
+ const skills = Array.isArray(data?.skills) ? data.skills : [];
2366
+ for (const sk of skills) {
2367
+ if (!sk.description && sk.sourceSkillId) {
2368
+ const local = this.store.getSkill(sk.sourceSkillId);
2369
+ if (local) {
2370
+ sk.description = sk.description || local.description;
2371
+ sk.name = sk.name || local.name;
2372
+ }
2373
+ }
2374
+ }
2375
+ this.jsonResponse(res, { skills });
2025
2376
  }
2026
2377
  catch (err) {
2027
2378
  this.jsonResponse(res, { skills: [], error: String(err) });
@@ -2046,7 +2397,18 @@ class ViewerServer {
2046
2397
  return this.jsonResponse(res, { memories: [], error: "not_configured" });
2047
2398
  try {
2048
2399
  const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
2049
- this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
2400
+ const memories = Array.isArray(data?.memories) ? data.memories : [];
2401
+ for (const m of memories) {
2402
+ if (!m.content && m.sourceChunkId) {
2403
+ const local = this.store.getChunk(m.sourceChunkId);
2404
+ if (local) {
2405
+ m.content = local.content;
2406
+ if (!m.summary && local.summary)
2407
+ m.summary = local.summary;
2408
+ }
2409
+ }
2410
+ }
2411
+ this.jsonResponse(res, { memories });
2050
2412
  }
2051
2413
  catch (err) {
2052
2414
  this.jsonResponse(res, { memories: [], error: String(err) });
@@ -2065,6 +2427,48 @@ class ViewerServer {
2065
2427
  this.jsonResponse(res, { ok: false, error: String(err) });
2066
2428
  }
2067
2429
  }
2430
+ async serveSharingNotifications(res, url) {
2431
+ const hub = this.resolveHubConnection();
2432
+ if (!hub)
2433
+ return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
2434
+ try {
2435
+ const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
2436
+ const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`);
2437
+ this.jsonResponse(res, data);
2438
+ }
2439
+ catch {
2440
+ this.jsonResponse(res, { notifications: [], unreadCount: 0 });
2441
+ }
2442
+ }
2443
+ handleSharingNotificationsRead(req, res) {
2444
+ const hub = this.resolveHubConnection();
2445
+ if (!hub)
2446
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
2447
+ this.readBody(req, async (raw) => {
2448
+ try {
2449
+ const body = JSON.parse(raw || "{}");
2450
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
2451
+ this.jsonResponse(res, { ok: true });
2452
+ }
2453
+ catch (err) {
2454
+ this.jsonResponse(res, { ok: false, error: String(err) });
2455
+ }
2456
+ });
2457
+ }
2458
+ handleSharingNotificationsClear(req, res) {
2459
+ const hub = this.resolveHubConnection();
2460
+ if (!hub)
2461
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
2462
+ this.readBody(req, async () => {
2463
+ try {
2464
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
2465
+ this.jsonResponse(res, { ok: true });
2466
+ }
2467
+ catch (err) {
2468
+ this.jsonResponse(res, { ok: false, error: String(err) });
2469
+ }
2470
+ });
2471
+ }
2068
2472
  getLocalIPs() {
2069
2473
  const nets = node_os_1.default.networkInterfaces();
2070
2474
  const ips = [];