@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10

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 (98) hide show
  1. package/.env.example +7 -0
  2. package/README.md +24 -24
  3. package/dist/capture/index.d.ts +1 -1
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +34 -2
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/client/connector.d.ts +5 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +173 -14
  10. package/dist/client/connector.js.map +1 -1
  11. package/dist/client/hub.d.ts.map +1 -1
  12. package/dist/client/hub.js +22 -0
  13. package/dist/client/hub.js.map +1 -1
  14. package/dist/client/skill-sync.d.ts +7 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -1
  16. package/dist/client/skill-sync.js +10 -0
  17. package/dist/client/skill-sync.js.map +1 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +9 -11
  20. package/dist/config.js.map +1 -1
  21. package/dist/hub/server.d.ts +7 -0
  22. package/dist/hub/server.d.ts.map +1 -1
  23. package/dist/hub/server.js +301 -106
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +3 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +18 -1
  28. package/dist/hub/user-manager.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +7 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest/providers/index.d.ts.map +1 -1
  33. package/dist/ingest/providers/index.js +37 -6
  34. package/dist/ingest/providers/index.js.map +1 -1
  35. package/dist/recall/engine.d.ts.map +1 -1
  36. package/dist/recall/engine.js +91 -1
  37. package/dist/recall/engine.js.map +1 -1
  38. package/dist/shared/llm-call.d.ts +1 -0
  39. package/dist/shared/llm-call.d.ts.map +1 -1
  40. package/dist/shared/llm-call.js +82 -8
  41. package/dist/shared/llm-call.js.map +1 -1
  42. package/dist/sharing/types.d.ts +1 -1
  43. package/dist/sharing/types.d.ts.map +1 -1
  44. package/dist/skill/evolver.d.ts +2 -0
  45. package/dist/skill/evolver.d.ts.map +1 -1
  46. package/dist/skill/evolver.js +3 -0
  47. package/dist/skill/evolver.js.map +1 -1
  48. package/dist/storage/ensure-binding.d.ts +12 -0
  49. package/dist/storage/ensure-binding.d.ts.map +1 -0
  50. package/dist/storage/ensure-binding.js +53 -0
  51. package/dist/storage/ensure-binding.js.map +1 -0
  52. package/dist/storage/sqlite.d.ts +74 -20
  53. package/dist/storage/sqlite.d.ts.map +1 -1
  54. package/dist/storage/sqlite.js +301 -207
  55. package/dist/storage/sqlite.js.map +1 -1
  56. package/dist/telemetry.d.ts +12 -5
  57. package/dist/telemetry.d.ts.map +1 -1
  58. package/dist/telemetry.js +156 -40
  59. package/dist/telemetry.js.map +1 -1
  60. package/dist/tools/memory-search.d.ts +3 -1
  61. package/dist/tools/memory-search.d.ts.map +1 -1
  62. package/dist/tools/memory-search.js +3 -1
  63. package/dist/tools/memory-search.js.map +1 -1
  64. package/dist/types.d.ts +1 -2
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/viewer/html.d.ts.map +1 -1
  68. package/dist/viewer/html.js +2991 -1041
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +32 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +1122 -261
  73. package/dist/viewer/server.js.map +1 -1
  74. package/index.ts +384 -43
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +3 -2
  77. package/scripts/postinstall.cjs +1 -1
  78. package/skill/memos-memory-guide/SKILL.md +64 -26
  79. package/src/capture/index.ts +37 -1
  80. package/src/client/connector.ts +173 -16
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +9 -11
  84. package/src/hub/server.ts +285 -98
  85. package/src/hub/user-manager.ts +20 -3
  86. package/src/index.ts +10 -2
  87. package/src/ingest/providers/index.ts +41 -7
  88. package/src/recall/engine.ts +84 -1
  89. package/src/shared/llm-call.ts +97 -9
  90. package/src/sharing/types.ts +1 -1
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/storage/ensure-binding.ts +52 -0
  93. package/src/storage/sqlite.ts +310 -233
  94. package/src/telemetry.ts +172 -41
  95. package/src/tools/memory-search.ts +2 -1
  96. package/src/types.ts +1 -2
  97. package/src/viewer/html.ts +2991 -1041
  98. package/src/viewer/server.ts +984 -190
@@ -91,6 +91,11 @@ class ViewerServer {
91
91
  ppAbort = false;
92
92
  ppState = { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
93
93
  ppSSEClients = [];
94
+ notifSSEClients = [];
95
+ notifPollTimer;
96
+ lastKnownNotifCount = 0;
97
+ hubHeartbeatTimer;
98
+ static HUB_HEARTBEAT_INTERVAL_MS = 45_000;
94
99
  constructor(opts) {
95
100
  this.store = opts.store;
96
101
  this.embedder = opts.embedder;
@@ -109,16 +114,17 @@ class ViewerServer {
109
114
  this.server.on("error", (err) => {
110
115
  if (err.code === "EADDRINUSE") {
111
116
  this.log.warn(`Viewer port ${this.port} in use, trying ${this.port + 1}`);
112
- this.server.listen(this.port + 1, "127.0.0.1");
117
+ this.server.listen(this.port + 1, "0.0.0.0");
113
118
  }
114
119
  else {
115
120
  reject(err);
116
121
  }
117
122
  });
118
- this.server.listen(this.port, "127.0.0.1", () => {
123
+ this.server.listen(this.port, "0.0.0.0", () => {
119
124
  const addr = this.server.address();
120
125
  const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
121
126
  this.autoCleanupPolluted();
127
+ this.startHubHeartbeat();
122
128
  resolve(`http://127.0.0.1:${actualPort}`);
123
129
  });
124
130
  });
@@ -141,6 +147,15 @@ class ViewerServer {
141
147
  }
142
148
  }
143
149
  stop() {
150
+ this.stopHubHeartbeat();
151
+ this.stopNotifPoll();
152
+ for (const c of this.notifSSEClients) {
153
+ try {
154
+ c.end();
155
+ }
156
+ catch { }
157
+ }
158
+ this.notifSSEClients = [];
144
159
  this.server?.close();
145
160
  this.server = null;
146
161
  }
@@ -228,6 +243,16 @@ class ViewerServer {
228
243
  }
229
244
  if (p === "/api/memories" && req.method === "GET")
230
245
  this.serveMemories(res, url);
246
+ else if (p === "/api/memories/share-local" && req.method === "POST")
247
+ this.handleMemoryLocalShare(req, res);
248
+ else if (p === "/api/memories/unshare-local" && req.method === "POST")
249
+ this.handleMemoryLocalUnshare(req, res);
250
+ else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT")
251
+ this.handleMemoryScope(req, res, p);
252
+ else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT")
253
+ this.handleTaskScope(req, res, p);
254
+ else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT")
255
+ this.handleSkillScope(req, res, p);
231
256
  else if (p === "/api/stats")
232
257
  this.serveStats(res, url);
233
258
  else if (p === "/api/metrics")
@@ -282,6 +307,12 @@ class ViewerServer {
282
307
  this.handleSharingApproveUser(req, res);
283
308
  else if (p === "/api/sharing/reject-user" && req.method === "POST")
284
309
  this.handleSharingRejectUser(req, res);
310
+ else if (p === "/api/sharing/remove-user" && req.method === "POST")
311
+ this.handleSharingRemoveUser(req, res);
312
+ else if (p === "/api/sharing/change-role" && req.method === "POST")
313
+ this.handleSharingChangeRole(req, res);
314
+ else if (p === "/api/sharing/retry-join" && req.method === "POST")
315
+ this.handleRetryJoin(req, res);
285
316
  else if (p === "/api/sharing/search/memories" && req.method === "POST")
286
317
  this.handleSharingMemorySearch(req, res);
287
318
  else if (p === "/api/sharing/memories/list" && req.method === "GET")
@@ -300,6 +331,8 @@ class ViewerServer {
300
331
  this.handleSharingTaskUnshare(req, res);
301
332
  else if (p === "/api/sharing/update-username" && req.method === "POST")
302
333
  this.handleUpdateUsername(req, res);
334
+ else if (p === "/api/sharing/rename-user" && req.method === "POST")
335
+ this.handleAdminRenameUser(req, res);
303
336
  else if (p === "/api/sharing/test-hub" && req.method === "POST")
304
337
  this.handleTestHubConnection(req, res);
305
338
  else if (p === "/api/sharing/memories/share" && req.method === "POST")
@@ -312,28 +345,26 @@ class ViewerServer {
312
345
  this.handleSharingSkillShare(req, res);
313
346
  else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
314
347
  this.handleSharingSkillUnshare(req, res);
315
- else if (p === "/api/sharing/groups" && req.method === "GET")
316
- this.serveSharingGroups(res);
317
- else if (p === "/api/sharing/groups" && req.method === "POST")
318
- this.handleSharingGroupCreate(req, res);
319
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT")
320
- this.handleSharingGroupUpdate(req, res, p);
321
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE")
322
- this.handleSharingGroupDelete(res, p);
323
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET")
324
- this.serveSharingGroupMembers(res, p);
325
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST")
326
- this.handleSharingGroupAddMember(req, res, p);
327
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE")
328
- this.handleSharingGroupRemoveMember(req, res, p);
329
348
  else if (p === "/api/sharing/users" && req.method === "GET")
330
349
  this.serveSharingUsers(res);
350
+ else if (p === "/api/sharing/notifications" && req.method === "GET")
351
+ this.serveSharingNotifications(res, url);
352
+ else if (p === "/api/sharing/notifications/read" && req.method === "POST")
353
+ this.handleSharingNotificationsRead(req, res);
354
+ else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
355
+ this.handleSharingNotificationsClear(req, res);
356
+ else if (p === "/api/notifications/stream" && req.method === "GET")
357
+ this.handleNotifSSE(req, res);
331
358
  else if (p === "/api/admin/shared-tasks" && req.method === "GET")
332
359
  this.serveAdminSharedTasks(res);
360
+ else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET")
361
+ this.serveHubTaskDetail(res, p);
333
362
  else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
334
363
  this.handleAdminDeleteTask(res, p);
335
364
  else if (p === "/api/admin/shared-skills" && req.method === "GET")
336
365
  this.serveAdminSharedSkills(res);
366
+ else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET")
367
+ this.serveHubSkillDetail(res, p);
337
368
  else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
338
369
  this.handleAdminDeleteSkill(res, p);
339
370
  else if (p === "/api/admin/shared-memories" && req.method === "GET")
@@ -511,7 +542,11 @@ class ViewerServer {
511
542
  conditions.push("role = ?");
512
543
  params.push(role);
513
544
  }
514
- if (owner) {
545
+ if (owner && owner.startsWith("agent:")) {
546
+ conditions.push("(owner = ? OR owner = 'public')");
547
+ params.push(owner);
548
+ }
549
+ else if (owner) {
515
550
  conditions.push("owner = ?");
516
551
  params.push(owner);
517
552
  }
@@ -525,16 +560,20 @@ class ViewerServer {
525
560
  }
526
561
  const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
527
562
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
528
- const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
563
+ const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY CASE WHEN dedup_status IN ('duplicate','merged') THEN 1 ELSE 0 END ASC, created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
529
564
  const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
530
565
  const chunkIds = rawMemories.map((m) => m.id);
531
566
  const sharingMap = new Map();
567
+ const localShareMap = new Map();
532
568
  if (chunkIds.length > 0) {
533
569
  try {
534
570
  const placeholders = chunkIds.map(() => "?").join(",");
535
571
  const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
536
572
  for (const r of sharedRows)
537
573
  sharingMap.set(r.source_chunk_id, r);
574
+ const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
575
+ for (const r of localRows)
576
+ localShareMap.set(r.chunk_id, r);
538
577
  }
539
578
  catch {
540
579
  }
@@ -546,8 +585,12 @@ class ViewerServer {
546
585
  out.merge_sources = sources;
547
586
  }
548
587
  const shared = sharingMap.get(m.id);
588
+ const localShared = localShareMap.get(m.id);
549
589
  out.sharingVisibility = shared?.visibility ?? null;
550
590
  out.sharingGroupId = shared?.group_id ?? null;
591
+ out.localSharing = out.owner === "public";
592
+ out.localSharingManaged = !!localShared;
593
+ out.localOriginalOwner = localShared?.original_owner ?? null;
551
594
  return out;
552
595
  });
553
596
  this.store.recordViewerEvent("list");
@@ -562,19 +605,35 @@ class ViewerServer {
562
605
  this.jsonResponse(res, data);
563
606
  }
564
607
  serveToolMetrics(res, url) {
565
- const minutes = Math.min(1440, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
608
+ const fromParam = url.searchParams.get("from");
609
+ const toParam = url.searchParams.get("to");
610
+ if (fromParam) {
611
+ const fromMs = new Date(fromParam).getTime();
612
+ const toMs = toParam ? new Date(toParam).getTime() : Date.now();
613
+ if (isNaN(fromMs) || isNaN(toMs)) {
614
+ this.jsonResponse(res, { error: "Invalid date" }, 400);
615
+ return;
616
+ }
617
+ const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
618
+ const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
619
+ this.jsonResponse(res, data);
620
+ return;
621
+ }
622
+ const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
566
623
  const data = this.store.getToolMetrics(minutes);
567
624
  this.jsonResponse(res, data);
568
625
  }
569
626
  serveTasks(res, url) {
570
627
  this.store.recordViewerEvent("tasks_list");
571
628
  const status = url.searchParams.get("status") ?? undefined;
629
+ const owner = url.searchParams.get("owner") ?? undefined;
572
630
  const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
573
631
  const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
574
- const { tasks, total } = this.store.listTasks({ status, limit, offset });
632
+ const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
575
633
  const db = this.store.db;
576
634
  const items = tasks.map((t) => {
577
- const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id);
635
+ const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
636
+ const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id);
578
637
  return {
579
638
  id: t.id,
580
639
  sessionKey: t.sessionKey,
@@ -585,6 +644,8 @@ class ViewerServer {
585
644
  endedAt: t.endedAt,
586
645
  chunkCount: this.store.countChunksByTask(t.id),
587
646
  skillStatus: meta?.skill_status ?? null,
647
+ owner: meta?.owner ?? "agent:main",
648
+ sharingVisibility: sharedTask?.visibility ?? null,
588
649
  };
589
650
  });
590
651
  this.jsonResponse(res, { tasks: items, total, limit, offset });
@@ -620,6 +681,7 @@ class ViewerServer {
620
681
  title: task.title,
621
682
  summary: task.summary,
622
683
  status: task.status,
684
+ owner: task.owner ?? "agent:main",
623
685
  startedAt: task.startedAt,
624
686
  endedAt: task.endedAt,
625
687
  chunks: chunkItems,
@@ -628,6 +690,7 @@ class ViewerServer {
628
690
  skillLinks,
629
691
  sharingVisibility: sharedTask?.visibility ?? null,
630
692
  sharingGroupId: sharedTask?.group_id ?? null,
693
+ hubTaskId: sharedTask ? true : false,
631
694
  });
632
695
  }
633
696
  serveStats(res, url) {
@@ -661,12 +724,21 @@ class ViewerServer {
661
724
  embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
662
725
  }
663
726
  catch { /* table may not exist */ }
664
- const sessionQuery = ownerFilter
665
- ? "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE owner = ? GROUP BY session_key ORDER BY latest DESC"
666
- : "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC";
667
- const sessionList = (ownerFilter
668
- ? db.prepare(sessionQuery).all(ownerFilter)
669
- : db.prepare(sessionQuery).all());
727
+ let sessionQuery;
728
+ let sessionParams;
729
+ if (ownerFilter && ownerFilter.startsWith("agent:")) {
730
+ sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE (owner = ? OR owner = 'public') GROUP BY session_key ORDER BY latest DESC";
731
+ sessionParams = [ownerFilter];
732
+ }
733
+ else if (ownerFilter) {
734
+ sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE owner = ? GROUP BY session_key ORDER BY latest DESC";
735
+ sessionParams = [ownerFilter];
736
+ }
737
+ else {
738
+ sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC";
739
+ sessionParams = [];
740
+ }
741
+ const sessionList = db.prepare(sessionQuery).all(...sessionParams);
670
742
  let skillCount = 0;
671
743
  try {
672
744
  skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
@@ -680,10 +752,17 @@ class ViewerServer {
680
752
  catch { /* column may not exist yet */ }
681
753
  let owners = [];
682
754
  try {
683
- const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all();
755
+ const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all();
684
756
  owners = ownerRows.map((o) => o.owner);
685
757
  }
686
758
  catch { /* column may not exist yet */ }
759
+ let currentAgentOwner = "agent:main";
760
+ try {
761
+ const latest = db.prepare("SELECT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY created_at DESC LIMIT 1").get();
762
+ if (latest?.owner)
763
+ currentAgentOwner = latest.owner;
764
+ }
765
+ catch { /* best-effort */ }
687
766
  this.jsonResponse(res, {
688
767
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
689
768
  totalSkills: skillCount,
@@ -692,6 +771,7 @@ class ViewerServer {
692
771
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
693
772
  sessions: sessionList,
694
773
  owners,
774
+ currentAgentOwner,
695
775
  });
696
776
  }
697
777
  catch (e) {
@@ -815,7 +895,12 @@ class ViewerServer {
815
895
  if (visibility) {
816
896
  skills = skills.filter(s => s.visibility === visibility);
817
897
  }
818
- this.jsonResponse(res, { skills });
898
+ const db = this.store.db;
899
+ const enriched = skills.map(s => {
900
+ const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id);
901
+ return { ...s, sharingVisibility: hub?.visibility ?? null };
902
+ });
903
+ this.jsonResponse(res, { skills: enriched });
819
904
  }
820
905
  serveSkillDetail(res, urlPath) {
821
906
  const skillId = urlPath.replace("/api/skill/", "");
@@ -1075,7 +1160,7 @@ class ViewerServer {
1075
1160
  }
1076
1161
  });
1077
1162
  }
1078
- handleSkillDelete(res, urlPath) {
1163
+ async handleSkillDelete(res, urlPath) {
1079
1164
  const skillId = urlPath.replace("/api/skill/", "");
1080
1165
  const skill = this.store.getSkill(skillId);
1081
1166
  if (!skill) {
@@ -1083,7 +1168,18 @@ class ViewerServer {
1083
1168
  res.end(JSON.stringify({ error: "Skill not found" }));
1084
1169
  return;
1085
1170
  }
1086
- // Remove skill directory from disk
1171
+ try {
1172
+ const hub = this.resolveHubConnection();
1173
+ if (hub) {
1174
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
1175
+ method: "POST",
1176
+ body: JSON.stringify({ sourceSkillId: skillId }),
1177
+ }).catch(() => { });
1178
+ }
1179
+ const db = this.store.db;
1180
+ db.prepare("DELETE FROM hub_skills WHERE source_skill_id = ?").run(skillId);
1181
+ }
1182
+ catch (_) { }
1087
1183
  try {
1088
1184
  if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
1089
1185
  node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
@@ -1133,7 +1229,15 @@ class ViewerServer {
1133
1229
  const cleaned = chunk.role === "user" && chunk.content
1134
1230
  ? { ...chunk, content: (0, capture_1.stripInboundMetadata)(chunk.content) }
1135
1231
  : chunk;
1136
- this.jsonResponse(res, { memory: cleaned });
1232
+ const localShared = this.store.getLocalSharedMemory(chunkId);
1233
+ this.jsonResponse(res, {
1234
+ memory: {
1235
+ ...cleaned,
1236
+ localSharing: cleaned.owner === "public",
1237
+ localSharingManaged: !!localShared,
1238
+ localOriginalOwner: localShared?.originalOwner ?? null,
1239
+ },
1240
+ });
1137
1241
  }
1138
1242
  handleUpdate(req, res, urlPath) {
1139
1243
  const chunkId = urlPath.replace("/api/memory/", "");
@@ -1168,6 +1272,360 @@ class ViewerServer {
1168
1272
  res.end(JSON.stringify({ error: "Not found" }));
1169
1273
  }
1170
1274
  }
1275
+ handleMemoryLocalShare(req, res) {
1276
+ this.readBody(req, (body) => {
1277
+ try {
1278
+ const parsed = JSON.parse(body || "{}");
1279
+ const chunkId = String(parsed.chunkId || "");
1280
+ if (!chunkId)
1281
+ return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
1282
+ const result = this.store.markMemorySharedLocally(chunkId);
1283
+ if (!result.ok) {
1284
+ return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
1285
+ }
1286
+ this.jsonResponse(res, {
1287
+ ok: true,
1288
+ chunkId,
1289
+ owner: result.owner,
1290
+ localSharing: true,
1291
+ localSharingManaged: true,
1292
+ localOriginalOwner: result.originalOwner ?? null,
1293
+ });
1294
+ }
1295
+ catch (err) {
1296
+ this.jsonResponse(res, { ok: false, error: String(err) }, 400);
1297
+ }
1298
+ });
1299
+ }
1300
+ handleMemoryLocalUnshare(req, res) {
1301
+ this.readBody(req, (body) => {
1302
+ try {
1303
+ const parsed = JSON.parse(body || "{}");
1304
+ const chunkId = String(parsed.chunkId || "");
1305
+ const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
1306
+ if (!chunkId)
1307
+ return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
1308
+ const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
1309
+ if (!result.ok) {
1310
+ return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
1311
+ }
1312
+ this.jsonResponse(res, {
1313
+ ok: true,
1314
+ chunkId,
1315
+ owner: result.owner,
1316
+ localSharing: false,
1317
+ localOriginalOwner: result.originalOwner ?? null,
1318
+ });
1319
+ }
1320
+ catch (err) {
1321
+ this.jsonResponse(res, { ok: false, error: String(err) }, 400);
1322
+ }
1323
+ });
1324
+ }
1325
+ // ─── Unified scope API ───
1326
+ handleMemoryScope(req, res, urlPath) {
1327
+ const chunkId = urlPath.split("/")[3];
1328
+ this.readBody(req, async (body) => {
1329
+ try {
1330
+ const parsed = JSON.parse(body || "{}");
1331
+ const scope = parsed.scope;
1332
+ if (!["private", "local", "team"].includes(scope)) {
1333
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1334
+ }
1335
+ const db = this.store.db;
1336
+ const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
1337
+ if (!chunk)
1338
+ return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
1339
+ if (chunk.dedup_status && chunk.dedup_status !== "active") {
1340
+ return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
1341
+ }
1342
+ const isLocalShared = chunk.owner === "public";
1343
+ const hubMemory = this.getHubMemoryForChunk(chunkId);
1344
+ const isTeamShared = !!hubMemory;
1345
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1346
+ if (scope === currentScope) {
1347
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1348
+ }
1349
+ let hubSynced = false;
1350
+ if (scope === "team") {
1351
+ if (!isTeamShared) {
1352
+ const hubClient = await this.resolveHubClientAware();
1353
+ const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
1354
+ const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
1355
+ method: "POST",
1356
+ body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
1357
+ });
1358
+ if (!isLocalShared)
1359
+ this.store.markMemorySharedLocally(chunkId);
1360
+ if (hubClient.userId) {
1361
+ const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
1362
+ this.store.upsertHubMemory({
1363
+ id: response?.memoryId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1364
+ sourceChunkId: chunkId, sourceUserId: hubClient.userId,
1365
+ role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
1366
+ kind: refreshedChunk.kind, groupId: null, visibility: "public",
1367
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1368
+ });
1369
+ }
1370
+ hubSynced = true;
1371
+ }
1372
+ else {
1373
+ if (!isLocalShared)
1374
+ this.store.markMemorySharedLocally(chunkId);
1375
+ }
1376
+ }
1377
+ else if (scope === "local") {
1378
+ if (isTeamShared) {
1379
+ try {
1380
+ const hubClient = await this.resolveHubClientAware();
1381
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1382
+ method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1383
+ });
1384
+ if (hubClient.userId)
1385
+ this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1386
+ hubSynced = true;
1387
+ }
1388
+ catch (err) {
1389
+ this.log.warn(`Failed to unshare memory from team: ${err}`);
1390
+ }
1391
+ }
1392
+ if (!isLocalShared)
1393
+ this.store.markMemorySharedLocally(chunkId);
1394
+ }
1395
+ else {
1396
+ if (isTeamShared) {
1397
+ try {
1398
+ const hubClient = await this.resolveHubClientAware();
1399
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1400
+ method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
1401
+ });
1402
+ if (hubClient.userId)
1403
+ this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
1404
+ hubSynced = true;
1405
+ }
1406
+ catch (err) {
1407
+ this.log.warn(`Failed to unshare memory from team: ${err}`);
1408
+ }
1409
+ }
1410
+ if (isLocalShared)
1411
+ this.store.unmarkMemorySharedLocally(chunkId);
1412
+ }
1413
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1414
+ }
1415
+ catch (err) {
1416
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1417
+ }
1418
+ });
1419
+ }
1420
+ handleTaskScope(req, res, urlPath) {
1421
+ const taskId = urlPath.split("/")[3];
1422
+ this.readBody(req, async (body) => {
1423
+ try {
1424
+ const parsed = JSON.parse(body || "{}");
1425
+ const scope = parsed.scope;
1426
+ if (!["private", "local", "team"].includes(scope)) {
1427
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1428
+ }
1429
+ const task = this.store.getTask(taskId);
1430
+ if (!task)
1431
+ return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
1432
+ if (scope !== "private" && task.status !== "completed") {
1433
+ return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
1434
+ }
1435
+ const isLocalShared = task.owner === "public";
1436
+ const hubTask = this.getHubTaskForLocal(taskId);
1437
+ const isTeamShared = !!hubTask;
1438
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1439
+ if (scope === currentScope) {
1440
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1441
+ }
1442
+ let hubSynced = false;
1443
+ if (scope === "team") {
1444
+ if (!isTeamShared) {
1445
+ const chunks = this.store.getChunksByTask(taskId);
1446
+ const hubClient = await this.resolveHubClientAware();
1447
+ const refreshedTask = this.store.getTask(taskId);
1448
+ const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1449
+ method: "POST",
1450
+ body: JSON.stringify({
1451
+ 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() },
1452
+ 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() })),
1453
+ }),
1454
+ });
1455
+ if (hubClient.userId) {
1456
+ const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
1457
+ this.store.upsertHubTask({
1458
+ id: response?.taskId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1459
+ sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
1460
+ summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
1461
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1462
+ });
1463
+ }
1464
+ hubSynced = true;
1465
+ }
1466
+ if (!isLocalShared) {
1467
+ const originalOwner = task.owner;
1468
+ const db = this.store.db;
1469
+ 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());
1470
+ db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
1471
+ }
1472
+ }
1473
+ if (scope === "local") {
1474
+ if (!isLocalShared) {
1475
+ const originalOwner = task.owner;
1476
+ const db = this.store.db;
1477
+ 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());
1478
+ db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
1479
+ }
1480
+ }
1481
+ if (scope === "local" && isTeamShared) {
1482
+ try {
1483
+ const hubClient = await this.resolveHubClientAware();
1484
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1485
+ method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1486
+ });
1487
+ if (hubClient.userId)
1488
+ this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1489
+ hubSynced = true;
1490
+ }
1491
+ catch (err) {
1492
+ this.log.warn(`Failed to unshare task from team: ${err}`);
1493
+ }
1494
+ }
1495
+ if (scope === "private") {
1496
+ if (isTeamShared) {
1497
+ try {
1498
+ const hubClient = await this.resolveHubClientAware();
1499
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1500
+ method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
1501
+ });
1502
+ if (hubClient.userId)
1503
+ this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1504
+ hubSynced = true;
1505
+ }
1506
+ catch (err) {
1507
+ this.log.warn(`Failed to unshare task from team: ${err}`);
1508
+ }
1509
+ }
1510
+ if (isLocalShared) {
1511
+ const db = this.store.db;
1512
+ const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId);
1513
+ const restoreOwner = shared?.original_owner ?? task.owner;
1514
+ if (restoreOwner && restoreOwner !== "public") {
1515
+ db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
1516
+ }
1517
+ db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
1518
+ }
1519
+ }
1520
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1521
+ }
1522
+ catch (err) {
1523
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1524
+ }
1525
+ });
1526
+ }
1527
+ handleSkillScope(req, res, urlPath) {
1528
+ const skillId = urlPath.split("/")[3];
1529
+ this.readBody(req, async (body) => {
1530
+ try {
1531
+ const parsed = JSON.parse(body || "{}");
1532
+ const scope = parsed.scope;
1533
+ if (!["private", "local", "team"].includes(scope)) {
1534
+ return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
1535
+ }
1536
+ const skill = this.store.getSkill(skillId);
1537
+ if (!skill)
1538
+ return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
1539
+ if (scope !== "private" && skill.status !== "active") {
1540
+ return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
1541
+ }
1542
+ const isLocalShared = skill.visibility === "public";
1543
+ const hubSkill = this.getHubSkillForLocal(skillId);
1544
+ const isTeamShared = !!hubSkill;
1545
+ const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1546
+ if (scope === currentScope) {
1547
+ return this.jsonResponse(res, { ok: true, scope, changed: false });
1548
+ }
1549
+ let hubSynced = false;
1550
+ if (scope === "team") {
1551
+ if (!isTeamShared) {
1552
+ const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
1553
+ const hubClient = await this.resolveHubClientAware();
1554
+ const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
1555
+ method: "POST",
1556
+ body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
1557
+ });
1558
+ if (hubClient.userId) {
1559
+ const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
1560
+ this.store.upsertHubSkill({
1561
+ id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
1562
+ sourceSkillId: skillId, sourceUserId: hubClient.userId,
1563
+ name: skill.name, description: skill.description, version: skill.version,
1564
+ groupId: null, visibility: "public",
1565
+ bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
1566
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
1567
+ });
1568
+ }
1569
+ hubSynced = true;
1570
+ }
1571
+ if (!isLocalShared)
1572
+ this.store.setSkillVisibility(skillId, "public");
1573
+ }
1574
+ if (scope === "local") {
1575
+ if (!isLocalShared)
1576
+ this.store.setSkillVisibility(skillId, "public");
1577
+ }
1578
+ if (scope === "local" && isTeamShared) {
1579
+ try {
1580
+ const hubClient = await this.resolveHubClientAware();
1581
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1582
+ method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1583
+ });
1584
+ if (hubClient.userId)
1585
+ this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1586
+ hubSynced = true;
1587
+ }
1588
+ catch (err) {
1589
+ this.log.warn(`Failed to unpublish skill from team: ${err}`);
1590
+ }
1591
+ }
1592
+ if (scope === "private") {
1593
+ if (isTeamShared) {
1594
+ try {
1595
+ const hubClient = await this.resolveHubClientAware();
1596
+ await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1597
+ method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
1598
+ });
1599
+ if (hubClient.userId)
1600
+ this.store.deleteHubSkillBySource(hubClient.userId, skillId);
1601
+ hubSynced = true;
1602
+ }
1603
+ catch (err) {
1604
+ this.log.warn(`Failed to unpublish skill from team: ${err}`);
1605
+ }
1606
+ }
1607
+ if (isLocalShared)
1608
+ this.store.setSkillVisibility(skillId, "private");
1609
+ }
1610
+ this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
1611
+ }
1612
+ catch (err) {
1613
+ this.jsonResponse(res, { ok: false, error: String(err) }, 500);
1614
+ }
1615
+ });
1616
+ }
1617
+ getHubMemoryForChunk(chunkId) {
1618
+ const db = this.store.db;
1619
+ return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
1620
+ }
1621
+ getHubTaskForLocal(taskId) {
1622
+ const db = this.store.db;
1623
+ return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
1624
+ }
1625
+ getHubSkillForLocal(skillId) {
1626
+ const db = this.store.db;
1627
+ return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
1628
+ }
1171
1629
  handleDeleteSession(res, url) {
1172
1630
  const key = url.searchParams.get("key");
1173
1631
  if (!key) {
@@ -1205,7 +1663,8 @@ class ViewerServer {
1205
1663
  // ─── Config API ───
1206
1664
  getOpenClawConfigPath() {
1207
1665
  const home = process.env.HOME || process.env.USERPROFILE || "";
1208
- return node_path_1.default.join(home, ".openclaw", "openclaw.json");
1666
+ const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
1667
+ return node_path_1.default.join(ocHome, "openclaw.json");
1209
1668
  }
1210
1669
  getPluginEntryConfig(raw) {
1211
1670
  const entries = raw?.plugins?.entries ?? {};
@@ -1261,8 +1720,7 @@ class ViewerServer {
1261
1720
  base.admin.rejectSupported = true;
1262
1721
  base.connection.connected = true;
1263
1722
  base.connection.hubUrl = resolvedHubUrl ?? undefined;
1264
- // 通过 hub API 获取 admin 用户的真实信息(含分组)
1265
- let adminUser = { username: "hub-admin", role: "admin", groups: [] };
1723
+ let adminUser = { username: "hub-admin", role: "admin" };
1266
1724
  try {
1267
1725
  const hub = this.resolveHubConnection();
1268
1726
  if (hub) {
@@ -1272,7 +1730,6 @@ class ViewerServer {
1272
1730
  id: me.id,
1273
1731
  username: me.username ?? "hub-admin",
1274
1732
  role: me.role ?? "admin",
1275
- groups: Array.isArray(me.groups) ? me.groups : [],
1276
1733
  };
1277
1734
  }
1278
1735
  }
@@ -1290,13 +1747,23 @@ class ViewerServer {
1290
1747
  this.jsonResponse(res, base);
1291
1748
  return;
1292
1749
  }
1293
- if (!hasClientConfig) {
1750
+ const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
1751
+ if (!hasClientConfig && !hasPendingConnection) {
1294
1752
  this.jsonResponse(res, base);
1295
1753
  return;
1296
1754
  }
1297
1755
  try {
1298
1756
  const status = await (0, connector_1.getHubStatus)(this.store, this.ctx.config);
1299
1757
  const output = { ...base, connection: { ...base.connection, ...status } };
1758
+ if (status.user?.status === "pending") {
1759
+ output.connection.pendingApproval = true;
1760
+ }
1761
+ if (status.user?.status === "rejected") {
1762
+ output.connection.rejected = true;
1763
+ }
1764
+ if (status.user?.status === "removed") {
1765
+ output.connection.removed = true;
1766
+ }
1300
1767
  if (status.connected && status.hubUrl) {
1301
1768
  try {
1302
1769
  const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
@@ -1305,6 +1772,13 @@ class ViewerServer {
1305
1772
  }
1306
1773
  catch { }
1307
1774
  }
1775
+ else if (status.hubUrl) {
1776
+ try {
1777
+ const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
1778
+ output.connection.teamName = info?.teamName ?? null;
1779
+ }
1780
+ catch { }
1781
+ }
1308
1782
  output.admin.canManageUsers = status.connected && status.user?.role === "admin";
1309
1783
  output.admin.rejectSupported = output.admin.canManageUsers;
1310
1784
  this.jsonResponse(res, output);
@@ -1367,81 +1841,227 @@ class ViewerServer {
1367
1841
  }
1368
1842
  });
1369
1843
  }
1370
- async serveSharingMemoryList(res, url) {
1371
- if (!this.ctx)
1372
- return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
1373
- try {
1374
- const limit = Number(url.searchParams.get("limit") || 40);
1375
- const data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
1376
- this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
1377
- }
1378
- catch (err) {
1379
- this.jsonResponse(res, { memories: [], error: String(err) });
1380
- }
1381
- }
1382
- async serveSharingTaskList(res, url) {
1383
- if (!this.ctx)
1384
- return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
1385
- try {
1386
- const limit = Number(url.searchParams.get("limit") || 40);
1387
- const data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
1388
- this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
1389
- }
1390
- catch (err) {
1391
- this.jsonResponse(res, { tasks: [], error: String(err) });
1392
- }
1393
- }
1394
- async serveSharingSkillList(res, url) {
1395
- if (!this.ctx)
1396
- return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
1397
- try {
1398
- const limit = Number(url.searchParams.get("limit") || 40);
1399
- const data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
1400
- this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
1401
- }
1402
- catch (err) {
1403
- this.jsonResponse(res, { skills: [], error: String(err) });
1404
- }
1405
- }
1406
- handleSharingMemorySearch(req, res) {
1844
+ handleSharingChangeRole(req, res) {
1407
1845
  this.readBody(req, async (body) => {
1408
1846
  if (!this.ctx)
1409
- return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
1410
- const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
1847
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1411
1848
  try {
1412
1849
  const parsed = JSON.parse(body || "{}");
1413
- const query = String(parsed.query || "");
1414
- const role = typeof parsed.role === "string" ? parsed.role : undefined;
1415
- const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
1416
- const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
1417
- const local = this.searchLocalViewerMemories(query, { role, maxResults });
1418
- if (scope === "local") {
1419
- return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
1420
- }
1421
- try {
1422
- const hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
1423
- this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
1424
- }
1425
- catch (err) {
1426
- this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
1427
- }
1850
+ const hub = this.resolveHubConnection();
1851
+ if (!hub)
1852
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
1853
+ const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
1854
+ method: "POST",
1855
+ body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
1856
+ });
1857
+ this.jsonResponse(res, { ok: true, result });
1428
1858
  }
1429
1859
  catch (err) {
1430
- this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
1860
+ this.jsonResponse(res, { ok: false, error: String(err) });
1431
1861
  }
1432
1862
  });
1433
1863
  }
1434
- handleSharingMemoryDetail(req, res) {
1864
+ handleSharingRemoveUser(req, res) {
1435
1865
  this.readBody(req, async (body) => {
1436
1866
  if (!this.ctx)
1437
- return this.jsonResponse(res, { error: "sharing_unavailable" });
1867
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1438
1868
  try {
1439
1869
  const parsed = JSON.parse(body || "{}");
1440
- const detail = await (0, hub_1.hubGetMemoryDetail)(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
1441
- this.jsonResponse(res, detail);
1870
+ const hub = this.resolveHubConnection();
1871
+ if (!hub)
1872
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
1873
+ const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
1874
+ method: "POST",
1875
+ body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
1876
+ });
1877
+ this.jsonResponse(res, { ok: true, result });
1442
1878
  }
1443
1879
  catch (err) {
1444
- this.jsonResponse(res, { error: String(err) });
1880
+ this.jsonResponse(res, { ok: false, error: String(err) });
1881
+ }
1882
+ });
1883
+ }
1884
+ handleAdminRenameUser(req, res) {
1885
+ this.readBody(req, async (body) => {
1886
+ if (!this.ctx)
1887
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1888
+ try {
1889
+ const parsed = JSON.parse(body || "{}");
1890
+ const hub = this.resolveHubConnection();
1891
+ if (!hub)
1892
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
1893
+ const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
1894
+ method: "POST",
1895
+ body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
1896
+ });
1897
+ this.jsonResponse(res, { ok: true, result });
1898
+ }
1899
+ catch (err) {
1900
+ const errStr = String(err);
1901
+ if (errStr.includes("username_taken")) {
1902
+ this.jsonResponse(res, { ok: false, error: "username_taken" });
1903
+ }
1904
+ else if (errStr.includes("invalid_params")) {
1905
+ this.jsonResponse(res, { ok: false, error: "invalid_params" });
1906
+ }
1907
+ else {
1908
+ this.jsonResponse(res, { ok: false, error: errStr });
1909
+ }
1910
+ }
1911
+ });
1912
+ }
1913
+ handleRetryJoin(req, res) {
1914
+ this.readBody(req, async (_body) => {
1915
+ if (!this.ctx)
1916
+ return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1917
+ const sharing = this.ctx.config.sharing;
1918
+ if (!sharing?.enabled || sharing.role !== "client") {
1919
+ return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
1920
+ }
1921
+ const hubAddress = sharing.client?.hubAddress ?? "";
1922
+ const teamToken = sharing.client?.teamToken ?? "";
1923
+ if (!hubAddress || !teamToken) {
1924
+ return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
1925
+ }
1926
+ try {
1927
+ const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
1928
+ const localIPs = this.getLocalIPs();
1929
+ localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
1930
+ try {
1931
+ const u = new URL(hubUrl);
1932
+ if (localIPs.includes(u.hostname)) {
1933
+ return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
1934
+ }
1935
+ }
1936
+ catch { }
1937
+ const os = await Promise.resolve().then(() => __importStar(require("os")));
1938
+ const nickname = sharing.client?.nickname;
1939
+ const username = nickname || os.userInfo().username || "user";
1940
+ const hostname = os.hostname() || "unknown";
1941
+ const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
1942
+ method: "POST",
1943
+ body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true }),
1944
+ });
1945
+ this.store.setClientHubConnection({
1946
+ hubUrl,
1947
+ userId: String(result.userId || ""),
1948
+ username,
1949
+ userToken: result.userToken || "",
1950
+ role: "member",
1951
+ connectedAt: Date.now(),
1952
+ });
1953
+ this.jsonResponse(res, { ok: true, status: result.status || "pending" });
1954
+ }
1955
+ catch (err) {
1956
+ this.jsonResponse(res, { ok: false, error: String(err) });
1957
+ }
1958
+ });
1959
+ }
1960
+ async serveSharingMemoryList(res, url) {
1961
+ if (!this.ctx)
1962
+ return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
1963
+ try {
1964
+ const limit = Number(url.searchParams.get("limit") || 40);
1965
+ const hub = this.resolveHubConnection();
1966
+ let data;
1967
+ if (hub) {
1968
+ data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
1969
+ }
1970
+ else {
1971
+ data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
1972
+ }
1973
+ this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
1974
+ }
1975
+ catch (err) {
1976
+ this.jsonResponse(res, { memories: [], error: String(err) });
1977
+ }
1978
+ }
1979
+ async serveSharingTaskList(res, url) {
1980
+ if (!this.ctx)
1981
+ return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
1982
+ try {
1983
+ const limit = Number(url.searchParams.get("limit") || 40);
1984
+ const hub = this.resolveHubConnection();
1985
+ let data;
1986
+ if (hub) {
1987
+ data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
1988
+ }
1989
+ else {
1990
+ data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
1991
+ }
1992
+ this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
1993
+ }
1994
+ catch (err) {
1995
+ this.jsonResponse(res, { tasks: [], error: String(err) });
1996
+ }
1997
+ }
1998
+ async serveSharingSkillList(res, url) {
1999
+ if (!this.ctx)
2000
+ return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
2001
+ try {
2002
+ const limit = Number(url.searchParams.get("limit") || 40);
2003
+ const hub = this.resolveHubConnection();
2004
+ let data;
2005
+ if (hub) {
2006
+ data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
2007
+ }
2008
+ else {
2009
+ data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
2010
+ }
2011
+ this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
2012
+ }
2013
+ catch (err) {
2014
+ this.jsonResponse(res, { skills: [], error: String(err) });
2015
+ }
2016
+ }
2017
+ handleSharingMemorySearch(req, res) {
2018
+ this.readBody(req, async (body) => {
2019
+ if (!this.ctx)
2020
+ return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
2021
+ const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
2022
+ try {
2023
+ const parsed = JSON.parse(body || "{}");
2024
+ const query = String(parsed.query || "");
2025
+ const role = typeof parsed.role === "string" ? parsed.role : undefined;
2026
+ const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
2027
+ const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
2028
+ const local = this.searchLocalViewerMemories(query, { role, maxResults });
2029
+ if (scope === "local") {
2030
+ return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
2031
+ }
2032
+ try {
2033
+ const conn = this.resolveHubConnection();
2034
+ let hub;
2035
+ if (conn) {
2036
+ hub = await (0, hub_1.hubRequestJson)(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
2037
+ method: "POST", body: JSON.stringify({ query, maxResults, scope }),
2038
+ });
2039
+ }
2040
+ else {
2041
+ hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
2042
+ }
2043
+ this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
2044
+ }
2045
+ catch (err) {
2046
+ this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
2047
+ }
2048
+ }
2049
+ catch (err) {
2050
+ this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
2051
+ }
2052
+ });
2053
+ }
2054
+ handleSharingMemoryDetail(req, res) {
2055
+ this.readBody(req, async (body) => {
2056
+ if (!this.ctx)
2057
+ return this.jsonResponse(res, { error: "sharing_unavailable" });
2058
+ try {
2059
+ const parsed = JSON.parse(body || "{}");
2060
+ const detail = await (0, hub_1.hubGetMemoryDetail)(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
2061
+ this.jsonResponse(res, detail);
2062
+ }
2063
+ catch (err) {
2064
+ this.jsonResponse(res, { error: String(err) });
1445
2065
  }
1446
2066
  });
1447
2067
  }
@@ -1515,8 +2135,8 @@ class ViewerServer {
1515
2135
  try {
1516
2136
  const parsed = JSON.parse(body || "{}");
1517
2137
  const taskId = String(parsed.taskId || "");
1518
- const visibility = parsed.visibility === "group" ? "group" : "public";
1519
- const groupId = typeof parsed.groupId === "string" ? parsed.groupId : undefined;
2138
+ const visibility = "public";
2139
+ const groupId = undefined;
1520
2140
  const task = this.store.getTask(taskId);
1521
2141
  if (!task)
1522
2142
  return this.jsonResponse(res, { ok: false, error: "task_not_found" });
@@ -1532,7 +2152,7 @@ class ViewerServer {
1532
2152
  sourceTaskId: task.id,
1533
2153
  title: task.title,
1534
2154
  summary: task.summary,
1535
- groupId: visibility === "group" ? groupId ?? null : null,
2155
+ groupId: null,
1536
2156
  visibility,
1537
2157
  createdAt: task.startedAt ?? Date.now(),
1538
2158
  updatedAt: task.updatedAt ?? Date.now(),
@@ -1558,7 +2178,7 @@ class ViewerServer {
1558
2178
  sourceUserId: hubUserId,
1559
2179
  title: task.title,
1560
2180
  summary: task.summary,
1561
- groupId: visibility === "group" ? groupId ?? null : null,
2181
+ groupId: null,
1562
2182
  visibility,
1563
2183
  createdAt: task.startedAt ?? Date.now(),
1564
2184
  updatedAt: task.updatedAt ?? Date.now(),
@@ -1603,8 +2223,8 @@ class ViewerServer {
1603
2223
  try {
1604
2224
  const parsed = JSON.parse(body || "{}");
1605
2225
  const chunkId = String(parsed.chunkId || "");
1606
- const visibility = parsed.visibility === "group" ? "group" : "public";
1607
- const groupId = typeof parsed.groupId === "string" ? parsed.groupId : undefined;
2226
+ const visibility = "public";
2227
+ const groupId = undefined;
1608
2228
  const db = this.store.db;
1609
2229
  const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
1610
2230
  if (!chunk)
@@ -1619,7 +2239,7 @@ class ViewerServer {
1619
2239
  content: chunk.content,
1620
2240
  summary: chunk.summary,
1621
2241
  kind: chunk.kind,
1622
- groupId: visibility === "group" ? groupId ?? null : null,
2242
+ groupId: null,
1623
2243
  visibility,
1624
2244
  },
1625
2245
  }),
@@ -1636,7 +2256,7 @@ class ViewerServer {
1636
2256
  content: chunk.content,
1637
2257
  summary: chunk.summary ?? "",
1638
2258
  kind: chunk.kind,
1639
- groupId: visibility === "group" ? groupId ?? null : null,
2259
+ groupId: null,
1640
2260
  visibility,
1641
2261
  createdAt: existing?.createdAt ?? now,
1642
2262
  updatedAt: now,
@@ -1694,8 +2314,8 @@ class ViewerServer {
1694
2314
  try {
1695
2315
  const parsed = JSON.parse(body || "{}");
1696
2316
  const skillId = String(parsed.skillId || "");
1697
- const visibility = parsed.visibility === "group" ? "group" : "public";
1698
- const groupId = parsed.groupId ? String(parsed.groupId) : null;
2317
+ const visibility = "public";
2318
+ const groupId = null;
1699
2319
  const skill = this.store.getSkill(skillId);
1700
2320
  if (!skill)
1701
2321
  return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
@@ -1705,7 +2325,7 @@ class ViewerServer {
1705
2325
  method: "POST",
1706
2326
  body: JSON.stringify({
1707
2327
  visibility,
1708
- groupId: visibility === "group" ? groupId : null,
2328
+ groupId: null,
1709
2329
  metadata: bundle.metadata,
1710
2330
  bundle: bundle.bundle,
1711
2331
  }),
@@ -1720,7 +2340,7 @@ class ViewerServer {
1720
2340
  name: skill.name,
1721
2341
  description: skill.description,
1722
2342
  version: skill.version,
1723
- groupId: visibility === "group" ? groupId : null,
2343
+ groupId: null,
1724
2344
  visibility,
1725
2345
  bundle: JSON.stringify(bundle.bundle),
1726
2346
  qualityScore: skill.qualityScore,
@@ -1807,123 +2427,6 @@ class ViewerServer {
1807
2427
  }
1808
2428
  return (0, hub_1.resolveHubClient)(this.store, this.ctx);
1809
2429
  }
1810
- extractGroupId(path) {
1811
- const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
1812
- return m ? decodeURIComponent(m[1]) : "";
1813
- }
1814
- async serveSharingGroups(res) {
1815
- const hub = this.resolveHubConnection();
1816
- if (!hub)
1817
- return this.jsonResponse(res, { groups: [], error: "not_configured" });
1818
- try {
1819
- const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" });
1820
- this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
1821
- }
1822
- catch (err) {
1823
- this.jsonResponse(res, { groups: [], error: String(err) });
1824
- }
1825
- }
1826
- handleSharingGroupCreate(req, res) {
1827
- this.readBody(req, async (body) => {
1828
- const hub = this.resolveHubConnection();
1829
- if (!hub)
1830
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1831
- try {
1832
- const parsed = JSON.parse(body || "{}");
1833
- const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
1834
- method: "POST",
1835
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1836
- });
1837
- this.jsonResponse(res, { ok: true, ...data });
1838
- }
1839
- catch (err) {
1840
- this.jsonResponse(res, { ok: false, error: String(err) });
1841
- }
1842
- });
1843
- }
1844
- handleSharingGroupUpdate(req, res, p) {
1845
- this.readBody(req, async (body) => {
1846
- const hub = this.resolveHubConnection();
1847
- if (!hub)
1848
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1849
- const groupId = this.extractGroupId(p);
1850
- try {
1851
- const parsed = JSON.parse(body || "{}");
1852
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
1853
- method: "PUT",
1854
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1855
- });
1856
- this.jsonResponse(res, { ok: true });
1857
- }
1858
- catch (err) {
1859
- this.jsonResponse(res, { ok: false, error: String(err) });
1860
- }
1861
- });
1862
- }
1863
- async handleSharingGroupDelete(res, p) {
1864
- const hub = this.resolveHubConnection();
1865
- if (!hub)
1866
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1867
- const groupId = this.extractGroupId(p);
1868
- try {
1869
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
1870
- this.jsonResponse(res, { ok: true });
1871
- }
1872
- catch (err) {
1873
- this.jsonResponse(res, { ok: false, error: String(err) });
1874
- }
1875
- }
1876
- async serveSharingGroupMembers(res, p) {
1877
- const hub = this.resolveHubConnection();
1878
- if (!hub)
1879
- return this.jsonResponse(res, { members: [], error: "not_configured" });
1880
- const groupId = this.extractGroupId(p);
1881
- try {
1882
- const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" });
1883
- this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
1884
- }
1885
- catch (err) {
1886
- this.jsonResponse(res, { members: [], error: String(err) });
1887
- }
1888
- }
1889
- handleSharingGroupAddMember(req, res, p) {
1890
- this.readBody(req, async (body) => {
1891
- const hub = this.resolveHubConnection();
1892
- if (!hub)
1893
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1894
- const groupId = this.extractGroupId(p);
1895
- try {
1896
- const parsed = JSON.parse(body || "{}");
1897
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1898
- method: "POST",
1899
- body: JSON.stringify({ userId: parsed.userId }),
1900
- });
1901
- this.jsonResponse(res, { ok: true });
1902
- }
1903
- catch (err) {
1904
- this.jsonResponse(res, { ok: false, error: String(err) });
1905
- }
1906
- });
1907
- }
1908
- handleSharingGroupRemoveMember(req, res, p) {
1909
- this.readBody(req, async (body) => {
1910
- const hub = this.resolveHubConnection();
1911
- if (!hub)
1912
- return this.jsonResponse(res, { ok: false, error: "not_configured" });
1913
- const groupId = this.extractGroupId(p);
1914
- try {
1915
- const parsed = JSON.parse(body || "{}");
1916
- await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1917
- method: "DELETE",
1918
- body: JSON.stringify({ userId: parsed.userId }),
1919
- });
1920
- this.jsonResponse(res, { ok: true });
1921
- }
1922
- catch (err) {
1923
- this.jsonResponse(res, { ok: false, error: String(err) });
1924
- }
1925
- });
1926
- }
1927
2430
  async serveSharingUsers(res) {
1928
2431
  const hub = this.resolveHubConnection();
1929
2432
  if (!hub)
@@ -1943,7 +2446,17 @@ class ViewerServer {
1943
2446
  return this.jsonResponse(res, { tasks: [], error: "not_configured" });
1944
2447
  try {
1945
2448
  const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
1946
- this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
2449
+ const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
2450
+ for (const tk of tasks) {
2451
+ if (!tk.summary && tk.sourceTaskId) {
2452
+ const local = this.store.getTask(tk.sourceTaskId);
2453
+ if (local) {
2454
+ tk.summary = local.summary;
2455
+ tk.title = tk.title || local.title;
2456
+ }
2457
+ }
2458
+ }
2459
+ this.jsonResponse(res, { tasks });
1947
2460
  }
1948
2461
  catch (err) {
1949
2462
  this.jsonResponse(res, { tasks: [], error: String(err) });
@@ -1962,13 +2475,55 @@ class ViewerServer {
1962
2475
  this.jsonResponse(res, { ok: false, error: String(err) });
1963
2476
  }
1964
2477
  }
2478
+ async serveHubTaskDetail(res, p) {
2479
+ const hub = this.resolveHubConnection();
2480
+ if (!hub)
2481
+ return this.jsonResponse(res, { error: "not_configured" }, 500);
2482
+ const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
2483
+ if (!m)
2484
+ return this.jsonResponse(res, { error: "bad_request" }, 400);
2485
+ const taskId = decodeURIComponent(m[1]);
2486
+ try {
2487
+ const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" });
2488
+ this.jsonResponse(res, data);
2489
+ }
2490
+ catch (err) {
2491
+ this.jsonResponse(res, { error: String(err) }, 500);
2492
+ }
2493
+ }
2494
+ async serveHubSkillDetail(res, p) {
2495
+ const hub = this.resolveHubConnection();
2496
+ if (!hub)
2497
+ return this.jsonResponse(res, { error: "not_configured" }, 500);
2498
+ const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
2499
+ if (!m)
2500
+ return this.jsonResponse(res, { error: "bad_request" }, 400);
2501
+ const skillId = decodeURIComponent(m[1]);
2502
+ try {
2503
+ const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" });
2504
+ this.jsonResponse(res, data);
2505
+ }
2506
+ catch (err) {
2507
+ this.jsonResponse(res, { error: String(err) }, 500);
2508
+ }
2509
+ }
1965
2510
  async serveAdminSharedSkills(res) {
1966
2511
  const hub = this.resolveHubConnection();
1967
2512
  if (!hub)
1968
2513
  return this.jsonResponse(res, { skills: [], error: "not_configured" });
1969
2514
  try {
1970
2515
  const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
1971
- this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
2516
+ const skills = Array.isArray(data?.skills) ? data.skills : [];
2517
+ for (const sk of skills) {
2518
+ if (!sk.description && sk.sourceSkillId) {
2519
+ const local = this.store.getSkill(sk.sourceSkillId);
2520
+ if (local) {
2521
+ sk.description = sk.description || local.description;
2522
+ sk.name = sk.name || local.name;
2523
+ }
2524
+ }
2525
+ }
2526
+ this.jsonResponse(res, { skills });
1972
2527
  }
1973
2528
  catch (err) {
1974
2529
  this.jsonResponse(res, { skills: [], error: String(err) });
@@ -1993,7 +2548,18 @@ class ViewerServer {
1993
2548
  return this.jsonResponse(res, { memories: [], error: "not_configured" });
1994
2549
  try {
1995
2550
  const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
1996
- this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
2551
+ const memories = Array.isArray(data?.memories) ? data.memories : [];
2552
+ for (const m of memories) {
2553
+ if (!m.content && m.sourceChunkId) {
2554
+ const local = this.store.getChunk(m.sourceChunkId);
2555
+ if (local) {
2556
+ m.content = local.content;
2557
+ if (!m.summary && local.summary)
2558
+ m.summary = local.summary;
2559
+ }
2560
+ }
2561
+ }
2562
+ this.jsonResponse(res, { memories });
1997
2563
  }
1998
2564
  catch (err) {
1999
2565
  this.jsonResponse(res, { memories: [], error: String(err) });
@@ -2012,7 +2578,136 @@ class ViewerServer {
2012
2578
  this.jsonResponse(res, { ok: false, error: String(err) });
2013
2579
  }
2014
2580
  }
2015
- serveLocalIPs(res) {
2581
+ async serveSharingNotifications(res, url) {
2582
+ const hub = this.resolveHubConnection();
2583
+ if (!hub)
2584
+ return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
2585
+ try {
2586
+ const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
2587
+ const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`);
2588
+ this.jsonResponse(res, data);
2589
+ }
2590
+ catch {
2591
+ this.jsonResponse(res, { notifications: [], unreadCount: 0 });
2592
+ }
2593
+ }
2594
+ handleSharingNotificationsRead(req, res) {
2595
+ const hub = this.resolveHubConnection();
2596
+ if (!hub)
2597
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
2598
+ this.readBody(req, async (raw) => {
2599
+ try {
2600
+ const body = JSON.parse(raw || "{}");
2601
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
2602
+ this.jsonResponse(res, { ok: true });
2603
+ try {
2604
+ const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
2605
+ const count = data?.unreadCount ?? 0;
2606
+ this.lastKnownNotifCount = count;
2607
+ this.broadcastNotifSSE({ type: "update", unreadCount: count });
2608
+ }
2609
+ catch { /* best effort */ }
2610
+ }
2611
+ catch (err) {
2612
+ this.jsonResponse(res, { ok: false, error: String(err) });
2613
+ }
2614
+ });
2615
+ }
2616
+ handleSharingNotificationsClear(req, res) {
2617
+ const hub = this.resolveHubConnection();
2618
+ if (!hub)
2619
+ return this.jsonResponse(res, { ok: false, error: "not_configured" });
2620
+ this.readBody(req, async () => {
2621
+ try {
2622
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
2623
+ this.jsonResponse(res, { ok: true });
2624
+ this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
2625
+ }
2626
+ catch (err) {
2627
+ this.jsonResponse(res, { ok: false, error: String(err) });
2628
+ }
2629
+ });
2630
+ }
2631
+ handleNotifSSE(req, res) {
2632
+ res.writeHead(200, {
2633
+ "Content-Type": "text/event-stream",
2634
+ "Cache-Control": "no-cache",
2635
+ Connection: "keep-alive",
2636
+ "Access-Control-Allow-Origin": "*",
2637
+ });
2638
+ res.write("data: {\"type\":\"connected\"}\n\n");
2639
+ this.notifSSEClients.push(res);
2640
+ if (!this.notifPollTimer)
2641
+ this.startNotifPoll();
2642
+ req.on("close", () => {
2643
+ this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
2644
+ if (this.notifSSEClients.length === 0)
2645
+ this.stopNotifPoll();
2646
+ });
2647
+ }
2648
+ broadcastNotifSSE(data) {
2649
+ const msg = `data: ${JSON.stringify(data)}\n\n`;
2650
+ this.notifSSEClients = this.notifSSEClients.filter((c) => {
2651
+ try {
2652
+ c.write(msg);
2653
+ return true;
2654
+ }
2655
+ catch {
2656
+ return false;
2657
+ }
2658
+ });
2659
+ }
2660
+ startNotifPoll() {
2661
+ this.stopNotifPoll();
2662
+ const tick = async () => {
2663
+ const hub = this.resolveHubConnection();
2664
+ if (!hub)
2665
+ return;
2666
+ try {
2667
+ const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
2668
+ const count = data?.unreadCount ?? 0;
2669
+ if (count !== this.lastKnownNotifCount) {
2670
+ this.lastKnownNotifCount = count;
2671
+ this.broadcastNotifSSE({ type: "update", unreadCount: count });
2672
+ }
2673
+ }
2674
+ catch { /* ignore */ }
2675
+ };
2676
+ tick();
2677
+ this.notifPollTimer = setInterval(tick, 3000);
2678
+ }
2679
+ stopNotifPoll() {
2680
+ if (this.notifPollTimer) {
2681
+ clearInterval(this.notifPollTimer);
2682
+ this.notifPollTimer = undefined;
2683
+ }
2684
+ }
2685
+ startHubHeartbeat() {
2686
+ this.stopHubHeartbeat();
2687
+ const sendHeartbeat = async () => {
2688
+ try {
2689
+ const hub = this.resolveHubConnection();
2690
+ if (!hub) {
2691
+ const persisted = this.store.getClientHubConnection();
2692
+ if (persisted?.hubUrl && persisted?.userToken) {
2693
+ await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
2694
+ }
2695
+ return;
2696
+ }
2697
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
2698
+ }
2699
+ catch { /* best-effort */ }
2700
+ };
2701
+ sendHeartbeat();
2702
+ this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
2703
+ }
2704
+ stopHubHeartbeat() {
2705
+ if (this.hubHeartbeatTimer) {
2706
+ clearInterval(this.hubHeartbeatTimer);
2707
+ this.hubHeartbeatTimer = undefined;
2708
+ }
2709
+ }
2710
+ getLocalIPs() {
2016
2711
  const nets = node_os_1.default.networkInterfaces();
2017
2712
  const ips = [];
2018
2713
  for (const name of Object.keys(nets)) {
@@ -2022,6 +2717,10 @@ class ViewerServer {
2022
2717
  }
2023
2718
  }
2024
2719
  }
2720
+ return ips;
2721
+ }
2722
+ serveLocalIPs(res) {
2723
+ const ips = this.getLocalIPs();
2025
2724
  res.writeHead(200, { "Content-Type": "application/json" });
2026
2725
  res.end(JSON.stringify({ ips }));
2027
2726
  }
@@ -2059,7 +2758,7 @@ class ViewerServer {
2059
2758
  }
2060
2759
  }
2061
2760
  handleSaveConfig(req, res) {
2062
- this.readBody(req, (body) => {
2761
+ this.readBody(req, async (body) => {
2063
2762
  try {
2064
2763
  const newCfg = JSON.parse(body);
2065
2764
  const cfgPath = this.getOpenClawConfigPath();
@@ -2084,6 +2783,10 @@ class ViewerServer {
2084
2783
  if (!entry.config)
2085
2784
  entry.config = {};
2086
2785
  const config = entry.config;
2786
+ const oldSharing = config.sharing;
2787
+ const oldSharingRole = oldSharing?.role;
2788
+ const oldSharingEnabled = Boolean(oldSharing?.enabled);
2789
+ const oldClientHubAddress = String(oldSharing?.client?.hubAddress || "");
2087
2790
  if (newCfg.embedding)
2088
2791
  config.embedding = newCfg.embedding;
2089
2792
  if (newCfg.summarizer)
@@ -2097,15 +2800,70 @@ class ViewerServer {
2097
2800
  if (newCfg.sharing !== undefined) {
2098
2801
  const existing = config.sharing || {};
2099
2802
  const merged = { ...existing, ...newCfg.sharing };
2100
- // Deep-merge capabilities so new keys don't wipe existing ones
2101
2803
  if (newCfg.sharing.capabilities && existing.capabilities) {
2102
2804
  merged.capabilities = { ...existing.capabilities, ...newCfg.sharing.capabilities };
2103
2805
  }
2806
+ if (merged.role === "client" && merged.client) {
2807
+ const clientCfg = merged.client;
2808
+ const addr = String(clientCfg.hubAddress || "");
2809
+ if (addr) {
2810
+ const localIPs = this.getLocalIPs();
2811
+ localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
2812
+ try {
2813
+ const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
2814
+ if (localIPs.includes(u.hostname)) {
2815
+ res.writeHead(400, { "Content-Type": "application/json" });
2816
+ res.end(JSON.stringify({ error: "cannot_join_self" }));
2817
+ return;
2818
+ }
2819
+ }
2820
+ catch { }
2821
+ }
2822
+ }
2823
+ const newRole = merged.role;
2824
+ const newEnabled = Boolean(merged.enabled);
2825
+ // Detect disabling sharing or switching away from hub mode
2826
+ const wasHub = oldSharingEnabled && oldSharingRole === "hub";
2827
+ const isHub = newEnabled && newRole === "hub";
2828
+ if (wasHub && !isHub) {
2829
+ await this.notifyHubShutdown();
2830
+ this.stopHubHeartbeat();
2831
+ this.log.info("Hub shutting down: notified connected clients");
2832
+ }
2833
+ // Detect disabling sharing or switching away from client mode
2834
+ const wasClient = oldSharingEnabled && oldSharingRole === "client";
2835
+ const isClient = newEnabled && newRole === "client";
2836
+ if (wasClient && !isClient) {
2837
+ this.notifyHubLeave();
2838
+ this.store.clearClientHubConnection();
2839
+ this.log.info("Cleared client hub connection (sharing disabled or role changed)");
2840
+ }
2841
+ // Detect switching to a different Hub while still in client mode
2842
+ if (wasClient && isClient) {
2843
+ const newClientAddr = String(merged.client?.hubAddress || "");
2844
+ if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
2845
+ this.notifyHubLeave();
2846
+ this.store.clearClientHubConnection();
2847
+ this.log.info("Cleared client hub connection (switched to different Hub)");
2848
+ }
2849
+ }
2850
+ if (merged.role === "hub") {
2851
+ merged.client = { hubAddress: "", userToken: "", teamToken: "" };
2852
+ }
2853
+ else if (merged.role === "client") {
2854
+ merged.hub = { port: 18800, teamName: "", teamToken: "" };
2855
+ }
2104
2856
  config.sharing = merged;
2105
2857
  }
2106
2858
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
2107
2859
  node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
2108
2860
  this.log.info("Plugin config updated via Viewer");
2861
+ this.stopHubHeartbeat();
2862
+ // When switching to client mode, immediately send join request
2863
+ const finalSharing = config.sharing;
2864
+ if (finalSharing?.role === "client" && oldSharingRole !== "client") {
2865
+ this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
2866
+ }
2109
2867
  this.jsonResponse(res, { ok: true });
2110
2868
  }
2111
2869
  catch (e) {
@@ -2115,6 +2873,92 @@ class ViewerServer {
2115
2873
  }
2116
2874
  });
2117
2875
  }
2876
+ async autoJoinOnSave(sharing) {
2877
+ const clientCfg = sharing.client;
2878
+ const hubAddress = String(clientCfg?.hubAddress || "");
2879
+ const teamToken = String(clientCfg?.teamToken || "");
2880
+ if (!hubAddress || !teamToken)
2881
+ return;
2882
+ const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
2883
+ const os = await Promise.resolve().then(() => __importStar(require("os")));
2884
+ const nickname = String(clientCfg?.nickname || "");
2885
+ const username = nickname || os.userInfo().username || "user";
2886
+ const hostname = os.hostname() || "unknown";
2887
+ const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
2888
+ method: "POST",
2889
+ body: JSON.stringify({ teamToken, username, deviceName: hostname }),
2890
+ });
2891
+ this.store.setClientHubConnection({
2892
+ hubUrl,
2893
+ userId: String(result.userId || ""),
2894
+ username,
2895
+ userToken: result.userToken || "",
2896
+ role: "member",
2897
+ connectedAt: Date.now(),
2898
+ });
2899
+ this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
2900
+ if (result.userToken) {
2901
+ this.startHubHeartbeat();
2902
+ }
2903
+ }
2904
+ async notifyHubLeave() {
2905
+ try {
2906
+ const hub = this.resolveHubConnection();
2907
+ if (hub) {
2908
+ await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
2909
+ this.log.info("Notified Hub of voluntary leave");
2910
+ return;
2911
+ }
2912
+ const persisted = this.store.getClientHubConnection();
2913
+ if (persisted?.hubUrl && persisted?.userToken) {
2914
+ await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
2915
+ this.log.info("Notified Hub of voluntary leave (persisted connection)");
2916
+ }
2917
+ }
2918
+ catch (e) {
2919
+ this.log.warn(`Failed to notify Hub of leave: ${e}`);
2920
+ }
2921
+ }
2922
+ async notifyHubShutdown() {
2923
+ try {
2924
+ const sharing = this.ctx?.config.sharing;
2925
+ if (!sharing || sharing.role !== "hub")
2926
+ return;
2927
+ const hubPort = sharing.hub?.port ?? 18800;
2928
+ const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
2929
+ let adminToken;
2930
+ try {
2931
+ const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
2932
+ adminToken = authData?.bootstrapAdminToken;
2933
+ }
2934
+ catch {
2935
+ return;
2936
+ }
2937
+ if (!adminToken)
2938
+ return;
2939
+ const users = this.store.listHubUsers("active");
2940
+ const { v4: uuidv4 } = require("uuid");
2941
+ for (const u of users) {
2942
+ try {
2943
+ this.store.insertHubNotification({
2944
+ id: uuidv4(),
2945
+ userId: u.id,
2946
+ type: "hub_shutdown",
2947
+ resource: "hub",
2948
+ title: "Hub is shutting down",
2949
+ message: "The Hub server is shutting down. You may be disconnected.",
2950
+ });
2951
+ }
2952
+ catch (e) {
2953
+ this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
2954
+ }
2955
+ }
2956
+ this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
2957
+ }
2958
+ catch (e) {
2959
+ this.log.warn(`notifyHubShutdown error: ${e}`);
2960
+ }
2961
+ }
2118
2962
  handleUpdateUsername(req, res) {
2119
2963
  this.readBody(req, async (body) => {
2120
2964
  if (!this.ctx)
@@ -2174,6 +3018,16 @@ class ViewerServer {
2174
3018
  this.jsonResponse(res, { ok: false, error: "hubUrl is required" });
2175
3019
  return;
2176
3020
  }
3021
+ try {
3022
+ const localIPs = this.getLocalIPs();
3023
+ localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
3024
+ const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
3025
+ if (localIPs.includes(parsed.hostname)) {
3026
+ this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
3027
+ return;
3028
+ }
3029
+ }
3030
+ catch { }
2177
3031
  const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
2178
3032
  const ctrl = new AbortController();
2179
3033
  const timeout = setTimeout(() => ctrl.abort(), 8000);
@@ -2579,7 +3433,7 @@ class ViewerServer {
2579
3433
  // ─── Migration: scan OpenClaw built-in memory ───
2580
3434
  getOpenClawHome() {
2581
3435
  const home = process.env.HOME || process.env.USERPROFILE || "";
2582
- return node_path_1.default.join(home, ".openclaw");
3436
+ return process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
2583
3437
  }
2584
3438
  handleCleanupPolluted(res) {
2585
3439
  try {
@@ -2607,7 +3461,7 @@ class ViewerServer {
2607
3461
  try {
2608
3462
  const ocHome = this.getOpenClawHome();
2609
3463
  const memoryDir = node_path_1.default.join(ocHome, "memory");
2610
- const sessionsDir = node_path_1.default.join(ocHome, "agents", "main", "sessions");
3464
+ const agentsDir = node_path_1.default.join(ocHome, "agents");
2611
3465
  const sqliteFiles = [];
2612
3466
  if (node_fs_1.default.existsSync(memoryDir)) {
2613
3467
  for (const f of node_fs_1.default.readdirSync(memoryDir)) {
@@ -2625,38 +3479,45 @@ class ViewerServer {
2625
3479
  }
2626
3480
  let sessionCount = 0;
2627
3481
  let messageCount = 0;
2628
- if (node_fs_1.default.existsSync(sessionsDir)) {
2629
- const jsonlFiles = node_fs_1.default.readdirSync(sessionsDir).filter(f => f.includes(".jsonl"));
2630
- sessionCount = jsonlFiles.length;
2631
- for (const f of jsonlFiles) {
2632
- try {
2633
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessionsDir, f), "utf-8");
2634
- const lines = content.split("\n").filter(l => l.trim());
2635
- for (const line of lines) {
2636
- try {
2637
- const obj = JSON.parse(line);
2638
- if (obj.type === "message") {
2639
- const role = obj.message?.role ?? obj.role;
2640
- if (role === "user" || role === "assistant") {
2641
- const mc = obj.message?.content ?? obj.content;
2642
- let txt = "";
2643
- if (typeof mc === "string")
2644
- txt = mc;
2645
- else if (Array.isArray(mc))
2646
- txt = mc.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
2647
- else
2648
- txt = JSON.stringify(mc);
2649
- if (role === "user")
2650
- txt = (0, capture_1.stripInboundMetadata)(txt);
2651
- if (txt && txt.length >= 10)
2652
- messageCount++;
3482
+ if (node_fs_1.default.existsSync(agentsDir)) {
3483
+ for (const entry of node_fs_1.default.readdirSync(agentsDir, { withFileTypes: true })) {
3484
+ if (!entry.isDirectory())
3485
+ continue;
3486
+ const sessDir = node_path_1.default.join(agentsDir, entry.name, "sessions");
3487
+ if (!node_fs_1.default.existsSync(sessDir))
3488
+ continue;
3489
+ const jsonlFiles = node_fs_1.default.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
3490
+ sessionCount += jsonlFiles.length;
3491
+ for (const f of jsonlFiles) {
3492
+ try {
3493
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessDir, f), "utf-8");
3494
+ const lines = content.split("\n").filter(l => l.trim());
3495
+ for (const line of lines) {
3496
+ try {
3497
+ const obj = JSON.parse(line);
3498
+ if (obj.type === "message") {
3499
+ const role = obj.message?.role ?? obj.role;
3500
+ if (role === "user" || role === "assistant") {
3501
+ const mc = obj.message?.content ?? obj.content;
3502
+ let txt = "";
3503
+ if (typeof mc === "string")
3504
+ txt = mc;
3505
+ else if (Array.isArray(mc))
3506
+ txt = mc.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
3507
+ else
3508
+ txt = JSON.stringify(mc);
3509
+ if (role === "user")
3510
+ txt = (0, capture_1.stripInboundMetadata)(txt);
3511
+ if (txt && txt.length >= 10)
3512
+ messageCount++;
3513
+ }
2653
3514
  }
2654
3515
  }
3516
+ catch { /* skip bad lines */ }
2655
3517
  }
2656
- catch { /* skip bad lines */ }
2657
3518
  }
3519
+ catch { /* skip unreadable */ }
2658
3520
  }
2659
- catch { /* skip unreadable */ }
2660
3521
  }
2661
3522
  }
2662
3523
  const cfgPath = this.getOpenClawConfigPath();