@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
@@ -146,11 +146,23 @@ class SqliteStore {
146
146
  this.migrateSkillEmbeddingsAndFts();
147
147
  this.migrateFtsToTrigram();
148
148
  this.migrateHubTables();
149
+ this.migrateHubFtsToTrigram();
150
+ this.migrateLocalSharedTasksOwner();
149
151
  this.log.debug("Database schema initialized");
150
152
  }
151
153
  migrateChunksIndexesForRecall() {
152
154
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
153
155
  }
156
+ migrateLocalSharedTasksOwner() {
157
+ try {
158
+ const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all();
159
+ if (cols.length > 0 && !cols.some((c) => c.name === "original_owner")) {
160
+ this.db.exec("ALTER TABLE local_shared_tasks ADD COLUMN original_owner TEXT NOT NULL DEFAULT 'agent:main'");
161
+ this.log.info("Migrated: added original_owner column to local_shared_tasks");
162
+ }
163
+ }
164
+ catch { /* table may not exist yet */ }
165
+ }
154
166
  migrateOwnerFields() {
155
167
  const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all();
156
168
  if (!chunkCols.some((c) => c.name === "owner")) {
@@ -298,6 +310,54 @@ class SqliteStore {
298
310
  this.log.warn(`Failed to migrate skills_fts to trigram: ${err}`);
299
311
  }
300
312
  }
313
+ migrateHubFtsToTrigram() {
314
+ const tables = [
315
+ {
316
+ fts: "hub_chunks_fts", source: "hub_chunks", columns: "summary, content",
317
+ triggers: ["hub_chunks_ai", "hub_chunks_ad", "hub_chunks_au"],
318
+ },
319
+ {
320
+ fts: "hub_skills_fts", source: "hub_skills", columns: "name, description",
321
+ triggers: ["hub_skills_ai", "hub_skills_ad", "hub_skills_au"],
322
+ },
323
+ {
324
+ fts: "hub_memories_fts", source: "hub_memories", columns: "summary, content",
325
+ triggers: ["hub_memories_ai", "hub_memories_ad", "hub_memories_au"],
326
+ },
327
+ ];
328
+ for (const t of tables) {
329
+ try {
330
+ const row = this.db.prepare(`SELECT sql FROM sqlite_master WHERE name='${t.fts}'`).get();
331
+ if (!row || !row.sql)
332
+ continue;
333
+ if (row.sql.includes("trigram"))
334
+ continue;
335
+ this.log.info(`Migrating ${t.fts} to trigram tokenizer...`);
336
+ for (const tr of t.triggers)
337
+ this.db.exec(`DROP TRIGGER IF EXISTS ${tr}`);
338
+ this.db.exec(`DROP TABLE IF EXISTS ${t.fts}`);
339
+ this.db.exec(`CREATE VIRTUAL TABLE ${t.fts} USING fts5(${t.columns}, content='${t.source}', content_rowid='rowid', tokenize='trigram')`);
340
+ this.db.exec(`
341
+ CREATE TRIGGER ${t.triggers[0]} AFTER INSERT ON ${t.source} BEGIN
342
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
343
+ END;
344
+ CREATE TRIGGER ${t.triggers[1]} AFTER DELETE ON ${t.source} BEGIN
345
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
346
+ END;
347
+ CREATE TRIGGER ${t.triggers[2]} AFTER UPDATE ON ${t.source} BEGIN
348
+ INSERT INTO ${t.fts}(${t.fts}, rowid, ${t.columns}) VALUES ('delete', old.rowid, ${t.columns.split(", ").map(c => "old." + c).join(", ")});
349
+ INSERT INTO ${t.fts}(rowid, ${t.columns}) VALUES (new.rowid, ${t.columns.split(", ").map(c => "new." + c).join(", ")});
350
+ END
351
+ `);
352
+ this.db.exec(`INSERT INTO ${t.fts}(rowid, ${t.columns}) SELECT rowid, ${t.columns} FROM ${t.source}`);
353
+ const cnt = this.db.prepare(`SELECT COUNT(*) as c FROM ${t.fts}`).get().c;
354
+ this.log.info(`Migrated ${t.fts} to trigram: ${cnt} rows indexed`);
355
+ }
356
+ catch (err) {
357
+ this.log.warn(`Failed to migrate ${t.fts} to trigram: ${err}`);
358
+ }
359
+ }
360
+ }
301
361
  migrateTaskId() {
302
362
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all();
303
363
  if (!cols.some((c) => c.name === "task_id")) {
@@ -503,15 +563,16 @@ class SqliteStore {
503
563
  recordToolCall(toolName, durationMs, success) {
504
564
  this.db.prepare("INSERT INTO tool_calls (tool_name, duration_ms, success, called_at) VALUES (?, ?, ?, ?)").run(toolName, Math.round(durationMs), success ? 1 : 0, Date.now());
505
565
  }
506
- getToolMetrics(minutes) {
507
- const since = Date.now() - minutes * 60 * 1000;
566
+ getToolMetrics(minutes, fromMs, toMs) {
567
+ const since = fromMs ?? (Date.now() - minutes * 60 * 1000);
568
+ const until = toMs ?? Date.now();
508
569
  const rows = this.db.prepare(`SELECT tool_name,
509
570
  duration_ms,
510
571
  success,
511
572
  strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key
512
573
  FROM tool_calls
513
- WHERE called_at >= ?
514
- ORDER BY called_at`).all(since);
574
+ WHERE called_at >= ? AND called_at <= ?
575
+ ORDER BY called_at`).all(since, until);
515
576
  const toolSet = new Set();
516
577
  const minuteMap = new Map();
517
578
  const aggMap = new Map();
@@ -644,34 +705,27 @@ class SqliteStore {
644
705
  shared_at INTEGER NOT NULL
645
706
  );
646
707
 
708
+ CREATE TABLE IF NOT EXISTS local_shared_memories (
709
+ chunk_id TEXT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
710
+ original_owner TEXT NOT NULL,
711
+ shared_at INTEGER NOT NULL
712
+ );
713
+
647
714
  CREATE TABLE IF NOT EXISTS hub_users (
648
- id TEXT PRIMARY KEY,
649
- username TEXT NOT NULL UNIQUE,
650
- device_name TEXT NOT NULL DEFAULT '',
651
- role TEXT NOT NULL,
652
- status TEXT NOT NULL,
653
- token_hash TEXT NOT NULL DEFAULT '',
654
- created_at INTEGER NOT NULL,
655
- approved_at INTEGER
715
+ id TEXT PRIMARY KEY,
716
+ username TEXT NOT NULL UNIQUE,
717
+ device_name TEXT NOT NULL DEFAULT '',
718
+ role TEXT NOT NULL,
719
+ status TEXT NOT NULL,
720
+ token_hash TEXT NOT NULL DEFAULT '',
721
+ created_at INTEGER NOT NULL,
722
+ approved_at INTEGER,
723
+ last_ip TEXT NOT NULL DEFAULT '',
724
+ last_active_at INTEGER
656
725
  );
657
726
  CREATE INDEX IF NOT EXISTS idx_hub_users_status ON hub_users(status);
658
727
  CREATE INDEX IF NOT EXISTS idx_hub_users_role ON hub_users(role);
659
728
 
660
- CREATE TABLE IF NOT EXISTS hub_groups (
661
- id TEXT PRIMARY KEY,
662
- name TEXT NOT NULL UNIQUE,
663
- description TEXT NOT NULL DEFAULT '',
664
- created_at INTEGER NOT NULL
665
- );
666
-
667
- CREATE TABLE IF NOT EXISTS hub_group_members (
668
- group_id TEXT NOT NULL REFERENCES hub_groups(id) ON DELETE CASCADE,
669
- user_id TEXT NOT NULL REFERENCES hub_users(id) ON DELETE CASCADE,
670
- joined_at INTEGER NOT NULL,
671
- PRIMARY KEY (group_id, user_id)
672
- );
673
- CREATE INDEX IF NOT EXISTS idx_hub_group_members_user ON hub_group_members(user_id);
674
-
675
729
  CREATE TABLE IF NOT EXISTS hub_tasks (
676
730
  id TEXT PRIMARY KEY,
677
731
  source_task_id TEXT NOT NULL,
@@ -713,7 +767,7 @@ class SqliteStore {
713
767
  content,
714
768
  content='hub_chunks',
715
769
  content_rowid='rowid',
716
- tokenize='porter unicode61'
770
+ tokenize='trigram'
717
771
  );
718
772
 
719
773
  CREATE TRIGGER IF NOT EXISTS hub_chunks_ai AFTER INSERT ON hub_chunks BEGIN
@@ -763,7 +817,7 @@ class SqliteStore {
763
817
  description,
764
818
  content='hub_skills',
765
819
  content_rowid='rowid',
766
- tokenize='porter unicode61'
820
+ tokenize='trigram'
767
821
  );
768
822
 
769
823
  CREATE TRIGGER IF NOT EXISTS hub_skills_ai AFTER INSERT ON hub_skills BEGIN
@@ -813,7 +867,7 @@ class SqliteStore {
813
867
  content,
814
868
  content='hub_memories',
815
869
  content_rowid='rowid',
816
- tokenize='porter unicode61'
870
+ tokenize='trigram'
817
871
  );
818
872
 
819
873
  CREATE TRIGGER IF NOT EXISTS hub_memories_ai AFTER INSERT ON hub_memories BEGIN
@@ -833,6 +887,31 @@ class SqliteStore {
833
887
  VALUES (new.rowid, new.summary, new.content);
834
888
  END;
835
889
  `);
890
+ this.db.exec(`
891
+ CREATE TABLE IF NOT EXISTS hub_notifications (
892
+ id TEXT PRIMARY KEY,
893
+ user_id TEXT NOT NULL,
894
+ type TEXT NOT NULL,
895
+ resource TEXT NOT NULL,
896
+ title TEXT NOT NULL,
897
+ message TEXT NOT NULL DEFAULT '',
898
+ read INTEGER NOT NULL DEFAULT 0,
899
+ created_at INTEGER NOT NULL
900
+ );
901
+ CREATE INDEX IF NOT EXISTS idx_hub_notif_user ON hub_notifications(user_id, read, created_at DESC);
902
+ `);
903
+ try {
904
+ const cols = this.db.prepare("PRAGMA table_info(hub_users)").all();
905
+ if (cols.length > 0 && !cols.some(c => c.name === "last_ip")) {
906
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_ip TEXT NOT NULL DEFAULT ''");
907
+ this.log.info("Migrated: added last_ip column to hub_users");
908
+ }
909
+ if (cols.length > 0 && !cols.some(c => c.name === "last_active_at")) {
910
+ this.db.exec("ALTER TABLE hub_users ADD COLUMN last_active_at INTEGER");
911
+ this.log.info("Migrated: added last_active_at column to hub_users");
912
+ }
913
+ }
914
+ catch { /* table may not exist yet */ }
836
915
  }
837
916
  // ─── Write ───
838
917
  insertChunk(chunk) {
@@ -959,6 +1038,30 @@ class SqliteStore {
959
1038
  return [];
960
1039
  }
961
1040
  }
1041
+ hubMemoryPatternSearch(patterns, opts = {}) {
1042
+ if (patterns.length === 0)
1043
+ return [];
1044
+ const limit = opts.limit ?? 10;
1045
+ const conditions = patterns.map(() => "(hm.content LIKE ? OR hm.summary LIKE ?)");
1046
+ const params = [];
1047
+ for (const p of patterns) {
1048
+ params.push(`%${p}%`, `%${p}%`);
1049
+ }
1050
+ params.push(limit);
1051
+ try {
1052
+ const rows = this.db.prepare(`
1053
+ SELECT hm.id as memory_id, hm.content, hm.role, hm.created_at
1054
+ FROM hub_memories hm
1055
+ WHERE ${conditions.join(" OR ")}
1056
+ ORDER BY hm.created_at DESC
1057
+ LIMIT ?
1058
+ `).all(...params);
1059
+ return rows.map(r => ({ memoryId: r.memory_id, content: r.content, role: r.role, createdAt: r.created_at }));
1060
+ }
1061
+ catch {
1062
+ return [];
1063
+ }
1064
+ }
962
1065
  // ─── Vector Search ───
963
1066
  getAllEmbeddings(ownerFilter) {
964
1067
  let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
@@ -1097,6 +1200,8 @@ class SqliteStore {
1097
1200
  "skill_embeddings",
1098
1201
  "skill_versions",
1099
1202
  "skills",
1203
+ "local_shared_memories",
1204
+ "local_shared_tasks",
1100
1205
  "embeddings",
1101
1206
  "chunks",
1102
1207
  "tasks",
@@ -1519,6 +1624,61 @@ class SqliteStore {
1519
1624
  const rows = this.db.prepare('SELECT task_id, hub_task_id, visibility, group_id, synced_chunks FROM local_shared_tasks').all();
1520
1625
  return rows.map(r => ({ taskId: r.task_id, hubTaskId: r.hub_task_id, visibility: r.visibility, groupId: r.group_id, syncedChunks: r.synced_chunks }));
1521
1626
  }
1627
+ // ─── Local Shared Memories (client-side tracking) ───
1628
+ markMemorySharedLocally(chunkId) {
1629
+ const chunk = this.getChunk(chunkId);
1630
+ if (!chunk)
1631
+ return { ok: false, reason: "not_found" };
1632
+ if (chunk.owner === "public") {
1633
+ const existing = this.getLocalSharedMemory(chunkId);
1634
+ return {
1635
+ ok: true,
1636
+ owner: "public",
1637
+ originalOwner: existing?.originalOwner ?? undefined,
1638
+ sharedAt: existing?.sharedAt ?? undefined,
1639
+ };
1640
+ }
1641
+ const sharedAt = Date.now();
1642
+ this.db.transaction(() => {
1643
+ this.db.prepare(`
1644
+ INSERT INTO local_shared_memories (chunk_id, original_owner, shared_at)
1645
+ VALUES (?, ?, ?)
1646
+ ON CONFLICT(chunk_id) DO UPDATE SET
1647
+ original_owner = excluded.original_owner,
1648
+ shared_at = excluded.shared_at
1649
+ `).run(chunkId, chunk.owner, sharedAt);
1650
+ this.updateChunk(chunkId, { owner: "public" });
1651
+ })();
1652
+ return { ok: true, owner: "public", originalOwner: chunk.owner, sharedAt };
1653
+ }
1654
+ unmarkMemorySharedLocally(chunkId, fallbackOwner) {
1655
+ const chunk = this.getChunk(chunkId);
1656
+ if (!chunk)
1657
+ return { ok: false, reason: "not_found" };
1658
+ if (chunk.owner !== "public") {
1659
+ return { ok: true, owner: chunk.owner };
1660
+ }
1661
+ const existing = this.getLocalSharedMemory(chunkId);
1662
+ const restoreOwner = existing?.originalOwner ?? fallbackOwner;
1663
+ if (!restoreOwner || restoreOwner === "public") {
1664
+ return { ok: false, reason: "original_owner_missing" };
1665
+ }
1666
+ this.db.transaction(() => {
1667
+ this.updateChunk(chunkId, { owner: restoreOwner });
1668
+ this.db.prepare("DELETE FROM local_shared_memories WHERE chunk_id = ?").run(chunkId);
1669
+ })();
1670
+ return { ok: true, owner: restoreOwner, originalOwner: restoreOwner };
1671
+ }
1672
+ getLocalSharedMemory(chunkId) {
1673
+ const row = this.db.prepare("SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id = ?").get(chunkId);
1674
+ if (!row)
1675
+ return null;
1676
+ return {
1677
+ chunkId: row.chunk_id,
1678
+ originalOwner: row.original_owner,
1679
+ sharedAt: row.shared_at,
1680
+ };
1681
+ }
1522
1682
  // ─── Hub Users / Groups ───
1523
1683
  upsertHubUser(user) {
1524
1684
  this.db.prepare(`
@@ -1538,65 +1698,49 @@ class SqliteStore {
1538
1698
  const row = this.db.prepare('SELECT * FROM hub_users WHERE id = ?').get(userId);
1539
1699
  if (!row)
1540
1700
  return null;
1541
- return this.attachGroupsToHubUser(rowToHubUser(row));
1701
+ return rowToHubUser(row);
1542
1702
  }
1543
1703
  listHubUsers(status) {
1544
1704
  const rows = status
1545
1705
  ? this.db.prepare('SELECT * FROM hub_users WHERE status = ? ORDER BY created_at').all(status)
1546
1706
  : this.db.prepare('SELECT * FROM hub_users ORDER BY created_at').all();
1547
- return rows.map((row) => this.attachGroupsToHubUser(rowToHubUser(row)));
1548
- }
1549
- upsertHubGroup(group) {
1550
- this.db.prepare(`
1551
- INSERT INTO hub_groups (id, name, description, created_at)
1552
- VALUES (?, ?, ?, ?)
1553
- ON CONFLICT(id) DO UPDATE SET
1554
- name = excluded.name,
1555
- description = excluded.description,
1556
- created_at = excluded.created_at
1557
- `).run(group.id, group.name, group.description, group.createdAt);
1558
- }
1559
- listHubGroups() {
1560
- const rows = this.db.prepare('SELECT * FROM hub_groups ORDER BY name').all();
1561
- return rows.map(rowToHubGroup);
1562
- }
1563
- addHubGroupMember(groupId, userId, joinedAt = Date.now()) {
1564
- this.db.prepare(`
1565
- INSERT INTO hub_group_members (group_id, user_id, joined_at)
1566
- VALUES (?, ?, ?)
1567
- ON CONFLICT(group_id, user_id) DO UPDATE SET joined_at = excluded.joined_at
1568
- `).run(groupId, userId, joinedAt);
1569
- }
1570
- getHubGroupById(groupId) {
1571
- const row = this.db.prepare('SELECT * FROM hub_groups WHERE id = ?').get(groupId);
1572
- return row ? rowToHubGroup(row) : undefined;
1573
- }
1574
- deleteHubGroup(groupId) {
1575
- const result = this.db.prepare('DELETE FROM hub_groups WHERE id = ?').run(groupId);
1707
+ return rows.map(rowToHubUser);
1708
+ }
1709
+ deleteHubUser(userId, cleanResources = false) {
1710
+ if (cleanResources) {
1711
+ this.db.prepare('DELETE FROM hub_tasks WHERE source_user_id = ?').run(userId);
1712
+ this.db.prepare('DELETE FROM hub_skills WHERE source_user_id = ?').run(userId);
1713
+ this.db.prepare('DELETE FROM hub_memories WHERE source_user_id = ?').run(userId);
1714
+ const result = this.db.prepare('DELETE FROM hub_users WHERE id = ?').run(userId);
1715
+ return result.changes > 0;
1716
+ }
1717
+ const result = this.db.prepare("UPDATE hub_users SET status = 'removed', token_hash = '' WHERE id = ?").run(userId);
1576
1718
  return result.changes > 0;
1577
1719
  }
1578
- listHubGroupMembers(groupId) {
1579
- const rows = this.db.prepare(`
1580
- SELECT gm.user_id, hu.username, gm.joined_at
1581
- FROM hub_group_members gm
1582
- JOIN hub_users hu ON hu.id = gm.user_id
1583
- WHERE gm.group_id = ?
1584
- ORDER BY gm.joined_at
1585
- `).all(groupId);
1586
- return rows.map(r => ({ userId: r.user_id, username: r.username, joinedAt: r.joined_at }));
1587
- }
1588
- removeHubGroupMember(groupId, userId) {
1589
- this.db.prepare('DELETE FROM hub_group_members WHERE group_id = ? AND user_id = ?').run(groupId, userId);
1590
- }
1591
- getGroupsForHubUser(userId) {
1592
- const rows = this.db.prepare(`
1593
- SELECT g.*
1594
- FROM hub_group_members gm
1595
- JOIN hub_groups g ON g.id = gm.group_id
1596
- WHERE gm.user_id = ?
1597
- ORDER BY g.name
1598
- `).all(userId);
1599
- return rows.map((row) => ({ id: row.id, name: row.name, description: row.description || undefined }));
1720
+ updateHubUserActivity(userId, ip, timestamp) {
1721
+ this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
1722
+ }
1723
+ getHubUserContributions() {
1724
+ const result = {};
1725
+ const memRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_memories GROUP BY source_user_id').all();
1726
+ const taskRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_tasks GROUP BY source_user_id').all();
1727
+ const skillRows = this.db.prepare('SELECT source_user_id, COUNT(*) as cnt FROM hub_skills GROUP BY source_user_id').all();
1728
+ for (const r of memRows) {
1729
+ if (!result[r.source_user_id])
1730
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1731
+ result[r.source_user_id].memoryCount = r.cnt;
1732
+ }
1733
+ for (const r of taskRows) {
1734
+ if (!result[r.source_user_id])
1735
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1736
+ result[r.source_user_id].taskCount = r.cnt;
1737
+ }
1738
+ for (const r of skillRows) {
1739
+ if (!result[r.source_user_id])
1740
+ result[r.source_user_id] = { memoryCount: 0, taskCount: 0, skillCount: 0 };
1741
+ result[r.source_user_id].skillCount = r.cnt;
1742
+ }
1743
+ return result;
1600
1744
  }
1601
1745
  // ─── Hub Shared Data ───
1602
1746
  upsertHubTask(task) {
@@ -1616,6 +1760,10 @@ class SqliteStore {
1616
1760
  const row = this.db.prepare('SELECT * FROM hub_tasks WHERE source_user_id = ? AND source_task_id = ?').get(sourceUserId, sourceTaskId);
1617
1761
  return row ? rowToHubTask(row) : null;
1618
1762
  }
1763
+ getHubTaskById(taskId) {
1764
+ const row = this.db.prepare('SELECT * FROM hub_tasks WHERE id = ?').get(taskId);
1765
+ return row ? rowToHubTask(row) : null;
1766
+ }
1619
1767
  upsertHubChunk(chunk) {
1620
1768
  if (!chunk.sourceTaskId)
1621
1769
  throw new Error("sourceTaskId is required for hub chunk upserts");
@@ -1692,24 +1840,16 @@ class SqliteStore {
1692
1840
  const limit = options?.maxResults ?? 10;
1693
1841
  const userId = options?.userId ?? "";
1694
1842
  const rows = this.db.prepare(`
1695
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, hg.name as group_name, hu.username as owner_name,
1843
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
1696
1844
  bm25(hub_chunks_fts) as rank
1697
1845
  FROM hub_chunks_fts f
1698
1846
  JOIN hub_chunks hc ON hc.rowid = f.rowid
1699
1847
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1700
- LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1701
1848
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1702
1849
  WHERE hub_chunks_fts MATCH ?
1703
- AND (
1704
- ht.visibility = 'public'
1705
- OR EXISTS (
1706
- SELECT 1 FROM hub_group_members gm
1707
- WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1708
- )
1709
- )
1710
1850
  ORDER BY rank
1711
1851
  LIMIT ?
1712
- `).all(sanitizeFtsQuery(query), userId, limit);
1852
+ `).all(sanitizeFtsQuery(query), limit);
1713
1853
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1714
1854
  }
1715
1855
  upsertHubEmbedding(chunkId, vector) {
@@ -1732,12 +1872,7 @@ class SqliteStore {
1732
1872
  FROM hub_embeddings he
1733
1873
  JOIN hub_chunks hc ON hc.id = he.chunk_id
1734
1874
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1735
- WHERE ht.visibility = 'public'
1736
- OR EXISTS (
1737
- SELECT 1 FROM hub_group_members gm
1738
- WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1739
- )
1740
- `).all(userId);
1875
+ `).all();
1741
1876
  return rows.map(r => ({
1742
1877
  chunkId: r.chunk_id,
1743
1878
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1745,22 +1880,14 @@ class SqliteStore {
1745
1880
  }
1746
1881
  getVisibleHubSearchHitByChunkId(chunkId, userId) {
1747
1882
  const row = this.db.prepare(`
1748
- SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, hg.name as group_name, hu.username as owner_name,
1883
+ SELECT hc.id, hc.content, hc.summary, hc.role, hc.created_at, ht.title as task_title, ht.visibility, '' as group_name, hu.username as owner_name,
1749
1884
  0 as rank
1750
1885
  FROM hub_chunks hc
1751
1886
  JOIN hub_tasks ht ON ht.id = hc.hub_task_id
1752
- LEFT JOIN hub_groups hg ON hg.id = ht.group_id
1753
1887
  LEFT JOIN hub_users hu ON hu.id = ht.source_user_id
1754
1888
  WHERE hc.id = ?
1755
- AND (
1756
- ht.visibility = 'public'
1757
- OR EXISTS (
1758
- SELECT 1 FROM hub_group_members gm
1759
- WHERE gm.group_id = ht.group_id AND gm.user_id = ?
1760
- )
1761
- )
1762
1889
  LIMIT 1
1763
- `).get(chunkId, userId);
1890
+ `).get(chunkId);
1764
1891
  return row ?? null;
1765
1892
  }
1766
1893
  getHubChunkById(chunkId) {
@@ -1774,39 +1901,25 @@ class SqliteStore {
1774
1901
  let rows;
1775
1902
  if (sanitized) {
1776
1903
  rows = this.db.prepare(`
1777
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, hg.name AS group_name, hu.username AS owner_name, hs.quality_score,
1904
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
1778
1905
  bm25(hub_skills_fts) as rank
1779
1906
  FROM hub_skills_fts f
1780
1907
  JOIN hub_skills hs ON hs.rowid = f.rowid
1781
- LEFT JOIN hub_groups hg ON hg.id = hs.group_id
1782
1908
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
1783
1909
  WHERE hub_skills_fts MATCH ?
1784
- AND (
1785
- hs.visibility = 'public'
1786
- OR EXISTS (
1787
- SELECT 1 FROM hub_group_members gm
1788
- WHERE gm.group_id = hs.group_id AND gm.user_id = ?
1789
- )
1790
- )
1791
1910
  ORDER BY rank
1792
1911
  LIMIT ?
1793
- `).all(sanitized, userId, limit);
1912
+ `).all(sanitized, limit);
1794
1913
  }
1795
1914
  else {
1796
1915
  rows = this.db.prepare(`
1797
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, hg.name AS group_name, hu.username AS owner_name, hs.quality_score,
1916
+ SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' AS group_name, hu.username AS owner_name, hu.status AS owner_status, hs.quality_score,
1798
1917
  0 as rank
1799
1918
  FROM hub_skills hs
1800
- LEFT JOIN hub_groups hg ON hg.id = hs.group_id
1801
1919
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
1802
- WHERE hs.visibility = 'public'
1803
- OR EXISTS (
1804
- SELECT 1 FROM hub_group_members gm
1805
- WHERE gm.group_id = hs.group_id AND gm.user_id = ?
1806
- )
1807
1920
  ORDER BY hs.updated_at DESC
1808
1921
  LIMIT ?
1809
- `).all(userId, limit);
1922
+ `).all(limit);
1810
1923
  }
1811
1924
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1812
1925
  }
@@ -1815,81 +1928,71 @@ class SqliteStore {
1815
1928
  }
1816
1929
  listVisibleHubTasks(userId, limit = 40) {
1817
1930
  const rows = this.db.prepare(`
1818
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
1931
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
1819
1932
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1820
1933
  FROM hub_tasks t
1821
1934
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1822
- LEFT JOIN hub_groups g ON g.id = t.group_id
1823
- WHERE t.visibility = 'public'
1824
- OR EXISTS (
1825
- SELECT 1 FROM hub_group_members gm
1826
- WHERE gm.group_id = t.group_id AND gm.user_id = ?
1827
- )
1828
1935
  ORDER BY t.updated_at DESC
1829
1936
  LIMIT ?
1830
- `).all(userId, limit);
1937
+ `).all(limit);
1831
1938
  return rows.map(r => ({
1832
1939
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1833
1940
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1834
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
1941
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1835
1942
  createdAt: r.created_at, updatedAt: r.updated_at,
1836
1943
  }));
1837
1944
  }
1838
1945
  listAllHubTasks() {
1839
1946
  const rows = this.db.prepare(`
1840
- SELECT t.*, u.username AS owner_name, g.name AS group_name,
1947
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status,
1841
1948
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1842
1949
  FROM hub_tasks t
1843
1950
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1844
- LEFT JOIN hub_groups g ON g.id = t.group_id
1845
1951
  ORDER BY t.updated_at DESC
1846
1952
  `).all();
1847
1953
  return rows.map(r => ({
1848
1954
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1849
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1850
- visibility: r.visibility, ownerName: r.owner_name ?? "unknown", chunkCount: r.chunk_count ?? 0,
1955
+ title: r.title, summary: r.summary, groupId: r.group_id, groupName: null,
1956
+ visibility: r.visibility, ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", chunkCount: r.chunk_count ?? 0,
1851
1957
  createdAt: r.created_at, updatedAt: r.updated_at,
1852
1958
  }));
1853
1959
  }
1960
+ listHubChunksByTaskId(hubTaskId) {
1961
+ const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId);
1962
+ return rows.map(rowToHubChunk);
1963
+ }
1854
1964
  deleteHubTaskById(taskId) {
1855
1965
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
1856
1966
  return info.changes > 0;
1857
1967
  }
1858
1968
  listVisibleHubSkills(userId, limit = 40) {
1859
1969
  const rows = this.db.prepare(`
1860
- SELECT s.*, u.username AS owner_name, g.name AS group_name
1970
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
1861
1971
  FROM hub_skills s
1862
1972
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1863
- LEFT JOIN hub_groups g ON g.id = s.group_id
1864
- WHERE s.visibility = 'public'
1865
- OR EXISTS (
1866
- SELECT 1 FROM hub_group_members gm
1867
- WHERE gm.group_id = s.group_id AND gm.user_id = ?
1868
- )
1869
1973
  ORDER BY s.updated_at DESC
1870
1974
  LIMIT ?
1871
- `).all(userId, limit);
1975
+ `).all(limit);
1872
1976
  return rows.map(r => ({
1873
1977
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1874
1978
  name: r.name, description: r.description, version: r.version,
1875
1979
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1876
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
1980
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1877
1981
  createdAt: r.created_at, updatedAt: r.updated_at,
1878
1982
  }));
1879
1983
  }
1880
1984
  listAllHubSkills() {
1881
1985
  const rows = this.db.prepare(`
1882
- SELECT s.*, u.username AS owner_name, g.name AS group_name
1986
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status
1883
1987
  FROM hub_skills s
1884
1988
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1885
- LEFT JOIN hub_groups g ON g.id = s.group_id
1886
1989
  ORDER BY s.updated_at DESC
1887
1990
  `).all();
1888
1991
  return rows.map(r => ({
1889
1992
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1890
1993
  name: r.name, description: r.description, version: r.version,
1891
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1892
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
1994
+ groupId: r.group_id, groupName: null, visibility: r.visibility,
1995
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1893
1996
  createdAt: r.created_at, updatedAt: r.updated_at,
1894
1997
  }));
1895
1998
  }
@@ -1928,6 +2031,37 @@ class SqliteStore {
1928
2031
  const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
1929
2032
  return info.changes > 0;
1930
2033
  }
2034
+ // ─── Hub Notifications ───
2035
+ insertHubNotification(n) {
2036
+ this.db.prepare('INSERT INTO hub_notifications (id, user_id, type, resource, title, message, read, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)').run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
2037
+ }
2038
+ hasRecentHubNotification(userId, type, resource, windowMs = 300_000) {
2039
+ const since = Date.now() - windowMs;
2040
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?').get(userId, type, resource, since);
2041
+ return row.cnt > 0;
2042
+ }
2043
+ listHubNotifications(userId, opts) {
2044
+ const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
2045
+ const limit = opts?.limit ?? 50;
2046
+ const rows = this.db.prepare(`SELECT * FROM hub_notifications ${where} ORDER BY created_at DESC LIMIT ?`).all(userId, limit);
2047
+ return rows.map(r => ({ id: r.id, userId: r.user_id, type: r.type, resource: r.resource, title: r.title, message: r.message, read: !!r.read, createdAt: r.created_at }));
2048
+ }
2049
+ countUnreadHubNotifications(userId) {
2050
+ const row = this.db.prepare('SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND read = 0').get(userId);
2051
+ return row.cnt;
2052
+ }
2053
+ markHubNotificationsRead(userId, ids) {
2054
+ if (ids && ids.length > 0) {
2055
+ const placeholders = ids.map(() => '?').join(',');
2056
+ this.db.prepare(`UPDATE hub_notifications SET read = 1 WHERE user_id = ? AND id IN (${placeholders})`).run(userId, ...ids);
2057
+ }
2058
+ else {
2059
+ this.db.prepare('UPDATE hub_notifications SET read = 1 WHERE user_id = ?').run(userId);
2060
+ }
2061
+ }
2062
+ clearHubNotifications(userId) {
2063
+ this.db.prepare('DELETE FROM hub_notifications WHERE user_id = ?').run(userId);
2064
+ }
1931
2065
  upsertHubMemoryEmbedding(memoryId, vector) {
1932
2066
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1933
2067
  this.db.prepare(`
@@ -1949,23 +2083,15 @@ class SqliteStore {
1949
2083
  if (!sanitized)
1950
2084
  return [];
1951
2085
  const rows = this.db.prepare(`
1952
- SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, hg.name as group_name, hu.username as owner_name,
2086
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
1953
2087
  bm25(hub_memories_fts) as rank
1954
2088
  FROM hub_memories_fts f
1955
2089
  JOIN hub_memories hm ON hm.rowid = f.rowid
1956
- LEFT JOIN hub_groups hg ON hg.id = hm.group_id
1957
2090
  LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
1958
2091
  WHERE hub_memories_fts MATCH ?
1959
- AND (
1960
- hm.visibility = 'public'
1961
- OR EXISTS (
1962
- SELECT 1 FROM hub_group_members gm
1963
- WHERE gm.group_id = hm.group_id AND gm.user_id = ?
1964
- )
1965
- )
1966
2092
  ORDER BY rank
1967
2093
  LIMIT ?
1968
- `).all(sanitized, userId, limit);
2094
+ `).all(sanitized, limit);
1969
2095
  return rows.map((row, idx) => ({ hit: row, rank: idx + 1 }));
1970
2096
  }
1971
2097
  getVisibleHubMemoryEmbeddings(userId) {
@@ -1973,12 +2099,7 @@ class SqliteStore {
1973
2099
  SELECT hme.memory_id, hme.vector, hme.dimensions
1974
2100
  FROM hub_memory_embeddings hme
1975
2101
  JOIN hub_memories hm ON hm.id = hme.memory_id
1976
- WHERE hm.visibility = 'public'
1977
- OR EXISTS (
1978
- SELECT 1 FROM hub_group_members gm
1979
- WHERE gm.group_id = hm.group_id AND gm.user_id = ?
1980
- )
1981
- `).all(userId);
2102
+ `).all();
1982
2103
  return rows.map(r => ({
1983
2104
  memoryId: r.memory_id,
1984
2105
  vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
@@ -1986,57 +2107,42 @@ class SqliteStore {
1986
2107
  }
1987
2108
  getVisibleHubSearchHitByMemoryId(memoryId, userId) {
1988
2109
  const row = this.db.prepare(`
1989
- SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, hg.name as group_name, hu.username as owner_name,
2110
+ SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
1990
2111
  0 as rank
1991
2112
  FROM hub_memories hm
1992
- LEFT JOIN hub_groups hg ON hg.id = hm.group_id
1993
2113
  LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
1994
2114
  WHERE hm.id = ?
1995
- AND (
1996
- hm.visibility = 'public'
1997
- OR EXISTS (
1998
- SELECT 1 FROM hub_group_members gm
1999
- WHERE gm.group_id = hm.group_id AND gm.user_id = ?
2000
- )
2001
- )
2002
2115
  LIMIT 1
2003
- `).get(memoryId, userId);
2116
+ `).get(memoryId);
2004
2117
  return row ?? null;
2005
2118
  }
2006
2119
  listVisibleHubMemories(userId, limit = 40) {
2007
2120
  const rows = this.db.prepare(`
2008
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2121
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
2009
2122
  FROM hub_memories m
2010
2123
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2011
- LEFT JOIN hub_groups g ON g.id = m.group_id
2012
- WHERE m.visibility = 'public'
2013
- OR EXISTS (
2014
- SELECT 1 FROM hub_group_members gm
2015
- WHERE gm.group_id = m.group_id AND gm.user_id = ?
2016
- )
2017
2124
  ORDER BY m.updated_at DESC
2018
2125
  LIMIT ?
2019
- `).all(userId, limit);
2126
+ `).all(limit);
2020
2127
  return rows.map(r => ({
2021
2128
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2022
- role: r.role, summary: r.summary, kind: r.kind,
2129
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2023
2130
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2024
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2131
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2025
2132
  }));
2026
2133
  }
2027
2134
  listAllHubMemories() {
2028
2135
  const rows = this.db.prepare(`
2029
- SELECT m.*, u.username AS owner_name, g.name AS group_name
2136
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status
2030
2137
  FROM hub_memories m
2031
2138
  LEFT JOIN hub_users u ON u.id = m.source_user_id
2032
- LEFT JOIN hub_groups g ON g.id = m.group_id
2033
2139
  ORDER BY m.updated_at DESC
2034
2140
  `).all();
2035
2141
  return rows.map(r => ({
2036
2142
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
2037
- role: r.role, summary: r.summary, kind: r.kind,
2038
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
2039
- ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
2143
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
2144
+ groupId: r.group_id, groupName: null, visibility: r.visibility,
2145
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", createdAt: r.created_at, updatedAt: r.updated_at,
2040
2146
  }));
2041
2147
  }
2042
2148
  resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
@@ -2061,12 +2167,6 @@ class SqliteStore {
2061
2167
  }
2062
2168
  throw new Error(`source skill not found for skillId=${skillId}`);
2063
2169
  }
2064
- attachGroupsToHubUser(user) {
2065
- return {
2066
- ...user,
2067
- groups: this.getGroupsForHubUser(user.id),
2068
- };
2069
- }
2070
2170
  getSessionOwnerMap(sessionKeys) {
2071
2171
  const result = new Map();
2072
2172
  if (sessionKeys.length === 0)
@@ -2189,14 +2289,8 @@ function rowToHubUser(row) {
2189
2289
  tokenHash: row.token_hash,
2190
2290
  createdAt: row.created_at,
2191
2291
  approvedAt: row.approved_at,
2192
- };
2193
- }
2194
- function rowToHubGroup(row) {
2195
- return {
2196
- id: row.id,
2197
- name: row.name,
2198
- description: row.description,
2199
- createdAt: row.created_at,
2292
+ lastIp: row.last_ip || "",
2293
+ lastActiveAt: row.last_active_at ?? null,
2200
2294
  };
2201
2295
  }
2202
2296
  function rowToHubTask(row) {