@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 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 +1 -2
  8. package/dist/client/connector.d.ts.map +1 -1
  9. package/dist/client/connector.js +93 -26
  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 +0 -2
  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 +277 -87
  24. package/dist/hub/server.js.map +1 -1
  25. package/dist/hub/user-manager.d.ts +2 -0
  26. package/dist/hub/user-manager.d.ts.map +1 -1
  27. package/dist/hub/user-manager.js +5 -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 +286 -118
  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 +2660 -889
  69. package/dist/viewer/html.js.map +1 -1
  70. package/dist/viewer/server.d.ts +30 -8
  71. package/dist/viewer/server.d.ts.map +1 -1
  72. package/dist/viewer/server.js +965 -193
  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 +91 -28
  81. package/src/client/hub.ts +18 -0
  82. package/src/client/skill-sync.ts +14 -0
  83. package/src/config.ts +0 -2
  84. package/src/hub/server.ts +259 -78
  85. package/src/hub/user-manager.ts +7 -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 +295 -144
  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 +2660 -889
  98. package/src/viewer/server.ts +888 -177
@@ -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");
@@ -1753,7 +1901,7 @@ class SqliteStore {
1753
1901
  let rows;
1754
1902
  if (sanitized) {
1755
1903
  rows = this.db.prepare(`
1756
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' 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,
1757
1905
  bm25(hub_skills_fts) as rank
1758
1906
  FROM hub_skills_fts f
1759
1907
  JOIN hub_skills hs ON hs.rowid = f.rowid
@@ -1765,7 +1913,7 @@ class SqliteStore {
1765
1913
  }
1766
1914
  else {
1767
1915
  rows = this.db.prepare(`
1768
- SELECT hs.id, hs.name, hs.description, hs.version, hs.visibility, '' 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,
1769
1917
  0 as rank
1770
1918
  FROM hub_skills hs
1771
1919
  LEFT JOIN hub_users hu ON hu.id = hs.source_user_id
@@ -1780,7 +1928,7 @@ class SqliteStore {
1780
1928
  }
1781
1929
  listVisibleHubTasks(userId, limit = 40) {
1782
1930
  const rows = this.db.prepare(`
1783
- SELECT t.*, u.username AS owner_name, NULL AS group_name,
1931
+ SELECT t.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name,
1784
1932
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1785
1933
  FROM hub_tasks t
1786
1934
  LEFT JOIN hub_users u ON u.id = t.source_user_id
@@ -1790,33 +1938,36 @@ class SqliteStore {
1790
1938
  return rows.map(r => ({
1791
1939
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1792
1940
  title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1793
- 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,
1794
1942
  createdAt: r.created_at, updatedAt: r.updated_at,
1795
1943
  }));
1796
1944
  }
1797
1945
  listAllHubTasks() {
1798
1946
  const rows = this.db.prepare(`
1799
- 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,
1800
1948
  (SELECT COUNT(*) FROM hub_chunks c WHERE c.hub_task_id = t.id) AS chunk_count
1801
1949
  FROM hub_tasks t
1802
1950
  LEFT JOIN hub_users u ON u.id = t.source_user_id
1803
- LEFT JOIN hub_groups g ON g.id = t.group_id
1804
1951
  ORDER BY t.updated_at DESC
1805
1952
  `).all();
1806
1953
  return rows.map(r => ({
1807
1954
  id: r.id, sourceTaskId: r.source_task_id, sourceUserId: r.source_user_id,
1808
- title: r.title, summary: r.summary, groupId: r.group_id, groupName: r.group_name ?? null,
1809
- 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,
1810
1957
  createdAt: r.created_at, updatedAt: r.updated_at,
1811
1958
  }));
1812
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
+ }
1813
1964
  deleteHubTaskById(taskId) {
1814
1965
  const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
1815
1966
  return info.changes > 0;
1816
1967
  }
1817
1968
  listVisibleHubSkills(userId, limit = 40) {
1818
1969
  const rows = this.db.prepare(`
1819
- SELECT s.*, u.username AS owner_name, NULL AS group_name
1970
+ SELECT s.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
1820
1971
  FROM hub_skills s
1821
1972
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1822
1973
  ORDER BY s.updated_at DESC
@@ -1826,23 +1977,22 @@ class SqliteStore {
1826
1977
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1827
1978
  name: r.name, description: r.description, version: r.version,
1828
1979
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1829
- ownerName: r.owner_name ?? "unknown", qualityScore: r.quality_score,
1980
+ ownerName: r.owner_name ?? "unknown", ownerStatus: r.owner_status ?? "", qualityScore: r.quality_score,
1830
1981
  createdAt: r.created_at, updatedAt: r.updated_at,
1831
1982
  }));
1832
1983
  }
1833
1984
  listAllHubSkills() {
1834
1985
  const rows = this.db.prepare(`
1835
- 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
1836
1987
  FROM hub_skills s
1837
1988
  LEFT JOIN hub_users u ON u.id = s.source_user_id
1838
- LEFT JOIN hub_groups g ON g.id = s.group_id
1839
1989
  ORDER BY s.updated_at DESC
1840
1990
  `).all();
1841
1991
  return rows.map(r => ({
1842
1992
  id: r.id, sourceSkillId: r.source_skill_id, sourceUserId: r.source_user_id,
1843
1993
  name: r.name, description: r.description, version: r.version,
1844
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1845
- 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,
1846
1996
  createdAt: r.created_at, updatedAt: r.updated_at,
1847
1997
  }));
1848
1998
  }
@@ -1881,6 +2031,37 @@ class SqliteStore {
1881
2031
  const info = this.db.prepare('DELETE FROM hub_memories WHERE id = ?').run(memoryId);
1882
2032
  return info.changes > 0;
1883
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
+ }
1884
2065
  upsertHubMemoryEmbedding(memoryId, vector) {
1885
2066
  const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1886
2067
  this.db.prepare(`
@@ -1937,7 +2118,7 @@ class SqliteStore {
1937
2118
  }
1938
2119
  listVisibleHubMemories(userId, limit = 40) {
1939
2120
  const rows = this.db.prepare(`
1940
- SELECT m.*, u.username AS owner_name, NULL AS group_name
2121
+ SELECT m.*, u.username AS owner_name, u.status AS owner_status, NULL AS group_name
1941
2122
  FROM hub_memories m
1942
2123
  LEFT JOIN hub_users u ON u.id = m.source_user_id
1943
2124
  ORDER BY m.updated_at DESC
@@ -1945,24 +2126,23 @@ class SqliteStore {
1945
2126
  `).all(limit);
1946
2127
  return rows.map(r => ({
1947
2128
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
1948
- role: r.role, summary: r.summary, kind: r.kind,
2129
+ role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
1949
2130
  groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1950
- 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,
1951
2132
  }));
1952
2133
  }
1953
2134
  listAllHubMemories() {
1954
2135
  const rows = this.db.prepare(`
1955
- 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
1956
2137
  FROM hub_memories m
1957
2138
  LEFT JOIN hub_users u ON u.id = m.source_user_id
1958
- LEFT JOIN hub_groups g ON g.id = m.group_id
1959
2139
  ORDER BY m.updated_at DESC
1960
2140
  `).all();
1961
2141
  return rows.map(r => ({
1962
2142
  id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
1963
- role: r.role, summary: r.summary, kind: r.kind,
1964
- groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
1965
- 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,
1966
2146
  }));
1967
2147
  }
1968
2148
  resolveCanonicalHubTaskId(taskId, sourceUserId, sourceTaskId) {
@@ -1987,12 +2167,6 @@ class SqliteStore {
1987
2167
  }
1988
2168
  throw new Error(`source skill not found for skillId=${skillId}`);
1989
2169
  }
1990
- attachGroupsToHubUser(user) {
1991
- return {
1992
- ...user,
1993
- groups: this.getGroupsForHubUser(user.id),
1994
- };
1995
- }
1996
2170
  getSessionOwnerMap(sessionKeys) {
1997
2171
  const result = new Map();
1998
2172
  if (sessionKeys.length === 0)
@@ -2115,14 +2289,8 @@ function rowToHubUser(row) {
2115
2289
  tokenHash: row.token_hash,
2116
2290
  createdAt: row.created_at,
2117
2291
  approvedAt: row.approved_at,
2118
- };
2119
- }
2120
- function rowToHubGroup(row) {
2121
- return {
2122
- id: row.id,
2123
- name: row.name,
2124
- description: row.description,
2125
- createdAt: row.created_at,
2292
+ lastIp: row.last_ip || "",
2293
+ lastActiveAt: row.last_active_at ?? null,
2126
2294
  };
2127
2295
  }
2128
2296
  function rowToHubTask(row) {